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 c80b72023d9..a2d78da5dea 100644 --- a/crates/egui/src/text_selection/label_text_selection.rs +++ b/crates/egui/src/text_selection/label_text_selection.rs @@ -1,49 +1,19 @@ +use std::sync::Arc; + use crate::{ layers::ShapeIdx, text::CCursor, text_selection::CCursorRange, Context, CursorIcon, Event, Galley, Id, LayerId, Pos2, Rect, Response, Ui, }; 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 const DEBUG: bool = false; // Don't merge `true`! -fn paint_selection( - ui: &Ui, - _response: &Response, - galley_pos: Pos2, - galley: &Galley, - cursor_state: &TextCursorState, - painted_shape_idx: &mut Vec, -) { - let cursor_range = cursor_state.range(galley); - - if let Some(cursor_range) = cursor_range { - // We paint the cursor on top of the text, in case - // the text galley has backgrounds (as e.g. `code` snippets in markup do). - paint_text_selection( - ui.painter(), - ui.visuals(), - galley_pos, - galley, - &cursor_range, - Some(painted_shape_idx), - ); - } - - #[cfg(feature = "accesskit")] - super::accesskit_text::update_accesskit_for_text_widget( - ui.ctx(), - _response.id, - cursor_range, - accesskit::Role::Label, - galley_pos, - galley, - ); -} - /// One end of a text selection, inside any widget. #[derive(Clone, Copy)] struct WidgetTextCursor { @@ -124,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 { @@ -139,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(), } } } @@ -182,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); } @@ -205,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; + } + } + } + } + } + }); } } }); @@ -292,11 +282,28 @@ impl LabelSelectionState { /// /// Make sure the widget senses clicks and drags. /// - /// This should be called after painting the text, because this will also - /// paint the text cursor/selection on top. - pub fn label_text_selection(ui: &Ui, response: &Response, galley_pos: Pos2, galley: &Galley) { + /// This also takes care of painting the galley. + pub fn label_text_selection( + ui: &Ui, + response: &Response, + galley_pos: Pos2, + 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()); } @@ -470,7 +477,14 @@ impl LabelSelectionState { } } - fn on_label(&mut self, ui: &Ui, response: &Response, galley_pos: Pos2, galley: &Galley) { + /// Returns the painted selections, 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 { @@ -576,14 +590,30 @@ impl LabelSelectionState { } } - paint_selection( - ui, - response, + 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 new_vertex_indices), + ); + } + + #[cfg(feature = "accesskit")] + super::accesskit_text::update_accesskit_for_text_widget( + ui.ctx(), + response.id, + cursor_range, + accesskit::Role::Label, galley_pos, galley, - &cursor_state, - &mut self.painted_shape_idx, ); + + new_vertex_indices } } diff --git a/crates/egui/src/text_selection/visuals.rs b/crates/egui/src/text_selection/visuals.rs index 252f727a650..499d501e052 100644 --- a/crates/egui/src/text_selection/visuals.rs +++ b/crates/egui/src/text_selection/visuals.rs @@ -1,29 +1,37 @@ -use crate::*; +use std::sync::Arc; -use self::layers::ShapeIdx; +use crate::*; 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( - painter: &Painter, + galley: &mut Arc, visuals: &Visuals, - galley_pos: Pos2, - galley: &Galley, cursor_range: &CursorRange, - mut out_shaped_idx: Option<&mut Vec>, + mut new_vertex_indices: Option<&mut Vec>, ) { if cursor_range.is_empty() { return; } - // We paint the cursor selection on top of the text, so make it transparent: - let color = visuals.selection.bg_fill.linear_multiply(0.5); + // We need to modify the galley (add text selection painting to it), + // and so we need to clone it if it is shared: + let galley: &mut Galley = Arc::make_mut(galley); + + let color = visuals.selection.bg_fill; let [min, max] = cursor_range.sorted_cursors(); let min = min.rcursor; let max = max.rcursor; for ri in min.row..=max.row { - let row = &galley.rows[ri]; + let row = &mut galley.rows[ri]; let left = if ri == min.row { row.x_offset(min.column) } else { @@ -39,13 +47,43 @@ pub fn paint_text_selection( }; row.rect.right() + newline_size }; - let rect = Rect::from_min_max( - galley_pos + vec2(left, row.min_y()), - galley_pos + vec2(right, row.max_y()), - ); - let shape_idx = painter.rect_filled(rect, 0.0, color); - if let Some(out_shaped_idx) = &mut out_shaped_idx { - out_shaped_idx.push(shape_idx); + + let rect = Rect::from_min_max(pos2(left, row.min_y()), pos2(right, row.max_y())); + let mesh = &mut row.visuals.mesh; + + // Time to insert the selection rectangle into the row mesh. + // It should be on top (after) of any background in the galley, + // but behind (before) any glyphs. The row visuals has this information: + let glyph_index_start = row.visuals.glyph_index_start; + + // Start by appending the selection rectangle to end of the mesh, as two triangles (= 6 indices): + let num_indices_before = mesh.indices.len(); + mesh.add_colored_rect(rect, color); + assert_eq!(num_indices_before + 6, mesh.indices.len()); + + // Copy out the new triangles: + let selection_triangles = [ + mesh.indices[num_indices_before], + mesh.indices[num_indices_before + 1], + mesh.indices[num_indices_before + 2], + mesh.indices[num_indices_before + 3], + mesh.indices[num_indices_before + 4], + mesh.indices[num_indices_before + 5], + ]; + + // Move every old triangle forwards by 6 indices to make room for the new triangle: + for i in (glyph_index_start..num_indices_before).rev() { + mesh.indices.swap(i, i + 6); + } + // Put the new triangle in place: + mesh.indices[glyph_index_start..glyph_index_start + 6] + .clone_from_slice(&selection_triangles); + + 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 038cc761fe1..59c84227707 100644 --- a/crates/egui/src/widgets/hyperlink.rs +++ b/crates/egui/src/widgets/hyperlink.rs @@ -50,13 +50,15 @@ impl Widget for Link { Stroke::NONE }; - ui.painter().add( - epaint::TextShape::new(galley_pos, galley.clone(), color).with_underline(underline), - ); - let selectable = ui.style().interaction.selectable_labels; if selectable { - LabelSelectionState::label_text_selection(ui, &response, galley_pos, &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), + ); } if response.hovered() { diff --git a/crates/egui/src/widgets/label.rs b/crates/egui/src/widgets/label.rs index 55e20136379..e8d146388d9 100644 --- a/crates/egui/src/widgets/label.rs +++ b/crates/egui/src/widgets/label.rs @@ -267,14 +267,21 @@ impl Widget for Label { Stroke::NONE }; - ui.painter().add( - epaint::TextShape::new(galley_pos, galley.clone(), response_color) - .with_underline(underline), - ); - let selectable = selectable.unwrap_or_else(|| ui.style().interaction.selectable_labels); if selectable { - LabelSelectionState::label_text_selection(ui, &response, galley_pos, &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), + ); } } diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 2a6eb14c6af..8763bd1a3ae 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -662,8 +662,6 @@ impl<'t> TextEdit<'t> { }; if ui.is_rect_visible(rect) { - painter.galley(galley_pos, galley.clone(), text_color); - if text.as_str().is_empty() && !hint_text.is_empty() { let hint_text_color = ui.visuals().weak_text_color(); let hint_text_font_id = hint_text_font.unwrap_or(font_id.into()); @@ -689,19 +687,19 @@ impl<'t> TextEdit<'t> { painter.galley(galley_pos, galley, hint_text_color); } - if ui.memory(|mem| mem.has_focus(id)) { + let has_focus = ui.memory(|mem| mem.has_focus(id)); + + if has_focus { if let Some(cursor_range) = state.cursor.range(&galley) { - // We paint the cursor on top of the text, in case - // the text galley has backgrounds (as e.g. `code` snippets in markup do). - paint_text_selection( - &painter, - ui.visuals(), - galley_pos, - &galley, - &cursor_range, - None, - ); + // Add text selection rectangles to the galley: + paint_text_selection(&mut galley, ui.visuals(), &cursor_range, None); + } + } + + painter.galley(galley_pos, galley.clone(), text_color); + if has_focus { + if let Some(cursor_range) = state.cursor.range(&galley) { let primary_cursor_rect = cursor_rect(galley_pos, &galley, &cursor_range.primary, row_height); @@ -721,7 +719,8 @@ impl<'t> TextEdit<'t> { // This is for two reasons: // * Don't give the impression that the user can type into a window without focus // * Don't repaint the ui because of a blinking cursor in an app that is not in focus - if ui.ctx().input(|i| i.focused) { + let viewport_has_focus = ui.ctx().input(|i| i.focused); + if viewport_has_focus { text_selection::visuals::paint_text_cursor( ui, &painter, diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 7f1cd865710..87be7cd0076 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -703,6 +703,7 @@ fn tessellate_row( add_row_backgrounds(job, row, &mut mesh); } + let glyph_index_start = mesh.indices.len(); let glyph_vertex_start = mesh.vertices.len(); tessellate_glyphs(point_scale, job, row, &mut mesh); let glyph_vertex_end = mesh.vertices.len(); @@ -730,6 +731,7 @@ fn tessellate_row( RowVisuals { mesh, mesh_bounds, + glyph_index_start, glyph_vertex_range: glyph_vertex_start..glyph_vertex_end, } } diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 7011e0c7f1a..108a2c2fb1a 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -554,6 +554,12 @@ pub struct RowVisuals { /// Does NOT include leading or trailing whitespace glyphs!! pub mesh_bounds: Rect, + /// The number of triangle indices added before the first glyph triangle. + /// + /// This can be used to insert more triangles after the background but before the glyphs, + /// i.e. for text selection visualization. + pub glyph_index_start: usize, + /// The range of vertices in the mesh that contain glyphs (as opposed to background, underlines, strikethorugh, etc). /// /// The glyph vertices comes after backgrounds (if any), but before any underlines and strikethrough. @@ -565,6 +571,7 @@ impl Default for RowVisuals { Self { mesh: Default::default(), mesh_bounds: Rect::NOTHING, + glyph_index_start: 0, glyph_vertex_range: 0..0, } }