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::{InputTransformation, 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
55const EDIT_HEADROOM_BYTES: usize = 64;
58
59struct EditChange {
61 text_changed: bool,
62 continues_typing: bool,
65}
66
67impl EditChange {
68 fn between(before: &TextFieldValue, after: &TextFieldBuffer) -> Option<Self> {
69 let selection = after.selection();
70 let text_changed = after.text() != before.text;
71 if !text_changed
72 && selection == before.selection
73 && after.composition() == before.composition
74 {
75 return None;
76 }
77 let single_byte_insert = after.len() == before.text.len() + 1;
78 let cursor_followed = selection.start == before.selection.start + 1
79 || selection.start == before.selection.end + 1;
80 Some(Self {
81 text_changed,
82 continues_typing: single_byte_insert
83 && cursor_followed
84 && !after.text().ends_with(char::is_whitespace),
85 })
86 }
87}
88
89pub struct TextFieldStateInner {
90 is_editing: bool,
91 listeners: Vec<ChangeListener>,
92 undo_stack: VecDeque<TextFieldValue>,
93 redo_stack: VecDeque<TextFieldValue>,
94 desired_x: Cell<Option<f32>>,
95 text_revision: Cell<u64>,
96 last_edit_time: Cell<Option<web_time::Instant>>,
97 pending_undo_snapshot: RefCell<Option<TextFieldValue>>,
98}
99
100struct EditGuard<'a> {
101 inner: &'a RefCell<TextFieldStateInner>,
102}
103
104impl<'a> EditGuard<'a> {
105 fn new(inner: &'a RefCell<TextFieldStateInner>) -> Result<Self, ()> {
106 {
107 let borrowed = inner.borrow();
108 if borrowed.is_editing {
109 return Err(());
110 }
111 }
112 inner.borrow_mut().is_editing = true;
113 Ok(Self { inner })
114 }
115}
116
117impl Drop for EditGuard<'_> {
118 fn drop(&mut self) {
119 self.inner.borrow_mut().is_editing = false;
120 }
121}
122
123#[derive(Clone, Copy)]
149pub struct TextFieldState {
150 pub inner: MutableState<Rc<RefCell<TextFieldStateInner>>>,
153
154 value: MutableState<TextFieldValue>,
155}
156
157impl std::fmt::Debug for TextFieldState {
158 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159 self.value.read(|v| {
160 f.debug_struct("TextFieldState")
161 .field("text", &v.text)
162 .field("selection", &v.selection)
163 .finish()
164 })
165 }
166}
167
168impl TextFieldState {
169 pub fn new(initial_text: impl Into<String>) -> Self {
171 let initial_value = TextFieldValue::new(initial_text);
172 let runtime = cranpose_core::current_runtime_handle()
173 .expect("TextFieldState::new requires an active runtime");
174 Self {
175 inner: MutableState::with_runtime(
176 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_x: Cell::new(None),
182 text_revision: Cell::new(0),
183 last_edit_time: Cell::new(None),
184 pending_undo_snapshot: RefCell::new(None),
185 })),
186 runtime.clone(),
187 ),
188 value: MutableState::with_runtime(initial_value, runtime),
189 }
190 }
191
192 pub fn with_selection(initial_text: impl Into<String>, selection: TextRange) -> Self {
194 let initial_value = TextFieldValue::with_selection(initial_text, selection);
195 let runtime = cranpose_core::current_runtime_handle()
196 .expect("TextFieldState::with_selection requires an active runtime");
197 Self {
198 inner: MutableState::with_runtime(
199 Rc::new(RefCell::new(TextFieldStateInner {
200 is_editing: false,
201 listeners: Vec::new(),
202 undo_stack: VecDeque::new(),
203 redo_stack: VecDeque::new(),
204 desired_x: Cell::new(None),
205 text_revision: Cell::new(0),
206 last_edit_time: Cell::new(None),
207 pending_undo_snapshot: RefCell::new(None),
208 })),
209 runtime.clone(),
210 ),
211 value: MutableState::with_runtime(initial_value, runtime),
212 }
213 }
214
215 fn inner(&self) -> Rc<RefCell<TextFieldStateInner>> {
216 self.inner.get_non_reactive()
217 }
218
219 pub fn id(&self) -> u64 {
221 let mut hasher = std::collections::hash_map::DefaultHasher::new();
222 self.inner.runtime_state_id().hash(&mut hasher);
223 hasher.finish()
224 }
225
226 pub fn desired_x(&self) -> Option<f32> {
229 self.inner().borrow().desired_x.get()
230 }
231
232 pub fn set_desired_x(&self, x: Option<f32>) {
235 self.inner().borrow().desired_x.set(x);
236 }
237
238 pub fn text_revision(&self) -> u64 {
242 self.inner().borrow().text_revision.get()
243 }
244
245 fn note_text_change(&self) {
246 let inner = self.inner();
247 let inner = inner.borrow();
248 inner
249 .text_revision
250 .set(inner.text_revision.get().wrapping_add(1));
251 }
252
253 pub fn with_text<R>(&self, f: impl FnOnce(&str) -> R) -> R {
256 self.value.read(|value| f(&value.text))
257 }
258
259 pub fn text(&self) -> String {
262 self.value.read(|v| v.text.clone())
263 }
264
265 pub fn selection(&self) -> TextRange {
267 self.value.read(|v| v.selection)
268 }
269
270 pub fn composition(&self) -> Option<TextRange> {
272 self.value.read(|v| v.composition)
273 }
274
275 pub fn copy_selection(&self) -> Option<String> {
278 self.value.read(|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.read(Clone::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.read(|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();
334
335 let inner_state = self.inner();
336 let mut inner = inner_state.borrow_mut();
337 if let Some(previous_state) = inner.undo_stack.pop_back() {
338 let current = self.value.read(Clone::clone);
339 inner.redo_stack.push_back(current);
340 inner.last_edit_time.set(None);
341 drop(inner);
342 self.value.set(previous_state);
343 self.note_text_change();
344 true
345 } else {
346 false
347 }
348 }
349
350 pub fn redo(&self) -> bool {
353 let inner_state = self.inner();
354 let mut inner = inner_state.borrow_mut();
355 if let Some(redo_state) = inner.redo_stack.pop_back() {
356 let current = self.value.read(Clone::clone);
357 inner.undo_stack.push_back(current);
358 drop(inner);
359 self.value.set(redo_state);
360 self.note_text_change();
361 true
362 } else {
363 false
364 }
365 }
366
367 pub fn edit<F>(&self, f: F) -> bool
384 where
385 F: FnOnce(&mut TextFieldBuffer),
386 {
387 self.edit_as_user(None, f)
388 }
389
390 pub fn edit_as_user<F>(&self, transformation: Option<&InputTransformation>, f: F) -> bool
394 where
395 F: FnOnce(&mut TextFieldBuffer),
396 {
397 let inner = self.inner();
398 let Ok(guard) = EditGuard::new(&inner) else {
399 return false;
400 };
401
402 let mut buffer = self.value.read(|current| {
403 let mut text = String::with_capacity(current.text.len() + EDIT_HEADROOM_BYTES);
404 text.push_str(¤t.text);
405 let mut buffer = TextFieldBuffer::with_selection(text, current.selection);
406 buffer.set_composition(current.composition);
407 buffer
408 });
409
410 f(&mut buffer);
411 if let Some(transformation) = transformation {
412 self.value.read(|current| {
413 if buffer.text() != current.text {
414 transformation.transform(current, &mut buffer);
415 }
416 });
417 }
418
419 let Some(change) = self
420 .value
421 .read(|current| EditChange::between(current, &buffer))
422 else {
423 return true;
424 };
425
426 if !change.text_changed {
429 inner.borrow().last_edit_time.set(None);
430 } else {
431 let now = web_time::Instant::now();
432 let mut inner = inner.borrow_mut();
433 let timeout_expired = inner
434 .last_edit_time
435 .get()
436 .is_none_or(|last| now.duration_since(last).as_millis() > UNDO_COALESCE_MS);
437 let snapshot = || Some(self.value.read(Clone::clone));
439 if timeout_expired || !change.continues_typing {
440 if let Some(pending) = inner.pending_undo_snapshot.take() {
441 if inner.undo_stack.len() >= UNDO_CAPACITY {
442 inner.undo_stack.pop_front();
443 }
444 inner.undo_stack.push_back(pending);
445 }
446 inner.pending_undo_snapshot.replace(snapshot());
447 } else if inner.pending_undo_snapshot.borrow().is_none() {
448 inner.pending_undo_snapshot.replace(snapshot());
449 }
450 inner.redo_stack.clear();
451 inner.last_edit_time.set(Some(now));
452 }
453
454 self.value.set(buffer.into_value());
455 if change.text_changed {
456 self.note_text_change();
457 }
458
459 drop(guard);
460
461 let listener_count = inner.borrow().listeners.len();
462 if listener_count > 0 {
463 let value = self.value.get_non_reactive();
464 for i in 0..listener_count {
465 let inner = inner.borrow();
466 if i < inner.listeners.len() {
467 (inner.listeners[i])(&value);
468 }
469 }
470 }
471 true
472 }
473
474 pub fn flush_undo_group(&self) {
477 let inner_state = self.inner();
478 let inner = inner_state.borrow();
479 if let Some(snapshot) = inner.pending_undo_snapshot.take() {
480 drop(inner);
481 let inner_state = self.inner();
482 let mut inner = inner_state.borrow_mut();
483 if inner.undo_stack.len() >= UNDO_CAPACITY {
484 inner.undo_stack.pop_front();
485 }
486 inner.undo_stack.push_back(snapshot);
487 }
488 }
489
490 pub fn set_text(&self, text: impl Into<String>) -> bool {
492 let text = text.into();
493 self.edit(|buffer| {
494 buffer.clear();
495 buffer.insert(&text);
496 })
497 }
498}
499
500impl Default for TextFieldState {
501 fn default() -> Self {
502 Self::new("")
503 }
504}
505
506impl PartialEq for TextFieldState {
507 fn eq(&self, other: &Self) -> bool {
508 self.inner == other.inner
509 }
510}
511
512#[cfg(test)]
513#[path = "tests/state_tests.rs"]
514mod tests;