Text-editing follow-ups drain: BiDi split caret + compose-over-selection + clipboard flavors (T1–T3)#74
Merged
Merged
Conversation
At a bidirectional direction boundary the caret now paints TWO marks — the primary full-height bar plus a shorter SECONDARY indicator at the other run's edge — so the user can tell which direction the next typed character flows (editing-and-ime §§4.1/5). E3 shipped only the primary. cosmic-text 0.19's cursor_glyph is affinity-blind, so a single cursor_position cannot surface the second edge. New secondary_caret_rect_for walks layout_runs: at the caret's line it finds the before glyph (end == index) and after glyph (start == index); emits the secondary only when both exist and their bidi levels differ, at the BEFORE glyph's logical-end visual edge (RTL: x; LTR: x+w). Soft-wrapped logical lines emit multiple runs sharing line_i, so the scan CONTINUEs past a non-owning wrap segment and only concludes None after exhausting all runs (impl-review major). Forced by Bevy's 15-tuple extract query cap (PlaceholderBuffer is the 15th slot), the secondary rides CaretVisual as an Option<Rect> field rather than a new component — reusing Changed<CaretVisual>/RemovedComponents as its damage + clear triggers, zero new query surface. extract stamps a second solid glyph (same atlas entry/color/clip, no new insert). The blink writer (visual.rs) touches only .visible — preserved, carries secondary through. Tests assert the EXACT data-derived before-glyph edge (equality with primary.x allowed — coincident visual pen-x is two distinct logical insertion points; no spurious inequality). Headless text_caret_geometry 11/11 (incl. boundary + wrapped-line + no-boundary cases); GPU split-caret golden passes on RX 6700 XT (primary+secondary at a mixed-BiDi boundary). Spec §§4.1/5/13 + follow-ups.md LANDED. First text-track slice; built on the 0.19-rc base. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DHcf8nQ9PTT3m5E7u3Q6XV
When an IME composition starts while text is selected, the selection is now REPLACED by the preedit (platform/web convention) instead of leaving the selected text in place beside the preedit. editing-and-ime §§6.1/6.2. splice_preedit, on the FIRST splice of a composition (preedit.is_none) over a non-collapsed selection, deletes the selection inside a start_change/ finish_change pair and STASHES the reversible Change + pre-delete caret/ selection on TextEditState (a separate field, so PreeditSpan stays Eq), then splices the preedit at the collapsed caret and returns true so apply_ime fires TextChanged (the one §11 exception — the delete removed user text). At commit, the stashed delete's items are folded with the commit-insert items into ONE GroupKind::Composition undo unit whose caret_before/selection_before are the true pre-composition state — so a single Undo reverses BOTH the delete and the commit and restores the original selection. Cancel (empty preedit / Escape / blur) reverse-applies the stash (re-inserts the deleted text, restores the selection) → a true no-op, firing TextChanged back to the original value. Approach A over B (recorded in the spec note): Composition never coalesces (§6.2c), and the intervening preedit splices break caret-adjacency, so the delete cannot be a separate coalesced unit — it must be folded into the one commit unit. The unselected-caret path takes no branch and returns false → byte-identical to the E5 plain-text IME path (regression-guarded). No new GPU, no new event surface; §3.2 extract tripwire respected (delete rides the same mid-frame mutation + dirty-mark as the existing splice). Tests: text_ime_ops 9/9 (delete+stash, one-unit commit, single-undo restores both, redo), text_ime_system 10/10 (TextChanged on the delete), lib text 30/30; clippy -D warnings clean. Spec §§6.1/6.2/13 + follow-ups.md LANDED. 0.19-rc base. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DHcf8nQ9PTT3m5E7u3Q6XV
E4 shipped plain-text clipboard only; the F row names text + HTML + image MIME.
Adds HTML + image flavors to the ClipboardProvider facade without touching the
plain-text path or the section 3.3 newline policy.
ClipboardProvider gains get_html/set_html (always available) and
get_image/set_image (behind a new clipboard-image cargo feature ->
arboard/image-data). MemClipboard stores independent html/image slots (the
headless-testable path); ArboardClipboard delegates to arboard 3.6.1's
get().html()/set_html() and (gated) get_image()/set_image(). A Buiy-owned
ClipboardImage {width,height,bytes} owned struct keeps arboard's borrowed
ImageData<'a> out of the trait signature. Copy/Cut now ALSO set an html flavor
(the plain text escaped via a single-pass escape_html); Paste's section 3.3
text path is unchanged (the html getter is available to callers but Paste does
not consult it) - E4 plain-text behavior is byte-identical (regression-guarded).
arboard 3.6.1's HTML API verified present on the locked pin (no bump); image
requires turning arboard's image-data feature back on, hence the opt-in
clipboard-image feature (default build stays text-only). The gated image lane
(cargo test -p buiy_core --features clipboard-image) is documented in CLAUDE.md
Build and Test plus the follow-ups.md LANDED note, since the default workspace
gate does not enable it.
Tests: text_clipboard_undo 13/13 default + 15/15 with clipboard-image (html
round-trip, copy/cut dual-set html, paste-prefers-text, image round-trip);
clippy -D warnings clean both lanes. OQ#3 resolved. Spec sections 7/13 +
follow-ups.md LANDED. Final text-track slice. 0.19-rc base.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DHcf8nQ9PTT3m5E7u3Q6XV
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Third track of the follow-ups-drain campaign (
docs/plans/2026-06-18-followups-drain.md): the actionable text-editing follow-ups. Built on the 0.19-rc baseline (this branch is offmainafter the BSN/Bevy-0.19 migration merged), since the text follow-ups are independent of that migration.Slices
a79ab85BiDi split caret secondary indicator — at a bidirectional direction boundary the caret paints a primary bar plus a shorter secondary indicator at the other run's edge. cosmic-text 0.19'scursor_glyphis affinity-blind, sosecondary_caret_rect_forwalkslayout_runsand emits at the before-glyph's logical-end edge only when the abutting glyphs' bidi levels differ; it scans all runs so a boundary on a soft-wrapped continuation isn't dropped. Forced by Bevy's 15-tuple extract-query cap, the secondary ridesCaretVisualas anOption<Rect>(no new component). GPU split-caret golden verified on the RX 6700 XT.10cc34ecompose-over-selection — starting an IME composition over a selection now replaces it: the selection is deleted (stashed as a reversible change) and folded with the commit into oneCompositionundo unit, so a single undo restores both; cancel reverse-applies.TextChangedfires on the delete. The unselected-caret path is byte-identical to E5.4ee5a8bHTML + image clipboard flavors —ClipboardProvidergainsget/set_html(always) andget/set_image(behind a newclipboard-imagecargo feature →arboard/image-data). Copy/Cut also set an html flavor; Paste's plain-text §3.3 path is unchanged. arboard 3.6.1's HTML API verified on the locked pin (no bump). The gated image lane is documented in CLAUDE.md.Each slice updates its spec touchpoint (editing-and-ime.md) and flips its
follow-ups.mdentry to LANDED.Verification
cargo fmt --all --check,cargo clippy --workspace --all-targets -D warnings,RUSTDOCFLAGS=-D warnings cargo doc --workspace --no-deps,cargo test -p buiy_core(default) and--features clipboard-image— all green. GPU caret golden passes locally on the RX 6700 XT (#[ignore]; the lavapipe CI lane exercises it). Full OS test matrix runs here in CI.🤖 Generated with Claude Code