Skip to main content

egui/text_selection/
accesskit_text.rs

1use emath::TSTransform;
2use epaint::text::CharIndex;
3
4use crate::{Context, Galley, Id};
5
6use super::{CCursorRange, text_cursor_state::is_word_char};
7
8/// AccessKit's `word_starts` uses `u8` indices, so text runs cannot exceed this length.
9pub(crate) const MAX_CHARS_PER_TEXT_RUN: usize = 255;
10
11/// Convert a (row, column) layout cursor position to a text run node ID and character index,
12/// accounting for rows that are split into multiple text runs.
13fn text_run_position(parent_id: Id, row: usize, column: CharIndex) -> accesskit::TextPosition {
14    let column = column.0;
15    // When column lands exactly on a chunk boundary (e.g., 255), it refers to
16    // the end of the previous chunk, not the start of a new one.
17    let chunk_index = if column > 0 && column.is_multiple_of(MAX_CHARS_PER_TEXT_RUN) {
18        column / MAX_CHARS_PER_TEXT_RUN - 1
19    } else {
20        column / MAX_CHARS_PER_TEXT_RUN
21    };
22    let character_index = column - chunk_index * MAX_CHARS_PER_TEXT_RUN;
23    accesskit::TextPosition {
24        node: parent_id.with(row).with(chunk_index).accesskit_id(),
25        character_index,
26    }
27}
28
29/// Update accesskit with the current text state.
30pub fn update_accesskit_for_text_widget(
31    ctx: &Context,
32    widget_id: Id,
33    cursor_range: Option<CCursorRange>,
34    role: accesskit::Role,
35    global_from_galley: TSTransform,
36    galley: &Galley,
37) {
38    let parent_id = ctx.accesskit_node_builder(widget_id, |builder| {
39        let parent_id = widget_id;
40
41        if let Some(cursor_range) = &cursor_range {
42            let anchor = galley.layout_from_cursor(cursor_range.secondary);
43            let focus = galley.layout_from_cursor(cursor_range.primary);
44            builder.set_text_selection(accesskit::TextSelection {
45                anchor: text_run_position(parent_id, anchor.row, anchor.column),
46                focus: text_run_position(parent_id, focus.row, focus.column),
47            });
48        }
49
50        builder.set_role(role);
51
52        parent_id
53    });
54
55    let Some(parent_id) = parent_id else {
56        return;
57    };
58
59    let mut prev_row_ended_with_newline = true;
60
61    for (row_index, row) in galley.rows.iter().enumerate() {
62        let glyph_count = row.glyphs.len();
63        let mut value = String::with_capacity(glyph_count);
64        let mut character_lengths = Vec::<u8>::with_capacity(glyph_count);
65        let mut character_positions = Vec::<f32>::with_capacity(glyph_count);
66        let mut character_widths = Vec::<f32>::with_capacity(glyph_count);
67        let mut word_starts = Vec::<usize>::new();
68        // For soft-wrapped continuation rows, treat the start as a word
69        // boundary so the first word character gets a `word_starts` entry.
70        // Paragraph-starting runs (first row or after a newline) get an
71        // implicit word start from AccessKit, so they don't need this.
72        let mut was_at_word_end = !prev_row_ended_with_newline;
73
74        for glyph in &row.glyphs {
75            let is_word_char = is_word_char(glyph.chr);
76            if is_word_char && was_at_word_end {
77                word_starts.push(character_lengths.len());
78            }
79            was_at_word_end = !is_word_char;
80            let old_len = value.len();
81            value.push(glyph.chr);
82            character_lengths.push((value.len() - old_len) as _);
83            character_positions.push(glyph.pos.x - row.pos.x);
84            character_widths.push(glyph.advance_width);
85        }
86
87        if row.ends_with_newline {
88            value.push('\n');
89            character_lengths.push(1);
90            character_positions.push(row.size.x);
91            character_widths.push(0.0);
92        }
93
94        let total_chars = character_lengths.len();
95
96        if total_chars <= MAX_CHARS_PER_TEXT_RUN {
97            let run_id = parent_id.with(row_index).with(0usize);
98            ctx.register_accesskit_parent(run_id, parent_id);
99
100            ctx.accesskit_node_builder(run_id, |builder| {
101                builder.set_role(accesskit::Role::TextRun);
102                builder.set_text_direction(accesskit::TextDirection::LeftToRight);
103                // TODO(mwcampbell): Set more node fields for the row
104                // once AccessKit adapters expose text formatting info.
105
106                let rect = global_from_galley * row.rect_without_leading_space();
107                builder.set_bounds(accesskit::Rect {
108                    x0: rect.min.x.into(),
109                    y0: rect.min.y.into(),
110                    x1: rect.max.x.into(),
111                    y1: rect.max.y.into(),
112                });
113                builder.set_value(value);
114                builder.set_character_lengths(character_lengths);
115
116                let pos_offset = character_positions.first().copied().unwrap_or(0.0);
117                for p in &mut character_positions {
118                    *p -= pos_offset;
119                }
120                builder.set_character_positions(character_positions);
121                builder.set_character_widths(character_widths);
122
123                let chunk_word_starts: Vec<u8> = word_starts.iter().map(|&ws| ws as u8).collect();
124                builder.set_word_starts(chunk_word_starts);
125            });
126        } else {
127            let num_chunks = total_chars.div_ceil(MAX_CHARS_PER_TEXT_RUN);
128            let mut byte_offset = 0usize;
129
130            for chunk_idx in 0..num_chunks {
131                let char_start = chunk_idx * MAX_CHARS_PER_TEXT_RUN;
132                let char_end = (char_start + MAX_CHARS_PER_TEXT_RUN).min(total_chars);
133
134                let byte_start = byte_offset;
135                let chunk_byte_len: usize = character_lengths[char_start..char_end]
136                    .iter()
137                    .map(|&l| l as usize)
138                    .sum();
139                let byte_end = byte_start + chunk_byte_len;
140                byte_offset = byte_end;
141
142                let run_id = parent_id.with(row_index).with(chunk_idx);
143                ctx.register_accesskit_parent(run_id, parent_id);
144
145                ctx.accesskit_node_builder(run_id, |builder| {
146                    builder.set_role(accesskit::Role::TextRun);
147                    builder.set_text_direction(accesskit::TextDirection::LeftToRight);
148                    // TODO(mwcampbell): Set more node fields for the row
149                    // once AccessKit adapters expose text formatting info.
150
151                    if chunk_idx > 0 {
152                        let prev_id = parent_id.with(row_index).with(chunk_idx - 1);
153                        builder.set_previous_on_line(prev_id.accesskit_id());
154                    }
155                    if chunk_idx + 1 < num_chunks {
156                        let next_id = parent_id.with(row_index).with(chunk_idx + 1);
157                        builder.set_next_on_line(next_id.accesskit_id());
158                    }
159
160                    let row_rect = row.rect_without_leading_space();
161                    let chunk_x0 = row.pos.x + character_positions[char_start];
162                    let chunk_x1 = row.pos.x
163                        + character_positions[char_end - 1]
164                        + character_widths[char_end - 1];
165                    let chunk_rect = emath::Rect::from_min_max(
166                        emath::pos2(chunk_x0, row_rect.min.y),
167                        emath::pos2(chunk_x1, row_rect.max.y),
168                    );
169                    let rect = global_from_galley * chunk_rect;
170                    builder.set_bounds(accesskit::Rect {
171                        x0: rect.min.x.into(),
172                        y0: rect.min.y.into(),
173                        x1: rect.max.x.into(),
174                        y1: rect.max.y.into(),
175                    });
176                    builder.set_value(value[byte_start..byte_end].to_owned());
177                    builder.set_character_lengths(character_lengths[char_start..char_end].to_vec());
178
179                    let pos_offset = character_positions[char_start];
180                    let chunk_positions: Vec<f32> = character_positions[char_start..char_end]
181                        .iter()
182                        .map(|&p| p - pos_offset)
183                        .collect();
184                    builder.set_character_positions(chunk_positions);
185                    builder.set_character_widths(character_widths[char_start..char_end].to_vec());
186
187                    let chunk_word_starts: Vec<u8> = word_starts
188                        .iter()
189                        .filter(|&&ws| ws >= char_start && ws < char_end)
190                        .map(|&ws| (ws - char_start) as u8)
191                        .collect();
192                    builder.set_word_starts(chunk_word_starts);
193                });
194            }
195        }
196
197        prev_row_ended_with_newline = row.ends_with_newline;
198    }
199}