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