cranpose_foundation/text/
state.rs1use super::{TextFieldBuffer, TextRange};
7use std::cell::{Cell, RefCell};
8use std::collections::VecDeque;
9use std::rc::Rc;
10
11#[derive(Debug, Clone, PartialEq, Eq, Default)]
15pub struct TextFieldValue {
16 pub text: String,
18 pub selection: TextRange,
20 pub composition: Option<TextRange>,
22}
23
24impl TextFieldValue {
25 pub fn new(text: impl Into<String>) -> Self {
27 let text = text.into();
28 let len = text.len();
29 Self {
30 text,
31 selection: TextRange::cursor(len),
32 composition: None,
33 }
34 }
35
36 pub fn with_selection(text: impl Into<String>, selection: TextRange) -> Self {
38 let text = text.into();
39 let selection = selection.coerce_in(text.len());
40 Self {
41 text,
42 selection,
43 composition: None,
44 }
45 }
46}
47
48type ChangeListener = Box<dyn Fn(&TextFieldValue)>;
49
50const UNDO_CAPACITY: usize = 100;
52
53const UNDO_COALESCE_MS: u128 = 1000;
56
57pub struct TextFieldStateInner {
60 is_editing: bool,
62 listeners: Vec<ChangeListener>,
64 undo_stack: VecDeque<TextFieldValue>,
66 redo_stack: VecDeque<TextFieldValue>,
68 desired_column: Cell<Option<usize>>,
70 last_edit_time: Cell<Option<web_time::Instant>>,
72 pending_undo_snapshot: RefCell<Option<TextFieldValue>>,
75 line_offsets_cache: RefCell<Option<Vec<usize>>>,
79}
80
81struct EditGuard<'a> {
83 inner: &'a RefCell<TextFieldStateInner>,
84}
85
86impl<'a> EditGuard<'a> {
87 fn new(inner: &'a RefCell<TextFieldStateInner>) -> Result<Self, ()> {
88 {
89 let borrowed = inner.borrow();
90 if borrowed.is_editing {
91 return Err(()); }
93 }
94 inner.borrow_mut().is_editing = true;
95 Ok(Self { inner })
96 }
97}
98
99impl Drop for EditGuard<'_> {
100 fn drop(&mut self) {
101 self.inner.borrow_mut().is_editing = false;
102 }
103}
104
105#[derive(Clone)]
131pub struct TextFieldState {
132 pub inner: Rc<RefCell<TextFieldStateInner>>,
135
136 value: Rc<cranpose_core::OwnedMutableState<TextFieldValue>>,
139}
140
141impl std::fmt::Debug for TextFieldState {
142 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143 self.value.with(|v| {
144 f.debug_struct("TextFieldState")
145 .field("text", &v.text)
146 .field("selection", &v.selection)
147 .finish()
148 })
149 }
150}
151
152impl TextFieldState {
153 pub fn new(initial_text: impl Into<String>) -> Self {
155 let initial_value = TextFieldValue::new(initial_text);
156 Self {
157 inner: Rc::new(RefCell::new(TextFieldStateInner {
158 is_editing: false,
159 listeners: Vec::new(),
160 undo_stack: VecDeque::new(),
161 redo_stack: VecDeque::new(),
162 desired_column: Cell::new(None),
163 last_edit_time: Cell::new(None),
164 pending_undo_snapshot: RefCell::new(None),
165 line_offsets_cache: RefCell::new(None),
166 })),
167 value: Rc::new(cranpose_core::ownedMutableStateOf(initial_value)),
168 }
169 }
170
171 pub fn with_selection(initial_text: impl Into<String>, selection: TextRange) -> Self {
173 let initial_value = TextFieldValue::with_selection(initial_text, selection);
174 Self {
175 inner: Rc::new(RefCell::new(TextFieldStateInner {
176 is_editing: false,
177 listeners: Vec::new(),
178 undo_stack: VecDeque::new(),
179 redo_stack: VecDeque::new(),
180 desired_column: Cell::new(None),
181 last_edit_time: Cell::new(None),
182 pending_undo_snapshot: RefCell::new(None),
183 line_offsets_cache: RefCell::new(None),
184 })),
185 value: Rc::new(cranpose_core::ownedMutableStateOf(initial_value)),
186 }
187 }
188
189 pub fn desired_column(&self) -> Option<usize> {
191 self.inner.borrow().desired_column.get()
192 }
193
194 pub fn set_desired_column(&self, col: Option<usize>) {
196 self.inner.borrow().desired_column.set(col);
197 }
198
199 pub fn text(&self) -> String {
202 self.value.with(|v| v.text.clone())
203 }
204
205 pub fn selection(&self) -> TextRange {
207 self.value.with(|v| v.selection)
208 }
209
210 pub fn composition(&self) -> Option<TextRange> {
212 self.value.with(|v| v.composition)
213 }
214
215 pub fn line_offsets(&self) -> Vec<usize> {
224 let inner = self.inner.borrow();
225
226 if let Some(ref offsets) = *inner.line_offsets_cache.borrow() {
228 return offsets.clone();
229 }
230
231 let text = self.text();
233 let mut offsets = vec![0];
234 for (i, c) in text.char_indices() {
235 if c == '\n' {
236 offsets.push(i + 1);
239 }
240 }
241
242 *inner.line_offsets_cache.borrow_mut() = Some(offsets.clone());
244 offsets
245 }
246
247 fn invalidate_line_cache(&self) {
249 self.inner.borrow().line_offsets_cache.borrow_mut().take();
250 }
251
252 pub fn copy_selection(&self) -> Option<String> {
255 self.value.with(|v| {
256 let selection = v.selection;
257 if selection.collapsed() {
258 return None;
259 }
260 let start = selection.min();
261 let end = selection.max();
262 Some(v.text[start..end].to_string())
263 })
264 }
265
266 pub fn value(&self) -> TextFieldValue {
269 self.value.with(|v| v.clone())
270 }
271
272 pub fn add_listener(&self, listener: impl Fn(&TextFieldValue) + 'static) -> usize {
276 let mut inner = self.inner.borrow_mut();
277 let index = inner.listeners.len();
278 inner.listeners.push(Box::new(listener));
279 index
280 }
281
282 pub fn set_selection(&self, selection: TextRange) {
285 let new_value = self.value.with(|v| {
286 let len = v.text.len();
287 TextFieldValue {
288 text: v.text.clone(),
289 selection: selection.coerce_in(len),
290 composition: v.composition,
291 }
292 });
293 self.value.set(new_value);
294 }
295
296 pub fn can_undo(&self) -> bool {
298 !self.inner.borrow().undo_stack.is_empty()
299 }
300
301 pub fn can_redo(&self) -> bool {
303 !self.inner.borrow().redo_stack.is_empty()
304 }
305
306 pub fn undo(&self) -> bool {
309 self.flush_undo_group();
311
312 let mut inner = self.inner.borrow_mut();
313 if let Some(previous_state) = inner.undo_stack.pop_back() {
314 let current = self.value.with(|v| v.clone());
316 inner.redo_stack.push_back(current);
317 inner.last_edit_time.set(None);
319 drop(inner);
320 self.value.set(previous_state);
322 true
323 } else {
324 false
325 }
326 }
327
328 pub fn redo(&self) -> bool {
331 let mut inner = self.inner.borrow_mut();
332 if let Some(redo_state) = inner.redo_stack.pop_back() {
333 let current = self.value.with(|v| v.clone());
335 inner.undo_stack.push_back(current);
336 drop(inner);
337 self.value.set(redo_state);
339 true
340 } else {
341 false
342 }
343 }
344
345 pub fn edit<F>(&self, f: F) -> bool
362 where
363 F: FnOnce(&mut TextFieldBuffer),
364 {
365 let Ok(guard) = EditGuard::new(&self.inner) else {
366 return false;
367 };
368
369 let current = self.value();
371 let mut buffer = TextFieldBuffer::with_selection(¤t.text, current.selection);
372 if let Some(comp) = current.composition {
373 buffer.set_composition(Some(comp));
374 }
375
376 f(&mut buffer);
378
379 let new_value = TextFieldValue {
381 text: buffer.text().to_string(),
382 selection: buffer.selection(),
383 composition: buffer.composition(),
384 };
385
386 let changed = new_value != current;
388 let text_changed = new_value.text != current.text;
389
390 if text_changed {
392 self.invalidate_line_cache();
393 }
394
395 if changed {
396 let now = web_time::Instant::now();
397
398 let should_break_group = {
400 let inner = self.inner.borrow();
401
402 let timeout_expired = inner
404 .last_edit_time
405 .get()
406 .map(|last| now.duration_since(last).as_millis() > UNDO_COALESCE_MS)
407 .unwrap_or(true);
408
409 if timeout_expired {
410 true
411 } else {
412 let text_delta = new_value.text.len() as i64 - current.text.len() as i64;
414 let is_single_char_insert = text_delta == 1;
415
416 let ends_with_whitespace = new_value.text.ends_with(char::is_whitespace);
418
419 let cursor_jumped = new_value.selection.start != current.selection.start + 1
421 && new_value.selection.start != current.selection.end + 1;
422
423 !is_single_char_insert || ends_with_whitespace || cursor_jumped
425 }
426 };
427
428 {
429 let inner = self.inner.borrow();
430
431 if should_break_group {
432 let pending = inner.pending_undo_snapshot.take();
434 drop(inner);
435
436 let mut inner = self.inner.borrow_mut();
437 if let Some(snapshot) = pending {
438 if inner.undo_stack.len() >= UNDO_CAPACITY {
439 inner.undo_stack.pop_front();
440 }
441 inner.undo_stack.push_back(snapshot);
442 }
443 inner.redo_stack.clear();
445 drop(inner);
447 self.inner
448 .borrow()
449 .pending_undo_snapshot
450 .replace(Some(current.clone()));
451 } else {
452 if inner.pending_undo_snapshot.borrow().is_none() {
455 inner.pending_undo_snapshot.replace(Some(current.clone()));
456 }
457 drop(inner);
458 self.inner.borrow_mut().redo_stack.clear();
460 }
461
462 self.inner.borrow().last_edit_time.set(Some(now));
464 }
465
466 self.value.set(new_value.clone());
468 }
469
470 drop(guard);
473
474 if changed {
476 let listener_count = self.inner.borrow().listeners.len();
477 for i in 0..listener_count {
478 let inner = self.inner.borrow();
479 if i < inner.listeners.len() {
480 (inner.listeners[i])(&new_value);
481 }
482 }
483 }
484 true
485 }
486
487 pub fn flush_undo_group(&self) {
490 let inner = self.inner.borrow();
491 if let Some(snapshot) = inner.pending_undo_snapshot.take() {
492 drop(inner);
493 let mut inner = self.inner.borrow_mut();
494 if inner.undo_stack.len() >= UNDO_CAPACITY {
495 inner.undo_stack.pop_front();
496 }
497 inner.undo_stack.push_back(snapshot);
498 }
499 }
500
501 pub fn set_text(&self, text: impl Into<String>) -> bool {
503 let text = text.into();
504 self.edit(|buffer| {
505 buffer.clear();
506 buffer.insert(&text);
507 })
508 }
509
510 pub fn set_text_and_select_all(&self, text: impl Into<String>) -> bool {
512 let text = text.into();
513 self.edit(|buffer| {
514 buffer.clear();
515 buffer.insert(&text);
516 buffer.select_all();
517 })
518 }
519}
520
521impl Default for TextFieldState {
522 fn default() -> Self {
523 Self::new("")
524 }
525}
526
527impl PartialEq for TextFieldState {
528 fn eq(&self, other: &Self) -> bool {
529 Rc::ptr_eq(&self.inner, &other.inner)
531 }
532}
533
534#[cfg(test)]
535mod tests {
536 use super::*;
537 use cranpose_core::{DefaultScheduler, Runtime};
538 use std::sync::Arc;
539
540 fn with_test_runtime<T>(f: impl FnOnce() -> T) -> T {
544 let _runtime = Runtime::new(Arc::new(DefaultScheduler));
545 f()
546 }
547
548 #[test]
549 fn new_state_has_cursor_at_end() {
550 with_test_runtime(|| {
551 let state = TextFieldState::new("Hello");
552 assert_eq!(state.text(), "Hello");
553 assert_eq!(state.selection(), TextRange::cursor(5));
554 });
555 }
556
557 #[test]
558 fn edit_updates_text() {
559 with_test_runtime(|| {
560 let state = TextFieldState::new("Hello");
561 state.edit(|buffer| {
562 buffer.place_cursor_at_end();
563 buffer.insert(", World!");
564 });
565 assert_eq!(state.text(), "Hello, World!");
566 });
567 }
568
569 #[test]
570 fn edit_updates_selection() {
571 with_test_runtime(|| {
572 let state = TextFieldState::new("Hello");
573 state.edit(|buffer| {
574 buffer.select_all();
575 });
576 assert_eq!(state.selection(), TextRange::new(0, 5));
577 });
578 }
579
580 #[test]
581 fn set_text_replaces_content() {
582 with_test_runtime(|| {
583 let state = TextFieldState::new("Hello");
584 state.set_text("Goodbye");
585 assert_eq!(state.text(), "Goodbye");
586 assert_eq!(state.selection(), TextRange::cursor(7));
587 });
588 }
589
590 #[test]
591 fn nested_edit_is_rejected() {
592 with_test_runtime(|| {
593 use std::cell::Cell;
594 use std::rc::Rc;
595
596 let state = TextFieldState::new("Hello");
597 let state_clone = state.clone();
598 let nested_result = Rc::new(Cell::new(true));
599 let nested_result_for_edit = nested_result.clone();
600 let outer_result = state.edit(move |_buffer| {
601 nested_result_for_edit.set(state_clone.edit(|_| {}));
602 });
603 assert!(outer_result);
604 assert!(!nested_result.get());
605 });
606 }
607
608 #[test]
609 fn listener_is_called_on_change() {
610 with_test_runtime(|| {
611 use std::cell::Cell;
612 use std::rc::Rc;
613
614 let state = TextFieldState::new("Hello");
615 let called = Rc::new(Cell::new(false));
616 let called_clone = called.clone();
617
618 state.add_listener(move |_value| {
619 called_clone.set(true);
620 });
621
622 state.edit(|buffer| {
623 buffer.insert("!");
624 });
625
626 assert!(called.get());
627 });
628 }
629}