1
0
mirror of https://github.com/emilk/egui.git synced 2026-09-01 14:20:04 -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

@@ -1,7 +1,7 @@
use crate::atomics::ATOMS_SMALL_VEC_SIZE;
use crate::{
AtomKind, Atoms, FontSelection, Frame, Id, Image, IntoAtoms, Response, Sense, SizedAtom,
SizedAtomKind, Ui, Widget,
SizedAtomKind, Stroke, Ui, Widget, text_selection::LabelSelectionState,
};
use emath::{Align2, GuiRounding as _, NumExt as _, Rect, Vec2};
use epaint::text::TextWrapMode;
@@ -35,6 +35,7 @@ pub struct AtomLayout<'a> {
gap: Option<f32>,
pub(crate) frame: Frame,
pub(crate) sense: Sense,
selectable: bool,
fallback_text_color: Option<Color32>,
fallback_font: Option<FontSelection>,
min_size: Vec2,
@@ -57,6 +58,7 @@ impl<'a> AtomLayout<'a> {
gap: None,
frame: Frame::default(),
sense: Sense::hover(),
selectable: false,
fallback_text_color: None,
fallback_font: None,
min_size: Vec2::ZERO,
@@ -89,6 +91,18 @@ impl<'a> AtomLayout<'a> {
self
}
/// Make the text in this layout selectable with the mouse.
///
/// This is opt-in (default `false`): [`AtomLayout`] backs widgets like
/// [`crate::Button`] and [`crate::Checkbox`] whose labels should not be
/// selectable, so enabling it unconditionally would break them. When enabled,
/// the layout also senses clicks and drags so the selection can be made.
#[inline]
pub fn selectable(mut self, selectable: bool) -> Self {
self.selectable = selectable;
self
}
/// Set the fallback (default) text color.
///
/// Default: [`crate::Visuals::text_color`]
@@ -187,7 +201,8 @@ impl<'a> AtomLayout<'a> {
mut atoms,
gap,
frame,
sense,
mut sense,
selectable,
fallback_text_color,
min_size,
mut max_size,
@@ -198,6 +213,19 @@ impl<'a> AtomLayout<'a> {
let fallback_font = fallback_font.unwrap_or_default();
if selectable {
// Mirror `Label`: sense clicks and drags so the text can be selected,
// but don't take keyboard focus on TAB.
let allow_drag_to_select = ui.input(|i| !i.has_touch_screen());
let mut select_sense = if allow_drag_to_select {
Sense::click_and_drag()
} else {
Sense::click()
};
select_sense -= Sense::FOCUSABLE;
sense |= select_sense;
}
let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode());
// If the TextWrapMode is not Extend, ensure there is some item marked as `shrink`.
@@ -331,6 +359,7 @@ impl<'a> AtomLayout<'a> {
desired_size,
align2,
gap,
selectable,
}
}
}
@@ -347,6 +376,7 @@ pub struct AllocatedAtomLayout<'a> {
desired_size: Vec2,
align2: Align2,
gap: f32,
selectable: bool,
}
impl<'atom> AllocatedAtomLayout<'atom> {
@@ -434,6 +464,7 @@ impl<'atom> AllocatedAtomLayout<'atom> {
desired_size,
align2,
gap,
selectable,
} = self;
let inner_rect = response.rect - self.frame.total_margin();
@@ -476,7 +507,21 @@ impl<'atom> AllocatedAtomLayout<'atom> {
match sized.kind {
SizedAtomKind::Text(galley) => {
ui.painter().galley(rect.min, galley, fallback_text_color);
if selectable {
// Route through the label selection machinery, which also
// paints the galley. `Stroke::NONE` keeps the rendering
// identical to the non-selectable path (no focus underline).
LabelSelectionState::label_text_selection(
ui,
&response.response,
rect.min,
galley,
fallback_text_color,
Stroke::NONE,
);
} else {
ui.painter().galley(rect.min, galley, fallback_text_color);
}
}
SizedAtomKind::Image { image, size: _ } => {
image.paint_at(ui, rect);