text: drag-select and Ctrl+C copy#3315
Conversation
|
Can't really look through the code right now, does this have keyboard selection as well? |
|
No, but I can try to look into that! Will come back later. |
5146b02 to
a71794d
Compare
Implemented keyboard navigation/selection |
|
I was only able to gloss over the code for now, but testing it out (with selection enabled for the markdown example) everything looks pretty good! It feels a bit weird that selecting to e.g. the end of the next word selects the space after that word as well, but works nicely nonetheless. One small note is that CI will fail because two |
a71794d to
e14c0d5
Compare
|
Thanks! Fixed both text::Style literals (and a third one in that I caught locally in an example file). Also tweaked step_byte_word to land at the end of the next word instead of the start of the one after, mirroring text_input::next_end_of_word. The trailing space is no longer pulled into the selection.
|
|
Is there a specific reason you only have grouping and (as far as I can see) keyboard selection for |
|
That was my use case and the effort was focused there; I forgot to extend it further. I can extend it to Text, yes, I'll do it now. Thanks. |
c748ebd to
23a5d40
Compare
|
@pml68 Done, extended to plain Two extras while I was in there:
|
|
@wilsonglasser Great work! Some feedback:
|
|
@inanc-g Thanks for the review! Pushed b785a0b addressing the three points.
Known residual: the gate clears via mouse press dispatch. If the user transitions to a text_editor via Tab or programmatic focus (no click), the group keeps its anchor and would still steal Ctrl+A. Properly fixing that needs Operation::focusable integration, which felt like a separate change.
Shift+click takes priority over the count escalation, so it still extends from the existing anchor. Added examples/text_selection_test to exercise the flows side by side: two markdown views, a custom mixed selectable_group, standalone text and rich_text, and a text_editor. One UX gap I noticed but didn't tackle: dragging past the parent scrollable's viewport doesn't auto-scroll. Selection clamps to the start / end of the content, so it isn't broken, just limited. Happy to follow up in a separate PR if useful. Either a new shell.scroll_ancestor primitive or coordination from scrollable itself, both bigger than this fix bundle. |
Closes iced-rs#36. <!-- drag-drop the multi-paragraph selection screenshot here --> Drag-select on `text(...)` and `rich_text![...]` widgets, plus a `selectable_group` to coordinate selection across siblings (which is how `markdown::view_with` ends up with multi-paragraph selection). Off by default; existing apps see no behavior change. ## Usage ```rust text("hello").selectable(true) rich_text![span("foo")].selectable(true) markdown::view_with( &items, Settings { selectable: true, group_selection: true, ..settings }, &viewer, ) ``` ## What's in here - Selection on `Rich` and `Text`: per-widget drag, `Ctrl+C` copies the rendered text. Multibyte chars at the selection edges are snapped to UTF-8 boundaries. - New `selectable_group(content)` widget that walks its subtree, finds the `Rich` widgets, and coordinates a single drag across them. When the drag crosses into a sibling, the originator's selection extends to its end, intermediate widgets get fully selected, and the destination is selected from its start to the cursor. `Ctrl+C` joins them with a newline. - `markdown::Settings::{selectable, group_selection}` to opt in for markdown surfaces. - Selection rectangles use the same glyph-walking algorithm as `text_editor`'s `highlight_line`, so wrapping and `align_x` / `align_y` work without extra code. - New `Paragraph::selection_bounds(start, end) -> Vec<Rectangle>` trait method with a default empty impl, so non-glyph renderers (e.g. the null one) are unaffected. - `text::Style::selection: Color` for the highlight, defaulting to `palette.primary.weak.color` to match `text_input` / `text_editor`. - Mouse cursor switches to `Interaction::Text` over selectable widgets. ## Cross-widget coordination When `selectable_group` is in the tree, it sets an `externally_managed` flag on every `Rich::State` it walks into, so individual widgets skip their own drag-select / `Ctrl+C` handling and just render whatever selection the group writes in. The group owns drag origin + focus tracking and recomputes per-widget byte ranges on each `CursorMoved`. `Rich::State` is exposed as a `pub struct` with a minimal accessor set (`selection`, `set_selection`, `paragraph`, `text_len`, `selection_text`, `set_externally_managed`). I couldn't find a clean way to downcast `tree::State` to a `&mut dyn Trait` so the group ends up concretely aware of `Rich`. Open to suggestions if there's a more elegant pattern. The within-widget coordination (clicking outside a `Rich`'s text drops focus, so siblings self-clear on the same event) is the same trick `text_input` uses — every widget sees every press and reacts to its own bounds. ## Out of scope - Plain `text(...)` inside `selectable_group` — only `rich_text` is coordinated. Plain text widgets still select on their own. - RTL / bidi — same level as the existing `rich_text` rendering. - Programmatic access to a group's selection through Operations — could be a follow-up if there's a use case. - Screen reader narration — not regressed, not improved. ## Migration - `text::Style` gained a `selection` field. Three internal callsites in `checkbox` / `radio` / `toggler`, plus the `color_maybe` helpers on `Rich` / `Text`, were updated to spread `..Style::default()`. - `markdown::Settings` gained `selectable` and `group_selection`. Most consumers build it via `with_text_size` / `with_style` and won't notice; explicit struct literals need the new fields. - `core::widget::text::State<P>` keeps its existing `paragraph::Plain<P>` alias so label-style widgets (`checkbox`, `radio`, `toggler`) compile unchanged. The `Text` widget itself uses a private `Internal<P>` that wraps `Plain<P>` with selection state. ## Note on markdown helpers `unordered_list` and `ordered_list` were calling `view_with` recursively for nested bullet content. With the new wrapping behavior that would create a nested `selectable_group` per bullet, breaking cross-list selection. Switched them to call `items()` directly, which produces just the column without wrapping. `quote`, `table`, and `code_block` already used `item()` / `items()` and didn't need changes. ## Tested against I've been running this in [oryxis](https://github.com/wilsonglasser/oryxis), my SSH client. The screenshot above is its AI chat panel rendering markdown via `view_with`. Selection works across paragraphs, headings, code blocks, numbered lists, nested bullets, and tables.
b785a0b to
5c2b68e
Compare
The selection model works in byte offsets into the whole fragment, but the cosmic-text paragraph methods worked in per-buffer-line offsets, which restart near 0 on every newline. For single-line text the two coincide; multi-line broke: - selection_bounds applied the global range to every buffer line, so a selection on one line painted the same columns on all of them. - hit_test returned the per-line index, dropping the line component, so drag and click landed on the wrong byte past the first line. - byte_position had the same flaw, breaking vertical nav and Home/End. All three now share a buffer_line_byte_len stride to translate between global and per-line offsets (single-byte \n separators). line_edge_byte now walks the logical, \n-delimited line from the text itself instead of hit-testing at the visual y, so triple-click and Home/End cover a whole wrapped line instead of stopping at a soft wrap. The unused bounds_width trait method (its only caller) is removed. Adds headless regression tests for the per-line offset and logical edge behavior, plus multi-line and wrapped cases to the example.
|
@OlofBlomqvist updated to apply a fix for those cases |
|
@wilsonglasser works great, thanks! |


