cranpose_foundation/text/
state.rs1use std::{
2 cell::{Cell, RefCell},
3 collections::VecDeque,
4 hash::{Hash, Hasher},
5 rc::Rc,
6};
7
8use cranpose_core::MutableState;
9
10use super::{TextFieldBuffer, TextRange};
11
12#[derive(Debug, Clone, PartialEq, Eq, Default)]
16pub struct TextFieldValue {
17 pub text: String,
19 pub selection: TextRange,
21 pub composition: Option<TextRange>,
23}
24
25impl TextFieldValue {
26 pub fn new(text: impl Into<String>) -> Self {
28 let text = text.into();
29 let len = text.len();
30 Self {
31 text,
32 selection: TextRange::cursor(len),
33 composition: None,
34 }
35 }
36
37 pub fn with_selection(text: impl Into<String>, selection: TextRange) -> Self {
39 let text = text.into();
40 let selection = selection.coerce_in(text.len());
41 Self {
42 text,
43 selection,
44 composition: None,
45 }
46 }
47}
48
49type ChangeListener = Box<dyn Fn(&TextFieldValue)>;
50
51const UNDO_CAPACITY: usize = 100;
52
53const UNDO_COALESCE_MS: u128 = 1000;
54
55pub struct TextFieldStateInner {
56 is_editing: bool,
57 listeners: Vec<ChangeListener>,
58 undo_stack: VecDeque<TextFieldValue>,
59 redo_stack: VecDeque<TextFieldValue>,
60 desired_column: Cell<Option<usize>>,
61 last_edit_time: Cell<Option<web_time::Instant>>,
62 pending_undo_snapshot: RefCell<Option<TextFieldValue>>,
63 line_offsets_cache: RefCell<Option<Vec<usize>>>,
64}
65
66struct EditGuard<'a> {
67 inner: &'a RefCell<TextFieldStateInner>,
68}
69
70impl<'a> EditGuard<'a> {
71 fn new(inner: &'a RefCell<TextFieldStateInner>) -> Result<Self, ()> {
72 {
73 let borrowed = inner.borrow();
74 if borrowed.is_editing {
75 return Err(());
76 }
77 }
78 inner.borrow_mut().is_editing = true;
79 Ok(Self { inner })
80 }
81}
82
83impl Drop for EditGuard<'_> {
84 fn drop(&mut self) {
85 self.inner.borrow_mut().is_editing = false;
86 }
87}
88
89#[derive(Clone, Copy)]
115pub struct TextFieldState {
116 pub inner: MutableState<Rc<RefCell<TextFieldStateInner>>>,
119
120 value: MutableState<TextFieldValue>,
121}
122
123impl std::fmt::Debug for TextFieldState {
124 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125 self.value.with(|v| {
126 f.debug_struct("TextFieldState")
127 .field("text", &v.text)
128 .field("selection", &v.selection)
129 .finish()
130 })
131 }
132}
133
134impl TextFieldState {
135 pub fn new(initial_text: impl Into<String>) -> Self {
137 let initial_value = TextFieldValue::new(initial_text);
138 let runtime = cranpose_core::current_runtime_handle()
139 .expect("TextFieldState::new requires an active runtime");
140 Self {
141 inner: MutableState::with_runtime(
142 Rc::new(RefCell::new(TextFieldStateInner {
143 is_editing: false,
144 listeners: Vec::new(),
145 undo_stack: VecDeque::new(),
146 redo_stack: VecDeque::new(),
147 desired_column: Cell::new(None),
148 last_edit_time: Cell::new(None),
149 pending_undo_snapshot: RefCell::new(None),
150 line_offsets_cache: RefCell::new(None),
151 })),
152 runtime.clone(),
153 ),
154 value: MutableState::with_runtime(initial_value, runtime),
155 }
156 }
157
158 pub fn with_selection(initial_text: impl Into<String>, selection: TextRange) -> Self {
160 let initial_value = TextFieldValue::with_selection(initial_text, selection);
161 let runtime = cranpose_core::current_runtime_handle()
162 .expect("TextFieldState::with_selection requires an active runtime");
163 Self {
164 inner: MutableState::with_runtime(
165 Rc::new(RefCell::new(TextFieldStateInner {
166 is_editing: false,
167 listeners: Vec::new(),
168 undo_stack: VecDeque::new(),
169 redo_stack: VecDeque::new(),
170 desired_column: Cell::new(None),
171 last_edit_time: Cell::new(None),
172 pending_undo_snapshot: RefCell::new(None),
173 line_offsets_cache: RefCell::new(None),
174 })),
175 runtime.clone(),
176 ),
177 value: MutableState::with_runtime(initial_value, runtime),
178 }
179 }
180
181 fn inner(&self) -> Rc<RefCell<TextFieldStateInner>> {
182 self.inner.get_non_reactive()
183 }
184
185 pub fn id(&self) -> u64 {
186 let mut hasher = std::collections::hash_map::DefaultHasher::new();
187 self.inner.runtime_state_id().hash(&mut hasher);
188 hasher.finish()
189 }
190
191 pub fn desired_column(&self) -> Option<usize> {
193 self.inner().borrow().desired_column.get()
194 }
195
196 pub fn set_desired_column(&self, col: Option<usize>) {
198 self.inner().borrow().desired_column.set(col);
199 }
200
201 pub fn text(&self) -> String {
204 self.value.with(|v| v.text.clone())
205 }
206
207 pub fn selection(&self) -> TextRange {
209 self.value.with(|v| v.selection)
210 }
211
212 pub fn composition(&self) -> Option<TextRange> {
214 self.value.with(|v| v.composition)
215 }
216
217 pub fn line_offsets(&self) -> Vec<usize> {
226 let inner_state = self.inner();
227 let inner = inner_state.borrow();
228
229 if let Some(ref offsets) = *inner.line_offsets_cache.borrow() {
230 return offsets.clone();
231 }
232
233 let text = self.text();
234 let mut offsets = vec![0];
235 for (i, c) in text.char_indices() {
236 if c == '\n' {
237 offsets.push(i + 1);
238 }
239 }
240
241 *inner.line_offsets_cache.borrow_mut() = Some(offsets.clone());
242 offsets
243 }
244
245 fn invalidate_line_cache(&self) {
246 self.inner().borrow().line_offsets_cache.borrow_mut().take();
247 }
248
249 pub fn copy_selection(&self) -> Option<String> {
252 self.value.with(|v| {
253 let selection = v.selection;
254 if selection.collapsed() {
255 return None;
256 }
257 let start = selection.min();
258 let end = selection.max();
259 Some(v.text[start..end].to_string())
260 })
261 }
262
263 pub fn value(&self) -> TextFieldValue {
266 self.value.with(|v| v.clone())
267 }
268
269 pub fn add_listener(&self, listener: impl Fn(&TextFieldValue) + 'static) -> usize {
273 let inner_state = self.inner();
274 let mut inner = inner_state.borrow_mut();
275 let index = inner.listeners.len();
276 inner.listeners.push(Box::new(listener));
277 index
278 }
279
280 pub fn set_selection(&self, selection: TextRange) {
283 let new_value = self.value.with(|v| {
284 let len = v.text.len();
285 TextFieldValue {
286 text: v.text.clone(),
287 selection: selection.coerce_in(len),
288 composition: v.composition,
289 }
290 });
291 self.value.set(new_value);
292 }
293
294 pub fn can_undo(&self) -> bool {
296 !self.inner().borrow().undo_stack.is_empty()
297 }
298
299 pub fn can_redo(&self) -> bool {
301 !self.inner().borrow().redo_stack.is_empty()
302 }
303
304 pub fn undo(&self) -> bool {
307 self.flush_undo_group();
308
309 let inner_state = self.inner();
310 let mut inner = inner_state.borrow_mut();
311 if let Some(previous_state) = inner.undo_stack.pop_back() {
312 let current = self.value.with(|v| v.clone());
313 inner.redo_stack.push_back(current);
314 inner.last_edit_time.set(None);
315 drop(inner);
316 self.value.set(previous_state);
317 true
318 } else {
319 false
320 }
321 }
322
323 pub fn redo(&self) -> bool {
326 let inner_state = self.inner();
327 let mut inner = inner_state.borrow_mut();
328 if let Some(redo_state) = inner.redo_stack.pop_back() {
329 let current = self.value.with(|v| v.clone());
330 inner.undo_stack.push_back(current);
331 drop(inner);
332 self.value.set(redo_state);
333 true
334 } else {
335 false
336 }
337 }
338
339 pub fn edit<F>(&self, f: F) -> bool
356 where
357 F: FnOnce(&mut TextFieldBuffer),
358 {
359 let inner = self.inner();
360 let Ok(guard) = EditGuard::new(&inner) else {
361 return false;
362 };
363
364 let current = self.value();
365 let mut buffer = TextFieldBuffer::with_selection(¤t.text, current.selection);
366 if let Some(comp) = current.composition {
367 buffer.set_composition(Some(comp));
368 }
369
370 f(&mut buffer);
371
372 let new_value = TextFieldValue {
373 text: buffer.text().to_string(),
374 selection: buffer.selection(),
375 composition: buffer.composition(),
376 };
377
378 let changed = new_value != current;
379 let text_changed = new_value.text != current.text;
380
381 if text_changed {
382 self.invalidate_line_cache();
383 }
384
385 if changed {
386 let now = web_time::Instant::now();
387
388 let should_break_group = {
389 let inner_state = self.inner();
390 let inner = inner_state.borrow();
391
392 let timeout_expired = inner
393 .last_edit_time
394 .get()
395 .map(|last| now.duration_since(last).as_millis() > UNDO_COALESCE_MS)
396 .unwrap_or(true);
397
398 if timeout_expired {
399 true
400 } else {
401 let text_delta = new_value.text.len() as i64 - current.text.len() as i64;
402 let is_single_char_insert = text_delta == 1;
403
404 let ends_with_whitespace = new_value.text.ends_with(char::is_whitespace);
405
406 let cursor_jumped = new_value.selection.start != current.selection.start + 1
407 && new_value.selection.start != current.selection.end + 1;
408
409 !is_single_char_insert || ends_with_whitespace || cursor_jumped
410 }
411 };
412
413 {
414 let inner_state = self.inner();
415 let inner = inner_state.borrow();
416
417 if should_break_group {
418 let pending = inner.pending_undo_snapshot.take();
419 drop(inner);
420
421 let inner_state = self.inner();
422 let mut inner = inner_state.borrow_mut();
423 if let Some(snapshot) = pending {
424 if inner.undo_stack.len() >= UNDO_CAPACITY {
425 inner.undo_stack.pop_front();
426 }
427 inner.undo_stack.push_back(snapshot);
428 }
429 inner.redo_stack.clear();
430 drop(inner);
431 self.inner()
432 .borrow()
433 .pending_undo_snapshot
434 .replace(Some(current.clone()));
435 } else {
436 if inner.pending_undo_snapshot.borrow().is_none() {
437 inner.pending_undo_snapshot.replace(Some(current.clone()));
438 }
439 drop(inner);
440 self.inner().borrow_mut().redo_stack.clear();
441 }
442
443 self.inner().borrow().last_edit_time.set(Some(now));
444 }
445
446 self.value.set(new_value.clone());
447 }
448
449 drop(guard);
450
451 if changed {
452 let listener_count = self.inner().borrow().listeners.len();
453 for i in 0..listener_count {
454 let inner_state = self.inner();
455 let inner = inner_state.borrow();
456 if i < inner.listeners.len() {
457 (inner.listeners[i])(&new_value);
458 }
459 }
460 }
461 true
462 }
463
464 pub fn flush_undo_group(&self) {
467 let inner_state = self.inner();
468 let inner = inner_state.borrow();
469 if let Some(snapshot) = inner.pending_undo_snapshot.take() {
470 drop(inner);
471 let inner_state = self.inner();
472 let mut inner = inner_state.borrow_mut();
473 if inner.undo_stack.len() >= UNDO_CAPACITY {
474 inner.undo_stack.pop_front();
475 }
476 inner.undo_stack.push_back(snapshot);
477 }
478 }
479
480 pub fn set_text(&self, text: impl Into<String>) -> bool {
482 let text = text.into();
483 self.edit(|buffer| {
484 buffer.clear();
485 buffer.insert(&text);
486 })
487 }
488}
489
490impl Default for TextFieldState {
491 fn default() -> Self {
492 Self::new("")
493 }
494}
495
496impl PartialEq for TextFieldState {
497 fn eq(&self, other: &Self) -> bool {
498 self.inner == other.inner
499 }
500}
501
502#[cfg(test)]
503mod tests {
504 use std::sync::Arc;
505
506 use cranpose_core::{DefaultScheduler, Runtime};
507
508 use super::*;
509
510 fn with_test_runtime<T>(f: impl FnOnce() -> T) -> T {
511 let _runtime = Runtime::new(Arc::new(DefaultScheduler));
512 f()
513 }
514
515 #[test]
516 fn a_desired_column_is_remembered_until_it_is_cleared() {
517 with_test_runtime(|| {
518 let state = TextFieldState::new("Hello");
519 assert_eq!(
520 state.desired_column(),
521 None,
522 "a field nobody navigated vertically had a remembered column"
523 );
524
525 state.set_desired_column(Some(7));
526 assert_eq!(state.desired_column(), Some(7));
527
528 state.set_desired_column(None);
529 assert_eq!(state.desired_column(), None);
530 });
531 }
532
533 #[test]
534 fn flushing_an_undo_group_is_harmless_when_nothing_is_pending() {
535 with_test_runtime(|| {
536 let state = TextFieldState::new("Hello");
537 state.flush_undo_group();
538 state.flush_undo_group();
539 assert_eq!(state.text(), "Hello");
540 });
541 }
542
543 #[test]
544 fn flushing_an_undo_group_breaks_the_coalescing_between_two_edits() {
545 with_test_runtime(|| {
546 let state = TextFieldState::new("");
547 state.edit(|buffer| buffer.insert("ab"));
548 state.flush_undo_group();
549 state.edit(|buffer| buffer.insert("cd"));
550 assert_eq!(state.text(), "abcd");
551
552 state.undo();
553 assert_eq!(
554 state.text(),
555 "ab",
556 "the flush did not break the two edits apart"
557 );
558 });
559 }
560
561 #[test]
562 fn new_state_has_cursor_at_end() {
563 with_test_runtime(|| {
564 let state = TextFieldState::new("Hello");
565 assert_eq!(state.text(), "Hello");
566 assert_eq!(state.selection(), TextRange::cursor(5));
567 });
568 }
569
570 #[test]
571 fn edit_updates_text() {
572 with_test_runtime(|| {
573 let state = TextFieldState::new("Hello");
574 state.edit(|buffer| {
575 buffer.place_cursor_at_end();
576 buffer.insert(", World!");
577 });
578 assert_eq!(state.text(), "Hello, World!");
579 });
580 }
581
582 #[test]
583 fn edit_updates_selection() {
584 with_test_runtime(|| {
585 let state = TextFieldState::new("Hello");
586 state.edit(|buffer| {
587 buffer.select_all();
588 });
589 assert_eq!(state.selection(), TextRange::new(0, 5));
590 });
591 }
592
593 #[test]
594 fn set_text_replaces_content() {
595 with_test_runtime(|| {
596 let state = TextFieldState::new("Hello");
597 state.set_text("Goodbye");
598 assert_eq!(state.text(), "Goodbye");
599 assert_eq!(state.selection(), TextRange::cursor(7));
600 });
601 }
602
603 #[test]
604 fn nested_edit_is_rejected() {
605 with_test_runtime(|| {
606 use std::{cell::Cell, rc::Rc};
607
608 let state = TextFieldState::new("Hello");
609 let state_clone = state;
610 let nested_result = Rc::new(Cell::new(true));
611 let nested_result_for_edit = nested_result.clone();
612 let outer_result = state.edit(move |_buffer| {
613 nested_result_for_edit.set(state_clone.edit(|_| {}));
614 });
615 assert!(outer_result);
616 assert!(!nested_result.get());
617 });
618 }
619
620 #[test]
621 fn listener_is_called_on_change() {
622 with_test_runtime(|| {
623 use std::{cell::Cell, rc::Rc};
624
625 let state = TextFieldState::new("Hello");
626 let called = Rc::new(Cell::new(false));
627 let called_clone = called.clone();
628
629 state.add_listener(move |_value| {
630 called_clone.set(true);
631 });
632
633 state.edit(|buffer| {
634 buffer.insert("!");
635 });
636
637 assert!(called.get());
638 });
639 }
640}