1#![warn(missing_docs)]
33
34use std::ops::Range;
35
36use crate::tree::{El, Kind};
37use crate::widgets::text_input::TextSelection;
38
39#[derive(Clone, Debug, Default, PartialEq, Eq)]
44pub struct Selection {
45 pub range: Option<SelectionRange>,
47}
48
49#[derive(Clone, Debug, PartialEq, Eq)]
53pub struct SelectionRange {
54 pub anchor: SelectionPoint,
56 pub head: SelectionPoint,
59}
60
61#[derive(Clone, Debug, PartialEq, Eq)]
66pub struct SelectionPoint {
67 pub key: String,
69 pub byte: usize,
72}
73
74impl SelectionPoint {
75 pub fn new(key: impl Into<String>, byte: usize) -> Self {
77 Self {
78 key: key.into(),
79 byte,
80 }
81 }
82}
83
84#[derive(Clone, Debug, Default, PartialEq, Eq)]
94pub struct SelectionSource {
95 pub source: String,
98 pub visible: String,
100 pub spans: Vec<SelectionSourceSpan>,
103 pub full_selection_group: Option<String>,
108}
109
110#[derive(Clone, Debug, PartialEq, Eq)]
113pub struct SelectionSourceSpan {
114 pub visible: Range<usize>,
116 pub source: Range<usize>,
119 pub source_full: Range<usize>,
123 pub atomic: bool,
126}
127
128impl SelectionSource {
129 pub fn new(source: impl Into<String>, visible: impl Into<String>) -> Self {
133 Self {
134 source: source.into(),
135 visible: visible.into(),
136 spans: Vec::new(),
137 full_selection_group: None,
138 }
139 }
140
141 pub fn identity(text: impl Into<String>) -> Self {
144 let text = text.into();
145 let len = text.len();
146 Self {
147 source: text.clone(),
148 visible: text,
149 spans: vec![SelectionSourceSpan {
150 visible: 0..len,
151 source: 0..len,
152 source_full: 0..len,
153 atomic: false,
154 }],
155 full_selection_group: None,
156 }
157 }
158
159 pub fn full_selection_group(mut self, group: impl Into<String>) -> Self {
163 self.full_selection_group = Some(group.into());
164 self
165 }
166
167 pub fn push_span(&mut self, visible: Range<usize>, source: Range<usize>, atomic: bool) {
171 self.push_span_with_full_source(visible, source.clone(), source, atomic);
172 }
173
174 pub fn push_span_with_full_source(
180 &mut self,
181 visible: Range<usize>,
182 source: Range<usize>,
183 source_full: Range<usize>,
184 atomic: bool,
185 ) {
186 if visible.start <= visible.end
187 && visible.end <= self.visible.len()
188 && source.start <= source.end
189 && source.end <= self.source.len()
190 && source_full.start <= source_full.end
191 && source_full.end <= self.source.len()
192 {
193 self.spans.push(SelectionSourceSpan {
194 visible,
195 source,
196 source_full,
197 atomic,
198 });
199 }
200 }
201
202 pub fn visible_len(&self) -> usize {
205 self.visible.len()
206 }
207
208 pub fn source_slice_for_visible(&self, a: usize, b: usize) -> Option<&str> {
216 let (a, b) = (a.min(b), a.max(b));
217 if a == 0 && b >= self.visible.len() && !self.source.is_empty() {
218 return Some(&self.source);
219 }
220 let a = clamp_to_char_boundary(&self.visible, a.min(self.visible.len()));
221 let b = clamp_to_char_boundary(&self.visible, b.min(self.visible.len()));
222 let lo = self.source_offset_for_visible(a, Bias::Start)?;
223 let hi = self.source_offset_for_visible(b, Bias::End)?;
224 let (lo, hi) = (lo.min(hi), lo.max(hi));
225 let lo = clamp_to_char_boundary(&self.source, lo.min(self.source.len()));
226 let hi = clamp_to_char_boundary(&self.source, hi.min(self.source.len()));
227 (lo < hi).then(|| &self.source[lo..hi])
228 }
229
230 pub fn source_text_for_visible(&self, a: usize, b: usize) -> Option<String> {
238 let (a, b) = (a.min(b), a.max(b));
239 if a == 0 && b >= self.visible.len() && !self.source.is_empty() {
240 return Some(self.source.clone());
241 }
242 let a = clamp_to_char_boundary(&self.visible, a.min(self.visible.len()));
243 let b = clamp_to_char_boundary(&self.visible, b.min(self.visible.len()));
244 if a >= b {
245 return None;
246 }
247 if self.spans.is_empty() {
248 return self.source_slice_for_visible(a, b).map(str::to_string);
249 }
250
251 let mut out = String::new();
252 for span in &self.spans {
253 let start = a.max(span.visible.start);
254 let end = b.min(span.visible.end);
255 if start >= end {
256 continue;
257 }
258 if span.atomic || (start == span.visible.start && end == span.visible.end) {
259 out.push_str(&self.source[span.source_full.clone()]);
260 continue;
261 }
262 let lo = source_offset_in_span(span, start, Bias::Start)?;
263 let hi = source_offset_in_span(span, end, Bias::End)?;
264 let (lo, hi) = (lo.min(hi), lo.max(hi));
265 let lo = clamp_to_char_boundary(&self.source, lo.min(self.source.len()));
266 let hi = clamp_to_char_boundary(&self.source, hi.min(self.source.len()));
267 if lo < hi {
268 out.push_str(&self.source[lo..hi]);
269 }
270 }
271 if out.is_empty() { None } else { Some(out) }
272 }
273
274 fn full_group_for_visible(&self, start: usize, end: usize) -> Option<&str> {
275 (start == 0 && end >= self.visible.len())
276 .then_some(self.full_selection_group.as_deref())
277 .flatten()
278 }
279
280 fn source_offset_for_visible(&self, byte: usize, bias: Bias) -> Option<usize> {
281 if self.spans.is_empty() {
282 return Some(byte.min(self.source.len()));
283 }
284 for span in &self.spans {
285 if byte < span.visible.start || byte > span.visible.end {
286 continue;
287 }
288 if byte == span.visible.end && byte != span.visible.start && matches!(bias, Bias::Start)
289 {
290 continue;
291 }
292 if span.atomic {
293 return Some(match bias {
294 Bias::Start => span.source.start,
295 Bias::End => span.source.end,
296 });
297 }
298 let visible_len = span.visible.end.saturating_sub(span.visible.start);
299 let source_len = span.source.end.saturating_sub(span.source.start);
300 if visible_len == 0 {
301 return Some(match bias {
302 Bias::Start => span.source.start,
303 Bias::End => span.source.end,
304 });
305 }
306 let offset = byte.saturating_sub(span.visible.start).min(visible_len);
307 let mapped = if source_len == visible_len {
308 span.source.start + offset
309 } else {
310 span.source.start
311 + ((offset as f32 / visible_len as f32) * source_len as f32) as usize
312 };
313 return Some(mapped.min(span.source.end));
314 }
315 let first = self.spans.first()?;
316 if byte <= first.visible.start {
317 return Some(first.source.start);
318 }
319 let last = self.spans.last()?;
320 if byte >= last.visible.end {
321 return Some(last.source.end);
322 }
323 self.spans
324 .windows(2)
325 .find(|pair| byte > pair[0].visible.end && byte < pair[1].visible.start)
326 .map(|pair| match bias {
327 Bias::Start => pair[0].source.end,
328 Bias::End => pair[1].source.start,
329 })
330 }
331}
332
333fn source_offset_in_span(span: &SelectionSourceSpan, byte: usize, bias: Bias) -> Option<usize> {
334 if span.atomic {
335 return Some(match bias {
336 Bias::Start => span.source_full.start,
337 Bias::End => span.source_full.end,
338 });
339 }
340 let visible_len = span.visible.end.saturating_sub(span.visible.start);
341 let source_len = span.source.end.saturating_sub(span.source.start);
342 if visible_len == 0 {
343 return Some(match bias {
344 Bias::Start => span.source.start,
345 Bias::End => span.source.end,
346 });
347 }
348 let offset = byte.saturating_sub(span.visible.start).min(visible_len);
349 let mapped = if source_len == visible_len {
350 span.source.start + offset
351 } else {
352 span.source.start + ((offset as f32 / visible_len as f32) * source_len as f32) as usize
353 };
354 Some(mapped.min(span.source.end))
355}
356
357#[derive(Clone, Copy)]
358enum Bias {
359 Start,
360 End,
361}
362
363impl Selection {
364 pub fn caret(key: impl Into<String>, byte: usize) -> Self {
367 let pt = SelectionPoint::new(key, byte);
368 Self {
369 range: Some(SelectionRange {
370 anchor: pt.clone(),
371 head: pt,
372 }),
373 }
374 }
375
376 pub fn is_empty(&self) -> bool {
378 self.range.is_none()
379 }
380
381 pub fn is_within(&self, key: &str) -> bool {
385 match &self.range {
386 Some(r) => r.anchor.key == key && r.head.key == key,
387 None => false,
388 }
389 }
390
391 pub fn anchored_at(&self, key: &str) -> bool {
393 self.range.as_ref().is_some_and(|r| r.anchor.key == key)
394 }
395
396 pub fn within(&self, key: &str) -> Option<TextSelection> {
402 let r = self.range.as_ref()?;
403 if r.anchor.key == key && r.head.key == key {
404 Some(TextSelection {
405 anchor: r.anchor.byte,
406 head: r.head.byte,
407 })
408 } else {
409 None
410 }
411 }
412
413 pub fn set_within(&mut self, key: &str, sel: TextSelection) {
419 let Some(r) = self.range.as_mut() else { return };
420 if r.anchor.key == key && r.head.key == key {
421 r.anchor.byte = sel.anchor;
422 r.head.byte = sel.head;
423 }
424 }
425
426 pub fn clear(&mut self) {
428 self.range = None;
429 }
430}
431
432pub fn slice_for_leaf(
452 selection: &Selection,
453 order: &[crate::event::UiTarget],
454 key: &str,
455 text_len: usize,
456) -> Option<(usize, usize)> {
457 let r = selection.range.as_ref()?;
458 if r.anchor.key == r.head.key {
459 if r.anchor.key != key {
460 return None;
461 }
462 let (lo, hi) = (
463 r.anchor.byte.min(r.head.byte).min(text_len),
464 r.anchor.byte.max(r.head.byte).min(text_len),
465 );
466 return (lo < hi).then_some((lo, hi));
467 }
468 let pos = |k: &str| order.iter().position(|t| t.key == k);
469 let (a_idx, h_idx, key_idx) = (pos(&r.anchor.key)?, pos(&r.head.key)?, pos(key)?);
470 let (lo_idx, lo_byte, hi_idx, hi_byte) = if a_idx <= h_idx {
471 (a_idx, r.anchor.byte, h_idx, r.head.byte)
472 } else {
473 (h_idx, r.head.byte, a_idx, r.anchor.byte)
474 };
475 if key_idx < lo_idx || key_idx > hi_idx {
476 return None;
477 }
478 let lo = if key_idx == lo_idx {
479 lo_byte.min(text_len)
480 } else {
481 0
482 };
483 let hi = if key_idx == hi_idx {
484 hi_byte.min(text_len)
485 } else {
486 text_len
487 };
488 (lo < hi).then_some((lo, hi))
489}
490
491pub fn selected_text(tree: &El, selection: &Selection) -> Option<String> {
501 let r = selection.range.as_ref()?;
502 if r.anchor.key == r.head.key {
503 if let Some(source) = find_keyed_selection_source(tree, &r.anchor.key) {
504 let lo = r.anchor.byte.min(r.head.byte);
505 let hi = r.anchor.byte.max(r.head.byte);
506 return source.source_text_for_visible(lo, hi);
507 }
508 let value = find_keyed_text(tree, &r.anchor.key)?;
509 let lo = clamp_to_char_boundary(&value, r.anchor.byte.min(r.head.byte).min(value.len()));
513 let hi = clamp_to_char_boundary(&value, r.anchor.byte.max(r.head.byte).min(value.len()));
514 if lo >= hi {
515 return None;
516 }
517 return Some(value[lo..hi].to_string());
518 }
519 let mut leaves: Vec<(String, LeafSelectionText)> = Vec::new();
521 collect_keyed_selection_leaves(tree, &mut leaves);
522 let anchor_idx = leaves.iter().position(|(k, _)| *k == r.anchor.key)?;
523 let head_idx = leaves.iter().position(|(k, _)| *k == r.head.key)?;
524 let (lo_idx, lo_byte, hi_idx, hi_byte) = if anchor_idx <= head_idx {
525 (anchor_idx, r.anchor.byte, head_idx, r.head.byte)
526 } else {
527 (head_idx, r.head.byte, anchor_idx, r.anchor.byte)
528 };
529 let mut out = String::new();
530 let mut last_group: Option<String> = None;
531 for (i, (_, value)) in leaves
532 .iter()
533 .enumerate()
534 .skip(lo_idx)
535 .take(hi_idx - lo_idx + 1)
536 {
537 let start = if i == lo_idx {
538 lo_byte.min(value.visible_len())
539 } else {
540 0
541 };
542 let end = if i == hi_idx {
543 hi_byte.min(value.visible_len())
544 } else {
545 value.visible_len()
546 };
547 if start >= end {
548 continue;
549 }
550 let Some(slice) = value.source_text_for_visible(start, end) else {
551 continue;
552 };
553 let group = value.full_group_for_visible(start, end).map(str::to_string);
554 if group.is_some() && group == last_group {
555 continue;
556 }
557 if !out.is_empty() {
558 out.push('\n');
559 }
560 out.push_str(&slice);
561 last_group = group;
562 }
563 if out.is_empty() { None } else { Some(out) }
564}
565
566pub(crate) fn find_keyed_text(node: &El, key: &str) -> Option<String> {
567 if node.key.as_deref() == Some(key) {
568 if let Some(source) = &node.selection_source {
569 return Some(source.visible.clone());
570 }
571 if matches!(node.kind, Kind::Text | Kind::Heading)
572 && let Some(t) = &node.text
573 {
574 return Some(t.clone());
575 }
576 let mut out = String::new();
577 collect_text_content(node, &mut out);
578 if !out.is_empty() {
579 return Some(out);
580 }
581 }
582 node.children.iter().find_map(|c| find_keyed_text(c, key))
583}
584
585pub(crate) fn find_keyed_selection_source(node: &El, key: &str) -> Option<SelectionSource> {
586 if node.key.as_deref() == Some(key)
587 && let Some(source) = &node.selection_source
588 {
589 return Some((**source).clone());
590 }
591 node.children
592 .iter()
593 .find_map(|c| find_keyed_selection_source(c, key))
594}
595
596fn collect_text_content(node: &El, out: &mut String) {
597 if matches!(node.kind, Kind::Text | Kind::Heading)
598 && let Some(t) = &node.text
599 {
600 out.push_str(t);
601 }
602 for c in &node.children {
603 collect_text_content(c, out);
604 }
605}
606
607enum LeafSelectionText {
608 Source(SelectionSource),
609 Text(String),
610}
611
612impl LeafSelectionText {
613 fn visible_len(&self) -> usize {
614 match self {
615 LeafSelectionText::Source(source) => source.visible_len(),
616 LeafSelectionText::Text(text) => text.len(),
617 }
618 }
619
620 fn source_text_for_visible(&self, start: usize, end: usize) -> Option<String> {
621 match self {
622 LeafSelectionText::Source(source) => source.source_text_for_visible(start, end),
623 LeafSelectionText::Text(text) => {
624 let start = clamp_to_char_boundary(text, start.min(text.len()));
625 let end = clamp_to_char_boundary(text, end.min(text.len()));
626 (start < end).then(|| text[start..end].to_string())
627 }
628 }
629 }
630
631 fn full_group_for_visible(&self, start: usize, end: usize) -> Option<&str> {
632 match self {
633 LeafSelectionText::Source(source) => source.full_group_for_visible(start, end),
634 LeafSelectionText::Text(_) => None,
635 }
636 }
637}
638
639fn collect_keyed_selection_leaves(node: &El, out: &mut Vec<(String, LeafSelectionText)>) {
640 if let (Some(k), Some(source)) = (&node.key, &node.selection_source) {
641 out.push((k.clone(), LeafSelectionText::Source((**source).clone())));
642 return;
643 }
644 if matches!(node.kind, Kind::Text | Kind::Heading)
645 && let (Some(k), Some(t)) = (&node.key, &node.text)
646 {
647 out.push((k.clone(), LeafSelectionText::Text(t.clone())));
648 }
649 for c in &node.children {
650 collect_keyed_selection_leaves(c, out);
651 }
652}
653
654pub fn word_range_at(text: &str, byte: usize) -> (usize, usize) {
666 if text.is_empty() {
667 return (0, 0);
668 }
669 let byte = clamp_to_char_boundary(text, byte.min(text.len()));
670 let probe = if byte == text.len() {
674 prev_char_boundary(text, byte)
675 } else {
676 byte
677 };
678 let probe_char = text[probe..].chars().next().unwrap_or(' ');
679 if !is_word_char(probe_char) {
680 return (probe, probe + probe_char.len_utf8());
684 }
685
686 let mut lo = probe;
688 while lo > 0 {
689 let p = prev_char_boundary(text, lo);
690 let ch = text[p..].chars().next().unwrap();
691 if !is_word_char(ch) {
692 break;
693 }
694 lo = p;
695 }
696 let mut hi = probe;
697 while hi < text.len() {
698 let ch = text[hi..].chars().next().unwrap();
699 if !is_word_char(ch) {
700 break;
701 }
702 hi += ch.len_utf8();
703 }
704 (lo, hi)
705}
706
707pub fn line_range_at(text: &str, byte: usize) -> (usize, usize) {
715 let byte = clamp_to_char_boundary(text, byte.min(text.len()));
716 let lo = text[..byte].rfind('\n').map(|i| i + 1).unwrap_or(0);
717 let hi = text[byte..]
718 .find('\n')
719 .map(|i| byte + i)
720 .unwrap_or(text.len());
721 (lo, hi)
722}
723
724fn is_word_char(c: char) -> bool {
725 c.is_alphanumeric() || c == '_' || c == '\''
726}
727
728pub fn next_word_boundary(text: &str, byte: usize) -> usize {
738 let mut i = clamp_to_char_boundary(text, byte.min(text.len()));
739 while i < text.len() {
741 let ch = text[i..].chars().next().unwrap();
742 if is_word_char(ch) {
743 break;
744 }
745 i += ch.len_utf8();
746 }
747 while i < text.len() {
749 let ch = text[i..].chars().next().unwrap();
750 if !is_word_char(ch) {
751 break;
752 }
753 i += ch.len_utf8();
754 }
755 i
756}
757
758pub fn prev_word_boundary(text: &str, byte: usize) -> usize {
767 let mut i = clamp_to_char_boundary(text, byte.min(text.len()));
768 while i > 0 {
770 let p = prev_char_boundary(text, i);
771 let ch = text[p..].chars().next().unwrap();
772 if is_word_char(ch) {
773 break;
774 }
775 i = p;
776 }
777 while i > 0 {
779 let p = prev_char_boundary(text, i);
780 let ch = text[p..].chars().next().unwrap();
781 if !is_word_char(ch) {
782 break;
783 }
784 i = p;
785 }
786 i
787}
788
789fn clamp_to_char_boundary(text: &str, byte: usize) -> usize {
790 let mut b = byte;
791 while b > 0 && !text.is_char_boundary(b) {
792 b -= 1;
793 }
794 b
795}
796
797fn prev_char_boundary(text: &str, byte: usize) -> usize {
798 let mut b = byte.saturating_sub(1);
799 while b > 0 && !text.is_char_boundary(b) {
800 b -= 1;
801 }
802 b
803}
804
805#[cfg(test)]
806mod tests {
807 use super::*;
808
809 #[test]
810 fn empty_selection_has_no_views() {
811 let sel = Selection::default();
812 assert!(sel.is_empty());
813 assert!(!sel.is_within("name"));
814 assert!(sel.within("name").is_none());
815 }
816
817 #[test]
818 fn caret_constructor_is_within_its_key() {
819 let sel = Selection::caret("name", 3);
820 assert!(!sel.is_empty());
821 assert!(sel.is_within("name"));
822 assert!(!sel.is_within("email"));
823 let view = sel.within("name").expect("within name");
824 assert_eq!(view, TextSelection::caret(3));
825 }
826
827 #[test]
828 fn within_returns_none_for_cross_element_selection() {
829 let sel = Selection {
830 range: Some(SelectionRange {
831 anchor: SelectionPoint::new("para_a", 0),
832 head: SelectionPoint::new("para_b", 5),
833 }),
834 };
835 assert!(sel.within("para_a").is_none());
837 assert!(sel.within("para_b").is_none());
838 assert!(sel.anchored_at("para_a"));
840 assert!(!sel.anchored_at("para_b"));
841 }
842
843 #[test]
844 fn set_within_writes_back_a_modified_slice() {
845 let mut sel = Selection::caret("name", 0);
846 let mut view = sel.within("name").expect("caret");
847 view.head = 5; sel.set_within("name", view);
849 let view_back = sel.within("name").expect("still within name");
850 assert_eq!(view_back, TextSelection::range(0, 5));
851 }
852
853 #[test]
854 fn set_within_is_a_noop_when_selection_is_not_in_key() {
855 let mut sel = Selection::caret("name", 0);
856 sel.set_within("email", TextSelection::range(0, 9));
857 assert_eq!(sel.within("name"), Some(TextSelection::caret(0)));
859 assert!(sel.within("email").is_none());
860 }
861
862 #[test]
863 fn selected_text_returns_single_leaf_substring() {
864 let tree = crate::widgets::text::text("Hello, world!").key("p");
865 let sel = Selection {
866 range: Some(SelectionRange {
867 anchor: SelectionPoint::new("p", 7),
868 head: SelectionPoint::new("p", 12),
869 }),
870 };
871 assert_eq!(selected_text(&tree, &sel).as_deref(), Some("world"));
872 }
873
874 #[test]
875 fn selected_text_snaps_stale_mid_codepoint_offsets_single_leaf() {
876 let tree = crate::widgets::text::text("aé€b").key("p");
881 let sel = Selection {
882 range: Some(SelectionRange {
883 anchor: SelectionPoint::new("p", 2), head: SelectionPoint::new("p", 4), }),
886 };
887 assert_eq!(selected_text(&tree, &sel).as_deref(), Some("é"));
888 }
889
890 #[test]
891 fn selected_text_snaps_stale_mid_codepoint_offsets_cross_leaf() {
892 let tree = crate::column([
893 crate::widgets::text::text("héllo").key("a"),
894 crate::widgets::text::text("wörld").key("b"),
895 ]);
896 let sel = Selection {
897 range: Some(SelectionRange {
898 anchor: SelectionPoint::new("a", 2), head: SelectionPoint::new("b", 2), }),
901 };
902 assert_eq!(selected_text(&tree, &sel).as_deref(), Some("éllo\nw"));
903 }
904
905 #[test]
906 fn line_range_at_snaps_mid_codepoint_byte() {
907 assert_eq!(line_range_at("é\nö", 1), (0, 2));
909 assert_eq!(line_range_at("é\nö", 4), (3, 5));
910 }
911
912 #[test]
913 fn selected_text_reads_text_inside_keyed_composite_widget() {
914 let sel = Selection {
915 range: Some(SelectionRange {
916 anchor: SelectionPoint::new("name", 1),
917 head: SelectionPoint::new("name", 4),
918 }),
919 };
920 let tree = crate::widgets::text_input::text_input("name", "hello", &sel);
921 assert_eq!(selected_text(&tree, &sel).as_deref(), Some("ell"));
922 }
923
924 #[test]
925 fn selected_text_walks_tree_order_for_cross_leaf_selection() {
926 let tree = crate::column([
927 crate::widgets::text::text("alpha").key("a"),
928 crate::widgets::text::text("bravo").key("b"),
929 crate::widgets::text::text("charlie").key("c"),
930 ]);
931 let sel = Selection {
935 range: Some(SelectionRange {
936 anchor: SelectionPoint::new("a", 2),
937 head: SelectionPoint::new("c", 4),
938 }),
939 };
940 assert_eq!(
941 selected_text(&tree, &sel).as_deref(),
942 Some("pha\nbravo\nchar")
943 );
944 }
945
946 #[test]
947 fn selected_text_uses_source_payload_for_single_leaf() {
948 let mut source = SelectionSource::new("This is **bold**.", "This is bold.");
949 source.push_span(0..8, 0..8, false);
950 source.push_span_with_full_source(8..12, 10..14, 8..16, false);
951 source.push_span(12..13, 16..17, false);
952 let tree = crate::text_runs([crate::text("This is "), crate::text("bold").bold()])
953 .key("md:p")
954 .selectable()
955 .selection_source(source);
956
957 let inner_only = Selection {
958 range: Some(SelectionRange {
959 anchor: SelectionPoint::new("md:p", 8),
960 head: SelectionPoint::new("md:p", 12),
961 }),
962 };
963 assert_eq!(
964 selected_text(&tree, &inner_only).as_deref(),
965 Some("**bold**")
966 );
967
968 let partial_inner = Selection {
969 range: Some(SelectionRange {
970 anchor: SelectionPoint::new("md:p", 9),
971 head: SelectionPoint::new("md:p", 11),
972 }),
973 };
974 assert_eq!(selected_text(&tree, &partial_inner).as_deref(), Some("ol"));
975
976 let through_styled_span = Selection {
977 range: Some(SelectionRange {
978 anchor: SelectionPoint::new("md:p", 0),
979 head: SelectionPoint::new("md:p", 12),
980 }),
981 };
982 assert_eq!(
983 selected_text(&tree, &through_styled_span).as_deref(),
984 Some("This is **bold**")
985 );
986
987 let whole = Selection {
988 range: Some(SelectionRange {
989 anchor: SelectionPoint::new("md:p", 0),
990 head: SelectionPoint::new("md:p", 13),
991 }),
992 };
993 assert_eq!(
994 selected_text(&tree, &whole).as_deref(),
995 Some("This is **bold**.")
996 );
997 }
998
999 #[test]
1000 fn selected_text_dedupes_adjacent_full_source_group_leaves() {
1001 let mut first = SelectionSource::new("| **Ada** | dev |", "Ada");
1002 first.push_span_with_full_source(0..3, 4..7, 0..17, false);
1003 let first = first.full_selection_group("row:0");
1004
1005 let mut second = SelectionSource::new("| **Ada** | dev |", "dev");
1006 second.push_span_with_full_source(0..3, 12..15, 0..17, false);
1007 let second = second.full_selection_group("row:0");
1008
1009 let tree = crate::row([
1010 crate::text("Ada")
1011 .key("a")
1012 .selectable()
1013 .selection_source(first),
1014 crate::text("dev")
1015 .key("b")
1016 .selectable()
1017 .selection_source(second),
1018 ]);
1019 let sel = Selection {
1020 range: Some(SelectionRange {
1021 anchor: SelectionPoint::new("a", 0),
1022 head: SelectionPoint::new("b", 3),
1023 }),
1024 };
1025
1026 assert_eq!(
1027 selected_text(&tree, &sel).as_deref(),
1028 Some("| **Ada** | dev |")
1029 );
1030 }
1031
1032 #[test]
1033 fn slice_for_leaf_single_leaf() {
1034 let order = order_for(&["a", "b", "c"]);
1035 let sel = Selection {
1036 range: Some(SelectionRange {
1037 anchor: SelectionPoint::new("b", 2),
1038 head: SelectionPoint::new("b", 5),
1039 }),
1040 };
1041 assert_eq!(slice_for_leaf(&sel, &order, "b", 10), Some((2, 5)));
1042 assert_eq!(slice_for_leaf(&sel, &order, "a", 10), None);
1043 assert_eq!(slice_for_leaf(&sel, &order, "c", 10), None);
1044 }
1045
1046 #[test]
1047 fn slice_for_leaf_cross_leaf_anchor_to_head_in_doc_order() {
1048 let order = order_for(&["a", "b", "c"]);
1050 let sel = Selection {
1051 range: Some(SelectionRange {
1052 anchor: SelectionPoint::new("a", 2),
1053 head: SelectionPoint::new("c", 4),
1054 }),
1055 };
1056 assert_eq!(
1057 slice_for_leaf(&sel, &order, "a", 10),
1058 Some((2, 10)),
1059 "anchor leaf: from anchor.byte to text_len"
1060 );
1061 assert_eq!(
1062 slice_for_leaf(&sel, &order, "b", 8),
1063 Some((0, 8)),
1064 "middle leaf: fully selected"
1065 );
1066 assert_eq!(
1067 slice_for_leaf(&sel, &order, "c", 10),
1068 Some((0, 4)),
1069 "head leaf: from 0 to head.byte"
1070 );
1071 }
1072
1073 #[test]
1074 fn slice_for_leaf_cross_leaf_reversed_drag() {
1075 let order = order_for(&["a", "b", "c"]);
1078 let sel = Selection {
1079 range: Some(SelectionRange {
1080 anchor: SelectionPoint::new("c", 3),
1081 head: SelectionPoint::new("a", 1),
1082 }),
1083 };
1084 assert_eq!(slice_for_leaf(&sel, &order, "a", 5), Some((1, 5)));
1086 assert_eq!(slice_for_leaf(&sel, &order, "b", 6), Some((0, 6)));
1087 assert_eq!(slice_for_leaf(&sel, &order, "c", 9), Some((0, 3)));
1088 }
1089
1090 #[test]
1091 fn slice_for_leaf_returns_none_for_leaves_outside_range() {
1092 let order = order_for(&["a", "b", "c", "d", "e"]);
1094 let sel = Selection {
1095 range: Some(SelectionRange {
1096 anchor: SelectionPoint::new("b", 0),
1097 head: SelectionPoint::new("d", 0),
1098 }),
1099 };
1100 assert_eq!(slice_for_leaf(&sel, &order, "a", 10), None);
1101 assert_eq!(slice_for_leaf(&sel, &order, "e", 10), None);
1102 assert_eq!(slice_for_leaf(&sel, &order, "b", 4), Some((0, 4)));
1106 assert_eq!(slice_for_leaf(&sel, &order, "c", 7), Some((0, 7)));
1107 assert_eq!(slice_for_leaf(&sel, &order, "d", 5), None);
1108 }
1109
1110 fn order_for(keys: &[&str]) -> Vec<crate::event::UiTarget> {
1111 keys.iter()
1112 .map(|k| crate::event::UiTarget {
1113 key: (*k).to_string(),
1114 node_id: format!("root.{k}").into(),
1115 rect: crate::tree::Rect::new(0.0, 0.0, 0.0, 0.0),
1116 tooltip: None,
1117 scroll_offset_y: 0.0,
1118 })
1119 .collect()
1120 }
1121
1122 #[test]
1123 fn selected_text_returns_none_for_empty_or_unknown_keys() {
1124 let tree = crate::widgets::text::text("hi").key("p");
1125 assert!(selected_text(&tree, &Selection::default()).is_none());
1126 let unknown = Selection::caret("missing", 0);
1127 assert!(selected_text(&tree, &unknown).is_none());
1128 }
1129
1130 #[test]
1131 fn word_range_at_picks_run_around_byte() {
1132 let text = "Hello, world!";
1133 assert_eq!(word_range_at(text, 0), (0, 5));
1135 assert_eq!(word_range_at(text, 3), (0, 5));
1137 assert_eq!(word_range_at(text, 5), (5, 6));
1139 assert_eq!(word_range_at(text, 6), (6, 7));
1141 assert_eq!(word_range_at(text, 7), (7, 12));
1143 assert_eq!(word_range_at(text, 12), (12, 13));
1145 }
1146
1147 #[test]
1148 fn word_range_at_treats_apostrophe_and_underscore_as_word_chars() {
1149 assert_eq!(word_range_at("don't stop", 2), (0, 5));
1151 assert_eq!(word_range_at("foo_bar baz", 4), (0, 7));
1153 }
1154
1155 #[test]
1156 fn word_range_at_handles_end_of_text_and_empty() {
1157 let text = "hello";
1158 assert_eq!(word_range_at(text, 5), (0, 5));
1160 assert_eq!(word_range_at("", 0), (0, 0));
1162 }
1163
1164 #[test]
1165 fn word_range_at_clamps_off_utf8_boundary() {
1166 let text = "café";
1169 let (lo, hi) = word_range_at(text, 1);
1170 assert_eq!((lo, hi), (0, text.len()));
1171 }
1172
1173 #[test]
1174 fn line_range_at_returns_line_around_byte() {
1175 let text = "first\nsecond line\nthird";
1176 assert_eq!(line_range_at(text, 0), (0, 5));
1178 assert_eq!(line_range_at(text, 3), (0, 5));
1179 assert_eq!(line_range_at(text, 5), (0, 5));
1180 assert_eq!(line_range_at(text, 6), (6, 17));
1182 assert_eq!(line_range_at(text, 12), (6, 17));
1183 assert_eq!(line_range_at(text, 17), (6, 17));
1184 assert_eq!(line_range_at(text, 18), (18, 23));
1186 assert_eq!(line_range_at(text, 23), (18, 23));
1187 }
1188
1189 #[test]
1190 fn line_range_at_handles_empty_and_single_line() {
1191 assert_eq!(line_range_at("", 0), (0, 0));
1192 assert_eq!(line_range_at("just one line", 4), (0, 13));
1193 }
1194}