cranpose_foundation/text/
state.rs1use super::{TextFieldBuffer, TextRange};
7use cranpose_core::MutableState;
8use std::cell::{Cell, RefCell};
9use std::collections::VecDeque;
10use std::rc::Rc;
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;
53
54const UNDO_COALESCE_MS: u128 = 1000;
57
58pub struct TextFieldStateInner {
61 is_editing: bool,
63 listeners: Vec<ChangeListener>,
65 undo_stack: VecDeque<TextFieldValue>,
67 redo_stack: VecDeque<TextFieldValue>,
69 desired_column: Cell<Option<usize>>,
71 last_edit_time: Cell<Option<web_time::Instant>>,
73 pending_undo_snapshot: RefCell<Option<TextFieldValue>>,
76 line_offsets_cache: RefCell<Option<Vec<usize>>>,
80}
81
82struct EditGuard<'a> {
84 inner: &'a RefCell<TextFieldStateInner>,
85}
86
87impl<'a> EditGuard<'a> {
88 fn new(inner: &'a RefCell<TextFieldStateInner>) -> Result<Self, ()> {
89 {
90 let borrowed = inner.borrow();
91 if borrowed.is_editing {
92 return Err(()); }
94 }
95 inner.borrow_mut().is_editing = true;
96 Ok(Self { inner })
97 }
98}
99
100impl Drop for EditGuard<'_> {
101 fn drop(&mut self) {
102 self.inner.borrow_mut().is_editing = false;
103 }
104}
105
106#[derive(Clone)]
132pub struct TextFieldState {
133 pub inner: Rc<RefCell<TextFieldStateInner>>,
136
137 value: Rc<MutableState<TextFieldValue>>,
140}
141
142impl std::fmt::Debug for TextFieldState {
143 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144 self.value.with(|v| {
145 f.debug_struct("TextFieldState")
146 .field("text", &v.text)
147 .field("selection", &v.selection)
148 .finish()
149 })
150 }
151}
152
153impl TextFieldState {
154 pub fn new(initial_text: impl Into<String>) -> Self {
156 let initial_value = TextFieldValue::new(initial_text);
157 Self {
158 inner: Rc::new(RefCell::new(TextFieldStateInner {
159 is_editing: false,
160 listeners: Vec::new(),
161 undo_stack: VecDeque::new(),
162 redo_stack: VecDeque::new(),
163 desired_column: Cell::new(None),
164 last_edit_time: Cell::new(None),
165 pending_undo_snapshot: RefCell::new(None),
166 line_offsets_cache: RefCell::new(None),
167 })),
168 value: Rc::new(cranpose_core::mutableStateOf(initial_value)),
169 }
170 }
171
172 pub fn with_selection(initial_text: impl Into<String>, selection: TextRange) -> Self {
174 let initial_value = TextFieldValue::with_selection(initial_text, selection);
175 Self {
176 inner: Rc::new(RefCell::new(TextFieldStateInner {
177 is_editing: false,
178 listeners: Vec::new(),
179 undo_stack: VecDeque::new(),
180 redo_stack: VecDeque::new(),
181 desired_column: Cell::new(None),
182 last_edit_time: Cell::new(None),
183 pending_undo_snapshot: RefCell::new(None),
184 line_offsets_cache: RefCell::new(None),
185 })),
186 value: Rc::new(cranpose_core::mutableStateOf(initial_value)),
187 }
188 }
189
190 pub fn desired_column(&self) -> Option<usize> {
192 self.inner.borrow().desired_column.get()
193 }
194
195 pub fn set_desired_column(&self, col: Option<usize>) {
197 self.inner.borrow().desired_column.set(col);
198 }
199
200 pub fn text(&self) -> String {
203 self.value.with(|v| v.text.clone())
204 }
205
206 pub fn selection(&self) -> TextRange {
208 self.value.with(|v| v.selection)
209 }
210
211 pub fn composition(&self) -> Option<TextRange> {
213 self.value.with(|v| v.composition)
214 }
215
216 pub fn line_offsets(&self) -> Vec<usize> {
225 let inner = self.inner.borrow();
226
227 if let Some(ref offsets) = *inner.line_offsets_cache.borrow() {
229 return offsets.clone();
230 }
231
232 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);
240 }
241 }
242
243 *inner.line_offsets_cache.borrow_mut() = Some(offsets.clone());
245 offsets
246 }
247
248 fn invalidate_line_cache(&self) {
250 self.inner.borrow().line_offsets_cache.borrow_mut().take();
251 }
252
253 pub fn copy_selection(&self) -> Option<String> {
256 self.value.with(|v| {
257 let selection = v.selection;
258 if selection.collapsed() {
259 return None;
260 }
261 let start = selection.min();
262 let end = selection.max();
263 Some(v.text[start..end].to_string())
264 })
265 }
266
267 pub fn value(&self) -> TextFieldValue {
270 self.value.with(|v| v.clone())
271 }
272
273 pub fn add_listener(&self, listener: impl Fn(&TextFieldValue) + 'static) -> usize {
277 let mut inner = self.inner.borrow_mut();
278 let index = inner.listeners.len();
279 inner.listeners.push(Box::new(listener));
280 index
281 }
282
283 pub fn set_selection(&self, selection: TextRange) {
286 let new_value = self.value.with(|v| {
287 let len = v.text.len();
288 TextFieldValue {
289 text: v.text.clone(),
290 selection: selection.coerce_in(len),
291 composition: v.composition,
292 }
293 });
294 self.value.set(new_value);
295 }
296
297 pub fn can_undo(&self) -> bool {
299 !self.inner.borrow().undo_stack.is_empty()
300 }
301
302 pub fn can_redo(&self) -> bool {
304 !self.inner.borrow().redo_stack.is_empty()
305 }
306
307 pub fn undo(&self) -> bool {
310 self.flush_undo_group();
312
313 let mut inner = self.inner.borrow_mut();
314 if let Some(previous_state) = inner.undo_stack.pop_back() {
315 let current = self.value.with(|v| v.clone());
317 inner.redo_stack.push_back(current);
318 inner.last_edit_time.set(None);
320 drop(inner);
321 self.value.set(previous_state);
323 true
324 } else {
325 false
326 }
327 }
328
329 pub fn redo(&self) -> bool {
332 let mut inner = self.inner.borrow_mut();
333 if let Some(redo_state) = inner.redo_stack.pop_back() {
334 let current = self.value.with(|v| v.clone());
336 inner.undo_stack.push_back(current);
337 drop(inner);
338 self.value.set(redo_state);
340 true
341 } else {
342 false
343 }
344 }
345
346 pub fn edit<F>(&self, f: F)
365 where
366 F: FnOnce(&mut TextFieldBuffer),
367 {
368 let _guard = EditGuard::new(&self.inner)
370 .expect("TextFieldState does not support concurrent or nested editing");
371
372 let current = self.value();
374 let mut buffer = TextFieldBuffer::with_selection(¤t.text, current.selection);
375 if let Some(comp) = current.composition {
376 buffer.set_composition(Some(comp));
377 }
378
379 f(&mut buffer);
381
382 let new_value = TextFieldValue {
384 text: buffer.text().to_string(),
385 selection: buffer.selection(),
386 composition: buffer.composition(),
387 };
388
389 let changed = new_value != current;
391 let text_changed = new_value.text != current.text;
392
393 if text_changed {
395 self.invalidate_line_cache();
396 }
397
398 if changed {
399 let now = web_time::Instant::now();
400
401 let should_break_group = {
403 let inner = self.inner.borrow();
404
405 let timeout_expired = inner
407 .last_edit_time
408 .get()
409 .map(|last| now.duration_since(last).as_millis() > UNDO_COALESCE_MS)
410 .unwrap_or(true);
411
412 if timeout_expired {
413 true
414 } else {
415 let text_delta = new_value.text.len() as i64 - current.text.len() as i64;
417 let is_single_char_insert = text_delta == 1;
418
419 let ends_with_whitespace = new_value.text.ends_with(char::is_whitespace);
421
422 let cursor_jumped = new_value.selection.start != current.selection.start + 1
424 && new_value.selection.start != current.selection.end + 1;
425
426 !is_single_char_insert || ends_with_whitespace || cursor_jumped
428 }
429 };
430
431 {
432 let inner = self.inner.borrow();
433
434 if should_break_group {
435 let pending = inner.pending_undo_snapshot.take();
437 drop(inner);
438
439 let mut inner = self.inner.borrow_mut();
440 if let Some(snapshot) = pending {
441 if inner.undo_stack.len() >= UNDO_CAPACITY {
442 inner.undo_stack.pop_front();
443 }
444 inner.undo_stack.push_back(snapshot);
445 }
446 inner.redo_stack.clear();
448 drop(inner);
450 self.inner
451 .borrow()
452 .pending_undo_snapshot
453 .replace(Some(current.clone()));
454 } else {
455 if inner.pending_undo_snapshot.borrow().is_none() {
458 inner.pending_undo_snapshot.replace(Some(current.clone()));
459 }
460 drop(inner);
461 self.inner.borrow_mut().redo_stack.clear();
463 }
464
465 self.inner.borrow().last_edit_time.set(Some(now));
467 }
468
469 self.value.set(new_value.clone());
471 }
472
473 drop(_guard);
476
477 if changed {
479 let listener_count = self.inner.borrow().listeners.len();
480 for i in 0..listener_count {
481 let inner = self.inner.borrow();
482 if i < inner.listeners.len() {
483 (inner.listeners[i])(&new_value);
484 }
485 }
486 }
487 }
488
489 pub fn flush_undo_group(&self) {
492 let inner = self.inner.borrow();
493 if let Some(snapshot) = inner.pending_undo_snapshot.take() {
494 drop(inner);
495 let mut inner = self.inner.borrow_mut();
496 if inner.undo_stack.len() >= UNDO_CAPACITY {
497 inner.undo_stack.pop_front();
498 }
499 inner.undo_stack.push_back(snapshot);
500 }
501 }
502
503 pub fn set_text(&self, text: impl Into<String>) {
505 let text = text.into();
506 self.edit(|buffer| {
507 buffer.clear();
508 buffer.insert(&text);
509 });
510 }
511
512 pub fn set_text_and_select_all(&self, text: impl Into<String>) {
514 let text = text.into();
515 self.edit(|buffer| {
516 buffer.clear();
517 buffer.insert(&text);
518 buffer.select_all();
519 });
520 }
521}
522
523impl Default for TextFieldState {
524 fn default() -> Self {
525 Self::new("")
526 }
527}
528
529impl PartialEq for TextFieldState {
530 fn eq(&self, other: &Self) -> bool {
531 Rc::ptr_eq(&self.inner, &other.inner)
533 }
534}
535
536#[cfg(test)]
537mod tests {
538 use super::*;
539 use cranpose_core::{DefaultScheduler, Runtime};
540 use std::sync::Arc;
541
542 fn with_test_runtime<T>(f: impl FnOnce() -> T) -> T {
546 let _runtime = Runtime::new(Arc::new(DefaultScheduler));
547 f()
548 }
549
550 #[test]
551 fn new_state_has_cursor_at_end() {
552 with_test_runtime(|| {
553 let state = TextFieldState::new("Hello");
554 assert_eq!(state.text(), "Hello");
555 assert_eq!(state.selection(), TextRange::cursor(5));
556 });
557 }
558
559 #[test]
560 fn edit_updates_text() {
561 with_test_runtime(|| {
562 let state = TextFieldState::new("Hello");
563 state.edit(|buffer| {
564 buffer.place_cursor_at_end();
565 buffer.insert(", World!");
566 });
567 assert_eq!(state.text(), "Hello, World!");
568 });
569 }
570
571 #[test]
572 fn edit_updates_selection() {
573 with_test_runtime(|| {
574 let state = TextFieldState::new("Hello");
575 state.edit(|buffer| {
576 buffer.select_all();
577 });
578 assert_eq!(state.selection(), TextRange::new(0, 5));
579 });
580 }
581
582 #[test]
583 fn set_text_replaces_content() {
584 with_test_runtime(|| {
585 let state = TextFieldState::new("Hello");
586 state.set_text("Goodbye");
587 assert_eq!(state.text(), "Goodbye");
588 assert_eq!(state.selection(), TextRange::cursor(7));
589 });
590 }
591
592 #[test]
593 #[should_panic(expected = "concurrent or nested editing")]
594 fn nested_edit_panics() {
595 with_test_runtime(|| {
596 let state = TextFieldState::new("Hello");
597 let state_clone = state.clone();
598 state.edit(move |_buffer| {
599 state_clone.edit(|_| {}); });
601 });
602 }
603
604 #[test]
605 fn listener_is_called_on_change() {
606 with_test_runtime(|| {
607 use std::cell::Cell;
608 use std::rc::Rc;
609
610 let state = TextFieldState::new("Hello");
611 let called = Rc::new(Cell::new(false));
612 let called_clone = called.clone();
613
614 state.add_listener(move |_value| {
615 called_clone.set(true);
616 });
617
618 state.edit(|buffer| {
619 buffer.insert("!");
620 });
621
622 assert!(called.get());
623 });
624 }
625}