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)
364 where
365 F: FnOnce(&mut TextFieldBuffer),
366 {
367 let _guard = EditGuard::new(&self.inner)
369 .expect("TextFieldState does not support concurrent or nested editing");
370
371 let current = self.value();
373 let mut buffer = TextFieldBuffer::with_selection(¤t.text, current.selection);
374 if let Some(comp) = current.composition {
375 buffer.set_composition(Some(comp));
376 }
377
378 f(&mut buffer);
380
381 let new_value = TextFieldValue {
383 text: buffer.text().to_string(),
384 selection: buffer.selection(),
385 composition: buffer.composition(),
386 };
387
388 let changed = new_value != current;
390 let text_changed = new_value.text != current.text;
391
392 if text_changed {
394 self.invalidate_line_cache();
395 }
396
397 if changed {
398 let now = web_time::Instant::now();
399
400 let should_break_group = {
402 let inner = self.inner.borrow();
403
404 let timeout_expired = inner
406 .last_edit_time
407 .get()
408 .map(|last| now.duration_since(last).as_millis() > UNDO_COALESCE_MS)
409 .unwrap_or(true);
410
411 if timeout_expired {
412 true
413 } else {
414 let text_delta = new_value.text.len() as i64 - current.text.len() as i64;
416 let is_single_char_insert = text_delta == 1;
417
418 let ends_with_whitespace = new_value.text.ends_with(char::is_whitespace);
420
421 let cursor_jumped = new_value.selection.start != current.selection.start + 1
423 && new_value.selection.start != current.selection.end + 1;
424
425 !is_single_char_insert || ends_with_whitespace || cursor_jumped
427 }
428 };
429
430 {
431 let inner = self.inner.borrow();
432
433 if should_break_group {
434 let pending = inner.pending_undo_snapshot.take();
436 drop(inner);
437
438 let mut inner = self.inner.borrow_mut();
439 if let Some(snapshot) = pending {
440 if inner.undo_stack.len() >= UNDO_CAPACITY {
441 inner.undo_stack.pop_front();
442 }
443 inner.undo_stack.push_back(snapshot);
444 }
445 inner.redo_stack.clear();
447 drop(inner);
449 self.inner
450 .borrow()
451 .pending_undo_snapshot
452 .replace(Some(current.clone()));
453 } else {
454 if inner.pending_undo_snapshot.borrow().is_none() {
457 inner.pending_undo_snapshot.replace(Some(current.clone()));
458 }
459 drop(inner);
460 self.inner.borrow_mut().redo_stack.clear();
462 }
463
464 self.inner.borrow().last_edit_time.set(Some(now));
466 }
467
468 self.value.set(new_value.clone());
470 }
471
472 drop(_guard);
475
476 if changed {
478 let listener_count = self.inner.borrow().listeners.len();
479 for i in 0..listener_count {
480 let inner = self.inner.borrow();
481 if i < inner.listeners.len() {
482 (inner.listeners[i])(&new_value);
483 }
484 }
485 }
486 }
487
488 pub fn flush_undo_group(&self) {
491 let inner = self.inner.borrow();
492 if let Some(snapshot) = inner.pending_undo_snapshot.take() {
493 drop(inner);
494 let mut inner = self.inner.borrow_mut();
495 if inner.undo_stack.len() >= UNDO_CAPACITY {
496 inner.undo_stack.pop_front();
497 }
498 inner.undo_stack.push_back(snapshot);
499 }
500 }
501
502 pub fn set_text(&self, text: impl Into<String>) {
504 let text = text.into();
505 self.edit(|buffer| {
506 buffer.clear();
507 buffer.insert(&text);
508 });
509 }
510
511 pub fn set_text_and_select_all(&self, text: impl Into<String>) {
513 let text = text.into();
514 self.edit(|buffer| {
515 buffer.clear();
516 buffer.insert(&text);
517 buffer.select_all();
518 });
519 }
520}
521
522impl Default for TextFieldState {
523 fn default() -> Self {
524 Self::new("")
525 }
526}
527
528impl PartialEq for TextFieldState {
529 fn eq(&self, other: &Self) -> bool {
530 Rc::ptr_eq(&self.inner, &other.inner)
532 }
533}
534
535#[cfg(test)]
536mod tests {
537 use super::*;
538 use cranpose_core::{DefaultScheduler, Runtime};
539 use std::sync::Arc;
540
541 fn with_test_runtime<T>(f: impl FnOnce() -> T) -> T {
545 let _runtime = Runtime::new(Arc::new(DefaultScheduler));
546 f()
547 }
548
549 #[test]
550 fn new_state_has_cursor_at_end() {
551 with_test_runtime(|| {
552 let state = TextFieldState::new("Hello");
553 assert_eq!(state.text(), "Hello");
554 assert_eq!(state.selection(), TextRange::cursor(5));
555 });
556 }
557
558 #[test]
559 fn edit_updates_text() {
560 with_test_runtime(|| {
561 let state = TextFieldState::new("Hello");
562 state.edit(|buffer| {
563 buffer.place_cursor_at_end();
564 buffer.insert(", World!");
565 });
566 assert_eq!(state.text(), "Hello, World!");
567 });
568 }
569
570 #[test]
571 fn edit_updates_selection() {
572 with_test_runtime(|| {
573 let state = TextFieldState::new("Hello");
574 state.edit(|buffer| {
575 buffer.select_all();
576 });
577 assert_eq!(state.selection(), TextRange::new(0, 5));
578 });
579 }
580
581 #[test]
582 fn set_text_replaces_content() {
583 with_test_runtime(|| {
584 let state = TextFieldState::new("Hello");
585 state.set_text("Goodbye");
586 assert_eq!(state.text(), "Goodbye");
587 assert_eq!(state.selection(), TextRange::cursor(7));
588 });
589 }
590
591 #[test]
592 #[should_panic(expected = "concurrent or nested editing")]
593 fn nested_edit_panics() {
594 with_test_runtime(|| {
595 let state = TextFieldState::new("Hello");
596 let state_clone = state.clone();
597 state.edit(move |_buffer| {
598 state_clone.edit(|_| {}); });
600 });
601 }
602
603 #[test]
604 fn listener_is_called_on_change() {
605 with_test_runtime(|| {
606 use std::cell::Cell;
607 use std::rc::Rc;
608
609 let state = TextFieldState::new("Hello");
610 let called = Rc::new(Cell::new(false));
611 let called_clone = called.clone();
612
613 state.add_listener(move |_value| {
614 called_clone.set(true);
615 });
616
617 state.edit(|buffer| {
618 buffer.insert("!");
619 });
620
621 assert!(called.get());
622 });
623 }
624}