1use crate::tree::{El, Kind};
32use crate::widgets::text_input::TextSelection;
33
34#[derive(Clone, Debug, Default, PartialEq, Eq)]
39pub struct Selection {
40 pub range: Option<SelectionRange>,
41}
42
43#[derive(Clone, Debug, PartialEq, Eq)]
47pub struct SelectionRange {
48 pub anchor: SelectionPoint,
49 pub head: SelectionPoint,
50}
51
52#[derive(Clone, Debug, PartialEq, Eq)]
57pub struct SelectionPoint {
58 pub key: String,
59 pub byte: usize,
60}
61
62impl SelectionPoint {
63 pub fn new(key: impl Into<String>, byte: usize) -> Self {
64 Self {
65 key: key.into(),
66 byte,
67 }
68 }
69}
70
71impl Selection {
72 pub fn caret(key: impl Into<String>, byte: usize) -> Self {
75 let pt = SelectionPoint::new(key, byte);
76 Self {
77 range: Some(SelectionRange {
78 anchor: pt.clone(),
79 head: pt,
80 }),
81 }
82 }
83
84 pub fn is_empty(&self) -> bool {
86 self.range.is_none()
87 }
88
89 pub fn is_within(&self, key: &str) -> bool {
93 match &self.range {
94 Some(r) => r.anchor.key == key && r.head.key == key,
95 None => false,
96 }
97 }
98
99 pub fn anchored_at(&self, key: &str) -> bool {
101 self.range.as_ref().is_some_and(|r| r.anchor.key == key)
102 }
103
104 pub fn within(&self, key: &str) -> Option<TextSelection> {
110 let r = self.range.as_ref()?;
111 if r.anchor.key == key && r.head.key == key {
112 Some(TextSelection {
113 anchor: r.anchor.byte,
114 head: r.head.byte,
115 })
116 } else {
117 None
118 }
119 }
120
121 pub fn set_within(&mut self, key: &str, sel: TextSelection) {
127 let Some(r) = self.range.as_mut() else { return };
128 if r.anchor.key == key && r.head.key == key {
129 r.anchor.byte = sel.anchor;
130 r.head.byte = sel.head;
131 }
132 }
133
134 pub fn clear(&mut self) {
136 self.range = None;
137 }
138}
139
140pub fn slice_for_leaf(
160 selection: &Selection,
161 order: &[crate::event::UiTarget],
162 key: &str,
163 text_len: usize,
164) -> Option<(usize, usize)> {
165 let r = selection.range.as_ref()?;
166 if r.anchor.key == r.head.key {
167 if r.anchor.key != key {
168 return None;
169 }
170 let (lo, hi) = (
171 r.anchor.byte.min(r.head.byte).min(text_len),
172 r.anchor.byte.max(r.head.byte).min(text_len),
173 );
174 return (lo < hi).then_some((lo, hi));
175 }
176 let pos = |k: &str| order.iter().position(|t| t.key == k);
177 let (a_idx, h_idx, key_idx) = (pos(&r.anchor.key)?, pos(&r.head.key)?, pos(key)?);
178 let (lo_idx, lo_byte, hi_idx, hi_byte) = if a_idx <= h_idx {
179 (a_idx, r.anchor.byte, h_idx, r.head.byte)
180 } else {
181 (h_idx, r.head.byte, a_idx, r.anchor.byte)
182 };
183 if key_idx < lo_idx || key_idx > hi_idx {
184 return None;
185 }
186 let lo = if key_idx == lo_idx {
187 lo_byte.min(text_len)
188 } else {
189 0
190 };
191 let hi = if key_idx == hi_idx {
192 hi_byte.min(text_len)
193 } else {
194 text_len
195 };
196 (lo < hi).then_some((lo, hi))
197}
198
199pub fn selected_text(tree: &El, selection: &Selection) -> Option<String> {
209 let r = selection.range.as_ref()?;
210 if r.anchor.key == r.head.key {
211 let value = find_keyed_text(tree, &r.anchor.key)?;
212 let lo = r.anchor.byte.min(r.head.byte).min(value.len());
213 let hi = r.anchor.byte.max(r.head.byte).min(value.len());
214 if lo >= hi {
215 return None;
216 }
217 return Some(value[lo..hi].to_string());
218 }
219 let mut leaves: Vec<(String, String)> = Vec::new();
221 collect_keyed_text_leaves(tree, &mut leaves);
222 let anchor_idx = leaves.iter().position(|(k, _)| *k == r.anchor.key)?;
223 let head_idx = leaves.iter().position(|(k, _)| *k == r.head.key)?;
224 let (lo_idx, lo_byte, hi_idx, hi_byte) = if anchor_idx <= head_idx {
225 (anchor_idx, r.anchor.byte, head_idx, r.head.byte)
226 } else {
227 (head_idx, r.head.byte, anchor_idx, r.anchor.byte)
228 };
229 let mut out = String::new();
230 for (i, (_, value)) in leaves
231 .iter()
232 .enumerate()
233 .skip(lo_idx)
234 .take(hi_idx - lo_idx + 1)
235 {
236 let start = if i == lo_idx {
237 lo_byte.min(value.len())
238 } else {
239 0
240 };
241 let end = if i == hi_idx {
242 hi_byte.min(value.len())
243 } else {
244 value.len()
245 };
246 if start >= end {
247 continue;
248 }
249 if !out.is_empty() {
250 out.push('\n');
251 }
252 out.push_str(&value[start..end]);
253 }
254 if out.is_empty() { None } else { Some(out) }
255}
256
257pub(crate) fn find_keyed_text(node: &El, key: &str) -> Option<String> {
258 if matches!(node.kind, Kind::Text | Kind::Heading)
259 && node.key.as_deref() == Some(key)
260 && let Some(t) = &node.text
261 {
262 return Some(t.clone());
263 }
264 node.children.iter().find_map(|c| find_keyed_text(c, key))
265}
266
267fn collect_keyed_text_leaves(node: &El, out: &mut Vec<(String, String)>) {
268 if matches!(node.kind, Kind::Text | Kind::Heading)
269 && let (Some(k), Some(t)) = (&node.key, &node.text)
270 {
271 out.push((k.clone(), t.clone()));
272 }
273 for c in &node.children {
274 collect_keyed_text_leaves(c, out);
275 }
276}
277
278pub fn word_range_at(text: &str, byte: usize) -> (usize, usize) {
290 if text.is_empty() {
291 return (0, 0);
292 }
293 let byte = clamp_to_char_boundary(text, byte.min(text.len()));
294 let probe = if byte == text.len() {
298 prev_char_boundary(text, byte)
299 } else {
300 byte
301 };
302 let probe_char = text[probe..].chars().next().unwrap_or(' ');
303 if !is_word_char(probe_char) {
304 return (probe, probe + probe_char.len_utf8());
308 }
309
310 let mut lo = probe;
312 while lo > 0 {
313 let p = prev_char_boundary(text, lo);
314 let ch = text[p..].chars().next().unwrap();
315 if !is_word_char(ch) {
316 break;
317 }
318 lo = p;
319 }
320 let mut hi = probe;
321 while hi < text.len() {
322 let ch = text[hi..].chars().next().unwrap();
323 if !is_word_char(ch) {
324 break;
325 }
326 hi += ch.len_utf8();
327 }
328 (lo, hi)
329}
330
331pub fn line_range_at(text: &str, byte: usize) -> (usize, usize) {
336 let byte = byte.min(text.len());
337 let lo = text[..byte].rfind('\n').map(|i| i + 1).unwrap_or(0);
338 let hi = text[byte..]
339 .find('\n')
340 .map(|i| byte + i)
341 .unwrap_or(text.len());
342 (lo, hi)
343}
344
345fn is_word_char(c: char) -> bool {
346 c.is_alphanumeric() || c == '_' || c == '\''
347}
348
349fn clamp_to_char_boundary(text: &str, byte: usize) -> usize {
350 let mut b = byte;
351 while b > 0 && !text.is_char_boundary(b) {
352 b -= 1;
353 }
354 b
355}
356
357fn prev_char_boundary(text: &str, byte: usize) -> usize {
358 let mut b = byte.saturating_sub(1);
359 while b > 0 && !text.is_char_boundary(b) {
360 b -= 1;
361 }
362 b
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368
369 #[test]
370 fn empty_selection_has_no_views() {
371 let sel = Selection::default();
372 assert!(sel.is_empty());
373 assert!(!sel.is_within("name"));
374 assert!(sel.within("name").is_none());
375 }
376
377 #[test]
378 fn caret_constructor_is_within_its_key() {
379 let sel = Selection::caret("name", 3);
380 assert!(!sel.is_empty());
381 assert!(sel.is_within("name"));
382 assert!(!sel.is_within("email"));
383 let view = sel.within("name").expect("within name");
384 assert_eq!(view, TextSelection::caret(3));
385 }
386
387 #[test]
388 fn within_returns_none_for_cross_element_selection() {
389 let sel = Selection {
390 range: Some(SelectionRange {
391 anchor: SelectionPoint::new("para_a", 0),
392 head: SelectionPoint::new("para_b", 5),
393 }),
394 };
395 assert!(sel.within("para_a").is_none());
397 assert!(sel.within("para_b").is_none());
398 assert!(sel.anchored_at("para_a"));
400 assert!(!sel.anchored_at("para_b"));
401 }
402
403 #[test]
404 fn set_within_writes_back_a_modified_slice() {
405 let mut sel = Selection::caret("name", 0);
406 let mut view = sel.within("name").expect("caret");
407 view.head = 5; sel.set_within("name", view);
409 let view_back = sel.within("name").expect("still within name");
410 assert_eq!(view_back, TextSelection::range(0, 5));
411 }
412
413 #[test]
414 fn set_within_is_a_noop_when_selection_is_not_in_key() {
415 let mut sel = Selection::caret("name", 0);
416 sel.set_within("email", TextSelection::range(0, 9));
417 assert_eq!(sel.within("name"), Some(TextSelection::caret(0)));
419 assert!(sel.within("email").is_none());
420 }
421
422 #[test]
423 fn selected_text_returns_single_leaf_substring() {
424 let tree = crate::widgets::text::text("Hello, world!").key("p");
425 let sel = Selection {
426 range: Some(SelectionRange {
427 anchor: SelectionPoint::new("p", 7),
428 head: SelectionPoint::new("p", 12),
429 }),
430 };
431 assert_eq!(selected_text(&tree, &sel).as_deref(), Some("world"));
432 }
433
434 #[test]
435 fn selected_text_walks_tree_order_for_cross_leaf_selection() {
436 let tree = crate::column([
437 crate::widgets::text::text("alpha").key("a"),
438 crate::widgets::text::text("bravo").key("b"),
439 crate::widgets::text::text("charlie").key("c"),
440 ]);
441 let sel = Selection {
445 range: Some(SelectionRange {
446 anchor: SelectionPoint::new("a", 2),
447 head: SelectionPoint::new("c", 4),
448 }),
449 };
450 assert_eq!(
451 selected_text(&tree, &sel).as_deref(),
452 Some("pha\nbravo\nchar")
453 );
454 }
455
456 #[test]
457 fn slice_for_leaf_single_leaf() {
458 let order = order_for(&["a", "b", "c"]);
459 let sel = Selection {
460 range: Some(SelectionRange {
461 anchor: SelectionPoint::new("b", 2),
462 head: SelectionPoint::new("b", 5),
463 }),
464 };
465 assert_eq!(slice_for_leaf(&sel, &order, "b", 10), Some((2, 5)));
466 assert_eq!(slice_for_leaf(&sel, &order, "a", 10), None);
467 assert_eq!(slice_for_leaf(&sel, &order, "c", 10), None);
468 }
469
470 #[test]
471 fn slice_for_leaf_cross_leaf_anchor_to_head_in_doc_order() {
472 let order = order_for(&["a", "b", "c"]);
474 let sel = Selection {
475 range: Some(SelectionRange {
476 anchor: SelectionPoint::new("a", 2),
477 head: SelectionPoint::new("c", 4),
478 }),
479 };
480 assert_eq!(
481 slice_for_leaf(&sel, &order, "a", 10),
482 Some((2, 10)),
483 "anchor leaf: from anchor.byte to text_len"
484 );
485 assert_eq!(
486 slice_for_leaf(&sel, &order, "b", 8),
487 Some((0, 8)),
488 "middle leaf: fully selected"
489 );
490 assert_eq!(
491 slice_for_leaf(&sel, &order, "c", 10),
492 Some((0, 4)),
493 "head leaf: from 0 to head.byte"
494 );
495 }
496
497 #[test]
498 fn slice_for_leaf_cross_leaf_reversed_drag() {
499 let order = order_for(&["a", "b", "c"]);
502 let sel = Selection {
503 range: Some(SelectionRange {
504 anchor: SelectionPoint::new("c", 3),
505 head: SelectionPoint::new("a", 1),
506 }),
507 };
508 assert_eq!(slice_for_leaf(&sel, &order, "a", 5), Some((1, 5)));
510 assert_eq!(slice_for_leaf(&sel, &order, "b", 6), Some((0, 6)));
511 assert_eq!(slice_for_leaf(&sel, &order, "c", 9), Some((0, 3)));
512 }
513
514 #[test]
515 fn slice_for_leaf_returns_none_for_leaves_outside_range() {
516 let order = order_for(&["a", "b", "c", "d", "e"]);
518 let sel = Selection {
519 range: Some(SelectionRange {
520 anchor: SelectionPoint::new("b", 0),
521 head: SelectionPoint::new("d", 0),
522 }),
523 };
524 assert_eq!(slice_for_leaf(&sel, &order, "a", 10), None);
525 assert_eq!(slice_for_leaf(&sel, &order, "e", 10), None);
526 assert_eq!(slice_for_leaf(&sel, &order, "b", 4), Some((0, 4)));
530 assert_eq!(slice_for_leaf(&sel, &order, "c", 7), Some((0, 7)));
531 assert_eq!(slice_for_leaf(&sel, &order, "d", 5), None);
532 }
533
534 fn order_for(keys: &[&str]) -> Vec<crate::event::UiTarget> {
535 keys.iter()
536 .map(|k| crate::event::UiTarget {
537 key: (*k).to_string(),
538 node_id: format!("root.{k}"),
539 rect: crate::tree::Rect::new(0.0, 0.0, 0.0, 0.0),
540 tooltip: None,
541 scroll_offset_y: 0.0,
542 })
543 .collect()
544 }
545
546 #[test]
547 fn selected_text_returns_none_for_empty_or_unknown_keys() {
548 let tree = crate::widgets::text::text("hi").key("p");
549 assert!(selected_text(&tree, &Selection::default()).is_none());
550 let unknown = Selection::caret("missing", 0);
551 assert!(selected_text(&tree, &unknown).is_none());
552 }
553
554 #[test]
555 fn word_range_at_picks_run_around_byte() {
556 let text = "Hello, world!";
557 assert_eq!(word_range_at(text, 0), (0, 5));
559 assert_eq!(word_range_at(text, 3), (0, 5));
561 assert_eq!(word_range_at(text, 5), (5, 6));
563 assert_eq!(word_range_at(text, 6), (6, 7));
565 assert_eq!(word_range_at(text, 7), (7, 12));
567 assert_eq!(word_range_at(text, 12), (12, 13));
569 }
570
571 #[test]
572 fn word_range_at_treats_apostrophe_and_underscore_as_word_chars() {
573 assert_eq!(word_range_at("don't stop", 2), (0, 5));
575 assert_eq!(word_range_at("foo_bar baz", 4), (0, 7));
577 }
578
579 #[test]
580 fn word_range_at_handles_end_of_text_and_empty() {
581 let text = "hello";
582 assert_eq!(word_range_at(text, 5), (0, 5));
584 assert_eq!(word_range_at("", 0), (0, 0));
586 }
587
588 #[test]
589 fn word_range_at_clamps_off_utf8_boundary() {
590 let text = "café";
593 let (lo, hi) = word_range_at(text, 1);
594 assert_eq!((lo, hi), (0, text.len()));
595 }
596
597 #[test]
598 fn line_range_at_returns_line_around_byte() {
599 let text = "first\nsecond line\nthird";
600 assert_eq!(line_range_at(text, 0), (0, 5));
602 assert_eq!(line_range_at(text, 3), (0, 5));
603 assert_eq!(line_range_at(text, 5), (0, 5));
604 assert_eq!(line_range_at(text, 6), (6, 17));
606 assert_eq!(line_range_at(text, 12), (6, 17));
607 assert_eq!(line_range_at(text, 17), (6, 17));
608 assert_eq!(line_range_at(text, 18), (18, 23));
610 assert_eq!(line_range_at(text, 23), (18, 23));
611 }
612
613 #[test]
614 fn line_range_at_handles_empty_and_single_line() {
615 assert_eq!(line_range_at("", 0), (0, 0));
616 assert_eq!(line_range_at("just one line", 4), (0, 13));
617 }
618}