1use std::sync::atomic::{AtomicU64, Ordering};
32use std::sync::{Arc, Mutex};
33
34use rosace_core::types::Rect;
35use rosace_render::{Color, FontWeight};
36use unicode_segmentation::UnicodeSegmentation;
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
47pub enum Affinity {
48 #[default]
49 Upstream,
50 Downstream,
51}
52
53#[derive(Clone, Copy, Debug, PartialEq)]
60pub struct SelectionRange {
61 pub anchor: usize,
62 pub head: usize,
63 pub affinity: Affinity,
64}
65
66impl SelectionRange {
67 pub fn collapsed_at(pos: usize) -> Self {
68 Self { anchor: pos, head: pos, affinity: Affinity::default() }
69 }
70 pub fn collapsed(&self) -> bool {
71 self.anchor == self.head
72 }
73 pub fn normalized(&self) -> (usize, usize) {
76 (self.anchor.min(self.head), self.anchor.max(self.head))
77 }
78}
79
80#[derive(Clone, Debug, PartialEq)]
86pub struct Selection {
87 ranges: Vec<SelectionRange>,
88}
89
90impl Selection {
91 pub fn single(pos: usize) -> Self {
92 Self { ranges: vec![SelectionRange::collapsed_at(pos)] }
93 }
94 pub fn range(anchor: usize, head: usize) -> Self {
95 Self { ranges: vec![SelectionRange { anchor, head, affinity: Affinity::default() }] }
96 }
97 pub fn primary(&self) -> &SelectionRange {
100 self.ranges.last().expect("Selection is never empty")
101 }
102 pub fn primary_range(&self) -> (usize, usize) {
103 self.primary().normalized()
104 }
105 pub fn ranges(&self) -> &[SelectionRange] {
106 &self.ranges
107 }
108}
109
110impl Default for Selection {
111 fn default() -> Self {
112 Selection::single(0)
113 }
114}
115
116#[derive(Clone, Debug, PartialEq)]
124pub struct Edit {
125 pub range: (usize, usize),
126 pub replacement: String,
127}
128
129#[derive(Clone, Debug, PartialEq, Default)]
138pub struct Transaction {
139 pub edits: Vec<Edit>,
140}
141
142impl Transaction {
143 pub fn single(range: (usize, usize), replacement: impl Into<String>) -> Self {
144 Transaction { edits: vec![Edit { range, replacement: replacement.into() }] }
145 }
146
147 pub fn apply(&self, value: &str) -> (String, Transaction) {
150 let mut edits = self.edits.clone();
151 edits.sort_by_key(|e| std::cmp::Reverse(e.range.0)); let mut result = value.to_string();
154 let mut inverse_edits = Vec::with_capacity(edits.len());
155 for e in &edits {
156 let bs = char_byte_offset(&result, e.range.0);
157 let be = char_byte_offset(&result, e.range.1);
158 let removed = result[bs..be].to_string();
159 let mut next = String::with_capacity(result.len() - (be - bs) + e.replacement.len());
160 next.push_str(&result[..bs]);
161 next.push_str(&e.replacement);
162 next.push_str(&result[be..]);
163 let new_end = e.range.0 + char_count(&e.replacement);
164 inverse_edits.push(Edit { range: (e.range.0, new_end), replacement: removed });
165 result = next;
166 }
167 (result, Transaction { edits: inverse_edits })
168 }
169}
170
171fn edits_affected_range(edits: &[Edit]) -> Option<(usize, usize)> {
178 edits.iter().map(|e| (e.range.0, e.range.0 + char_count(&e.replacement)))
179 .fold(None, |acc: Option<(usize, usize)>, r| Some(match acc {
180 None => r,
181 Some(a) => (a.0.min(r.0), a.1.max(r.1)),
182 }))
183}
184
185pub fn char_count(s: &str) -> usize {
191 s.chars().count()
192}
193
194pub fn char_byte_offset(s: &str, idx: usize) -> usize {
197 s.char_indices().nth(idx).map(|(b, _)| b).unwrap_or(s.len())
198}
199
200pub fn grapheme_boundaries(s: &str) -> Vec<usize> {
212 let mut bounds = Vec::with_capacity(s.len() + 1);
213 bounds.push(0usize);
214 let mut char_idx = 0usize;
215 for g in s.graphemes(true) {
216 char_idx += g.chars().count();
217 bounds.push(char_idx);
218 }
219 bounds
220}
221
222pub fn prev_grapheme_boundary(s: &str, pos: usize) -> usize {
224 grapheme_boundaries(s).into_iter().rev().find(|&b| b < pos).unwrap_or(0)
225}
226
227pub fn next_grapheme_boundary(s: &str, pos: usize) -> usize {
230 let bounds = grapheme_boundaries(s);
231 bounds.iter().copied().find(|&b| b > pos).unwrap_or_else(|| *bounds.last().unwrap())
232}
233
234fn word_bound_boundaries(s: &str) -> Vec<(usize, bool)> {
237 let mut out = Vec::new();
239 let mut char_idx = 0usize;
240 for w in s.split_word_bounds() {
241 let is_word = w.chars().next().map(|c| c.is_alphanumeric()).unwrap_or(false);
242 out.push((char_idx, is_word));
243 char_idx += char_count(w);
244 }
245 out.push((char_idx, false)); out
247}
248
249pub fn prev_word_boundary(s: &str, pos: usize) -> usize {
253 let runs = word_bound_boundaries(s);
254 let mut idx = runs.len().saturating_sub(1);
256 for i in (0..runs.len() - 1).rev() {
257 if runs[i].0 < pos {
258 idx = i;
259 break;
260 }
261 }
262 let mut i = idx;
265 loop {
266 let (start, is_word) = runs[i];
267 if start < pos && is_word {
268 return start;
269 }
270 if i == 0 {
271 return 0;
272 }
273 i -= 1;
274 }
275}
276
277pub fn next_word_boundary(s: &str, pos: usize) -> usize {
282 let runs = word_bound_boundaries(s); let n = char_count(s);
284
285 let mut i = 0;
287 while i + 1 < runs.len() && runs[i + 1].0 <= pos {
288 i += 1;
289 }
290
291 if runs[i].1 {
292 let end = runs.get(i + 1).map(|&(st, _)| st).unwrap_or(n);
293 if end > pos {
294 return end;
295 }
296 }
297 let mut j = i + 1;
300 while j < runs.len() {
301 if runs[j].1 {
302 return runs.get(j + 1).map(|&(st, _)| st).unwrap_or(n);
303 }
304 j += 1;
305 }
306 n
307}
308
309pub fn word_range_at(s: &str, pos: usize) -> (usize, usize) {
314 let runs = word_bound_boundaries(s);
315 let n = char_count(s);
316 let mut i = 0;
317 while i + 1 < runs.len() && runs[i + 1].0 <= pos {
318 i += 1;
319 }
320 let start = runs[i].0;
321 let end = runs.get(i + 1).map(|&(st, _)| st).unwrap_or(n);
322 (start, end)
323}
324
325#[derive(Clone, Debug, Default, PartialEq)]
340pub struct LineLayout {
341 pub char_range: (usize, usize),
344 pub y: f32,
348 pub height: f32,
349 pub boundary_chars: Vec<usize>,
352 pub boundary_x: Vec<f32>,
354}
355
356impl LineLayout {
357 pub fn x_at(&self, target: usize) -> f32 {
363 let clamped = target.clamp(self.char_range.0, self.char_range.1);
364 if let Some(i) = self.boundary_chars.iter().position(|&c| c == clamped) {
365 self.boundary_x[i]
366 } else if clamped <= self.char_range.0 {
367 self.boundary_x.first().copied().unwrap_or(0.0)
368 } else {
369 self.boundary_x.last().copied().unwrap_or(0.0)
370 }
371 }
372}
373
374#[derive(Clone, Debug, Default, PartialEq)]
378pub struct TextLayoutSnapshot {
379 pub lines: Vec<LineLayout>,
380}
381
382impl TextLayoutSnapshot {
383 fn line_for_y(&self, y: f32) -> Option<&LineLayout> {
384 if self.lines.is_empty() {
385 return None;
386 }
387 for line in &self.lines {
388 if y < line.y + line.height {
389 return Some(line);
390 }
391 }
392 self.lines.last()
393 }
394
395 pub fn position_at(&self, x: f32, y: f32) -> usize {
403 let Some(line) = self.line_for_y(y) else { return 0; };
404 if line.boundary_x.is_empty() {
405 return line.char_range.0;
406 }
407 let mut idx = 0usize;
408 for (i, &bx) in line.boundary_x.iter().enumerate() {
409 if bx <= x {
410 idx = i;
411 } else {
412 break;
413 }
414 }
415 if idx + 1 < line.boundary_x.len() {
416 let mid = (line.boundary_x[idx] + line.boundary_x[idx + 1]) / 2.0;
417 if x > mid {
418 idx += 1;
419 }
420 }
421 line.boundary_chars[idx]
422 }
423
424 pub fn x_of(&self, char_idx: usize) -> Option<f32> {
429 for line in &self.lines {
430 if let Some(i) = line.boundary_chars.iter().position(|&c| c == char_idx) {
431 return Some(line.boundary_x[i]);
432 }
433 }
434 None
435 }
436
437 pub fn line_range_at(&self, char_idx: usize) -> (usize, usize) {
442 for line in &self.lines {
443 if char_idx >= line.char_range.0 && char_idx <= line.char_range.1 {
444 return line.char_range;
445 }
446 }
447 self.lines.last().map(|l| l.char_range).unwrap_or((0, 0))
448 }
449}
450
451#[derive(Clone, Debug)]
467pub struct Span {
468 pub range: (usize, usize),
469 pub color: Option<Color>,
470 pub weight: Option<FontWeight>,
471}
472
473impl PartialEq for Span {
474 fn eq(&self, other: &Self) -> bool {
475 self.range == other.range
476 && self.color.map(color_bits) == other.color.map(color_bits)
477 && self.weight == other.weight
478 }
479}
480
481fn color_bits(c: Color) -> (u8, u8, u8, u8) { (c.r, c.g, c.b, c.a) }
485
486impl Span {
487 pub fn new(range: (usize, usize)) -> Self {
488 Self { range, color: None, weight: None }
489 }
490 pub fn color(mut self, c: Color) -> Self { self.color = Some(c); self }
491 pub fn weight(mut self, w: FontWeight) -> Self { self.weight = Some(w); self }
492}
493
494pub type SpanFn = dyn Fn(&str, Option<(usize, usize)>) -> Vec<Span> + Send + Sync;
502
503pub fn style_runs(spans: &[Span], ls: usize, le: usize) -> Vec<(usize, usize, Option<Color>, Option<FontWeight>)> {
510 if ls >= le {
511 return Vec::new();
512 }
513 let mut points: Vec<usize> = vec![ls, le];
514 for s in spans {
515 if s.range.0 > ls && s.range.0 < le { points.push(s.range.0); }
516 if s.range.1 > ls && s.range.1 < le { points.push(s.range.1); }
517 }
518 points.sort_unstable();
519 points.dedup();
520 points.windows(2).map(|w| {
521 let (a, b) = (w[0], w[1]);
522 let cover = spans.iter().rev().find(|s| s.range.0 <= a && s.range.1 >= b);
523 (a, b, cover.and_then(|s| s.color), cover.and_then(|s| s.weight))
524 }).collect()
525}
526
527pub type CursorPainter = Arc<dyn Fn(&mut super::PaintCtx, Rect) + Send + Sync>;
540
541#[derive(Clone)]
546pub enum CursorShape {
547 Bar,
549 Block,
552 Underline,
554 Custom(CursorPainter),
556}
557
558impl std::fmt::Debug for CursorShape {
559 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
560 match self {
561 CursorShape::Bar => write!(f, "Bar"),
562 CursorShape::Block => write!(f, "Block"),
563 CursorShape::Underline => write!(f, "Underline"),
564 CursorShape::Custom(_) => write!(f, "Custom(..)"),
565 }
566 }
567}
568
569impl PartialEq for CursorShape {
570 fn eq(&self, other: &Self) -> bool {
571 matches!(
572 (self, other),
573 (CursorShape::Bar, CursorShape::Bar)
574 | (CursorShape::Block, CursorShape::Block)
575 | (CursorShape::Underline, CursorShape::Underline)
576 ) }
578}
579
580#[derive(Clone, Debug)]
581pub struct CursorStyle {
582 pub width: f32,
583 pub color: Color,
584 pub corner_radius: f32,
585 pub blink_rate: f32,
588 pub shape: CursorShape,
589}
590
591impl PartialEq for CursorStyle {
592 fn eq(&self, other: &Self) -> bool {
593 self.width == other.width
594 && color_bits(self.color) == color_bits(other.color)
595 && self.corner_radius == other.corner_radius
596 && self.blink_rate == other.blink_rate
597 && self.shape == other.shape
598 }
599}
600
601impl Default for CursorStyle {
602 fn default() -> Self {
603 Self {
604 width: 1.5,
605 color: Color::rgb(180, 160, 255),
606 corner_radius: 0.0,
607 blink_rate: 0.53,
608 shape: CursorShape::Bar,
609 }
610 }
611}
612
613#[derive(Clone)]
628pub enum InputFilter {
629 MaxLength(usize),
632 CharClass(Arc<dyn Fn(char) -> bool + Send + Sync>),
634}
635
636impl InputFilter {
637 pub fn max_length(n: usize) -> Self { InputFilter::MaxLength(n) }
638 pub fn char_class(f: impl Fn(char) -> bool + Send + Sync + 'static) -> Self {
639 InputFilter::CharClass(Arc::new(f))
640 }
641 pub fn digits() -> Self { Self::char_class(|c| c.is_ascii_digit()) }
643 pub fn alphanumeric() -> Self { Self::char_class(|c| c.is_alphanumeric()) }
645}
646
647pub fn apply_filters(value: &str, filters: &[InputFilter]) -> String {
651 let mut v = value.to_string();
652 for f in filters {
653 v = match f {
654 InputFilter::CharClass(pred) => v.chars().filter(|&c| pred(c)).collect(),
655 InputFilter::MaxLength(n) => v.chars().take(*n).collect(),
656 };
657 }
658 v
659}
660
661pub struct EditableDecl {
674 pub value: String,
675 pub rect: Rect,
677 pub multiline: bool,
678 pub obscure: bool,
679 pub on_change: Arc<dyn Fn(String) + Send + Sync>,
680 pub controller: Option<EditController>,
683 pub layout: TextLayoutSnapshot,
687 pub filters: Vec<InputFilter>,
689}
690
691const COALESCE_WINDOW_SECS: f32 = 0.5;
696
697#[derive(Clone, Debug, PartialEq)]
698struct UndoEntry {
699 inverse: Transaction,
702 selection_before: Selection,
704}
705
706#[derive(Clone, Copy, Debug, PartialEq)]
707struct CoalesceInfo {
708 at: f32,
709 cursor_after: usize,
713}
714
715#[derive(Clone, Debug, PartialEq)]
722pub struct TextEditState {
723 pub selection: Selection,
724 pub last_edit_at: f32,
728 undo_stack: Vec<UndoEntry>,
729 redo_stack: Vec<UndoEntry>,
730 coalesce: Option<CoalesceInfo>,
735 pub scroll_x: f32,
741 pub goal_x: Option<f32>,
749 pub last_edit_range: Option<(usize, usize)>,
756 pub ime_range: Option<(usize, usize)>,
763 ime_origin: Option<String>,
771 pub scrolled_cursor: Option<usize>,
781}
782
783impl Default for TextEditState {
784 fn default() -> Self {
785 Self {
786 selection: Selection::default(),
787 last_edit_at: 0.0,
788 undo_stack: Vec::new(),
789 redo_stack: Vec::new(),
790 coalesce: None,
791 scroll_x: 0.0,
792 goal_x: None,
793 last_edit_range: None,
794 ime_range: None,
795 ime_origin: None,
796 scrolled_cursor: None,
797 }
798 }
799}
800
801impl TextEditState {
802 pub fn cursor(&self) -> usize {
805 self.selection.primary().head
806 }
807 pub fn selection_range(&self) -> Option<(usize, usize)> {
810 let r = self.selection.primary();
811 if r.collapsed() { None } else { Some(r.normalized()) }
812 }
813 pub fn can_undo(&self) -> bool {
814 !self.undo_stack.is_empty()
815 }
816 pub fn can_redo(&self) -> bool {
817 !self.redo_stack.is_empty()
818 }
819 pub fn with_selection(&self, selection: Selection, now: f32) -> TextEditState {
825 moved(self, selection, now)
826 }
827}
828
829fn apply_and_record(
836 value: &str,
837 state: &TextEditState,
838 txn: Transaction,
839 new_selection: Selection,
840 now: f32,
841 coalesce_key: Option<usize>,
842) -> (String, TextEditState) {
843 apply_and_record_with_inverse(value, state, txn, None, new_selection, now, coalesce_key)
844}
845
846fn apply_and_record_with_inverse(
852 value: &str,
853 state: &TextEditState,
854 txn: Transaction,
855 inverse_override: Option<Transaction>,
856 new_selection: Selection,
857 now: f32,
858 coalesce_key: Option<usize>,
859) -> (String, TextEditState) {
860 let (new_value, auto_inverse) = txn.apply(value);
861 let inverse = inverse_override.unwrap_or(auto_inverse);
862
863 let can_coalesce = matches!(
864 (coalesce_key, &state.coalesce),
865 (Some(start), Some(info)) if start == info.cursor_after && (now - info.at) < COALESCE_WINDOW_SECS
866 );
867
868 let mut undo_stack = state.undo_stack.clone();
869 if can_coalesce {
870 if let (Some(top), Some(new_edit)) =
871 (undo_stack.last_mut().and_then(|e| e.inverse.edits.first_mut()), inverse.edits.first())
872 {
873 top.range.1 = new_edit.range.1;
876 }
877 } else {
878 undo_stack.push(UndoEntry { inverse, selection_before: state.selection.clone() });
879 }
880
881 let coalesce = coalesce_key.map(|_| CoalesceInfo { at: now, cursor_after: new_selection.primary().head });
882 let last_edit_range = edits_affected_range(&txn.edits);
883
884 let ns = TextEditState {
885 selection: new_selection,
886 last_edit_at: now,
887 undo_stack,
888 redo_stack: Vec::new(),
889 coalesce,
890 scroll_x: state.scroll_x,
891 goal_x: None,
892 last_edit_range,
893 ime_range: None,
894 ime_origin: None,
895 scrolled_cursor: state.scrolled_cursor,
896 };
897 (new_value, ns)
898}
899
900pub fn ime_set_preedit(
917 value: &str, state: &TextEditState, text: &str, cursor_in_text: Option<usize>, now: f32,
918) -> (String, TextEditState) {
919 let (start, end) = state.ime_range.unwrap_or_else(|| state.selection.primary_range());
920 let origin = state.ime_origin.clone().unwrap_or_else(|| {
924 let sb = char_byte_offset(value, start);
925 let eb = char_byte_offset(value, end);
926 value[sb..eb].to_string()
927 });
928 let txn = Transaction::single((start, end), text);
929 let (new_value, _auto_inverse) = txn.apply(value);
930 let len = char_count(text);
931 let new_range = if len == 0 { None } else { Some((start, start + len)) };
932 let new_origin = if len == 0 { None } else { Some(origin) };
933 let cursor = start + cursor_in_text.unwrap_or(len).min(len);
934 let ns = TextEditState {
935 selection: Selection::single(cursor),
936 last_edit_at: now,
937 coalesce: None,
938 goal_x: None,
939 last_edit_range: Some((start, start + len)),
940 ime_range: new_range,
941 ime_origin: new_origin,
942 ..state.clone()
943 };
944 (new_value, ns)
945}
946
947pub fn ime_commit(value: &str, state: &TextEditState, text: &str, now: f32) -> (String, TextEditState) {
957 let (start, end) = state.ime_range.unwrap_or_else(|| state.selection.primary_range());
958 let origin = state.ime_origin.clone().unwrap_or_else(|| {
959 let sb = char_byte_offset(value, start);
960 let eb = char_byte_offset(value, end);
961 value[sb..eb].to_string()
962 });
963 let txn = Transaction::single((start, end), text);
964 let committed_len = char_count(text);
965 let real_inverse = Transaction::single((start, start + committed_len), origin);
966 let cursor = start + committed_len;
967 let (new_value, ns) = apply_and_record_with_inverse(
968 value, state, txn, Some(real_inverse), Selection::single(cursor), now, None,
969 );
970 (new_value, TextEditState { ime_range: None, ime_origin: None, ..ns })
971}
972
973pub fn insert_str(value: &str, state: &TextEditState, text: &str, now: f32) -> (String, TextEditState) {
983 let (start, end) = state.selection.primary_range();
984 let txn = Transaction::single((start, end), text);
985 let new_cursor = start + char_count(text);
986 let coalesce_key = if start == end { Some(start) } else { None };
987 apply_and_record(value, state, txn, Selection::single(new_cursor), now, coalesce_key)
988}
989
990pub fn insert_char(value: &str, state: &TextEditState, ch: char, now: f32) -> (String, TextEditState) {
993 let mut buf = [0u8; 4];
994 insert_str(value, state, ch.encode_utf8(&mut buf), now)
995}
996
997pub fn replace_range(value: &str, state: &TextEditState, start: usize, end: usize, text: &str, now: f32) -> (String, TextEditState) {
1001 let n = char_count(value);
1002 let (s, e) = (start.min(n), end.min(n));
1003 let (s, e) = (s.min(e), s.max(e));
1004 let txn = Transaction::single((s, e), text);
1005 let new_cursor = s + char_count(text);
1006 apply_and_record(value, state, txn, Selection::single(new_cursor), now, None)
1007}
1008
1009pub fn backspace(value: &str, state: &TextEditState, now: f32) -> (String, TextEditState) {
1015 let (start, end) = state.selection.primary_range();
1016 if start != end {
1017 let txn = Transaction::single((start, end), "");
1018 return apply_and_record(value, state, txn, Selection::single(start), now, None);
1019 }
1020 if start == 0 {
1021 return (value.to_string(), TextEditState { last_edit_at: now, coalesce: None, goal_x: None, last_edit_range: None, ime_range: None, ..state.clone() });
1022 }
1023 let prev = prev_grapheme_boundary(value, start);
1024 let txn = Transaction::single((prev, start), "");
1025 apply_and_record(value, state, txn, Selection::single(prev), now, None)
1026}
1027
1028pub fn delete_forward(value: &str, state: &TextEditState, now: f32) -> (String, TextEditState) {
1030 let (start, end) = state.selection.primary_range();
1031 if start != end {
1032 let txn = Transaction::single((start, end), "");
1033 return apply_and_record(value, state, txn, Selection::single(start), now, None);
1034 }
1035 let n = char_count(value);
1036 if start >= n {
1037 return (value.to_string(), TextEditState { last_edit_at: now, coalesce: None, goal_x: None, last_edit_range: None, ime_range: None, ..state.clone() });
1038 }
1039 let next = next_grapheme_boundary(value, start);
1040 let txn = Transaction::single((start, next), "");
1041 apply_and_record(value, state, txn, Selection::single(start), now, None)
1042}
1043
1044pub fn delete_word_back(value: &str, state: &TextEditState, now: f32) -> (String, TextEditState) {
1048 let (start, end) = state.selection.primary_range();
1049 if start != end {
1050 let txn = Transaction::single((start, end), "");
1051 return apply_and_record(value, state, txn, Selection::single(start), now, None);
1052 }
1053 let prev = prev_word_boundary(value, start);
1054 if prev == start {
1055 return (value.to_string(), TextEditState { last_edit_at: now, coalesce: None, goal_x: None, last_edit_range: None, ime_range: None, ..state.clone() });
1056 }
1057 let txn = Transaction::single((prev, start), "");
1058 apply_and_record(value, state, txn, Selection::single(prev), now, None)
1059}
1060
1061pub fn delete_word_forward(value: &str, state: &TextEditState, now: f32) -> (String, TextEditState) {
1063 let (start, end) = state.selection.primary_range();
1064 if start != end {
1065 let txn = Transaction::single((start, end), "");
1066 return apply_and_record(value, state, txn, Selection::single(start), now, None);
1067 }
1068 let next = next_word_boundary(value, start);
1069 if next == start {
1070 return (value.to_string(), TextEditState { last_edit_at: now, coalesce: None, goal_x: None, last_edit_range: None, ime_range: None, ..state.clone() });
1071 }
1072 let txn = Transaction::single((start, next), "");
1073 apply_and_record(value, state, txn, Selection::single(start), now, None)
1074}
1075
1076fn moved(state: &TextEditState, selection: Selection, now: f32) -> TextEditState {
1083 TextEditState { selection, last_edit_at: now, coalesce: None, goal_x: None, last_edit_range: None, ime_range: None, ..state.clone() }
1084}
1085
1086pub fn move_left(value: &str, state: &TextEditState, extend: bool, now: f32) -> TextEditState {
1090 let sel = state.selection.primary();
1091 if !extend && !sel.collapsed() {
1092 return moved(state, Selection::single(sel.normalized().0), now);
1093 }
1094 let prev = prev_grapheme_boundary(value, sel.head);
1095 let new_sel = if extend { Selection::range(sel.anchor, prev) } else { Selection::single(prev) };
1096 moved(state, new_sel, now)
1097}
1098
1099pub fn move_right(value: &str, state: &TextEditState, extend: bool, now: f32) -> TextEditState {
1102 let sel = state.selection.primary();
1103 if !extend && !sel.collapsed() {
1104 return moved(state, Selection::single(sel.normalized().1), now);
1105 }
1106 let next = next_grapheme_boundary(value, sel.head);
1107 let new_sel = if extend { Selection::range(sel.anchor, next) } else { Selection::single(next) };
1108 moved(state, new_sel, now)
1109}
1110
1111pub fn move_word_left(value: &str, state: &TextEditState, extend: bool, now: f32) -> TextEditState {
1113 let sel = state.selection.primary();
1114 let prev = prev_word_boundary(value, sel.head);
1115 let new_sel = if extend { Selection::range(sel.anchor, prev) } else { Selection::single(prev) };
1116 moved(state, new_sel, now)
1117}
1118
1119pub fn move_word_right(value: &str, state: &TextEditState, extend: bool, now: f32) -> TextEditState {
1121 let sel = state.selection.primary();
1122 let next = next_word_boundary(value, sel.head);
1123 let new_sel = if extend { Selection::range(sel.anchor, next) } else { Selection::single(next) };
1124 moved(state, new_sel, now)
1125}
1126
1127pub fn move_home(state: &TextEditState, extend: bool, now: f32) -> TextEditState {
1128 let sel = state.selection.primary();
1129 let new_sel = if extend { Selection::range(sel.anchor, 0) } else { Selection::single(0) };
1130 moved(state, new_sel, now)
1131}
1132
1133pub fn move_end(value: &str, state: &TextEditState, extend: bool, now: f32) -> TextEditState {
1134 let n = char_count(value);
1135 let sel = state.selection.primary();
1136 let new_sel = if extend { Selection::range(sel.anchor, n) } else { Selection::single(n) };
1137 moved(state, new_sel, now)
1138}
1139
1140pub fn select_all(value: &str, state: &TextEditState, now: f32) -> TextEditState {
1141 moved(state, Selection::range(0, char_count(value)), now)
1142}
1143
1144pub fn selected_text(value: &str, state: &TextEditState) -> Option<String> {
1147 state.selection_range().map(|(s, e)| {
1148 let bs = char_byte_offset(value, s);
1149 let be = char_byte_offset(value, e);
1150 value[bs..be].to_string()
1151 })
1152}
1153
1154pub fn undo(value: &str, state: &TextEditState, now: f32) -> Option<(String, TextEditState)> {
1161 let mut undo_stack = state.undo_stack.clone();
1162 let entry = undo_stack.pop()?;
1163 let last_edit_range = edits_affected_range(&entry.inverse.edits);
1164 let (new_value, redo_inverse) = entry.inverse.apply(value);
1165 let mut redo_stack = state.redo_stack.clone();
1166 redo_stack.push(UndoEntry { inverse: redo_inverse, selection_before: state.selection.clone() });
1167 Some((
1168 new_value,
1169 TextEditState {
1170 selection: entry.selection_before, last_edit_at: now, undo_stack, redo_stack,
1171 coalesce: None, scroll_x: state.scroll_x, goal_x: None, last_edit_range, ime_range: None, ime_origin: None,
1172 scrolled_cursor: state.scrolled_cursor,
1173 },
1174 ))
1175}
1176
1177pub fn redo(value: &str, state: &TextEditState, now: f32) -> Option<(String, TextEditState)> {
1180 let mut redo_stack = state.redo_stack.clone();
1181 let entry = redo_stack.pop()?;
1182 let last_edit_range = edits_affected_range(&entry.inverse.edits);
1183 let (new_value, undo_inverse) = entry.inverse.apply(value);
1184 let mut undo_stack = state.undo_stack.clone();
1185 undo_stack.push(UndoEntry { inverse: undo_inverse, selection_before: state.selection.clone() });
1186 Some((
1187 new_value,
1188 TextEditState {
1189 selection: entry.selection_before, last_edit_at: now, undo_stack, redo_stack,
1190 coalesce: None, scroll_x: state.scroll_x, goal_x: None, last_edit_range, ime_range: None, ime_origin: None,
1191 scrolled_cursor: state.scrolled_cursor,
1192 },
1193 ))
1194}
1195
1196#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1204pub enum Command {
1205 MoveLeft, MoveRight, MoveWordLeft, MoveWordRight, MoveHome, MoveEnd,
1206 ExtendLeft, ExtendRight, ExtendWordLeft, ExtendWordRight, ExtendHome, ExtendEnd,
1207 Backspace, DeleteForward, DeleteWordBack, DeleteWordForward,
1208 SelectAll, Copy, Cut, Paste, Undo, Redo,
1209}
1210
1211pub fn apply_command(value: &str, state: &TextEditState, cmd: Command, now: f32) -> Option<(String, TextEditState)> {
1218 use Command::*;
1219 Some(match cmd {
1220 MoveLeft => (value.to_string(), move_left(value, state, false, now)),
1221 ExtendLeft => (value.to_string(), move_left(value, state, true, now)),
1222 MoveRight => (value.to_string(), move_right(value, state, false, now)),
1223 ExtendRight => (value.to_string(), move_right(value, state, true, now)),
1224 MoveWordLeft => (value.to_string(), move_word_left(value, state, false, now)),
1225 ExtendWordLeft => (value.to_string(), move_word_left(value, state, true, now)),
1226 MoveWordRight => (value.to_string(), move_word_right(value, state, false, now)),
1227 ExtendWordRight => (value.to_string(), move_word_right(value, state, true, now)),
1228 MoveHome => (value.to_string(), move_home(state, false, now)),
1229 ExtendHome => (value.to_string(), move_home(state, true, now)),
1230 MoveEnd => (value.to_string(), move_end(value, state, false, now)),
1231 ExtendEnd => (value.to_string(), move_end(value, state, true, now)),
1232 Backspace => backspace(value, state, now),
1233 DeleteForward => delete_forward(value, state, now),
1234 DeleteWordBack => delete_word_back(value, state, now),
1235 DeleteWordForward => delete_word_forward(value, state, now),
1236 SelectAll => (value.to_string(), select_all(value, state, now)),
1237 Undo => return undo(value, state, now),
1238 Redo => return redo(value, state, now),
1239 Copy | Cut | Paste => return None,
1240 })
1241}
1242
1243static CONTROLLER_ID: AtomicU64 = AtomicU64::new(1);
1259
1260#[derive(Clone, Debug)]
1263pub enum ControllerOp {
1264 ReplaceRange(usize, usize, String),
1265 InsertAtCursor(String),
1266 SetSelection(Selection),
1267 SelectAll,
1268 Undo,
1269 Redo,
1270}
1271
1272struct ControllerInner {
1273 id: u64,
1274 ops: Mutex<Vec<ControllerOp>>,
1275 snapshot: Mutex<(String, Selection)>,
1276}
1277
1278#[derive(Clone)]
1279pub struct EditController(Arc<ControllerInner>);
1280
1281impl EditController {
1282 pub fn new() -> Self {
1283 Self(Arc::new(ControllerInner {
1284 id: CONTROLLER_ID.fetch_add(1, Ordering::Relaxed),
1285 ops: Mutex::new(Vec::new()),
1286 snapshot: Mutex::new((String::new(), Selection::default())),
1287 }))
1288 }
1289
1290 pub fn id(&self) -> u64 {
1293 self.0.id
1294 }
1295
1296 fn enqueue(&self, op: ControllerOp) {
1297 self.0.ops.lock().unwrap_or_else(|e| e.into_inner()).push(op);
1298 rosace_state::request_frame();
1299 }
1300
1301 pub fn replace_range(&self, start: usize, end: usize, text: impl Into<String>) {
1305 self.enqueue(ControllerOp::ReplaceRange(start, end, text.into()));
1306 }
1307 pub fn insert_at_cursor(&self, text: impl Into<String>) {
1308 self.enqueue(ControllerOp::InsertAtCursor(text.into()));
1309 }
1310 pub fn set_selection(&self, sel: Selection) {
1311 self.enqueue(ControllerOp::SetSelection(sel));
1312 }
1313 pub fn select_all(&self) {
1314 self.enqueue(ControllerOp::SelectAll);
1315 }
1316 pub fn undo(&self) {
1317 self.enqueue(ControllerOp::Undo);
1318 }
1319 pub fn redo(&self) {
1320 self.enqueue(ControllerOp::Redo);
1321 }
1322
1323 pub fn value(&self) -> String {
1327 self.0.snapshot.lock().unwrap_or_else(|e| e.into_inner()).0.clone()
1328 }
1329 pub fn selection(&self) -> Selection {
1330 self.0.snapshot.lock().unwrap_or_else(|e| e.into_inner()).1.clone()
1331 }
1332
1333 #[doc(hidden)]
1337 pub fn take_ops(&self) -> Vec<ControllerOp> {
1338 std::mem::take(&mut *self.0.ops.lock().unwrap_or_else(|e| e.into_inner()))
1339 }
1340 #[doc(hidden)]
1342 pub fn update_snapshot(&self, value: String, selection: Selection) {
1343 *self.0.snapshot.lock().unwrap_or_else(|e| e.into_inner()) = (value, selection);
1344 }
1345}
1346
1347impl Default for EditController {
1348 fn default() -> Self {
1349 Self::new()
1350 }
1351}
1352
1353impl std::fmt::Debug for EditController {
1354 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1355 write!(f, "EditController(id={})", self.0.id)
1356 }
1357}
1358
1359impl PartialEq for EditController {
1360 fn eq(&self, other: &Self) -> bool {
1361 self.0.id == other.0.id
1362 }
1363}
1364
1365#[cfg(test)]
1366mod tests {
1367 use super::*;
1368
1369 fn st(cursor: usize) -> TextEditState {
1370 TextEditState { selection: Selection::single(cursor), ..Default::default() }
1371 }
1372 fn st_sel(anchor: usize, head: usize) -> TextEditState {
1373 TextEditState { selection: Selection::range(anchor, head), ..Default::default() }
1374 }
1375
1376 #[test]
1379 fn insert_char_at_end() {
1380 let s = st(5);
1381 let (v, ns) = insert_char("hello", &s, '!', 1.0);
1382 assert_eq!(v, "hello!");
1383 assert_eq!(ns.cursor(), 6);
1384 assert_eq!(ns.last_edit_at, 1.0);
1385 assert!(ns.selection_range().is_none());
1386 }
1387
1388 #[test]
1389 fn insert_char_in_middle() {
1390 let (v, ns) = insert_char("helo", &st(3), 'l', 0.0);
1391 assert_eq!(v, "hello");
1392 assert_eq!(ns.cursor(), 4);
1393 }
1394
1395 #[test]
1396 fn insert_str_replaces_selection() {
1397 let s = st_sel(6, 11); let (v, ns) = insert_str("hello world", &s, "there", 2.0);
1399 assert_eq!(v, "hello there");
1400 assert_eq!(ns.cursor(), 11);
1401 assert!(ns.selection_range().is_none());
1402 }
1403
1404 #[test]
1405 fn insert_handles_multibyte_utf8_without_panicking() {
1406 let (v, ns) = insert_char("café", &st(4), '!', 0.0);
1407 assert_eq!(v, "café!");
1408 assert_eq!(ns.cursor(), 5);
1409 }
1410
1411 #[test]
1412 fn backspace_removes_char_before_cursor() {
1413 let (v, ns) = backspace("hello", &st(5), 1.0);
1414 assert_eq!(v, "hell");
1415 assert_eq!(ns.cursor(), 4);
1416 }
1417
1418 #[test]
1419 fn backspace_at_start_is_noop() {
1420 let (v, ns) = backspace("hello", &st(0), 1.0);
1421 assert_eq!(v, "hello");
1422 assert_eq!(ns.cursor(), 0);
1423 }
1424
1425 #[test]
1426 fn backspace_deletes_selection_instead_of_one_char() {
1427 let s = st_sel(1, 5);
1428 let (v, ns) = backspace("hello", &s, 1.0);
1429 assert_eq!(v, "h");
1430 assert_eq!(ns.cursor(), 1);
1431 assert!(ns.selection_range().is_none());
1432 }
1433
1434 #[test]
1435 fn delete_forward_removes_char_after_cursor() {
1436 let (v, ns) = delete_forward("hello", &st(0), 1.0);
1437 assert_eq!(v, "ello");
1438 assert_eq!(ns.cursor(), 0);
1439 }
1440
1441 #[test]
1442 fn delete_forward_at_end_is_noop() {
1443 let (v, _ns) = delete_forward("hello", &st(5), 1.0);
1444 assert_eq!(v, "hello");
1445 }
1446
1447 #[test]
1448 fn move_left_decrements_and_clears_selection() {
1449 let ns = move_left("hello", &st(3), false, 1.0);
1450 assert_eq!(ns.cursor(), 2);
1451 assert!(ns.selection_range().is_none());
1452 }
1453
1454 #[test]
1455 fn move_left_saturates_at_zero() {
1456 let ns = move_left("hello", &st(0), false, 1.0);
1457 assert_eq!(ns.cursor(), 0);
1458 }
1459
1460 #[test]
1461 fn move_left_without_extend_collapses_selection_to_start() {
1462 let s = st_sel(1, 5);
1463 let ns = move_left("hello", &s, false, 1.0);
1464 assert_eq!(ns.cursor(), 1, "must jump to selection start, not head-1");
1465 assert!(ns.selection_range().is_none());
1466 }
1467
1468 #[test]
1469 fn move_right_without_extend_collapses_selection_to_end() {
1470 let s = st_sel(5, 1);
1471 let ns = move_right("hello", &s, false, 1.0);
1472 assert_eq!(ns.cursor(), 5);
1473 assert!(ns.selection_range().is_none());
1474 }
1475
1476 #[test]
1477 fn move_right_extends_selection_from_fresh_anchor() {
1478 let ns = move_right("hello", &st(2), true, 1.0);
1479 assert_eq!(ns.cursor(), 3);
1480 assert_eq!(ns.selection.primary().anchor, 2, "anchor seeds at the pre-move cursor");
1481 }
1482
1483 #[test]
1484 fn move_right_saturates_at_length() {
1485 let ns = move_right("hi", &st(2), false, 1.0);
1486 assert_eq!(ns.cursor(), 2);
1487 }
1488
1489 #[test]
1490 fn shift_arrow_sequence_grows_then_shrinks_selection() {
1491 let s0 = st(2);
1492 let s1 = move_right("hello world", &s0, true, 0.0);
1493 let s2 = move_right("hello world", &s1, true, 0.0);
1494 assert_eq!(s2.selection_range(), Some((2, 4)));
1495 let s3 = move_left("hello world", &s2, true, 0.0);
1496 assert_eq!(s3.selection_range(), Some((2, 3)));
1497 }
1498
1499 #[test]
1500 fn move_home_and_end() {
1501 let h = move_home(&st(3), false, 1.0);
1502 assert_eq!(h.cursor(), 0);
1503 let e = move_end("hello", &st(0), false, 1.0);
1504 assert_eq!(e.cursor(), 5);
1505 }
1506
1507 #[test]
1508 fn select_all_selects_full_range() {
1509 let s = select_all("hello", &st(0), 1.0);
1510 assert_eq!(s.cursor(), 5);
1511 assert_eq!(s.selection_range(), Some((0, 5)));
1512 }
1513
1514 #[test]
1515 fn selected_text_extracts_the_right_substring() {
1516 let s = st_sel(6, 11);
1517 assert_eq!(selected_text("hello world", &s).as_deref(), Some("world"));
1518 }
1519
1520 #[test]
1521 fn selected_text_none_when_anchor_equals_cursor() {
1522 let s = st(3);
1523 assert_eq!(selected_text("hello", &s), None);
1524 assert_eq!(s.selection_range(), None);
1525 }
1526
1527 #[test]
1528 fn selection_range_normalizes_backward_selection() {
1529 let s = st_sel(5, 2); assert_eq!(s.selection_range(), Some((2, 5)));
1531 }
1532
1533 #[test]
1536 fn transaction_apply_and_invert_round_trips() {
1537 let txn = Transaction::single((2, 2), "XY");
1538 let (v1, inv) = txn.apply("hello");
1539 assert_eq!(v1, "heXYllo");
1540 let (v2, _) = inv.apply(&v1);
1541 assert_eq!(v2, "hello", "applying the inverse must reconstruct the original exactly");
1542 }
1543
1544 #[test]
1545 fn undo_reverts_an_insertion_and_restores_prior_selection() {
1546 let s0 = st(0);
1547 let (v1, s1) = insert_str("", &s0, "hi", 1.0);
1548 assert_eq!(v1, "hi");
1549 assert!(s1.can_undo());
1550
1551 let (v2, s2) = undo(&v1, &s1, 2.0).expect("undo must produce a result");
1552 assert_eq!(v2, "");
1553 assert_eq!(s2.cursor(), 0, "must restore the pre-edit selection");
1554 assert!(!s2.can_undo());
1555 assert!(s2.can_redo());
1556 }
1557
1558 #[test]
1559 fn redo_reapplies_an_undone_edit() {
1560 let s0 = st(0);
1561 let (v1, s1) = insert_str("", &s0, "hi", 1.0);
1562 let (v2, s2) = undo(&v1, &s1, 2.0).unwrap();
1563 let (v3, s3) = redo(&v2, &s2, 3.0).expect("redo must produce a result");
1564 assert_eq!(v3, "hi");
1565 assert_eq!(s3.cursor(), 2);
1566 assert!(s3.can_undo());
1567 assert!(!s3.can_redo());
1568 }
1569
1570 #[test]
1571 fn undo_on_empty_stack_is_none() {
1572 let s0 = st(0);
1573 assert!(undo("hello", &s0, 1.0).is_none());
1574 }
1575
1576 #[test]
1577 fn a_real_edit_after_undo_clears_the_redo_stack() {
1578 let s0 = st(0);
1579 let (v1, s1) = insert_str("", &s0, "a", 1.0);
1580 let (v2, s2) = undo(&v1, &s1, 2.0).unwrap();
1581 assert!(s2.can_redo());
1582 let (_, s3) = insert_str(&v2, &s2, "b", 3.0);
1583 assert!(!s3.can_redo(), "a fresh edit must invalidate the old redo branch");
1584 }
1585
1586 #[test]
1587 fn consecutive_typing_coalesces_into_one_undo_unit() {
1588 let s0 = st(0);
1589 let (v1, s1) = insert_char("", &s0, 'a', 1.0);
1590 let (v2, s2) = insert_char(&v1, &s1, 'b', 1.1);
1591 let (v3, s3) = insert_char(&v2, &s2, 'c', 1.2);
1592 assert_eq!(v3, "abc");
1593
1594 let (v4, s4) = undo(&v3, &s3, 2.0).expect("one undo");
1595 assert_eq!(v4, "", "one undo must remove the WHOLE typed group");
1596 assert_eq!(s4.cursor(), 0, "must restore the selection from BEFORE the whole group");
1597 assert!(!s4.can_undo(), "the group must have been a single undo entry");
1598 }
1599
1600 #[test]
1601 fn typing_separated_by_a_pause_does_not_coalesce() {
1602 let s0 = st(0);
1603 let (v1, s1) = insert_char("", &s0, 'a', 0.0);
1604 let (v2, s2) = insert_char(&v1, &s1, 'b', 0.0 + COALESCE_WINDOW_SECS + 0.01);
1606 assert_eq!(v2, "ab");
1607 let (v3, s3) = undo(&v2, &s2, 1.0).unwrap();
1608 assert_eq!(v3, "a", "only the second, un-coalesced char should undo");
1609 assert!(s3.can_undo(), "the first char's group must still be on the stack");
1610 }
1611
1612 #[test]
1613 fn typing_after_a_cursor_move_does_not_coalesce_with_earlier_typing() {
1614 let s0 = st(0);
1615 let (v1, s1) = insert_char("", &s0, 'a', 1.0);
1616 let s1_moved = move_left("a", &s1, false, 1.05); let (v2, s2) = insert_char(&v1, &s1_moved, 'b', 1.06);
1618 assert_eq!(v2, "ba");
1619 let (v3, s3) = undo(&v2, &s2, 2.0).unwrap();
1620 assert_eq!(v3, "a", "only the second char's group should undo");
1621 assert!(s3.can_undo());
1622 }
1623
1624 #[test]
1625 fn replacing_a_selection_does_not_coalesce_with_prior_typing() {
1626 let s0 = st(0);
1627 let (v1, s1) = insert_str("", &s0, "hello", 1.0);
1628 let s1_sel = TextEditState { selection: Selection::range(1, 3), ..s1.clone() };
1629 let (v2, s2) = insert_str(&v1, &s1_sel, "X", 1.1);
1630 assert_eq!(v2, "hXlo");
1631 let (v3, _) = undo(&v2, &s2, 2.0).unwrap();
1632 assert_eq!(v3, "hello", "undoing the selection-replace must not also undo the typed word");
1633 }
1634
1635 #[test]
1638 fn backspace_deletes_a_whole_zwj_family_emoji_in_one_press() {
1639 let family = "👨\u{200D}👩\u{200D}👧\u{200D}👦";
1641 let n = char_count(family);
1642 let s = st(n);
1643 let (v, ns) = backspace(family, &s, 1.0);
1644 assert_eq!(v, "", "the whole cluster must vanish in one Backspace, not one char at a time");
1645 assert_eq!(ns.cursor(), 0);
1646 }
1647
1648 #[test]
1649 fn move_left_steps_over_a_combining_accent_as_one_unit() {
1650 let s = "e\u{0301}x"; assert_eq!(char_count(s), 3);
1653 let state = st(3); let after_one_left = move_left(s, &state, false, 1.0);
1655 assert_eq!(after_one_left.cursor(), 2, "must land after the é-cluster, before x");
1656 let after_two_left = move_left(s, &after_one_left, false, 1.0);
1657 assert_eq!(after_two_left.cursor(), 0, "the combining accent must not be a stop of its own");
1658 }
1659
1660 #[test]
1661 fn delete_forward_removes_a_flag_emoji_as_one_grapheme() {
1662 let flag = "🇮🇳x";
1665 let s = st(0);
1666 let (v, ns) = delete_forward(flag, &s, 1.0);
1667 assert_eq!(v, "x", "the flag must vanish as one unit, not one regional indicator at a time");
1668 assert_eq!(ns.cursor(), 0);
1669 }
1670
1671 #[test]
1672 fn plain_ascii_grapheme_boundaries_match_char_boundaries() {
1673 assert_eq!(grapheme_boundaries("abc"), vec![0, 1, 2, 3]);
1676 }
1677
1678 #[test]
1681 fn move_word_right_lands_at_the_end_of_the_next_word() {
1682 let s = st(0);
1683 let ns = move_word_right("hello world", &s, false, 1.0);
1684 assert_eq!(ns.cursor(), 5);
1685 let ns2 = move_word_right("hello world", &ns, false, 1.0);
1686 assert_eq!(ns2.cursor(), 11);
1687 }
1688
1689 #[test]
1690 fn move_word_left_lands_at_the_start_of_the_previous_word() {
1691 let s = st(11); let ns = move_word_left("hello world", &s, false, 1.0);
1693 assert_eq!(ns.cursor(), 6);
1694 let ns2 = move_word_left("hello world", &ns, false, 1.0);
1695 assert_eq!(ns2.cursor(), 0);
1696 }
1697
1698 #[test]
1699 fn delete_word_back_removes_the_preceding_word() {
1700 let s = st(11); let (v, ns) = delete_word_back("hello world", &s, 1.0);
1702 assert_eq!(v, "hello ");
1703 assert_eq!(ns.cursor(), 6);
1704 }
1705
1706 #[test]
1707 fn delete_word_forward_removes_the_following_word() {
1708 let s = st(0);
1709 let (v, ns) = delete_word_forward("hello world", &s, 1.0);
1710 assert_eq!(v, " world");
1711 assert_eq!(ns.cursor(), 0);
1712 }
1713
1714 #[test]
1715 fn extend_word_right_selects_through_a_word() {
1716 let s = st(0);
1717 let ns = move_word_right("hello world", &s, true, 1.0);
1718 assert_eq!(ns.selection_range(), Some((0, 5)));
1719 }
1720
1721 #[test]
1724 fn apply_command_backspace_matches_the_direct_call() {
1725 let s = st(5);
1726 let (v1, s1) = apply_command("hello", &s, Command::Backspace, 1.0).unwrap();
1727 let (v2, s2) = backspace("hello", &s, 1.0);
1728 assert_eq!(v1, v2);
1729 assert_eq!(s1.cursor(), s2.cursor());
1730 }
1731
1732 #[test]
1733 fn apply_command_clipboard_commands_return_none() {
1734 let s = st(0);
1735 assert!(apply_command("hello", &s, Command::Copy, 1.0).is_none());
1736 assert!(apply_command("hello", &s, Command::Cut, 1.0).is_none());
1737 assert!(apply_command("hello", &s, Command::Paste, 1.0).is_none());
1738 }
1739
1740 #[test]
1741 fn apply_command_undo_on_empty_history_returns_none() {
1742 let s = st(0);
1743 assert!(apply_command("hello", &s, Command::Undo, 1.0).is_none());
1744 }
1745
1746 #[test]
1749 fn edit_controller_replace_range_wraps_a_selection_like_a_toolbar_button() {
1750 let value = "hello world";
1751 let state = st_sel(6, 11); let controller = EditController::new();
1754 assert!(controller.take_ops().is_empty());
1755
1756 let (start, end) = state.selection_range().unwrap();
1759 controller.replace_range(start, end, format!("**{}**", &value[start..end]));
1760
1761 let ops = controller.take_ops();
1762 assert_eq!(ops.len(), 1);
1763 let ControllerOp::ReplaceRange(s, e, text) = &ops[0] else { panic!("expected ReplaceRange") };
1764 assert_eq!((*s, *e, text.as_str()), (6, 11, "**world**"));
1765
1766 let (new_value, new_state) = replace_range(value, &state, *s, *e, text, 1.0);
1767 assert_eq!(new_value, "hello **world**");
1768 assert_eq!(new_state.cursor(), 15);
1769
1770 controller.update_snapshot(new_value.clone(), new_state.selection.clone());
1771 assert_eq!(controller.value(), "hello **world**");
1772 }
1773
1774 #[test]
1775 fn edit_controller_has_a_stable_id_distinct_from_other_controllers() {
1776 let a = EditController::new();
1777 let b = EditController::new();
1778 assert_ne!(a.id(), b.id());
1779 assert_eq!(a.clone().id(), a.id(), "cloning must share identity, not create a new controller");
1780 }
1781
1782 #[test]
1783 fn edit_controller_undo_redo_ops_enqueue_correctly() {
1784 let c = EditController::new();
1785 c.undo();
1786 c.redo();
1787 c.select_all();
1788 let ops = c.take_ops();
1789 assert_eq!(ops.len(), 3);
1790 assert!(matches!(ops[0], ControllerOp::Undo));
1791 assert!(matches!(ops[1], ControllerOp::Redo));
1792 assert!(matches!(ops[2], ControllerOp::SelectAll));
1793 }
1794
1795 type RunBits = (usize, usize, Option<(u8, u8, u8, u8)>, Option<FontWeight>);
1802
1803 fn runs_bits(runs: &[(usize, usize, Option<Color>, Option<FontWeight>)]) -> Vec<RunBits> {
1804 runs.iter().map(|&(a, b, c, w)| (a, b, c.map(color_bits), w)).collect()
1805 }
1806
1807 #[test]
1808 fn style_runs_with_no_spans_is_one_default_run_covering_the_whole_line() {
1809 let runs = style_runs(&[], 0, 10);
1810 assert_eq!(runs_bits(&runs), vec![(0, 10, None, None)]);
1811 }
1812
1813 #[test]
1814 fn style_runs_splits_around_a_span_leaving_default_runs_in_the_gaps() {
1815 let spans = vec![Span::new((8, 13)).color(Color::rgb(255, 0, 0))];
1817 let runs = style_runs(&spans, 0, 15);
1818 assert_eq!(runs_bits(&runs), vec![
1819 (0, 8, None, None),
1820 (8, 13, Some((255, 0, 0, 255)), None),
1821 (13, 15, None, None),
1822 ]);
1823 }
1824
1825 #[test]
1826 fn style_runs_clips_a_span_that_extends_past_the_requested_range() {
1827 let spans = vec![Span::new((3, 20)).weight(FontWeight::Bold)];
1830 let runs = style_runs(&spans, 0, 5);
1831 assert_eq!(runs_bits(&runs), vec![(0, 3, None, None), (3, 5, None, Some(FontWeight::Bold))]);
1832 }
1833
1834 #[test]
1835 fn style_runs_last_matching_span_wins_on_overlap() {
1836 let spans = vec![
1837 Span::new((0, 10)).color(Color::rgb(1, 1, 1)),
1838 Span::new((0, 10)).color(Color::rgb(2, 2, 2)),
1839 ];
1840 let runs = style_runs(&spans, 0, 10);
1841 assert_eq!(runs_bits(&runs), vec![(0, 10, Some((2, 2, 2, 255)), None)]);
1842 }
1843
1844 #[test]
1845 fn style_runs_on_an_empty_range_returns_nothing() {
1846 assert!(style_runs(&[], 5, 5).is_empty());
1847 }
1848
1849 #[test]
1850 fn cursor_style_default_matches_the_pre_step5_hardcoded_caret() {
1851 let s = CursorStyle::default();
1852 assert_eq!(s.width, 1.5);
1853 assert_eq!(s.blink_rate, 0.53);
1854 assert_eq!(s.shape, CursorShape::Bar);
1855 }
1856
1857 #[test]
1858 fn typing_sets_last_edit_range_to_just_the_inserted_text_not_the_whole_document() {
1859 let value = "hello world, this is a long sentence";
1860 let state = st(value.chars().count());
1861 let (_, ns) = insert_char(value, &state, '!', 1.0);
1862 assert_eq!(
1863 ns.last_edit_range,
1864 Some((value.chars().count(), value.chars().count() + 1)),
1865 "an append must report only the newly inserted char's range, not (0, whole_len)"
1866 );
1867 }
1868
1869 #[test]
1870 fn moving_the_cursor_clears_last_edit_range() {
1871 let value = "hello";
1872 let state = st(0);
1873 let after_type = insert_char(value, &state, 'X', 1.0).1;
1874 assert!(after_type.last_edit_range.is_some());
1875 let after_move = move_right(value, &after_type, false, 1.0);
1876 assert_eq!(after_move.last_edit_range, None, "a pure cursor move is not a content edit");
1877 }
1878
1879 #[test]
1882 fn ime_preedit_inserts_provisional_text_at_the_cursor() {
1883 let (v, ns) = ime_set_preedit("hello ", &st(6), "に", None, 1.0);
1884 assert_eq!(v, "hello に");
1885 assert_eq!(ns.ime_range, Some((6, 7)));
1886 assert_eq!(ns.cursor(), 7, "cursor defaults to the end of the preedit text");
1887 }
1888
1889 #[test]
1890 fn ime_preedit_does_not_touch_the_undo_stack() {
1891 let s = st(0);
1892 assert!(!s.can_undo());
1893 let (_, ns) = ime_set_preedit("", &s, "に", None, 1.0);
1894 assert!(!ns.can_undo(), "a preedit update must not create an undo entry");
1895 }
1896
1897 #[test]
1898 fn a_second_preedit_update_replaces_the_first_not_appends() {
1899 let (v1, ns1) = ime_set_preedit("", &st(0), "に", None, 1.0);
1903 assert_eq!(v1, "に");
1904 let (v2, ns2) = ime_set_preedit(&v1, &ns1, "にほ", None, 1.0);
1905 assert_eq!(v2, "にほ");
1906 assert_eq!(ns2.ime_range, Some((0, 2)));
1907 }
1908
1909 #[test]
1910 fn ime_preedit_respects_the_platforms_cursor_position_within_the_text() {
1911 let (_, ns) = ime_set_preedit("", &st(0), "にほん", Some(1), 1.0);
1912 assert_eq!(ns.cursor(), 1, "cursor must land where the IME says, not always at the end");
1913 }
1914
1915 #[test]
1916 fn empty_preedit_clears_the_provisional_text_and_range() {
1917 let (v1, ns1) = ime_set_preedit("", &st(0), "に", None, 1.0);
1918 let (v2, ns2) = ime_set_preedit(&v1, &ns1, "", None, 1.0);
1919 assert_eq!(v2, "");
1920 assert_eq!(ns2.ime_range, None);
1921 }
1922
1923 #[test]
1924 fn ime_commit_finalizes_as_one_real_undoable_edit_and_clears_ime_range() {
1925 let (v1, ns1) = ime_set_preedit("", &st(0), "に", None, 1.0);
1926 let (v2, ns2) = ime_set_preedit(&v1, &ns1, "にほ", None, 1.0);
1927 let (v3, ns3) = ime_commit(&v2, &ns2, "日本", 1.0);
1928 assert_eq!(v3, "日本");
1929 assert_eq!(ns3.ime_range, None);
1930 assert_eq!(ns3.cursor(), char_count("日本"));
1931 assert!(ns3.can_undo(), "commit must produce a real, undoable edit");
1932
1933 let (v4, ns4) = undo(&v3, &ns3, 2.0).expect("commit must be undoable");
1935 assert_eq!(v4, "");
1936 assert!(!ns4.can_undo(), "undoing the commit must remove the ONLY undo entry the whole composition produced");
1937 }
1938
1939 #[test]
1940 fn ime_commit_with_no_prior_preedit_replaces_the_selection_like_a_normal_insert() {
1941 let (v, ns) = ime_commit("hello", &st_sel(1, 3), "X", 1.0);
1942 assert_eq!(v, "hXlo");
1943 assert_eq!(ns.cursor(), 2);
1944 }
1945
1946 #[test]
1949 fn apply_filters_with_no_filters_is_a_no_op() {
1950 assert_eq!(apply_filters("hello", &[]), "hello");
1951 }
1952
1953 #[test]
1954 fn max_length_truncates_from_the_end() {
1955 let f = [InputFilter::max_length(3)];
1956 assert_eq!(apply_filters("hello", &f), "hel");
1957 }
1958
1959 #[test]
1960 fn max_length_leaves_a_shorter_value_untouched() {
1961 let f = [InputFilter::max_length(10)];
1962 assert_eq!(apply_filters("hi", &f), "hi");
1963 }
1964
1965 #[test]
1966 fn digits_strips_non_digit_characters() {
1967 let f = [InputFilter::digits()];
1968 assert_eq!(apply_filters("a1b2c3", &f), "123");
1969 }
1970
1971 #[test]
1972 fn alphanumeric_strips_punctuation_and_spaces() {
1973 let f = [InputFilter::alphanumeric()];
1974 assert_eq!(apply_filters("ab! 12-cd", &f), "ab12cd");
1975 }
1976
1977 #[test]
1978 fn custom_char_class_filter() {
1979 let f = [InputFilter::char_class(|c| c == 'x' || c == 'y')];
1980 assert_eq!(apply_filters("xayzbx", &f), "xyx");
1981 }
1982
1983 #[test]
1984 fn filters_apply_in_order() {
1985 let f = [InputFilter::digits(), InputFilter::max_length(2)];
1987 assert_eq!(apply_filters("a1b2c3", &f), "12");
1988 }
1989}