cranpose_foundation/text/
state.rs1use super::{TextFieldBuffer, TextRange};
7use cranpose_core::MutableState;
8use std::cell::{Cell, RefCell};
9use std::collections::VecDeque;
10use std::hash::{Hash, Hasher};
11use std::rc::Rc;
12
13#[derive(Debug, Clone, PartialEq, Eq, Default)]
17pub struct TextFieldValue {
18 pub text: String,
20 pub selection: TextRange,
22 pub composition: Option<TextRange>,
24}
25
26impl TextFieldValue {
27 pub fn new(text: impl Into<String>) -> Self {
29 let text = text.into();
30 let len = text.len();
31 Self {
32 text,
33 selection: TextRange::cursor(len),
34 composition: None,
35 }
36 }
37
38 pub fn with_selection(text: impl Into<String>, selection: TextRange) -> Self {
40 let text = text.into();
41 let selection = selection.coerce_in(text.len());
42 Self {
43 text,
44 selection,
45 composition: None,
46 }
47 }
48}
49
50type ChangeListener = Box<dyn Fn(&TextFieldValue)>;
51
52const UNDO_CAPACITY: usize = 100;
54
55const UNDO_COALESCE_MS: u128 = 1000;
58
59pub struct TextFieldStateInner {
62 is_editing: bool,
64 listeners: Vec<ChangeListener>,
66 undo_stack: VecDeque<TextFieldValue>,
68 redo_stack: VecDeque<TextFieldValue>,
70 desired_column: Cell<Option<usize>>,
72 last_edit_time: Cell<Option<web_time::Instant>>,
74 pending_undo_snapshot: RefCell<Option<TextFieldValue>>,
77 line_offsets_cache: RefCell<Option<Vec<usize>>>,
81}
82
83struct EditGuard<'a> {
85 inner: &'a RefCell<TextFieldStateInner>,
86}
87
88impl<'a> EditGuard<'a> {
89 fn new(inner: &'a RefCell<TextFieldStateInner>) -> Result<Self, ()> {
90 {
91 let borrowed = inner.borrow();
92 if borrowed.is_editing {
93 return Err(()); }
95 }
96 inner.borrow_mut().is_editing = true;
97 Ok(Self { inner })
98 }
99}
100
101impl Drop for EditGuard<'_> {
102 fn drop(&mut self) {
103 self.inner.borrow_mut().is_editing = false;
104 }
105}
106
107#[derive(Clone, Copy)]
133pub struct TextFieldState {
134 pub inner: MutableState<Rc<RefCell<TextFieldStateInner>>>,
137
138 value: MutableState<TextFieldValue>,
141}
142
143impl std::fmt::Debug for TextFieldState {
144 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145 self.value.with(|v| {
146 f.debug_struct("TextFieldState")
147 .field("text", &v.text)
148 .field("selection", &v.selection)
149 .finish()
150 })
151 }
152}
153
154impl TextFieldState {
155 pub fn new(initial_text: impl Into<String>) -> Self {
157 let initial_value = TextFieldValue::new(initial_text);
158 let runtime = cranpose_core::current_runtime_handle()
159 .expect("TextFieldState::new requires an active runtime");
160 Self {
161 inner: MutableState::with_runtime(
162 Rc::new(RefCell::new(TextFieldStateInner {
163 is_editing: false,
164 listeners: Vec::new(),
165 undo_stack: VecDeque::new(),
166 redo_stack: VecDeque::new(),
167 desired_column: Cell::new(None),
168 last_edit_time: Cell::new(None),
169 pending_undo_snapshot: RefCell::new(None),
170 line_offsets_cache: RefCell::new(None),
171 })),
172 runtime.clone(),
173 ),
174 value: MutableState::with_runtime(initial_value, runtime),
175 }
176 }
177
178 pub fn with_selection(initial_text: impl Into<String>, selection: TextRange) -> Self {
180 let initial_value = TextFieldValue::with_selection(initial_text, selection);
181 let runtime = cranpose_core::current_runtime_handle()
182 .expect("TextFieldState::with_selection requires an active runtime");
183 Self {
184 inner: MutableState::with_runtime(
185 Rc::new(RefCell::new(TextFieldStateInner {
186 is_editing: false,
187 listeners: Vec::new(),
188 undo_stack: VecDeque::new(),
189 redo_stack: VecDeque::new(),
190 desired_column: Cell::new(None),
191 last_edit_time: Cell::new(None),
192 pending_undo_snapshot: RefCell::new(None),
193 line_offsets_cache: RefCell::new(None),
194 })),
195 runtime.clone(),
196 ),
197 value: MutableState::with_runtime(initial_value, runtime),
198 }
199 }
200
201 fn inner(&self) -> Rc<RefCell<TextFieldStateInner>> {
202 self.inner.get_non_reactive()
203 }
204
205 pub fn id(&self) -> u64 {
206 let mut hasher = std::collections::hash_map::DefaultHasher::new();
207 self.inner.runtime_state_id().hash(&mut hasher);
208 hasher.finish()
209 }
210
211 pub fn desired_column(&self) -> Option<usize> {
213 self.inner().borrow().desired_column.get()
214 }
215
216 pub fn set_desired_column(&self, col: Option<usize>) {
218 self.inner().borrow().desired_column.set(col);
219 }
220
221 pub fn text(&self) -> String {
224 self.value.with(|v| v.text.clone())
225 }
226
227 pub fn selection(&self) -> TextRange {
229 self.value.with(|v| v.selection)
230 }
231
232 pub fn composition(&self) -> Option<TextRange> {
234 self.value.with(|v| v.composition)
235 }
236
237 pub fn line_offsets(&self) -> Vec<usize> {
246 let inner_state = self.inner();
247 let inner = inner_state.borrow();
248
249 if let Some(ref offsets) = *inner.line_offsets_cache.borrow() {
251 return offsets.clone();
252 }
253
254 let text = self.text();
256 let mut offsets = vec![0];
257 for (i, c) in text.char_indices() {
258 if c == '\n' {
259 offsets.push(i + 1);
262 }
263 }
264
265 *inner.line_offsets_cache.borrow_mut() = Some(offsets.clone());
267 offsets
268 }
269
270 fn invalidate_line_cache(&self) {
272 self.inner().borrow().line_offsets_cache.borrow_mut().take();
273 }
274
275 pub fn copy_selection(&self) -> Option<String> {
278 self.value.with(|v| {
279 let selection = v.selection;
280 if selection.collapsed() {
281 return None;
282 }
283 let start = selection.min();
284 let end = selection.max();
285 Some(v.text[start..end].to_string())
286 })
287 }
288
289 pub fn value(&self) -> TextFieldValue {
292 self.value.with(|v| v.clone())
293 }
294
295 pub fn add_listener(&self, listener: impl Fn(&TextFieldValue) + 'static) -> usize {
299 let inner_state = self.inner();
300 let mut inner = inner_state.borrow_mut();
301 let index = inner.listeners.len();
302 inner.listeners.push(Box::new(listener));
303 index
304 }
305
306 pub fn set_selection(&self, selection: TextRange) {
309 let new_value = self.value.with(|v| {
310 let len = v.text.len();
311 TextFieldValue {
312 text: v.text.clone(),
313 selection: selection.coerce_in(len),
314 composition: v.composition,
315 }
316 });
317 self.value.set(new_value);
318 }
319
320 pub fn can_undo(&self) -> bool {
322 !self.inner().borrow().undo_stack.is_empty()
323 }
324
325 pub fn can_redo(&self) -> bool {
327 !self.inner().borrow().redo_stack.is_empty()
328 }
329
330 pub fn undo(&self) -> bool {
333 self.flush_undo_group();
335
336 let inner_state = self.inner();
337 let mut inner = inner_state.borrow_mut();
338 if let Some(previous_state) = inner.undo_stack.pop_back() {
339 let current = self.value.with(|v| v.clone());
341 inner.redo_stack.push_back(current);
342 inner.last_edit_time.set(None);
344 drop(inner);
345 self.value.set(previous_state);
347 true
348 } else {
349 false
350 }
351 }
352
353 pub fn redo(&self) -> bool {
356 let inner_state = self.inner();
357 let mut inner = inner_state.borrow_mut();
358 if let Some(redo_state) = inner.redo_stack.pop_back() {
359 let current = self.value.with(|v| v.clone());
361 inner.undo_stack.push_back(current);
362 drop(inner);
363 self.value.set(redo_state);
365 true
366 } else {
367 false
368 }
369 }
370
371 pub fn edit<F>(&self, f: F) -> bool
388 where
389 F: FnOnce(&mut TextFieldBuffer),
390 {
391 let inner = self.inner();
392 let Ok(guard) = EditGuard::new(&inner) else {
393 return false;
394 };
395
396 let current = self.value();
398 let mut buffer = TextFieldBuffer::with_selection(¤t.text, current.selection);
399 if let Some(comp) = current.composition {
400 buffer.set_composition(Some(comp));
401 }
402
403 f(&mut buffer);
405
406 let new_value = TextFieldValue {
408 text: buffer.text().to_string(),
409 selection: buffer.selection(),
410 composition: buffer.composition(),
411 };
412
413 let changed = new_value != current;
415 let text_changed = new_value.text != current.text;
416
417 if text_changed {
419 self.invalidate_line_cache();
420 }
421
422 if changed {
423 let now = web_time::Instant::now();
424
425 let should_break_group = {
427 let inner_state = self.inner();
428 let inner = inner_state.borrow();
429
430 let timeout_expired = inner
432 .last_edit_time
433 .get()
434 .map(|last| now.duration_since(last).as_millis() > UNDO_COALESCE_MS)
435 .unwrap_or(true);
436
437 if timeout_expired {
438 true
439 } else {
440 let text_delta = new_value.text.len() as i64 - current.text.len() as i64;
442 let is_single_char_insert = text_delta == 1;
443
444 let ends_with_whitespace = new_value.text.ends_with(char::is_whitespace);
446
447 let cursor_jumped = new_value.selection.start != current.selection.start + 1
449 && new_value.selection.start != current.selection.end + 1;
450
451 !is_single_char_insert || ends_with_whitespace || cursor_jumped
453 }
454 };
455
456 {
457 let inner_state = self.inner();
458 let inner = inner_state.borrow();
459
460 if should_break_group {
461 let pending = inner.pending_undo_snapshot.take();
463 drop(inner);
464
465 let inner_state = self.inner();
466 let mut inner = inner_state.borrow_mut();
467 if let Some(snapshot) = pending {
468 if inner.undo_stack.len() >= UNDO_CAPACITY {
469 inner.undo_stack.pop_front();
470 }
471 inner.undo_stack.push_back(snapshot);
472 }
473 inner.redo_stack.clear();
475 drop(inner);
477 self.inner()
478 .borrow()
479 .pending_undo_snapshot
480 .replace(Some(current.clone()));
481 } else {
482 if inner.pending_undo_snapshot.borrow().is_none() {
485 inner.pending_undo_snapshot.replace(Some(current.clone()));
486 }
487 drop(inner);
488 self.inner().borrow_mut().redo_stack.clear();
490 }
491
492 self.inner().borrow().last_edit_time.set(Some(now));
494 }
495
496 self.value.set(new_value.clone());
498 }
499
500 drop(guard);
503
504 if changed {
506 let listener_count = self.inner().borrow().listeners.len();
507 for i in 0..listener_count {
508 let inner_state = self.inner();
509 let inner = inner_state.borrow();
510 if i < inner.listeners.len() {
511 (inner.listeners[i])(&new_value);
512 }
513 }
514 }
515 true
516 }
517
518 pub fn flush_undo_group(&self) {
521 let inner_state = self.inner();
522 let inner = inner_state.borrow();
523 if let Some(snapshot) = inner.pending_undo_snapshot.take() {
524 drop(inner);
525 let inner_state = self.inner();
526 let mut inner = inner_state.borrow_mut();
527 if inner.undo_stack.len() >= UNDO_CAPACITY {
528 inner.undo_stack.pop_front();
529 }
530 inner.undo_stack.push_back(snapshot);
531 }
532 }
533
534 pub fn set_text(&self, text: impl Into<String>) -> bool {
536 let text = text.into();
537 self.edit(|buffer| {
538 buffer.clear();
539 buffer.insert(&text);
540 })
541 }
542}
543
544impl Default for TextFieldState {
545 fn default() -> Self {
546 Self::new("")
547 }
548}
549
550impl PartialEq for TextFieldState {
551 fn eq(&self, other: &Self) -> bool {
552 self.inner == other.inner
553 }
554}
555
556#[cfg(test)]
557mod tests {
558 use super::*;
559 use cranpose_core::{DefaultScheduler, Runtime};
560 use std::sync::Arc;
561
562 fn with_test_runtime<T>(f: impl FnOnce() -> T) -> T {
566 let _runtime = Runtime::new(Arc::new(DefaultScheduler));
567 f()
568 }
569
570 #[test]
571 fn a_desired_column_is_remembered_until_it_is_cleared() {
572 with_test_runtime(|| {
573 let state = TextFieldState::new("Hello");
574 assert_eq!(
575 state.desired_column(),
576 None,
577 "a field nobody navigated vertically had a remembered column"
578 );
579
580 state.set_desired_column(Some(7));
584 assert_eq!(state.desired_column(), Some(7));
585
586 state.set_desired_column(None);
587 assert_eq!(state.desired_column(), None);
588 });
589 }
590
591 #[test]
592 fn flushing_an_undo_group_is_harmless_when_nothing_is_pending() {
593 with_test_runtime(|| {
594 let state = TextFieldState::new("Hello");
595 state.flush_undo_group();
597 state.flush_undo_group();
598 assert_eq!(state.text(), "Hello");
599 });
600 }
601
602 #[test]
603 fn flushing_an_undo_group_breaks_the_coalescing_between_two_edits() {
604 with_test_runtime(|| {
605 let state = TextFieldState::new("");
606 state.edit(|buffer| buffer.insert("ab"));
607 state.flush_undo_group();
608 state.edit(|buffer| buffer.insert("cd"));
609 assert_eq!(state.text(), "abcd");
610
611 state.undo();
614 assert_eq!(
615 state.text(),
616 "ab",
617 "the flush did not break the two edits apart"
618 );
619 });
620 }
621
622 #[test]
623 fn new_state_has_cursor_at_end() {
624 with_test_runtime(|| {
625 let state = TextFieldState::new("Hello");
626 assert_eq!(state.text(), "Hello");
627 assert_eq!(state.selection(), TextRange::cursor(5));
628 });
629 }
630
631 #[test]
632 fn edit_updates_text() {
633 with_test_runtime(|| {
634 let state = TextFieldState::new("Hello");
635 state.edit(|buffer| {
636 buffer.place_cursor_at_end();
637 buffer.insert(", World!");
638 });
639 assert_eq!(state.text(), "Hello, World!");
640 });
641 }
642
643 #[test]
644 fn edit_updates_selection() {
645 with_test_runtime(|| {
646 let state = TextFieldState::new("Hello");
647 state.edit(|buffer| {
648 buffer.select_all();
649 });
650 assert_eq!(state.selection(), TextRange::new(0, 5));
651 });
652 }
653
654 #[test]
655 fn set_text_replaces_content() {
656 with_test_runtime(|| {
657 let state = TextFieldState::new("Hello");
658 state.set_text("Goodbye");
659 assert_eq!(state.text(), "Goodbye");
660 assert_eq!(state.selection(), TextRange::cursor(7));
661 });
662 }
663
664 #[test]
665 fn nested_edit_is_rejected() {
666 with_test_runtime(|| {
667 use std::cell::Cell;
668 use std::rc::Rc;
669
670 let state = TextFieldState::new("Hello");
671 let state_clone = state;
672 let nested_result = Rc::new(Cell::new(true));
673 let nested_result_for_edit = nested_result.clone();
674 let outer_result = state.edit(move |_buffer| {
675 nested_result_for_edit.set(state_clone.edit(|_| {}));
676 });
677 assert!(outer_result);
678 assert!(!nested_result.get());
679 });
680 }
681
682 #[test]
683 fn listener_is_called_on_change() {
684 with_test_runtime(|| {
685 use std::cell::Cell;
686 use std::rc::Rc;
687
688 let state = TextFieldState::new("Hello");
689 let called = Rc::new(Cell::new(false));
690 let called_clone = called.clone();
691
692 state.add_listener(move |_value| {
693 called_clone.set(true);
694 });
695
696 state.edit(|buffer| {
697 buffer.insert("!");
698 });
699
700 assert!(called.get());
701 });
702 }
703}