1use std::ops::Range;
32
33use crate::tree::{El, Kind};
34use crate::widgets::text_input::TextSelection;
35
36#[derive(Clone, Debug, Default, PartialEq, Eq)]
41pub struct Selection {
42 pub range: Option<SelectionRange>,
43}
44
45#[derive(Clone, Debug, PartialEq, Eq)]
49pub struct SelectionRange {
50 pub anchor: SelectionPoint,
51 pub head: SelectionPoint,
52}
53
54#[derive(Clone, Debug, PartialEq, Eq)]
59pub struct SelectionPoint {
60 pub key: String,
61 pub byte: usize,
62}
63
64impl SelectionPoint {
65 pub fn new(key: impl Into<String>, byte: usize) -> Self {
66 Self {
67 key: key.into(),
68 byte,
69 }
70 }
71}
72
73#[derive(Clone, Debug, Default, PartialEq, Eq)]
83pub struct SelectionSource {
84 pub source: String,
85 pub visible: String,
86 pub spans: Vec<SelectionSourceSpan>,
87 pub full_selection_group: Option<String>,
88}
89
90#[derive(Clone, Debug, PartialEq, Eq)]
91pub struct SelectionSourceSpan {
92 pub visible: Range<usize>,
93 pub source: Range<usize>,
94 pub source_full: Range<usize>,
95 pub atomic: bool,
96}
97
98impl SelectionSource {
99 pub fn new(source: impl Into<String>, visible: impl Into<String>) -> Self {
100 Self {
101 source: source.into(),
102 visible: visible.into(),
103 spans: Vec::new(),
104 full_selection_group: None,
105 }
106 }
107
108 pub fn identity(text: impl Into<String>) -> Self {
109 let text = text.into();
110 let len = text.len();
111 Self {
112 source: text.clone(),
113 visible: text,
114 spans: vec![SelectionSourceSpan {
115 visible: 0..len,
116 source: 0..len,
117 source_full: 0..len,
118 atomic: false,
119 }],
120 full_selection_group: None,
121 }
122 }
123
124 pub fn full_selection_group(mut self, group: impl Into<String>) -> Self {
125 self.full_selection_group = Some(group.into());
126 self
127 }
128
129 pub fn push_span(&mut self, visible: Range<usize>, source: Range<usize>, atomic: bool) {
130 self.push_span_with_full_source(visible, source.clone(), source, atomic);
131 }
132
133 pub fn push_span_with_full_source(
134 &mut self,
135 visible: Range<usize>,
136 source: Range<usize>,
137 source_full: Range<usize>,
138 atomic: bool,
139 ) {
140 if visible.start <= visible.end
141 && visible.end <= self.visible.len()
142 && source.start <= source.end
143 && source.end <= self.source.len()
144 && source_full.start <= source_full.end
145 && source_full.end <= self.source.len()
146 {
147 self.spans.push(SelectionSourceSpan {
148 visible,
149 source,
150 source_full,
151 atomic,
152 });
153 }
154 }
155
156 pub fn visible_len(&self) -> usize {
157 self.visible.len()
158 }
159
160 pub fn source_slice_for_visible(&self, a: usize, b: usize) -> Option<&str> {
161 let (a, b) = (a.min(b), a.max(b));
162 if a == 0 && b >= self.visible.len() && !self.source.is_empty() {
163 return Some(&self.source);
164 }
165 let a = clamp_to_char_boundary(&self.visible, a.min(self.visible.len()));
166 let b = clamp_to_char_boundary(&self.visible, b.min(self.visible.len()));
167 let lo = self.source_offset_for_visible(a, Bias::Start)?;
168 let hi = self.source_offset_for_visible(b, Bias::End)?;
169 let (lo, hi) = (lo.min(hi), lo.max(hi));
170 let lo = clamp_to_char_boundary(&self.source, lo.min(self.source.len()));
171 let hi = clamp_to_char_boundary(&self.source, hi.min(self.source.len()));
172 (lo < hi).then(|| &self.source[lo..hi])
173 }
174
175 pub fn source_text_for_visible(&self, a: usize, b: usize) -> Option<String> {
176 let (a, b) = (a.min(b), a.max(b));
177 if a == 0 && b >= self.visible.len() && !self.source.is_empty() {
178 return Some(self.source.clone());
179 }
180 let a = clamp_to_char_boundary(&self.visible, a.min(self.visible.len()));
181 let b = clamp_to_char_boundary(&self.visible, b.min(self.visible.len()));
182 if a >= b {
183 return None;
184 }
185 if self.spans.is_empty() {
186 return self.source_slice_for_visible(a, b).map(str::to_string);
187 }
188
189 let mut out = String::new();
190 for span in &self.spans {
191 let start = a.max(span.visible.start);
192 let end = b.min(span.visible.end);
193 if start >= end {
194 continue;
195 }
196 if span.atomic || (start == span.visible.start && end == span.visible.end) {
197 out.push_str(&self.source[span.source_full.clone()]);
198 continue;
199 }
200 let lo = source_offset_in_span(span, start, Bias::Start)?;
201 let hi = source_offset_in_span(span, end, Bias::End)?;
202 let (lo, hi) = (lo.min(hi), lo.max(hi));
203 let lo = clamp_to_char_boundary(&self.source, lo.min(self.source.len()));
204 let hi = clamp_to_char_boundary(&self.source, hi.min(self.source.len()));
205 if lo < hi {
206 out.push_str(&self.source[lo..hi]);
207 }
208 }
209 if out.is_empty() { None } else { Some(out) }
210 }
211
212 fn full_group_for_visible(&self, start: usize, end: usize) -> Option<&str> {
213 (start == 0 && end >= self.visible.len())
214 .then_some(self.full_selection_group.as_deref())
215 .flatten()
216 }
217
218 fn source_offset_for_visible(&self, byte: usize, bias: Bias) -> Option<usize> {
219 if self.spans.is_empty() {
220 return Some(byte.min(self.source.len()));
221 }
222 for span in &self.spans {
223 if byte < span.visible.start || byte > span.visible.end {
224 continue;
225 }
226 if byte == span.visible.end && byte != span.visible.start && matches!(bias, Bias::Start)
227 {
228 continue;
229 }
230 if span.atomic {
231 return Some(match bias {
232 Bias::Start => span.source.start,
233 Bias::End => span.source.end,
234 });
235 }
236 let visible_len = span.visible.end.saturating_sub(span.visible.start);
237 let source_len = span.source.end.saturating_sub(span.source.start);
238 if visible_len == 0 {
239 return Some(match bias {
240 Bias::Start => span.source.start,
241 Bias::End => span.source.end,
242 });
243 }
244 let offset = byte.saturating_sub(span.visible.start).min(visible_len);
245 let mapped = if source_len == visible_len {
246 span.source.start + offset
247 } else {
248 span.source.start
249 + ((offset as f32 / visible_len as f32) * source_len as f32) as usize
250 };
251 return Some(mapped.min(span.source.end));
252 }
253 let first = self.spans.first()?;
254 if byte <= first.visible.start {
255 return Some(first.source.start);
256 }
257 let last = self.spans.last()?;
258 if byte >= last.visible.end {
259 return Some(last.source.end);
260 }
261 self.spans
262 .windows(2)
263 .find(|pair| byte > pair[0].visible.end && byte < pair[1].visible.start)
264 .map(|pair| match bias {
265 Bias::Start => pair[0].source.end,
266 Bias::End => pair[1].source.start,
267 })
268 }
269}
270
271fn source_offset_in_span(span: &SelectionSourceSpan, byte: usize, bias: Bias) -> Option<usize> {
272 if span.atomic {
273 return Some(match bias {
274 Bias::Start => span.source_full.start,
275 Bias::End => span.source_full.end,
276 });
277 }
278 let visible_len = span.visible.end.saturating_sub(span.visible.start);
279 let source_len = span.source.end.saturating_sub(span.source.start);
280 if visible_len == 0 {
281 return Some(match bias {
282 Bias::Start => span.source.start,
283 Bias::End => span.source.end,
284 });
285 }
286 let offset = byte.saturating_sub(span.visible.start).min(visible_len);
287 let mapped = if source_len == visible_len {
288 span.source.start + offset
289 } else {
290 span.source.start + ((offset as f32 / visible_len as f32) * source_len as f32) as usize
291 };
292 Some(mapped.min(span.source.end))
293}
294
295#[derive(Clone, Copy)]
296enum Bias {
297 Start,
298 End,
299}
300
301impl Selection {
302 pub fn caret(key: impl Into<String>, byte: usize) -> Self {
305 let pt = SelectionPoint::new(key, byte);
306 Self {
307 range: Some(SelectionRange {
308 anchor: pt.clone(),
309 head: pt,
310 }),
311 }
312 }
313
314 pub fn is_empty(&self) -> bool {
316 self.range.is_none()
317 }
318
319 pub fn is_within(&self, key: &str) -> bool {
323 match &self.range {
324 Some(r) => r.anchor.key == key && r.head.key == key,
325 None => false,
326 }
327 }
328
329 pub fn anchored_at(&self, key: &str) -> bool {
331 self.range.as_ref().is_some_and(|r| r.anchor.key == key)
332 }
333
334 pub fn within(&self, key: &str) -> Option<TextSelection> {
340 let r = self.range.as_ref()?;
341 if r.anchor.key == key && r.head.key == key {
342 Some(TextSelection {
343 anchor: r.anchor.byte,
344 head: r.head.byte,
345 })
346 } else {
347 None
348 }
349 }
350
351 pub fn set_within(&mut self, key: &str, sel: TextSelection) {
357 let Some(r) = self.range.as_mut() else { return };
358 if r.anchor.key == key && r.head.key == key {
359 r.anchor.byte = sel.anchor;
360 r.head.byte = sel.head;
361 }
362 }
363
364 pub fn clear(&mut self) {
366 self.range = None;
367 }
368}
369
370pub fn slice_for_leaf(
390 selection: &Selection,
391 order: &[crate::event::UiTarget],
392 key: &str,
393 text_len: usize,
394) -> Option<(usize, usize)> {
395 let r = selection.range.as_ref()?;
396 if r.anchor.key == r.head.key {
397 if r.anchor.key != key {
398 return None;
399 }
400 let (lo, hi) = (
401 r.anchor.byte.min(r.head.byte).min(text_len),
402 r.anchor.byte.max(r.head.byte).min(text_len),
403 );
404 return (lo < hi).then_some((lo, hi));
405 }
406 let pos = |k: &str| order.iter().position(|t| t.key == k);
407 let (a_idx, h_idx, key_idx) = (pos(&r.anchor.key)?, pos(&r.head.key)?, pos(key)?);
408 let (lo_idx, lo_byte, hi_idx, hi_byte) = if a_idx <= h_idx {
409 (a_idx, r.anchor.byte, h_idx, r.head.byte)
410 } else {
411 (h_idx, r.head.byte, a_idx, r.anchor.byte)
412 };
413 if key_idx < lo_idx || key_idx > hi_idx {
414 return None;
415 }
416 let lo = if key_idx == lo_idx {
417 lo_byte.min(text_len)
418 } else {
419 0
420 };
421 let hi = if key_idx == hi_idx {
422 hi_byte.min(text_len)
423 } else {
424 text_len
425 };
426 (lo < hi).then_some((lo, hi))
427}
428
429pub fn selected_text(tree: &El, selection: &Selection) -> Option<String> {
439 let r = selection.range.as_ref()?;
440 if r.anchor.key == r.head.key {
441 if let Some(source) = find_keyed_selection_source(tree, &r.anchor.key) {
442 let lo = r.anchor.byte.min(r.head.byte);
443 let hi = r.anchor.byte.max(r.head.byte);
444 return source.source_text_for_visible(lo, hi);
445 }
446 let value = find_keyed_text(tree, &r.anchor.key)?;
447 let lo = r.anchor.byte.min(r.head.byte).min(value.len());
448 let hi = r.anchor.byte.max(r.head.byte).min(value.len());
449 if lo >= hi {
450 return None;
451 }
452 return Some(value[lo..hi].to_string());
453 }
454 let mut leaves: Vec<(String, LeafSelectionText)> = Vec::new();
456 collect_keyed_selection_leaves(tree, &mut leaves);
457 let anchor_idx = leaves.iter().position(|(k, _)| *k == r.anchor.key)?;
458 let head_idx = leaves.iter().position(|(k, _)| *k == r.head.key)?;
459 let (lo_idx, lo_byte, hi_idx, hi_byte) = if anchor_idx <= head_idx {
460 (anchor_idx, r.anchor.byte, head_idx, r.head.byte)
461 } else {
462 (head_idx, r.head.byte, anchor_idx, r.anchor.byte)
463 };
464 let mut out = String::new();
465 let mut last_group: Option<String> = None;
466 for (i, (_, value)) in leaves
467 .iter()
468 .enumerate()
469 .skip(lo_idx)
470 .take(hi_idx - lo_idx + 1)
471 {
472 let start = if i == lo_idx {
473 lo_byte.min(value.visible_len())
474 } else {
475 0
476 };
477 let end = if i == hi_idx {
478 hi_byte.min(value.visible_len())
479 } else {
480 value.visible_len()
481 };
482 if start >= end {
483 continue;
484 }
485 let Some(slice) = value.source_text_for_visible(start, end) else {
486 continue;
487 };
488 let group = value.full_group_for_visible(start, end).map(str::to_string);
489 if group.is_some() && group == last_group {
490 continue;
491 }
492 if !out.is_empty() {
493 out.push('\n');
494 }
495 out.push_str(&slice);
496 last_group = group;
497 }
498 if out.is_empty() { None } else { Some(out) }
499}
500
501pub(crate) fn find_keyed_text(node: &El, key: &str) -> Option<String> {
502 if node.key.as_deref() == Some(key) {
503 if let Some(source) = &node.selection_source {
504 return Some(source.visible.clone());
505 }
506 if matches!(node.kind, Kind::Text | Kind::Heading)
507 && let Some(t) = &node.text
508 {
509 return Some(t.clone());
510 }
511 let mut out = String::new();
512 collect_text_content(node, &mut out);
513 if !out.is_empty() {
514 return Some(out);
515 }
516 }
517 node.children.iter().find_map(|c| find_keyed_text(c, key))
518}
519
520pub(crate) fn find_keyed_selection_source(node: &El, key: &str) -> Option<SelectionSource> {
521 if node.key.as_deref() == Some(key)
522 && let Some(source) = &node.selection_source
523 {
524 return Some(source.clone());
525 }
526 node.children
527 .iter()
528 .find_map(|c| find_keyed_selection_source(c, key))
529}
530
531fn collect_text_content(node: &El, out: &mut String) {
532 if matches!(node.kind, Kind::Text | Kind::Heading)
533 && let Some(t) = &node.text
534 {
535 out.push_str(t);
536 }
537 for c in &node.children {
538 collect_text_content(c, out);
539 }
540}
541
542enum LeafSelectionText {
543 Source(SelectionSource),
544 Text(String),
545}
546
547impl LeafSelectionText {
548 fn visible_len(&self) -> usize {
549 match self {
550 LeafSelectionText::Source(source) => source.visible_len(),
551 LeafSelectionText::Text(text) => text.len(),
552 }
553 }
554
555 fn source_text_for_visible(&self, start: usize, end: usize) -> Option<String> {
556 match self {
557 LeafSelectionText::Source(source) => source.source_text_for_visible(start, end),
558 LeafSelectionText::Text(text) => {
559 let start = start.min(text.len());
560 let end = end.min(text.len());
561 (start < end).then(|| text[start..end].to_string())
562 }
563 }
564 }
565
566 fn full_group_for_visible(&self, start: usize, end: usize) -> Option<&str> {
567 match self {
568 LeafSelectionText::Source(source) => source.full_group_for_visible(start, end),
569 LeafSelectionText::Text(_) => None,
570 }
571 }
572}
573
574fn collect_keyed_selection_leaves(node: &El, out: &mut Vec<(String, LeafSelectionText)>) {
575 if let (Some(k), Some(source)) = (&node.key, &node.selection_source) {
576 out.push((k.clone(), LeafSelectionText::Source(source.clone())));
577 return;
578 }
579 if matches!(node.kind, Kind::Text | Kind::Heading)
580 && let (Some(k), Some(t)) = (&node.key, &node.text)
581 {
582 out.push((k.clone(), LeafSelectionText::Text(t.clone())));
583 }
584 for c in &node.children {
585 collect_keyed_selection_leaves(c, out);
586 }
587}
588
589pub fn word_range_at(text: &str, byte: usize) -> (usize, usize) {
601 if text.is_empty() {
602 return (0, 0);
603 }
604 let byte = clamp_to_char_boundary(text, byte.min(text.len()));
605 let probe = if byte == text.len() {
609 prev_char_boundary(text, byte)
610 } else {
611 byte
612 };
613 let probe_char = text[probe..].chars().next().unwrap_or(' ');
614 if !is_word_char(probe_char) {
615 return (probe, probe + probe_char.len_utf8());
619 }
620
621 let mut lo = probe;
623 while lo > 0 {
624 let p = prev_char_boundary(text, lo);
625 let ch = text[p..].chars().next().unwrap();
626 if !is_word_char(ch) {
627 break;
628 }
629 lo = p;
630 }
631 let mut hi = probe;
632 while hi < text.len() {
633 let ch = text[hi..].chars().next().unwrap();
634 if !is_word_char(ch) {
635 break;
636 }
637 hi += ch.len_utf8();
638 }
639 (lo, hi)
640}
641
642pub fn line_range_at(text: &str, byte: usize) -> (usize, usize) {
647 let byte = byte.min(text.len());
648 let lo = text[..byte].rfind('\n').map(|i| i + 1).unwrap_or(0);
649 let hi = text[byte..]
650 .find('\n')
651 .map(|i| byte + i)
652 .unwrap_or(text.len());
653 (lo, hi)
654}
655
656fn is_word_char(c: char) -> bool {
657 c.is_alphanumeric() || c == '_' || c == '\''
658}
659
660fn clamp_to_char_boundary(text: &str, byte: usize) -> usize {
661 let mut b = byte;
662 while b > 0 && !text.is_char_boundary(b) {
663 b -= 1;
664 }
665 b
666}
667
668fn prev_char_boundary(text: &str, byte: usize) -> usize {
669 let mut b = byte.saturating_sub(1);
670 while b > 0 && !text.is_char_boundary(b) {
671 b -= 1;
672 }
673 b
674}
675
676#[cfg(test)]
677mod tests {
678 use super::*;
679
680 #[test]
681 fn empty_selection_has_no_views() {
682 let sel = Selection::default();
683 assert!(sel.is_empty());
684 assert!(!sel.is_within("name"));
685 assert!(sel.within("name").is_none());
686 }
687
688 #[test]
689 fn caret_constructor_is_within_its_key() {
690 let sel = Selection::caret("name", 3);
691 assert!(!sel.is_empty());
692 assert!(sel.is_within("name"));
693 assert!(!sel.is_within("email"));
694 let view = sel.within("name").expect("within name");
695 assert_eq!(view, TextSelection::caret(3));
696 }
697
698 #[test]
699 fn within_returns_none_for_cross_element_selection() {
700 let sel = Selection {
701 range: Some(SelectionRange {
702 anchor: SelectionPoint::new("para_a", 0),
703 head: SelectionPoint::new("para_b", 5),
704 }),
705 };
706 assert!(sel.within("para_a").is_none());
708 assert!(sel.within("para_b").is_none());
709 assert!(sel.anchored_at("para_a"));
711 assert!(!sel.anchored_at("para_b"));
712 }
713
714 #[test]
715 fn set_within_writes_back_a_modified_slice() {
716 let mut sel = Selection::caret("name", 0);
717 let mut view = sel.within("name").expect("caret");
718 view.head = 5; sel.set_within("name", view);
720 let view_back = sel.within("name").expect("still within name");
721 assert_eq!(view_back, TextSelection::range(0, 5));
722 }
723
724 #[test]
725 fn set_within_is_a_noop_when_selection_is_not_in_key() {
726 let mut sel = Selection::caret("name", 0);
727 sel.set_within("email", TextSelection::range(0, 9));
728 assert_eq!(sel.within("name"), Some(TextSelection::caret(0)));
730 assert!(sel.within("email").is_none());
731 }
732
733 #[test]
734 fn selected_text_returns_single_leaf_substring() {
735 let tree = crate::widgets::text::text("Hello, world!").key("p");
736 let sel = Selection {
737 range: Some(SelectionRange {
738 anchor: SelectionPoint::new("p", 7),
739 head: SelectionPoint::new("p", 12),
740 }),
741 };
742 assert_eq!(selected_text(&tree, &sel).as_deref(), Some("world"));
743 }
744
745 #[test]
746 fn selected_text_reads_text_inside_keyed_composite_widget() {
747 let sel = Selection {
748 range: Some(SelectionRange {
749 anchor: SelectionPoint::new("name", 1),
750 head: SelectionPoint::new("name", 4),
751 }),
752 };
753 let tree = crate::widgets::text_input::text_input("hello", &sel, "name");
754 assert_eq!(selected_text(&tree, &sel).as_deref(), Some("ell"));
755 }
756
757 #[test]
758 fn selected_text_walks_tree_order_for_cross_leaf_selection() {
759 let tree = crate::column([
760 crate::widgets::text::text("alpha").key("a"),
761 crate::widgets::text::text("bravo").key("b"),
762 crate::widgets::text::text("charlie").key("c"),
763 ]);
764 let sel = Selection {
768 range: Some(SelectionRange {
769 anchor: SelectionPoint::new("a", 2),
770 head: SelectionPoint::new("c", 4),
771 }),
772 };
773 assert_eq!(
774 selected_text(&tree, &sel).as_deref(),
775 Some("pha\nbravo\nchar")
776 );
777 }
778
779 #[test]
780 fn selected_text_uses_source_payload_for_single_leaf() {
781 let mut source = SelectionSource::new("This is **bold**.", "This is bold.");
782 source.push_span(0..8, 0..8, false);
783 source.push_span_with_full_source(8..12, 10..14, 8..16, false);
784 source.push_span(12..13, 16..17, false);
785 let tree = crate::text_runs([crate::text("This is "), crate::text("bold").bold()])
786 .key("md:p")
787 .selectable()
788 .selection_source(source);
789
790 let inner_only = Selection {
791 range: Some(SelectionRange {
792 anchor: SelectionPoint::new("md:p", 8),
793 head: SelectionPoint::new("md:p", 12),
794 }),
795 };
796 assert_eq!(
797 selected_text(&tree, &inner_only).as_deref(),
798 Some("**bold**")
799 );
800
801 let partial_inner = Selection {
802 range: Some(SelectionRange {
803 anchor: SelectionPoint::new("md:p", 9),
804 head: SelectionPoint::new("md:p", 11),
805 }),
806 };
807 assert_eq!(selected_text(&tree, &partial_inner).as_deref(), Some("ol"));
808
809 let through_styled_span = Selection {
810 range: Some(SelectionRange {
811 anchor: SelectionPoint::new("md:p", 0),
812 head: SelectionPoint::new("md:p", 12),
813 }),
814 };
815 assert_eq!(
816 selected_text(&tree, &through_styled_span).as_deref(),
817 Some("This is **bold**")
818 );
819
820 let whole = Selection {
821 range: Some(SelectionRange {
822 anchor: SelectionPoint::new("md:p", 0),
823 head: SelectionPoint::new("md:p", 13),
824 }),
825 };
826 assert_eq!(
827 selected_text(&tree, &whole).as_deref(),
828 Some("This is **bold**.")
829 );
830 }
831
832 #[test]
833 fn selected_text_dedupes_adjacent_full_source_group_leaves() {
834 let mut first = SelectionSource::new("| **Ada** | dev |", "Ada");
835 first.push_span_with_full_source(0..3, 4..7, 0..17, false);
836 let first = first.full_selection_group("row:0");
837
838 let mut second = SelectionSource::new("| **Ada** | dev |", "dev");
839 second.push_span_with_full_source(0..3, 12..15, 0..17, false);
840 let second = second.full_selection_group("row:0");
841
842 let tree = crate::row([
843 crate::text("Ada")
844 .key("a")
845 .selectable()
846 .selection_source(first),
847 crate::text("dev")
848 .key("b")
849 .selectable()
850 .selection_source(second),
851 ]);
852 let sel = Selection {
853 range: Some(SelectionRange {
854 anchor: SelectionPoint::new("a", 0),
855 head: SelectionPoint::new("b", 3),
856 }),
857 };
858
859 assert_eq!(
860 selected_text(&tree, &sel).as_deref(),
861 Some("| **Ada** | dev |")
862 );
863 }
864
865 #[test]
866 fn slice_for_leaf_single_leaf() {
867 let order = order_for(&["a", "b", "c"]);
868 let sel = Selection {
869 range: Some(SelectionRange {
870 anchor: SelectionPoint::new("b", 2),
871 head: SelectionPoint::new("b", 5),
872 }),
873 };
874 assert_eq!(slice_for_leaf(&sel, &order, "b", 10), Some((2, 5)));
875 assert_eq!(slice_for_leaf(&sel, &order, "a", 10), None);
876 assert_eq!(slice_for_leaf(&sel, &order, "c", 10), None);
877 }
878
879 #[test]
880 fn slice_for_leaf_cross_leaf_anchor_to_head_in_doc_order() {
881 let order = order_for(&["a", "b", "c"]);
883 let sel = Selection {
884 range: Some(SelectionRange {
885 anchor: SelectionPoint::new("a", 2),
886 head: SelectionPoint::new("c", 4),
887 }),
888 };
889 assert_eq!(
890 slice_for_leaf(&sel, &order, "a", 10),
891 Some((2, 10)),
892 "anchor leaf: from anchor.byte to text_len"
893 );
894 assert_eq!(
895 slice_for_leaf(&sel, &order, "b", 8),
896 Some((0, 8)),
897 "middle leaf: fully selected"
898 );
899 assert_eq!(
900 slice_for_leaf(&sel, &order, "c", 10),
901 Some((0, 4)),
902 "head leaf: from 0 to head.byte"
903 );
904 }
905
906 #[test]
907 fn slice_for_leaf_cross_leaf_reversed_drag() {
908 let order = order_for(&["a", "b", "c"]);
911 let sel = Selection {
912 range: Some(SelectionRange {
913 anchor: SelectionPoint::new("c", 3),
914 head: SelectionPoint::new("a", 1),
915 }),
916 };
917 assert_eq!(slice_for_leaf(&sel, &order, "a", 5), Some((1, 5)));
919 assert_eq!(slice_for_leaf(&sel, &order, "b", 6), Some((0, 6)));
920 assert_eq!(slice_for_leaf(&sel, &order, "c", 9), Some((0, 3)));
921 }
922
923 #[test]
924 fn slice_for_leaf_returns_none_for_leaves_outside_range() {
925 let order = order_for(&["a", "b", "c", "d", "e"]);
927 let sel = Selection {
928 range: Some(SelectionRange {
929 anchor: SelectionPoint::new("b", 0),
930 head: SelectionPoint::new("d", 0),
931 }),
932 };
933 assert_eq!(slice_for_leaf(&sel, &order, "a", 10), None);
934 assert_eq!(slice_for_leaf(&sel, &order, "e", 10), None);
935 assert_eq!(slice_for_leaf(&sel, &order, "b", 4), Some((0, 4)));
939 assert_eq!(slice_for_leaf(&sel, &order, "c", 7), Some((0, 7)));
940 assert_eq!(slice_for_leaf(&sel, &order, "d", 5), None);
941 }
942
943 fn order_for(keys: &[&str]) -> Vec<crate::event::UiTarget> {
944 keys.iter()
945 .map(|k| crate::event::UiTarget {
946 key: (*k).to_string(),
947 node_id: format!("root.{k}"),
948 rect: crate::tree::Rect::new(0.0, 0.0, 0.0, 0.0),
949 tooltip: None,
950 scroll_offset_y: 0.0,
951 })
952 .collect()
953 }
954
955 #[test]
956 fn selected_text_returns_none_for_empty_or_unknown_keys() {
957 let tree = crate::widgets::text::text("hi").key("p");
958 assert!(selected_text(&tree, &Selection::default()).is_none());
959 let unknown = Selection::caret("missing", 0);
960 assert!(selected_text(&tree, &unknown).is_none());
961 }
962
963 #[test]
964 fn word_range_at_picks_run_around_byte() {
965 let text = "Hello, world!";
966 assert_eq!(word_range_at(text, 0), (0, 5));
968 assert_eq!(word_range_at(text, 3), (0, 5));
970 assert_eq!(word_range_at(text, 5), (5, 6));
972 assert_eq!(word_range_at(text, 6), (6, 7));
974 assert_eq!(word_range_at(text, 7), (7, 12));
976 assert_eq!(word_range_at(text, 12), (12, 13));
978 }
979
980 #[test]
981 fn word_range_at_treats_apostrophe_and_underscore_as_word_chars() {
982 assert_eq!(word_range_at("don't stop", 2), (0, 5));
984 assert_eq!(word_range_at("foo_bar baz", 4), (0, 7));
986 }
987
988 #[test]
989 fn word_range_at_handles_end_of_text_and_empty() {
990 let text = "hello";
991 assert_eq!(word_range_at(text, 5), (0, 5));
993 assert_eq!(word_range_at("", 0), (0, 0));
995 }
996
997 #[test]
998 fn word_range_at_clamps_off_utf8_boundary() {
999 let text = "café";
1002 let (lo, hi) = word_range_at(text, 1);
1003 assert_eq!((lo, hi), (0, text.len()));
1004 }
1005
1006 #[test]
1007 fn line_range_at_returns_line_around_byte() {
1008 let text = "first\nsecond line\nthird";
1009 assert_eq!(line_range_at(text, 0), (0, 5));
1011 assert_eq!(line_range_at(text, 3), (0, 5));
1012 assert_eq!(line_range_at(text, 5), (0, 5));
1013 assert_eq!(line_range_at(text, 6), (6, 17));
1015 assert_eq!(line_range_at(text, 12), (6, 17));
1016 assert_eq!(line_range_at(text, 17), (6, 17));
1017 assert_eq!(line_range_at(text, 18), (18, 23));
1019 assert_eq!(line_range_at(text, 23), (18, 23));
1020 }
1021
1022 #[test]
1023 fn line_range_at_handles_empty_and_single_line() {
1024 assert_eq!(line_range_at("", 0), (0, 0));
1025 assert_eq!(line_range_at("just one line", 4), (0, 13));
1026 }
1027}