Skip to content

Text-editing follow-ups drain: BiDi split caret + compose-over-selection + clipboard flavors (T1–T3)#74

Merged
intendednull merged 3 commits into
mainfrom
followups-text
Jun 19, 2026
Merged

Text-editing follow-ups drain: BiDi split caret + compose-over-selection + clipboard flavors (T1–T3)#74
intendednull merged 3 commits into
mainfrom
followups-text

Conversation

@intendednull

Copy link
Copy Markdown
Owner

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 off main after the BSN/Bevy-0.19 migration merged), since the text follow-ups are independent of that migration.

Slices

  1. a79ab85 BiDi 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's cursor_glyph is affinity-blind, so secondary_caret_rect_for walks layout_runs and 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 rides CaretVisual as an Option<Rect> (no new component). GPU split-caret golden verified on the RX 6700 XT.

  2. 10cc34e compose-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 one Composition undo unit, so a single undo restores both; cancel reverse-applies. TextChanged fires on the delete. The unselected-caret path is byte-identical to E5.

  3. 4ee5a8b HTML + image clipboard flavorsClipboardProvider gains get/set_html (always) and get/set_image (behind a new clipboard-image cargo 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.md entry 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

intendednull and others added 3 commits June 18, 2026 22:18
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
@intendednull intendednull merged commit 5d5cfe1 into main Jun 19, 2026
12 of 14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant