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
660pub fn next_word_boundary(text: &str, byte: usize) -> usize {
670 let mut i = clamp_to_char_boundary(text, byte.min(text.len()));
671 while i < text.len() {
673 let ch = text[i..].chars().next().unwrap();
674 if is_word_char(ch) {
675 break;
676 }
677 i += ch.len_utf8();
678 }
679 while i < text.len() {
681 let ch = text[i..].chars().next().unwrap();
682 if !is_word_char(ch) {
683 break;
684 }
685 i += ch.len_utf8();
686 }
687 i
688}
689
690pub fn prev_word_boundary(text: &str, byte: usize) -> usize {
699 let mut i = clamp_to_char_boundary(text, byte.min(text.len()));
700 while i > 0 {
702 let p = prev_char_boundary(text, i);
703 let ch = text[p..].chars().next().unwrap();
704 if is_word_char(ch) {
705 break;
706 }
707 i = p;
708 }
709 while i > 0 {
711 let p = prev_char_boundary(text, i);
712 let ch = text[p..].chars().next().unwrap();
713 if !is_word_char(ch) {
714 break;
715 }
716 i = p;
717 }
718 i
719}
720
721fn clamp_to_char_boundary(text: &str, byte: usize) -> usize {
722 let mut b = byte;
723 while b > 0 && !text.is_char_boundary(b) {
724 b -= 1;
725 }
726 b
727}
728
729fn prev_char_boundary(text: &str, byte: usize) -> usize {
730 let mut b = byte.saturating_sub(1);
731 while b > 0 && !text.is_char_boundary(b) {
732 b -= 1;
733 }
734 b
735}
736
737#[cfg(test)]
738mod tests {
739 use super::*;
740
741 #[test]
742 fn empty_selection_has_no_views() {
743 let sel = Selection::default();
744 assert!(sel.is_empty());
745 assert!(!sel.is_within("name"));
746 assert!(sel.within("name").is_none());
747 }
748
749 #[test]
750 fn caret_constructor_is_within_its_key() {
751 let sel = Selection::caret("name", 3);
752 assert!(!sel.is_empty());
753 assert!(sel.is_within("name"));
754 assert!(!sel.is_within("email"));
755 let view = sel.within("name").expect("within name");
756 assert_eq!(view, TextSelection::caret(3));
757 }
758
759 #[test]
760 fn within_returns_none_for_cross_element_selection() {
761 let sel = Selection {
762 range: Some(SelectionRange {
763 anchor: SelectionPoint::new("para_a", 0),
764 head: SelectionPoint::new("para_b", 5),
765 }),
766 };
767 assert!(sel.within("para_a").is_none());
769 assert!(sel.within("para_b").is_none());
770 assert!(sel.anchored_at("para_a"));
772 assert!(!sel.anchored_at("para_b"));
773 }
774
775 #[test]
776 fn set_within_writes_back_a_modified_slice() {
777 let mut sel = Selection::caret("name", 0);
778 let mut view = sel.within("name").expect("caret");
779 view.head = 5; sel.set_within("name", view);
781 let view_back = sel.within("name").expect("still within name");
782 assert_eq!(view_back, TextSelection::range(0, 5));
783 }
784
785 #[test]
786 fn set_within_is_a_noop_when_selection_is_not_in_key() {
787 let mut sel = Selection::caret("name", 0);
788 sel.set_within("email", TextSelection::range(0, 9));
789 assert_eq!(sel.within("name"), Some(TextSelection::caret(0)));
791 assert!(sel.within("email").is_none());
792 }
793
794 #[test]
795 fn selected_text_returns_single_leaf_substring() {
796 let tree = crate::widgets::text::text("Hello, world!").key("p");
797 let sel = Selection {
798 range: Some(SelectionRange {
799 anchor: SelectionPoint::new("p", 7),
800 head: SelectionPoint::new("p", 12),
801 }),
802 };
803 assert_eq!(selected_text(&tree, &sel).as_deref(), Some("world"));
804 }
805
806 #[test]
807 fn selected_text_reads_text_inside_keyed_composite_widget() {
808 let sel = Selection {
809 range: Some(SelectionRange {
810 anchor: SelectionPoint::new("name", 1),
811 head: SelectionPoint::new("name", 4),
812 }),
813 };
814 let tree = crate::widgets::text_input::text_input("hello", &sel, "name");
815 assert_eq!(selected_text(&tree, &sel).as_deref(), Some("ell"));
816 }
817
818 #[test]
819 fn selected_text_walks_tree_order_for_cross_leaf_selection() {
820 let tree = crate::column([
821 crate::widgets::text::text("alpha").key("a"),
822 crate::widgets::text::text("bravo").key("b"),
823 crate::widgets::text::text("charlie").key("c"),
824 ]);
825 let sel = Selection {
829 range: Some(SelectionRange {
830 anchor: SelectionPoint::new("a", 2),
831 head: SelectionPoint::new("c", 4),
832 }),
833 };
834 assert_eq!(
835 selected_text(&tree, &sel).as_deref(),
836 Some("pha\nbravo\nchar")
837 );
838 }
839
840 #[test]
841 fn selected_text_uses_source_payload_for_single_leaf() {
842 let mut source = SelectionSource::new("This is **bold**.", "This is bold.");
843 source.push_span(0..8, 0..8, false);
844 source.push_span_with_full_source(8..12, 10..14, 8..16, false);
845 source.push_span(12..13, 16..17, false);
846 let tree = crate::text_runs([crate::text("This is "), crate::text("bold").bold()])
847 .key("md:p")
848 .selectable()
849 .selection_source(source);
850
851 let inner_only = Selection {
852 range: Some(SelectionRange {
853 anchor: SelectionPoint::new("md:p", 8),
854 head: SelectionPoint::new("md:p", 12),
855 }),
856 };
857 assert_eq!(
858 selected_text(&tree, &inner_only).as_deref(),
859 Some("**bold**")
860 );
861
862 let partial_inner = Selection {
863 range: Some(SelectionRange {
864 anchor: SelectionPoint::new("md:p", 9),
865 head: SelectionPoint::new("md:p", 11),
866 }),
867 };
868 assert_eq!(selected_text(&tree, &partial_inner).as_deref(), Some("ol"));
869
870 let through_styled_span = Selection {
871 range: Some(SelectionRange {
872 anchor: SelectionPoint::new("md:p", 0),
873 head: SelectionPoint::new("md:p", 12),
874 }),
875 };
876 assert_eq!(
877 selected_text(&tree, &through_styled_span).as_deref(),
878 Some("This is **bold**")
879 );
880
881 let whole = Selection {
882 range: Some(SelectionRange {
883 anchor: SelectionPoint::new("md:p", 0),
884 head: SelectionPoint::new("md:p", 13),
885 }),
886 };
887 assert_eq!(
888 selected_text(&tree, &whole).as_deref(),
889 Some("This is **bold**.")
890 );
891 }
892
893 #[test]
894 fn selected_text_dedupes_adjacent_full_source_group_leaves() {
895 let mut first = SelectionSource::new("| **Ada** | dev |", "Ada");
896 first.push_span_with_full_source(0..3, 4..7, 0..17, false);
897 let first = first.full_selection_group("row:0");
898
899 let mut second = SelectionSource::new("| **Ada** | dev |", "dev");
900 second.push_span_with_full_source(0..3, 12..15, 0..17, false);
901 let second = second.full_selection_group("row:0");
902
903 let tree = crate::row([
904 crate::text("Ada")
905 .key("a")
906 .selectable()
907 .selection_source(first),
908 crate::text("dev")
909 .key("b")
910 .selectable()
911 .selection_source(second),
912 ]);
913 let sel = Selection {
914 range: Some(SelectionRange {
915 anchor: SelectionPoint::new("a", 0),
916 head: SelectionPoint::new("b", 3),
917 }),
918 };
919
920 assert_eq!(
921 selected_text(&tree, &sel).as_deref(),
922 Some("| **Ada** | dev |")
923 );
924 }
925
926 #[test]
927 fn slice_for_leaf_single_leaf() {
928 let order = order_for(&["a", "b", "c"]);
929 let sel = Selection {
930 range: Some(SelectionRange {
931 anchor: SelectionPoint::new("b", 2),
932 head: SelectionPoint::new("b", 5),
933 }),
934 };
935 assert_eq!(slice_for_leaf(&sel, &order, "b", 10), Some((2, 5)));
936 assert_eq!(slice_for_leaf(&sel, &order, "a", 10), None);
937 assert_eq!(slice_for_leaf(&sel, &order, "c", 10), None);
938 }
939
940 #[test]
941 fn slice_for_leaf_cross_leaf_anchor_to_head_in_doc_order() {
942 let order = order_for(&["a", "b", "c"]);
944 let sel = Selection {
945 range: Some(SelectionRange {
946 anchor: SelectionPoint::new("a", 2),
947 head: SelectionPoint::new("c", 4),
948 }),
949 };
950 assert_eq!(
951 slice_for_leaf(&sel, &order, "a", 10),
952 Some((2, 10)),
953 "anchor leaf: from anchor.byte to text_len"
954 );
955 assert_eq!(
956 slice_for_leaf(&sel, &order, "b", 8),
957 Some((0, 8)),
958 "middle leaf: fully selected"
959 );
960 assert_eq!(
961 slice_for_leaf(&sel, &order, "c", 10),
962 Some((0, 4)),
963 "head leaf: from 0 to head.byte"
964 );
965 }
966
967 #[test]
968 fn slice_for_leaf_cross_leaf_reversed_drag() {
969 let order = order_for(&["a", "b", "c"]);
972 let sel = Selection {
973 range: Some(SelectionRange {
974 anchor: SelectionPoint::new("c", 3),
975 head: SelectionPoint::new("a", 1),
976 }),
977 };
978 assert_eq!(slice_for_leaf(&sel, &order, "a", 5), Some((1, 5)));
980 assert_eq!(slice_for_leaf(&sel, &order, "b", 6), Some((0, 6)));
981 assert_eq!(slice_for_leaf(&sel, &order, "c", 9), Some((0, 3)));
982 }
983
984 #[test]
985 fn slice_for_leaf_returns_none_for_leaves_outside_range() {
986 let order = order_for(&["a", "b", "c", "d", "e"]);
988 let sel = Selection {
989 range: Some(SelectionRange {
990 anchor: SelectionPoint::new("b", 0),
991 head: SelectionPoint::new("d", 0),
992 }),
993 };
994 assert_eq!(slice_for_leaf(&sel, &order, "a", 10), None);
995 assert_eq!(slice_for_leaf(&sel, &order, "e", 10), None);
996 assert_eq!(slice_for_leaf(&sel, &order, "b", 4), Some((0, 4)));
1000 assert_eq!(slice_for_leaf(&sel, &order, "c", 7), Some((0, 7)));
1001 assert_eq!(slice_for_leaf(&sel, &order, "d", 5), None);
1002 }
1003
1004 fn order_for(keys: &[&str]) -> Vec<crate::event::UiTarget> {
1005 keys.iter()
1006 .map(|k| crate::event::UiTarget {
1007 key: (*k).to_string(),
1008 node_id: format!("root.{k}"),
1009 rect: crate::tree::Rect::new(0.0, 0.0, 0.0, 0.0),
1010 tooltip: None,
1011 scroll_offset_y: 0.0,
1012 })
1013 .collect()
1014 }
1015
1016 #[test]
1017 fn selected_text_returns_none_for_empty_or_unknown_keys() {
1018 let tree = crate::widgets::text::text("hi").key("p");
1019 assert!(selected_text(&tree, &Selection::default()).is_none());
1020 let unknown = Selection::caret("missing", 0);
1021 assert!(selected_text(&tree, &unknown).is_none());
1022 }
1023
1024 #[test]
1025 fn word_range_at_picks_run_around_byte() {
1026 let text = "Hello, world!";
1027 assert_eq!(word_range_at(text, 0), (0, 5));
1029 assert_eq!(word_range_at(text, 3), (0, 5));
1031 assert_eq!(word_range_at(text, 5), (5, 6));
1033 assert_eq!(word_range_at(text, 6), (6, 7));
1035 assert_eq!(word_range_at(text, 7), (7, 12));
1037 assert_eq!(word_range_at(text, 12), (12, 13));
1039 }
1040
1041 #[test]
1042 fn word_range_at_treats_apostrophe_and_underscore_as_word_chars() {
1043 assert_eq!(word_range_at("don't stop", 2), (0, 5));
1045 assert_eq!(word_range_at("foo_bar baz", 4), (0, 7));
1047 }
1048
1049 #[test]
1050 fn word_range_at_handles_end_of_text_and_empty() {
1051 let text = "hello";
1052 assert_eq!(word_range_at(text, 5), (0, 5));
1054 assert_eq!(word_range_at("", 0), (0, 0));
1056 }
1057
1058 #[test]
1059 fn word_range_at_clamps_off_utf8_boundary() {
1060 let text = "café";
1063 let (lo, hi) = word_range_at(text, 1);
1064 assert_eq!((lo, hi), (0, text.len()));
1065 }
1066
1067 #[test]
1068 fn line_range_at_returns_line_around_byte() {
1069 let text = "first\nsecond line\nthird";
1070 assert_eq!(line_range_at(text, 0), (0, 5));
1072 assert_eq!(line_range_at(text, 3), (0, 5));
1073 assert_eq!(line_range_at(text, 5), (0, 5));
1074 assert_eq!(line_range_at(text, 6), (6, 17));
1076 assert_eq!(line_range_at(text, 12), (6, 17));
1077 assert_eq!(line_range_at(text, 17), (6, 17));
1078 assert_eq!(line_range_at(text, 18), (18, 23));
1080 assert_eq!(line_range_at(text, 23), (18, 23));
1081 }
1082
1083 #[test]
1084 fn line_range_at_handles_empty_and_single_line() {
1085 assert_eq!(line_range_at("", 0), (0, 0));
1086 assert_eq!(line_range_at("just one line", 4), (0, 13));
1087 }
1088}