azul_layout/text3/
selection.rs1use azul_core::selection::{CursorAffinity, GraphemeClusterId, SelectionRange, TextCursor};
6
7use crate::text3::cache::{PositionedItem, ShapedCluster, ShapedItem, UnifiedLayout};
8
9pub fn select_word_at_cursor(
14 cursor: &TextCursor,
15 layout: &UnifiedLayout,
16) -> Option<SelectionRange> {
17 let (item_idx, cluster) = find_cluster_at_cursor(cursor, layout)?;
19
20 let line_text = extract_line_text_at_item(item_idx, layout);
22 let cursor_byte_offset = cursor.cluster_id.start_byte_in_run as usize;
23
24 let (word_start, word_end) = find_word_boundaries(&line_text, cursor_byte_offset);
26
27 let start_cursor = TextCursor {
29 cluster_id: GraphemeClusterId {
30 source_run: cursor.cluster_id.source_run,
31 start_byte_in_run: word_start as u32,
32 },
33 affinity: CursorAffinity::Leading,
34 };
35
36 let end_cursor = TextCursor {
37 cluster_id: GraphemeClusterId {
38 source_run: cursor.cluster_id.source_run,
39 start_byte_in_run: word_end as u32,
40 },
41 affinity: CursorAffinity::Trailing,
42 };
43
44 Some(SelectionRange {
45 start: start_cursor,
46 end: end_cursor,
47 })
48}
49
50pub fn select_paragraph_at_cursor(
55 cursor: &TextCursor,
56 layout: &UnifiedLayout,
57) -> Option<SelectionRange> {
58 let (item_idx, _) = find_cluster_at_cursor(cursor, layout)?;
60 let item = &layout.items[item_idx];
61 let line_index = item.line_index;
62
63 let line_items: Vec<(usize, &PositionedItem)> = layout
65 .items
66 .iter()
67 .enumerate()
68 .filter(|(_, item)| item.line_index == line_index)
69 .collect();
70
71 if line_items.is_empty() {
72 return None;
73 }
74
75 let first_cluster = line_items
77 .iter()
78 .find_map(|(_, item)| item.item.as_cluster())?;
79
80 let last_cluster = line_items
81 .iter()
82 .rev()
83 .find_map(|(_, item)| item.item.as_cluster())?;
84
85 Some(SelectionRange {
87 start: TextCursor {
88 cluster_id: first_cluster.source_cluster_id,
89 affinity: CursorAffinity::Leading,
90 },
91 end: TextCursor {
92 cluster_id: last_cluster.source_cluster_id,
93 affinity: CursorAffinity::Trailing,
94 },
95 })
96}
97
98fn find_cluster_at_cursor<'a>(
102 cursor: &TextCursor,
103 layout: &'a UnifiedLayout,
104) -> Option<(usize, &'a ShapedCluster)> {
105 layout.items.iter().enumerate().find_map(|(idx, item)| {
106 if let ShapedItem::Cluster(cluster) = &item.item {
107 if cluster.source_cluster_id == cursor.cluster_id {
108 return Some((idx, cluster));
109 }
110 }
111 None
112 })
113}
114
115fn extract_line_text_at_item(item_idx: usize, layout: &UnifiedLayout) -> String {
117 let line_index = layout.items[item_idx].line_index;
118
119 let mut text = String::new();
120 for item in &layout.items {
121 if item.line_index != line_index {
122 continue;
123 }
124
125 if let ShapedItem::Cluster(cluster) = &item.item {
126 text.push_str(&cluster.text);
127 }
128 }
129
130 text
131}
132
133fn find_word_boundaries(text: &str, cursor_offset: usize) -> (usize, usize) {
138 let cursor_offset = cursor_offset.min(text.len());
140
141 let mut word_start = 0;
143 let mut char_indices: Vec<(usize, char)> = text.char_indices().collect();
144
145 for (i, (byte_idx, ch)) in char_indices.iter().enumerate().rev() {
146 if *byte_idx >= cursor_offset {
147 continue;
148 }
149
150 if !is_word_char(*ch) {
151 word_start = if i + 1 < char_indices.len() {
153 char_indices[i + 1].0
154 } else {
155 text.len()
156 };
157 break;
158 }
159 }
160
161 let mut word_end = text.len();
163 for (byte_idx, ch) in char_indices.iter() {
164 if *byte_idx <= cursor_offset {
165 continue;
166 }
167
168 if !is_word_char(*ch) {
169 word_end = *byte_idx;
171 break;
172 }
173 }
174
175 if let Some((_, ch)) = char_indices.iter().find(|(idx, _)| *idx == cursor_offset) {
177 if !is_word_char(*ch) {
178 let start = char_indices
180 .iter()
181 .rev()
182 .find(|(idx, c)| *idx < cursor_offset && is_word_char(*c))
183 .map(|(idx, c)| idx + c.len_utf8())
184 .unwrap_or(0);
185
186 let end = char_indices
187 .iter()
188 .find(|(idx, c)| *idx > cursor_offset && is_word_char(*c))
189 .map(|(idx, _)| *idx)
190 .unwrap_or(text.len());
191
192 return (start, end);
193 }
194 }
195
196 (word_start, word_end)
197}
198
199#[inline]
201fn is_word_char(ch: char) -> bool {
202 ch.is_alphanumeric() || ch == '_'
203}