egui/text_selection/
accesskit_text.rs1use emath::TSTransform;
2
3use crate::{Context, Galley, Id};
4
5use super::{CCursorRange, text_cursor_state::is_word_char};
6
7pub(crate) const MAX_CHARS_PER_TEXT_RUN: usize = 255;
9
10fn text_run_position(parent_id: Id, row: usize, column: usize) -> accesskit::TextPosition {
13 let chunk_index = if column > 0 && column.is_multiple_of(MAX_CHARS_PER_TEXT_RUN) {
16 column / MAX_CHARS_PER_TEXT_RUN - 1
17 } else {
18 column / MAX_CHARS_PER_TEXT_RUN
19 };
20 let character_index = column - chunk_index * MAX_CHARS_PER_TEXT_RUN;
21 accesskit::TextPosition {
22 node: parent_id.with(row).with(chunk_index).accesskit_id(),
23 character_index,
24 }
25}
26
27pub fn update_accesskit_for_text_widget(
29 ctx: &Context,
30 widget_id: Id,
31 cursor_range: Option<CCursorRange>,
32 role: accesskit::Role,
33 global_from_galley: TSTransform,
34 galley: &Galley,
35) {
36 let parent_id = ctx.accesskit_node_builder(widget_id, |builder| {
37 let parent_id = widget_id;
38
39 if let Some(cursor_range) = &cursor_range {
40 let anchor = galley.layout_from_cursor(cursor_range.secondary);
41 let focus = galley.layout_from_cursor(cursor_range.primary);
42 builder.set_text_selection(accesskit::TextSelection {
43 anchor: text_run_position(parent_id, anchor.row, anchor.column),
44 focus: text_run_position(parent_id, focus.row, focus.column),
45 });
46 }
47
48 builder.set_role(role);
49
50 parent_id
51 });
52
53 let Some(parent_id) = parent_id else {
54 return;
55 };
56
57 let mut prev_row_ended_with_newline = true;
58
59 for (row_index, row) in galley.rows.iter().enumerate() {
60 let glyph_count = row.glyphs.len();
61 let mut value = String::with_capacity(glyph_count);
62 let mut character_lengths = Vec::<u8>::with_capacity(glyph_count);
63 let mut character_positions = Vec::<f32>::with_capacity(glyph_count);
64 let mut character_widths = Vec::<f32>::with_capacity(glyph_count);
65 let mut word_starts = Vec::<usize>::new();
66 let mut was_at_word_end = !prev_row_ended_with_newline;
71
72 for glyph in &row.glyphs {
73 let is_word_char = is_word_char(glyph.chr);
74 if is_word_char && was_at_word_end {
75 word_starts.push(character_lengths.len());
76 }
77 was_at_word_end = !is_word_char;
78 let old_len = value.len();
79 value.push(glyph.chr);
80 character_lengths.push((value.len() - old_len) as _);
81 character_positions.push(glyph.pos.x - row.pos.x);
82 character_widths.push(glyph.advance_width);
83 }
84
85 if row.ends_with_newline {
86 value.push('\n');
87 character_lengths.push(1);
88 character_positions.push(row.size.x);
89 character_widths.push(0.0);
90 }
91
92 let total_chars = character_lengths.len();
93
94 if total_chars <= MAX_CHARS_PER_TEXT_RUN {
95 let run_id = parent_id.with(row_index).with(0usize);
96 ctx.register_accesskit_parent(run_id, parent_id);
97
98 ctx.accesskit_node_builder(run_id, |builder| {
99 builder.set_role(accesskit::Role::TextRun);
100 builder.set_text_direction(accesskit::TextDirection::LeftToRight);
101 let rect = global_from_galley * row.rect_without_leading_space();
105 builder.set_bounds(accesskit::Rect {
106 x0: rect.min.x.into(),
107 y0: rect.min.y.into(),
108 x1: rect.max.x.into(),
109 y1: rect.max.y.into(),
110 });
111 builder.set_value(value);
112 builder.set_character_lengths(character_lengths);
113
114 let pos_offset = character_positions.first().copied().unwrap_or(0.0);
115 for p in &mut character_positions {
116 *p -= pos_offset;
117 }
118 builder.set_character_positions(character_positions);
119 builder.set_character_widths(character_widths);
120
121 let chunk_word_starts: Vec<u8> = word_starts.iter().map(|&ws| ws as u8).collect();
122 builder.set_word_starts(chunk_word_starts);
123 });
124 } else {
125 let num_chunks = total_chars.div_ceil(MAX_CHARS_PER_TEXT_RUN);
126 let mut byte_offset = 0usize;
127
128 for chunk_idx in 0..num_chunks {
129 let char_start = chunk_idx * MAX_CHARS_PER_TEXT_RUN;
130 let char_end = (char_start + MAX_CHARS_PER_TEXT_RUN).min(total_chars);
131
132 let byte_start = byte_offset;
133 let chunk_byte_len: usize = character_lengths[char_start..char_end]
134 .iter()
135 .map(|&l| l as usize)
136 .sum();
137 let byte_end = byte_start + chunk_byte_len;
138 byte_offset = byte_end;
139
140 let run_id = parent_id.with(row_index).with(chunk_idx);
141 ctx.register_accesskit_parent(run_id, parent_id);
142
143 ctx.accesskit_node_builder(run_id, |builder| {
144 builder.set_role(accesskit::Role::TextRun);
145 builder.set_text_direction(accesskit::TextDirection::LeftToRight);
146 if chunk_idx > 0 {
150 let prev_id = parent_id.with(row_index).with(chunk_idx - 1);
151 builder.set_previous_on_line(prev_id.accesskit_id());
152 }
153 if chunk_idx + 1 < num_chunks {
154 let next_id = parent_id.with(row_index).with(chunk_idx + 1);
155 builder.set_next_on_line(next_id.accesskit_id());
156 }
157
158 let row_rect = row.rect_without_leading_space();
159 let chunk_x0 = row.pos.x + character_positions[char_start];
160 let chunk_x1 = row.pos.x
161 + character_positions[char_end - 1]
162 + character_widths[char_end - 1];
163 let chunk_rect = emath::Rect::from_min_max(
164 emath::pos2(chunk_x0, row_rect.min.y),
165 emath::pos2(chunk_x1, row_rect.max.y),
166 );
167 let rect = global_from_galley * chunk_rect;
168 builder.set_bounds(accesskit::Rect {
169 x0: rect.min.x.into(),
170 y0: rect.min.y.into(),
171 x1: rect.max.x.into(),
172 y1: rect.max.y.into(),
173 });
174 builder.set_value(value[byte_start..byte_end].to_owned());
175 builder.set_character_lengths(character_lengths[char_start..char_end].to_vec());
176
177 let pos_offset = character_positions[char_start];
178 let chunk_positions: Vec<f32> = character_positions[char_start..char_end]
179 .iter()
180 .map(|&p| p - pos_offset)
181 .collect();
182 builder.set_character_positions(chunk_positions);
183 builder.set_character_widths(character_widths[char_start..char_end].to_vec());
184
185 let chunk_word_starts: Vec<u8> = word_starts
186 .iter()
187 .filter(|&&ws| ws >= char_start && ws < char_end)
188 .map(|&ws| (ws - char_start) as u8)
189 .collect();
190 builder.set_word_starts(chunk_word_starts);
191 });
192 }
193 }
194
195 prev_row_ended_with_newline = row.ends_with_newline;
196 }
197}