Skip to main content

egui/text_selection/
visuals.rs

1use std::sync::Arc;
2
3use emath::Pos2;
4use epaint::{
5    Stroke,
6    text::{
7        CharIndex,
8        cursor::{CCursor, LayoutCursor},
9    },
10};
11
12use crate::{
13    Galley, Painter, Rect, Ui, Visuals, pos2, text_selection::text_cursor_state::cursor_rect, vec2,
14};
15
16use super::CCursorRange;
17
18#[derive(Clone, Debug)]
19pub struct RowVertexIndices {
20    pub row: usize,
21    pub vertex_indices: [u32; 6],
22}
23
24/// Adds text selection rectangles to the galley.
25pub fn paint_text_selection(
26    galley: &mut Arc<Galley>,
27    visuals: &Visuals,
28    cursor_range: &CCursorRange,
29    mut new_vertex_indices: Option<&mut Vec<RowVertexIndices>>,
30) {
31    if cursor_range.is_empty() {
32        return;
33    }
34
35    // We need to modify the galley (add text selection painting to it),
36    // and so we need to clone it if it is shared:
37    let galley: &mut Galley = Arc::make_mut(galley);
38
39    let background_color = visuals.selection.bg_fill;
40    let text_color = visuals.selection.stroke.color;
41
42    let [min, max] = cursor_range.sorted_cursors();
43    let min = galley.layout_from_cursor(min);
44    let max = galley.layout_from_cursor(max);
45
46    for ri in min.row..=max.row {
47        let placed_row = &mut galley.rows[ri];
48        let row = Arc::make_mut(&mut placed_row.row);
49
50        let left = if ri == min.row {
51            row.x_offset(min.column)
52        } else {
53            0.0
54        };
55        let right = if ri == max.row {
56            row.x_offset(max.column)
57        } else {
58            let newline_size = if placed_row.ends_with_newline {
59                row.height() / 2.0 // visualize that we select the newline
60            } else {
61                0.0
62            };
63            row.size.x + newline_size
64        };
65
66        let rect = Rect::from_min_max(pos2(left, 0.0), pos2(right, row.size.y));
67        let mesh = &mut row.visuals.mesh;
68
69        if !row.glyphs.is_empty() {
70            // Change color of the selected text:
71            let first_glyph_index = if ri == min.row { min.column.0 } else { 0 };
72            let last_glyph_index = if ri == max.row {
73                max.column.0
74            } else {
75                row.glyphs.len()
76            };
77
78            let first_vertex_index = row
79                .glyphs
80                .get(first_glyph_index)
81                .map_or(row.visuals.glyph_vertex_range.end, |g| g.first_vertex as _);
82            let last_vertex_index = row
83                .glyphs
84                .get(last_glyph_index)
85                .map_or(row.visuals.glyph_vertex_range.end, |g| g.first_vertex as _);
86
87            for vi in first_vertex_index..last_vertex_index {
88                mesh.vertices[vi].color = text_color;
89            }
90        }
91
92        // Time to insert the selection rectangle into the row mesh.
93        // It should be on top (after) of any background in the galley,
94        // but behind (before) any glyphs. The row visuals has this information:
95        let glyph_index_start = row.visuals.glyph_index_start;
96
97        // Start by appending the selection rectangle to end of the mesh, as two triangles (= 6 indices):
98        let num_indices_before = mesh.indices.len();
99        mesh.add_colored_rect(rect, background_color);
100        assert_eq!(
101            num_indices_before + 6,
102            mesh.indices.len(),
103            "We expect exactly 6 new indices"
104        );
105
106        // Copy out the new triangles:
107        let selection_triangles = [
108            mesh.indices[num_indices_before],
109            mesh.indices[num_indices_before + 1],
110            mesh.indices[num_indices_before + 2],
111            mesh.indices[num_indices_before + 3],
112            mesh.indices[num_indices_before + 4],
113            mesh.indices[num_indices_before + 5],
114        ];
115
116        // Move every old triangle forwards by 6 indices to make room for the new triangle:
117        for i in (glyph_index_start..num_indices_before).rev() {
118            mesh.indices.swap(i, i + 6);
119        }
120        // Put the new triangle in place:
121        mesh.indices[glyph_index_start..glyph_index_start + 6]
122            .clone_from_slice(&selection_triangles);
123
124        row.visuals.mesh_bounds = mesh.calc_bounds();
125
126        if let Some(new_vertex_indices) = &mut new_vertex_indices {
127            new_vertex_indices.push(RowVertexIndices {
128                row: ri,
129                vertex_indices: selection_triangles,
130            });
131        }
132    }
133}
134
135#[expect(clippy::too_many_arguments)]
136pub(crate) fn paint_ime_preedit_text_visuals(
137    pos: Pos2,
138    ui: &Ui,
139    painter: &Painter,
140    galley: &Arc<Galley>,
141    row_height: f32,
142    preedit_range: core::ops::Range<CCursor>,
143    mut relative_active_range: Option<core::ops::Range<CCursor>>,
144    time_since_last_interaction: f64,
145) {
146    /// Instead of implementing [`PartialOrd`] and [`Ord`] for [`CCursor`] to
147    /// make [`std::ops::Range::is_empty`] available, we use this helper
148    /// function instead.
149    ///
150    /// These traits are intentionally not implemented because
151    /// [`CCursor::prefer_next_row`] makes it difficult to define a clear
152    /// ordering between two [`CCursor`]s.
153    fn is_cursor_range_empty(range: &core::ops::Range<CCursor>) -> bool {
154        range.start.index == range.end.index
155    }
156
157    if is_cursor_range_empty(&preedit_range) {
158        return;
159    }
160
161    if let Some(relative_active_range) = &mut relative_active_range
162        && relative_active_range.end.index > preedit_range.end.index - preedit_range.start.index
163    {
164        relative_active_range.end.index = preedit_range.end.index - preedit_range.start.index;
165    }
166
167    let visuals = ui.visuals();
168    let active_underline_stroke = visuals.ime_composition.active_underline_stroke;
169    let inactive_underline_stroke = visuals.ime_composition.inactive_underline_stroke;
170
171    if let Some(relative_active_range) = &relative_active_range
172        && !is_cursor_range_empty(relative_active_range)
173    {
174        if relative_active_range.start.index > CharIndex::ZERO {
175            paint_underlines(
176                pos,
177                painter,
178                galley,
179                galley.layout_from_cursor(preedit_range.start),
180                galley.layout_from_cursor(preedit_range.start + relative_active_range.start.index),
181                inactive_underline_stroke,
182            );
183        }
184
185        paint_underlines(
186            pos,
187            painter,
188            galley,
189            galley.layout_from_cursor(preedit_range.start + relative_active_range.start.index),
190            galley.layout_from_cursor(preedit_range.start + relative_active_range.end.index),
191            active_underline_stroke,
192        );
193
194        if !is_cursor_range_empty(
195            &(relative_active_range.end..(preedit_range.end - preedit_range.start.index)),
196        ) {
197            paint_underlines(
198                pos,
199                painter,
200                galley,
201                galley.layout_from_cursor(preedit_range.start + relative_active_range.end.index),
202                galley.layout_from_cursor(preedit_range.end),
203                inactive_underline_stroke,
204            );
205        }
206    } else {
207        paint_underlines(
208            pos,
209            painter,
210            galley,
211            galley.layout_from_cursor(preedit_range.start),
212            galley.layout_from_cursor(preedit_range.end),
213            inactive_underline_stroke,
214        );
215    }
216
217    if let Some(relative_active_range) = relative_active_range
218        && is_cursor_range_empty(&relative_active_range)
219    {
220        let active_cursor = preedit_range.start + relative_active_range.start.index;
221        let cursor_rect = cursor_rect(galley, &active_cursor, row_height);
222
223        paint_text_cursor(
224            ui,
225            painter,
226            cursor_rect.translate(pos.to_vec2()),
227            time_since_last_interaction,
228        );
229    }
230}
231
232fn paint_underlines(
233    pos: Pos2,
234    painter: &Painter,
235    galley: &Arc<Galley>,
236    min: LayoutCursor,
237    max: LayoutCursor,
238    stroke: Stroke,
239) {
240    for ri in min.row..=max.row {
241        let placed_row = &galley.rows[ri];
242        let row = &placed_row.row;
243
244        let left = if ri == min.row {
245            row.x_offset(min.column)
246        } else {
247            0.0
248        };
249        let right = if ri == max.row {
250            row.x_offset(max.column)
251        } else {
252            row.size.x
253        };
254
255        let offset_y = placed_row.pos.y + row.size.y;
256
257        painter.line_segment(
258            [pos + vec2(left, offset_y), pos + vec2(right, offset_y)],
259            stroke,
260        );
261    }
262}
263
264/// Paint one end of the selection, e.g. the primary cursor.
265///
266/// This will never blink.
267pub fn paint_cursor_end(painter: &Painter, visuals: &Visuals, cursor_rect: Rect) {
268    let stroke = visuals.text_cursor.stroke;
269
270    let top = cursor_rect.center_top();
271    let bottom = cursor_rect.center_bottom();
272
273    painter.line_segment([top, bottom], stroke);
274
275    if false {
276        // Roof/floor:
277        let extrusion = 3.0;
278        let width = 1.0;
279        painter.line_segment(
280            [top - vec2(extrusion, 0.0), top + vec2(extrusion, 0.0)],
281            (width, stroke.color),
282        );
283        painter.line_segment(
284            [bottom - vec2(extrusion, 0.0), bottom + vec2(extrusion, 0.0)],
285            (width, stroke.color),
286        );
287    }
288}
289
290/// Paint one end of the selection, e.g. the primary cursor, with blinking (if enabled).
291pub fn paint_text_cursor(
292    ui: &Ui,
293    painter: &Painter,
294    primary_cursor_rect: Rect,
295    time_since_last_interaction: f64,
296) {
297    if ui.visuals().text_cursor.blink {
298        let on_duration = ui.visuals().text_cursor.on_duration;
299        let off_duration = ui.visuals().text_cursor.off_duration;
300        let total_duration = on_duration + off_duration;
301
302        let time_in_cycle = (time_since_last_interaction % (total_duration as f64)) as f32;
303
304        let wake_in = if time_in_cycle < on_duration {
305            // Cursor is visible
306            paint_cursor_end(painter, ui.visuals(), primary_cursor_rect);
307            on_duration - time_in_cycle
308        } else {
309            // Cursor is not visible
310            total_duration - time_in_cycle
311        };
312
313        ui.request_repaint_after_secs(wake_in);
314    } else {
315        paint_cursor_end(painter, ui.visuals(), primary_cursor_rect);
316    }
317}