1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-26 14:49:06 -04:00

Add AtomLayout::selectable for opt-in text selection (#8224)

* Closes https://github.com/emilk/egui/issues/8217
* [x] I have followed the instructions in the PR template

## What

`AllocatedAtomLayout::paint` painted text via
`ui.painter().galley(...)`, so text in atom-based widgets could never be
selected. As discussed in #8217, routing it through
`LabelSelectionState::label_text_selection` unconditionally would break
`Button` / `Checkbox` / `RadioButton` / `TextEdit` (whose labels should
not be selectable, and whose click/drag handling would fight the
selection drag).

So this adds an **opt-in** `AtomLayout::selectable(bool)` (default
`false`). When enabled, the layout also senses clicks and drags
(mirroring `Label::layout_in_ui`) and paints its text through the
label-selection machinery. The underline is `Stroke::NONE`, so when
nothing is selected the painted output is identical to the existing path
— the default (`selectable == false`) behaviour, and existing snapshots,
are unchanged.

## Tests

Two tests in `tests/egui_tests/tests/test_atoms.rs`:

- `test_atom_selectable_senses_click_and_drag` — a `selectable(true)`
layout senses click+drag; the default stays inert.
- `test_atom_selectable_text_can_be_copied` — selecting (drag) the text
of a selectable layout and copying yields the text via
`OutputCommand::CopyText`, while a non-selectable layout yields nothing.

## Notes

- Verified locally: `cargo fmt --all -- --check`, `cargo clippy -p egui`
(clean), and the two new tests pass. I haven't run the full
`./scripts/check.sh` (wasm/typos) locally, hence opening as a draft.
- This only adds the opt-in API; no existing widget is made selectable.
Happy to wire it up on a specific widget and/or add an `egui_demo_lib`
demo if you'd like — just let me know.
This commit is contained in:
冷凍アカギ
2026-06-14 03:25:00 +09:00
committed by GitHub
parent 27a61934b1
commit 172fb54f7f
2 changed files with 139 additions and 3 deletions

View File

@@ -140,3 +140,94 @@ fn test_atom_letter_spacing() {
harness.snapshot("atom_letter_spacing");
}
/// `AtomLayout::selectable(true)` should opt the layout into click+drag sensing
/// so its text can be selected, while the default layout stays inert.
/// See <https://github.com/emilk/egui/issues/8217>.
#[test]
fn test_atom_selectable_senses_click_and_drag() {
use egui::{AtomLayout, Sense};
let mut captured = (Sense::hover(), Sense::hover());
{
let mut harness = HarnessBuilder::default().build_ui(|ui| {
let selectable = AtomLayout::new("selectable").selectable(true).show(ui);
let default = AtomLayout::new("default").show(ui);
captured = (selectable.response.sense, default.response.sense);
});
harness.run();
}
let (selectable_sense, default_sense) = captured;
assert!(
selectable_sense.senses_click() && selectable_sense.senses_drag(),
"a selectable AtomLayout should sense clicks and drags"
);
assert!(
!default_sense.senses_drag(),
"a non-selectable AtomLayout should stay inert"
);
}
/// Selecting the text of a `selectable` [`egui::AtomLayout`] and copying it should
/// yield the text, while a non-selectable one yields nothing.
/// See <https://github.com/emilk/egui/issues/8217>.
#[test]
fn test_atom_selectable_text_can_be_copied() {
use egui::{AtomLayout, Event, Modifiers, OutputCommand, PointerButton, Pos2, Rect};
use std::cell::Cell;
fn copied_text(selectable: bool) -> Option<String> {
let rect_cell = Cell::new(Rect::NOTHING);
let mut harness = HarnessBuilder::default()
.with_size(Vec2::new(400.0, 100.0))
.build_ui(|ui| {
let response = AtomLayout::new("selectable atoms")
.selectable(selectable)
.show(ui);
rect_cell.set(response.response.rect);
});
harness.run();
let rect = rect_cell.get();
let left = Pos2::new(rect.left() + 1.0, rect.center().y);
let right = Pos2::new(rect.right() - 1.0, rect.center().y);
// Press at the start of the text and drag to the end to select all of it.
harness.event(Event::PointerMoved(left));
harness.event(Event::PointerButton {
pos: left,
button: PointerButton::Primary,
pressed: true,
modifiers: Modifiers::NONE,
});
harness.run();
harness.event(Event::PointerMoved(right));
harness.run();
// Copy, then read back the clipboard command produced by this frame.
harness.event(Event::Copy);
harness.step();
harness
.output()
.platform_output
.commands
.iter()
.find_map(|cmd| match cmd {
OutputCommand::CopyText(text) => Some(text.clone()),
_ => None,
})
}
assert_eq!(
copied_text(true).as_deref(),
Some("selectable atoms"),
"selectable atom text should be copyable after selecting it"
);
assert_eq!(
copied_text(false),
None,
"non-selectable atom text should not be selectable"
);
}