egui/text_selection/
visuals.rs

1use std::sync::Arc;
2
3use crate::{Galley, Painter, Rect, Ui, Visuals, pos2, vec2};
4
5use super::CCursorRange;
6
7#[derive(Clone, Debug)]
8pub struct RowVertexIndices {
9    pub row: usize,
10    pub vertex_indices: [u32; 6],
11}
12
13/// Adds text selection rectangles to the galley.
14pub fn paint_text_selection(
15    galley: &mut Arc<Galley>,
16    visuals: &Visuals,
17    cursor_range: &CCursorRange,
18    mut new_vertex_indices: Option<&mut Vec<RowVertexIndices>>,
19) {
20    if cursor_range.is_empty() {
21        return;
22    }
23
24    // We need to modify the galley (add text selection painting to it),
25    // and so we need to clone it if it is shared:
26    let galley: &mut Galley = Arc::make_mut(galley);
27
28    let background_color = visuals.selection.bg_fill;
29    let text_color = visuals.selection.stroke.color;
30
31    let [min, max] = cursor_range.sorted_cursors();
32    let min = galley.layout_from_cursor(min);
33    let max = galley.layout_from_cursor(max);
34
35    for ri in min.row..=max.row {
36        let row = Arc::make_mut(&mut galley.rows[ri].row);
37
38        let left = if ri == min.row {
39            row.x_offset(min.column)
40        } else {
41            0.0
42        };
43        let right = if ri == max.row {
44            row.x_offset(max.column)
45        } else {
46            let newline_size = if row.ends_with_newline {
47                row.height() / 2.0 // visualize that we select the newline
48            } else {
49                0.0
50            };
51            row.size.x + newline_size
52        };
53
54        let rect = Rect::from_min_max(pos2(left, 0.0), pos2(right, row.size.y));
55        let mesh = &mut row.visuals.mesh;
56
57        if !row.glyphs.is_empty() {
58            // Change color of the selected text:
59            let first_glyph_index = if ri == min.row { min.column } else { 0 };
60            let last_glyph_index = if ri == max.row {
61                max.column
62            } else {
63                row.glyphs.len() - 1
64            };
65
66            let first_vertex_index = row
67                .glyphs
68                .get(first_glyph_index)
69                .map_or(row.visuals.glyph_vertex_range.start, |g| {
70                    g.first_vertex as _
71                });
72            let last_vertex_index = row
73                .glyphs
74                .get(last_glyph_index)
75                .map_or(row.visuals.glyph_vertex_range.end, |g| g.first_vertex as _);
76
77            for vi in first_vertex_index..last_vertex_index {
78                mesh.vertices[vi].color = text_color;
79            }
80        }
81
82        // Time to insert the selection rectangle into the row mesh.
83        // It should be on top (after) of any background in the galley,
84        // but behind (before) any glyphs. The row visuals has this information:
85        let glyph_index_start = row.visuals.glyph_index_start;
86
87        // Start by appending the selection rectangle to end of the mesh, as two triangles (= 6 indices):
88        let num_indices_before = mesh.indices.len();
89        mesh.add_colored_rect(rect, background_color);
90        assert_eq!(
91            num_indices_before + 6,
92            mesh.indices.len(),
93            "We expect exactly 6 new indices"
94        );
95
96        // Copy out the new triangles:
97        let selection_triangles = [
98            mesh.indices[num_indices_before],
99            mesh.indices[num_indices_before + 1],
100            mesh.indices[num_indices_before + 2],
101            mesh.indices[num_indices_before + 3],
102            mesh.indices[num_indices_before + 4],
103            mesh.indices[num_indices_before + 5],
104        ];
105
106        // Move every old triangle forwards by 6 indices to make room for the new triangle:
107        for i in (glyph_index_start..num_indices_before).rev() {
108            mesh.indices.swap(i, i + 6);
109        }
110        // Put the new triangle in place:
111        mesh.indices[glyph_index_start..glyph_index_start + 6]
112            .clone_from_slice(&selection_triangles);
113
114        row.visuals.mesh_bounds = mesh.calc_bounds();
115
116        if let Some(new_vertex_indices) = &mut new_vertex_indices {
117            new_vertex_indices.push(RowVertexIndices {
118                row: ri,
119                vertex_indices: selection_triangles,
120            });
121        }
122    }
123}
124
125/// Paint one end of the selection, e.g. the primary cursor.
126///
127/// This will never blink.
128pub fn paint_cursor_end(painter: &Painter, visuals: &Visuals, cursor_rect: Rect) {
129    let stroke = visuals.text_cursor.stroke;
130
131    let top = cursor_rect.center_top();
132    let bottom = cursor_rect.center_bottom();
133
134    painter.line_segment([top, bottom], (stroke.width, stroke.color));
135
136    if false {
137        // Roof/floor:
138        let extrusion = 3.0;
139        let width = 1.0;
140        painter.line_segment(
141            [top - vec2(extrusion, 0.0), top + vec2(extrusion, 0.0)],
142            (width, stroke.color),
143        );
144        painter.line_segment(
145            [bottom - vec2(extrusion, 0.0), bottom + vec2(extrusion, 0.0)],
146            (width, stroke.color),
147        );
148    }
149}
150
151/// Paint one end of the selection, e.g. the primary cursor, with blinking (if enabled).
152pub fn paint_text_cursor(
153    ui: &Ui,
154    painter: &Painter,
155    primary_cursor_rect: Rect,
156    time_since_last_interaction: f64,
157) {
158    if ui.visuals().text_cursor.blink {
159        let on_duration = ui.visuals().text_cursor.on_duration;
160        let off_duration = ui.visuals().text_cursor.off_duration;
161        let total_duration = on_duration + off_duration;
162
163        let time_in_cycle = (time_since_last_interaction % (total_duration as f64)) as f32;
164
165        let wake_in = if time_in_cycle < on_duration {
166            // Cursor is visible
167            paint_cursor_end(painter, ui.visuals(), primary_cursor_rect);
168            on_duration - time_in_cycle
169        } else {
170            // Cursor is not visible
171            total_duration - time_in_cycle
172        };
173
174        ui.ctx().request_repaint_after_secs(wake_in);
175    } else {
176        paint_cursor_end(painter, ui.visuals(), primary_cursor_rect);
177    }
178}