diff --git a/crates/egui/src/layers.rs b/crates/egui/src/layers.rs index 768099e23d6..6d5490082de 100644 --- a/crates/egui/src/layers.rs +++ b/crates/egui/src/layers.rs @@ -158,6 +158,11 @@ impl PaintList { self.0[idx.0].shape = Shape::Noop; } + /// Mutate the shape at the given index, if any. + pub fn mutate_shape(&mut self, idx: ShapeIdx, f: impl FnOnce(&mut ClippedShape)) { + self.0.get_mut(idx.0).map(f); + } + /// Transform each [`Shape`] and clip rectangle by this much, in-place pub fn transform(&mut self, transform: TSTransform) { for ClippedShape { clip_rect, shape } in &mut self.0 { diff --git a/crates/egui/src/text_selection/label_text_selection.rs b/crates/egui/src/text_selection/label_text_selection.rs index 470e700c5fd..e31feb381d7 100644 --- a/crates/egui/src/text_selection/label_text_selection.rs +++ b/crates/egui/src/text_selection/label_text_selection.rs @@ -6,7 +6,9 @@ use crate::{ }; use super::{ - text_cursor_state::cursor_rect, visuals::paint_text_selection, CursorRange, TextCursorState, + text_cursor_state::cursor_rect, + visuals::{paint_text_selection, RowVertexIndices}, + CursorRange, TextCursorState, }; /// Turn on to help debug this @@ -92,7 +94,9 @@ pub struct LabelSelectionState { last_copied_galley_rect: Option, /// Painted selections this frame. - painted_shape_idx: Vec, + /// + /// Kept so we can undo a bad selection visualization if we don't see both ends of the selection this frame. + painted_selections: Vec<(ShapeIdx, Vec)>, } impl Default for LabelSelectionState { @@ -107,7 +111,7 @@ impl Default for LabelSelectionState { has_reached_secondary: Default::default(), text_to_copy: Default::default(), last_copied_galley_rect: Default::default(), - painted_shape_idx: Default::default(), + painted_selections: Default::default(), } } } @@ -150,7 +154,7 @@ impl LabelSelectionState { state.has_reached_secondary = false; state.text_to_copy.clear(); state.last_copied_galley_rect = None; - state.painted_shape_idx.clear(); + state.painted_selections.clear(); state.store(ctx); } @@ -173,8 +177,26 @@ impl LabelSelectionState { // glitching by removing all painted selections: ctx.graphics_mut(|layers| { if let Some(list) = layers.get_mut(selection.layer_id) { - for shape_idx in state.painted_shape_idx.drain(..) { - list.reset_shape(shape_idx); + for (shape_idx, row_selections) in state.painted_selections.drain(..) { + list.mutate_shape(shape_idx, |shape| { + if let epaint::Shape::Text(text_shape) = &mut shape.shape { + let galley = Arc::make_mut(&mut text_shape.galley); + for row_selection in row_selections { + if let Some(row) = galley.rows.get_mut(row_selection.row) { + for vertex_index in row_selection.vertex_indices { + if let Some(vertex) = row + .visuals + .mesh + .vertices + .get_mut(vertex_index as usize) + { + vertex.color = epaint::Color32::TRANSPARENT; + } + } + } + } + } + }); } } }); @@ -260,16 +282,28 @@ impl LabelSelectionState { /// /// Make sure the widget senses clicks and drags. /// - /// This should be called before painting the text, because this will - /// add the text selection (if any) to the galley. + /// This also takes care of painting the galley. pub fn label_text_selection( ui: &Ui, response: &Response, galley_pos: Pos2, - galley: &mut Arc, + mut galley: Arc, + fallback_color: epaint::Color32, + underline: epaint::Stroke, ) { let mut state = Self::load(ui.ctx()); - state.on_label(ui, response, galley_pos, galley); + let new_vertex_indices = state.on_label(ui, response, galley_pos, &mut galley); + + let shape_idx = ui.painter().add( + epaint::TextShape::new(galley_pos, galley, fallback_color).with_underline(underline), + ); + + if !new_vertex_indices.is_empty() { + state + .painted_selections + .push((shape_idx, new_vertex_indices)); + } + state.store(ui.ctx()); } @@ -443,13 +477,14 @@ impl LabelSelectionState { } } + /// Returns indices of new vertices in the galley, if any. fn on_label( &mut self, ui: &Ui, response: &Response, galley_pos: Pos2, galley: &mut Arc, - ) { + ) -> Vec { let widget_id = response.id; if response.hovered { @@ -557,12 +592,14 @@ impl LabelSelectionState { let cursor_range = cursor_state.range(galley); + let mut new_vertex_indices = vec![]; + if let Some(cursor_range) = cursor_range { paint_text_selection( galley, ui.visuals(), &cursor_range, - Some(&mut self.painted_shape_idx), + Some(&mut new_vertex_indices), ); } @@ -575,6 +612,8 @@ impl LabelSelectionState { galley_pos, galley, ); + + new_vertex_indices } } diff --git a/crates/egui/src/text_selection/visuals.rs b/crates/egui/src/text_selection/visuals.rs index ec8f86fd340..499d501e052 100644 --- a/crates/egui/src/text_selection/visuals.rs +++ b/crates/egui/src/text_selection/visuals.rs @@ -2,16 +2,20 @@ use std::sync::Arc; use crate::*; -use self::layers::ShapeIdx; - use super::CursorRange; +#[derive(Clone, Debug)] +pub struct RowVertexIndices { + pub row: usize, + pub vertex_indices: [u32; 6], +} + /// Adds text selection rectangles to the galley. pub fn paint_text_selection( galley: &mut Arc, visuals: &Visuals, cursor_range: &CursorRange, - mut out_shaped_idx: Option<&mut Vec>, + mut new_vertex_indices: Option<&mut Vec>, ) { if cursor_range.is_empty() { return; @@ -75,8 +79,11 @@ pub fn paint_text_selection( mesh.indices[glyph_index_start..glyph_index_start + 6] .clone_from_slice(&selection_triangles); - if let Some(out_shaped_idx) = &mut out_shaped_idx { - // TODO + if let Some(new_vertex_indices) = &mut new_vertex_indices { + new_vertex_indices.push(RowVertexIndices { + row: ri, + vertex_indices: selection_triangles, + }); } } } diff --git a/crates/egui/src/widgets/hyperlink.rs b/crates/egui/src/widgets/hyperlink.rs index fad998b9a4b..59c84227707 100644 --- a/crates/egui/src/widgets/hyperlink.rs +++ b/crates/egui/src/widgets/hyperlink.rs @@ -36,7 +36,7 @@ impl Widget for Link { let Self { text } = self; let label = Label::new(text).sense(Sense::click()); - let (galley_pos, mut galley, response) = label.layout_in_ui(ui); + let (galley_pos, galley, response) = label.layout_in_ui(ui); response .widget_info(|| WidgetInfo::labeled(WidgetType::Link, ui.is_enabled(), galley.text())); @@ -52,12 +52,15 @@ impl Widget for Link { let selectable = ui.style().interaction.selectable_labels; if selectable { - LabelSelectionState::label_text_selection(ui, &response, galley_pos, &mut galley); + LabelSelectionState::label_text_selection( + ui, &response, galley_pos, galley, color, underline, + ); + } else { + ui.painter().add( + epaint::TextShape::new(galley_pos, galley, color).with_underline(underline), + ); } - ui.painter() - .add(epaint::TextShape::new(galley_pos, galley, color).with_underline(underline)); - if response.hovered() { ui.ctx().set_cursor_icon(CursorIcon::PointingHand); } diff --git a/crates/egui/src/widgets/label.rs b/crates/egui/src/widgets/label.rs index c90354ae592..e8d146388d9 100644 --- a/crates/egui/src/widgets/label.rs +++ b/crates/egui/src/widgets/label.rs @@ -245,7 +245,7 @@ impl Widget for Label { let selectable = self.selectable; - let (galley_pos, mut galley, mut response) = self.layout_in_ui(ui); + let (galley_pos, galley, mut response) = self.layout_in_ui(ui); response .widget_info(|| WidgetInfo::labeled(WidgetType::Label, ui.is_enabled(), galley.text())); @@ -269,13 +269,20 @@ impl Widget for Label { let selectable = selectable.unwrap_or_else(|| ui.style().interaction.selectable_labels); if selectable { - LabelSelectionState::label_text_selection(ui, &response, galley_pos, &mut galley); + LabelSelectionState::label_text_selection( + ui, + &response, + galley_pos, + galley, + response_color, + underline, + ); + } else { + ui.painter().add( + epaint::TextShape::new(galley_pos, galley, response_color) + .with_underline(underline), + ); } - - ui.painter().add( - epaint::TextShape::new(galley_pos, galley, response_color) - .with_underline(underline), - ); } response