Skip to content

Commit

Permalink
Cross-widget text select (#3870)
Browse files Browse the repository at this point in the history
* Closes #3816


![cross-widget-text-selection](https://github.com/emilk/egui/assets/1148717/5582b9d2-8b04-47b7-9637-f48e44064a70)

Turn off with `style.interaction.multi_widget_text_select`.

There is an API for this in `LabelSelectionState`, but it's pretty
bare-bones.

This became really hairy implementation-wise, but it works decently
well.

# Limitations
* Drag-select to scroll doesn't work
* A selection disappears if you scroll past one of its end-points
* Only the text of labels and links are selectable
 
## TODO
* [x] An option to turn it off
* [x] An API for querying about the selected text, and to deselect it.
* [x] Scrolling past selection behaves weird
* [x] Shift-click to select a range
  • Loading branch information
emilk authored Jan 24, 2024
1 parent bcf032a commit 41aad74
Show file tree
Hide file tree
Showing 19 changed files with 714 additions and 139 deletions.
19 changes: 15 additions & 4 deletions crates/egui/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -625,7 +625,7 @@ impl Context {
/// ```
pub fn begin_frame(&self, new_input: RawInput) {
crate::profile_function!();

crate::text_selection::LabelSelectionState::begin_frame(self);
self.write(|ctx| ctx.begin_frame_mut(new_input));
}
}
Expand Down Expand Up @@ -697,13 +697,13 @@ impl Context {

/// Read-write access to [`GraphicLayers`], where painted [`crate::Shape`]s are written to.
#[inline]
pub(crate) fn graphics_mut<R>(&self, writer: impl FnOnce(&mut GraphicLayers) -> R) -> R {
pub fn graphics_mut<R>(&self, writer: impl FnOnce(&mut GraphicLayers) -> R) -> R {
self.write(move |ctx| writer(&mut ctx.viewport().graphics))
}

/// Read-only access to [`GraphicLayers`], where painted [`crate::Shape`]s are written to.
#[inline]
pub(crate) fn graphics<R>(&self, reader: impl FnOnce(&GraphicLayers) -> R) -> R {
pub fn graphics<R>(&self, reader: impl FnOnce(&GraphicLayers) -> R) -> R {
self.write(move |ctx| reader(&ctx.viewport().graphics))
}

Expand Down Expand Up @@ -1616,6 +1616,8 @@ impl Context {
crate::gui_zoom::zoom_with_keyboard(self);
}

crate::text_selection::LabelSelectionState::end_frame(self);

let debug_texts = self.write(|ctx| std::mem::take(&mut ctx.debug_texts));
if !debug_texts.is_empty() {
// Show debug-text next to the cursor.
Expand Down Expand Up @@ -2041,7 +2043,7 @@ impl Context {
/// Can be used to implement drag-and-drop (see relevant demo).
pub fn translate_layer(&self, layer_id: LayerId, delta: Vec2) {
if delta != Vec2::ZERO {
self.graphics_mut(|g| g.list(layer_id).translate(delta));
self.graphics_mut(|g| g.entry(layer_id).translate(delta));
}
}

Expand Down Expand Up @@ -2352,6 +2354,15 @@ impl Context {
let font_image_size = self.fonts(|f| f.font_image_size());
crate::introspection::font_texture_ui(ui, font_image_size);
});

CollapsingHeader::new("Label text selection state")
.default_open(false)
.show(ui, |ui| {
ui.label(format!(
"{:#?}",
crate::text_selection::LabelSelectionState::load(ui.ctx())
));
});
}

/// Show stats about the allocated textures.
Expand Down
1 change: 1 addition & 0 deletions crates/egui/src/input_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -840,6 +840,7 @@ impl PointerState {
}

/// Was any pointer button pressed (`!down -> down`) this frame?
///
/// This can sometimes return `true` even if `any_down() == false`
/// because a press can be shorted than one frame.
pub fn any_pressed(&self) -> bool {
Expand Down
21 changes: 18 additions & 3 deletions crates/egui/src/layers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@ impl LayerId {
}

/// A unique identifier of a specific [`Shape`] in a [`PaintList`].
#[derive(Clone, Copy, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ShapeIdx(pub usize);

/// A list of [`Shape`]s paired with a clip rectangle.
Expand Down Expand Up @@ -151,6 +152,12 @@ impl PaintList {
self.0[idx.0] = ClippedShape { clip_rect, shape };
}

/// Set the given shape to be empty (a `Shape::Noop`).
#[inline(always)]
pub fn reset_shape(&mut self, idx: ShapeIdx) {
self.0[idx.0].shape = Shape::Noop;
}

/// Translate each [`Shape`] and clip rectangle by this much, in-place
pub fn translate(&mut self, delta: Vec2) {
for ClippedShape { clip_rect, shape } in &mut self.0 {
Expand All @@ -165,20 +172,28 @@ impl PaintList {
}
}

/// This is where painted [`Shape`]s end up during a frame.
#[derive(Clone, Default)]
pub(crate) struct GraphicLayers([IdMap<PaintList>; Order::COUNT]);
pub struct GraphicLayers([IdMap<PaintList>; Order::COUNT]);

impl GraphicLayers {
pub fn list(&mut self, layer_id: LayerId) -> &mut PaintList {
/// Get or insert the [`PaintList`] for the given [`LayerId`].
pub fn entry(&mut self, layer_id: LayerId) -> &mut PaintList {
self.0[layer_id.order as usize]
.entry(layer_id.id)
.or_default()
}

/// Get the [`PaintList`] for the given [`LayerId`].
pub fn get(&self, layer_id: LayerId) -> Option<&PaintList> {
self.0[layer_id.order as usize].get(&layer_id.id)
}

/// Get the [`PaintList`] for the given [`LayerId`].
pub fn get_mut(&mut self, layer_id: LayerId) -> Option<&mut PaintList> {
self.0[layer_id.order as usize].get_mut(&layer_id.id)
}

pub fn drain(&mut self, area_order: &[LayerId]) -> Vec<ClippedShape> {
crate::profile_function!();

Expand Down
53 changes: 32 additions & 21 deletions crates/egui/src/painter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ impl Painter {
impl Painter {
#[inline]
fn paint_list<R>(&self, writer: impl FnOnce(&mut PaintList) -> R) -> R {
self.ctx.graphics_mut(|g| writer(g.list(self.layer_id)))
self.ctx.graphics_mut(|g| writer(g.entry(self.layer_id)))
}

fn transform_shape(&self, shape: &mut Shape) {
Expand Down Expand Up @@ -257,21 +257,21 @@ impl Painter {
/// # Paint different primitives
impl Painter {
/// Paints a line from the first point to the second.
pub fn line_segment(&self, points: [Pos2; 2], stroke: impl Into<Stroke>) {
pub fn line_segment(&self, points: [Pos2; 2], stroke: impl Into<Stroke>) -> ShapeIdx {
self.add(Shape::LineSegment {
points,
stroke: stroke.into(),
});
})
}

/// Paints a horizontal line.
pub fn hline(&self, x: impl Into<Rangef>, y: f32, stroke: impl Into<Stroke>) {
self.add(Shape::hline(x, y, stroke));
pub fn hline(&self, x: impl Into<Rangef>, y: f32, stroke: impl Into<Stroke>) -> ShapeIdx {
self.add(Shape::hline(x, y, stroke))
}

/// Paints a vertical line.
pub fn vline(&self, x: f32, y: impl Into<Rangef>, stroke: impl Into<Stroke>) {
self.add(Shape::vline(x, y, stroke));
pub fn vline(&self, x: f32, y: impl Into<Rangef>, stroke: impl Into<Stroke>) -> ShapeIdx {
self.add(Shape::vline(x, y, stroke))
}

pub fn circle(
Expand All @@ -280,31 +280,36 @@ impl Painter {
radius: f32,
fill_color: impl Into<Color32>,
stroke: impl Into<Stroke>,
) {
) -> ShapeIdx {
self.add(CircleShape {
center,
radius,
fill: fill_color.into(),
stroke: stroke.into(),
});
})
}

pub fn circle_filled(&self, center: Pos2, radius: f32, fill_color: impl Into<Color32>) {
pub fn circle_filled(
&self,
center: Pos2,
radius: f32,
fill_color: impl Into<Color32>,
) -> ShapeIdx {
self.add(CircleShape {
center,
radius,
fill: fill_color.into(),
stroke: Default::default(),
});
})
}

pub fn circle_stroke(&self, center: Pos2, radius: f32, stroke: impl Into<Stroke>) {
pub fn circle_stroke(&self, center: Pos2, radius: f32, stroke: impl Into<Stroke>) -> ShapeIdx {
self.add(CircleShape {
center,
radius,
fill: Default::default(),
stroke: stroke.into(),
});
})
}

pub fn rect(
Expand All @@ -313,26 +318,26 @@ impl Painter {
rounding: impl Into<Rounding>,
fill_color: impl Into<Color32>,
stroke: impl Into<Stroke>,
) {
self.add(RectShape::new(rect, rounding, fill_color, stroke));
) -> ShapeIdx {
self.add(RectShape::new(rect, rounding, fill_color, stroke))
}

pub fn rect_filled(
&self,
rect: Rect,
rounding: impl Into<Rounding>,
fill_color: impl Into<Color32>,
) {
self.add(RectShape::filled(rect, rounding, fill_color));
) -> ShapeIdx {
self.add(RectShape::filled(rect, rounding, fill_color))
}

pub fn rect_stroke(
&self,
rect: Rect,
rounding: impl Into<Rounding>,
stroke: impl Into<Stroke>,
) {
self.add(RectShape::stroke(rect, rounding, stroke));
) -> ShapeIdx {
self.add(RectShape::stroke(rect, rounding, stroke))
}

/// Show an arrow starting at `origin` and going in the direction of `vec`, with the length `vec.length()`.
Expand Down Expand Up @@ -366,8 +371,14 @@ impl Painter {
/// .paint_at(ui, rect);
/// # });
/// ```
pub fn image(&self, texture_id: epaint::TextureId, rect: Rect, uv: Rect, tint: Color32) {
self.add(Shape::image(texture_id, rect, uv, tint));
pub fn image(
&self,
texture_id: epaint::TextureId,
rect: Rect,
uv: Rect,
tint: Color32,
) -> ShapeIdx {
self.add(Shape::image(texture_id, rect, uv, tint))
}
}

Expand Down
16 changes: 15 additions & 1 deletion crates/egui/src/style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,12 @@ pub struct Interaction {

/// Can you select the text on a [`crate::Label`] by default?
pub selectable_labels: bool,

/// Can the user select text that span multiple labels?
///
/// The default is `true`, but text seelction can be slightly glitchy,
/// so you may want to disable it.
pub multi_widget_text_select: bool,
}

/// Controls the visual style (colors etc) of egui.
Expand Down Expand Up @@ -1120,6 +1126,7 @@ impl Default for Interaction {
show_tooltips_only_when_still: true,
tooltip_delay: 0.0,
selectable_labels: true,
multi_widget_text_select: true,
}
}
}
Expand Down Expand Up @@ -1580,6 +1587,7 @@ impl Interaction {
show_tooltips_only_when_still,
tooltip_delay,
selectable_labels,
multi_widget_text_select,
} = self;
ui.add(Slider::new(resize_grab_radius_side, 0.0..=20.0).text("resize_grab_radius_side"));
ui.add(
Expand All @@ -1590,7 +1598,13 @@ impl Interaction {
"Only show tooltips if mouse is still",
);
ui.add(Slider::new(tooltip_delay, 0.0..=1.0).text("tooltip_delay"));
ui.checkbox(selectable_labels, "Selectable text in labels");

ui.horizontal(|ui| {
ui.checkbox(selectable_labels, "Selectable text in labels");
if *selectable_labels {
ui.checkbox(multi_widget_text_select, "Across multiple labels");
}
});

ui.vertical_centered(|ui| reset_button(ui, self));
}
Expand Down
6 changes: 3 additions & 3 deletions crates/egui/src/text_selection/cursor_range.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,10 +234,10 @@ impl CCursorRange {
}

#[inline]
pub fn two(min: CCursor, max: CCursor) -> Self {
pub fn two(min: impl Into<CCursor>, max: impl Into<CCursor>) -> Self {
Self {
primary: max,
secondary: min,
primary: max.into(),
secondary: min.into(),
}
}

Expand Down
Loading

0 comments on commit 41aad74

Please sign in to comment.