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:
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user