Closes #36.

Drag-select +
Ctrl+Context(...)andrich_text![...], plus aselectable_groupto drag across siblings (which is whatmarkdown::view_withuses). Off by default.What's in here
Rich/Text,Ctrl+Cto copy. UTF-8 boundary snapping at the edges.selectable_group(content)walks its subtree and coordinates one drag across itsRichchildren.Ctrl+Cjoins them with\n.Shift+Arrows,Ctrl+Shift+Arrows(word),Shift+Home/End,Ctrl+Shift+Home/End,Ctrl+A,Esc.Shift+Clickextends.text_editor'shighlight_lineglyph walk, so wrapping + alignment just work.markdown::Settings::{selectable, group_selection}to opt in.Paragraph::selection_boundstrait method, default empty impl.text::Style::selection: Color, defaults topalette.primary.weak.color.Coordination
Goes through
Operation, mirroringFocusable/Operation::focusable:Selectabletrait incore::widget::operation(selection, length, hit-test, byte stepping,set_externally_managed).Operation::selectable(id, bounds, &mut dyn Selectable)hook, default empty impl.Rich::Stateimplements it and routes throughWidget::operate. State is fully private; the group never sees the concrete type.selectable_groupruns anOperationthat visits everySelectablein tree order and reads/writes per child.set_externally_managedis what tells individual widgets to skip their own drag/copy while the group is driving. Group flips it at the top of eachupdate.Out of scope
text(...)inside a group (onlyrich_textis coordinated).rich_text.Migration
text::Stylegainedselection. Touchedcheckbox/radio/togglerand thecolor_maybehelpers to spread..Style::default().markdown::Settingsgainedselectableandgroup_selection. Builders are fine; struct literals need the fields.core::widget::text::State<P>still aliasesparagraph::Plain<P>, so label-style widgets compile unchanged.Textuses a privateInternal<P>wrappingPlain<P>plus selection state.unordered_list/ordered_listwere callingview_withrecursively for nested bullets, which would nest groups and break cross-list selection. Switched toitems().Tested
Running it in oryxis. Screenshot is its AI chat panel via
view_with. Selection works across paragraphs, headings, code blocks, lists (incl. nested), and tables.