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 {
220 let mut hasher = std::collections::hash_map::DefaultHasher::new();
221 self.inner.runtime_state_id().hash(&mut hasher);
222 hasher.finish()
223 }
224
225 pub fn desired_x(&self) -> Option<f32> {
228 self.inner().borrow().desired_x.get()
229 }
230
231 pub fn set_desired_x(&self, x: Option<f32>) {
234 self.inner().borrow().desired_x.set(x);
235 }
236
237 pub fn text_revision(&self) -> u64 {
241 self.inner().borrow().text_revision.get()
242 }
243
244 fn note_text_change(&self) {
245 let inner = self.inner();
246 let inner = inner.borrow();
247 inner
248 .text_revision
249 .set(inner.text_revision.get().wrapping_add(1));
250 }
251
252 pub fn with_text<R>(&self, f: impl FnOnce(&str) -> R) -> R {
255 self.value.read(|value| f(&value.text))
256 }
257
258 pub fn text(&self) -> String {
261 self.value.read(|v| v.text.clone())
262 }
263
264 pub fn selection(&self) -> TextRange {
266 self.value.read(|v| v.selection)
267 }
268
269 pub fn composition(&self) -> Option<TextRange> {
271 self.value.read(|v| v.composition)
272 }
273
274 pub fn copy_selection(&self) -> Option<String> {
277 self.value.read(|v| {
278 let selection = v.selection;
279 if selection.collapsed() {
280 return None;
281 }
282 let start = selection.min();
283 let end = selection.max();
284 Some(v.text[start..end].to_string())
285 })
286 }
287
288 pub fn value(&self) -> TextFieldValue {
291 self.value.read(Clone::clone)
292 }
293
294 pub fn add_listener(&self, listener: impl Fn(&TextFieldValue) + 'static) -> usize {
298 let inner_state = self.inner();
299 let mut inner = inner_state.borrow_mut();
300 let index = inner.listeners.len();
301 inner.listeners.push(Box::new(listener));
302 index
303 }
304
305 pub fn set_selection(&self, selection: TextRange) {
308 let new_value = self.value.read(|v| {
309 let len = v.text.len();
310 TextFieldValue {
311 text: v.text.clone(),
312 selection: selection.coerce_in(len),
313 composition: v.composition,
314 }
315 });
316 self.value.set(new_value);
317 }
318
319 pub fn can_undo(&self) -> bool {
321 !self.inner().borrow().undo_stack.is_empty()
322 }
323
324 pub fn can_redo(&self) -> bool {
326 !self.inner().borrow().redo_stack.is_empty()
327 }
328
329 pub fn undo(&self) -> bool {
332 self.flush_undo_group();
333
334 let inner_state = self.inner();
335 let mut inner = inner_state.borrow_mut();
336 if let Some(previous_state) = inner.undo_stack.pop_back() {
337 let current = self.value.read(Clone::clone);
338 inner.redo_stack.push_back(current);
339 inner.last_edit_time.set(None);
340 drop(inner);
341 self.value.set(previous_state);
342 self.note_text_change();
343 true
344 } else {
345 false
346 }
347 }
348
349 pub fn redo(&self) -> bool {
352 let inner_state = self.inner();
353 let mut inner = inner_state.borrow_mut();
354 if let Some(redo_state) = inner.redo_stack.pop_back() {
355 let current = self.value.read(Clone::clone);
356 inner.undo_stack.push_back(current);
357 drop(inner);
358 self.value.set(redo_state);
359 self.note_text_change();
360 true
361 } else {
362 false
363 }
364 }
365
366 pub fn edit<F>(&self, f: F) -> bool
383 where
384 F: FnOnce(&mut TextFieldBuffer),
385 {
386 self.edit_as_user(None, f)
387 }
388
389 pub fn edit_as_user<F>(&self, transformation: Option<&InputTransformation>, f: F) -> bool
393 where
394 F: FnOnce(&mut TextFieldBuffer),
395 {
396 let inner = self.inner();
397 let Ok(guard) = EditGuard::new(&inner) else {
398 return false;
399 };
400
401 let mut buffer = self.value.read(|current| {
402 let mut text = String::with_capacity(current.text.len() + EDIT_HEADROOM_BYTES);
403 text.push_str(¤t.text);
404 let mut buffer = TextFieldBuffer::with_selection(text, current.selection);
405 buffer.set_composition(current.composition);
406 buffer
407 });
408
409 f(&mut buffer);
410 if let Some(transformation) = transformation {
411 self.value.read(|current| {
412 if buffer.text() != current.text {
413 transformation.transform(current, &mut buffer);
414 }
415 });
416 }
417
418 let Some(change) = self
419 .value
420 .read(|current| EditChange::between(current, &buffer))
421 else {
422 return true;
423 };
424
425 if !change.text_changed {
428 inner.borrow().last_edit_time.set(None);
429 } else {
430 let now = web_time::Instant::now();
431 let mut inner = inner.borrow_mut();
432 let timeout_expired = inner
433 .last_edit_time
434 .get()
435 .is_none_or(|last| now.duration_since(last).as_millis() > UNDO_COALESCE_MS);
436 let snapshot = || Some(self.value.read(Clone::clone));
438 if timeout_expired || !change.continues_typing {
439 if let Some(pending) = inner.pending_undo_snapshot.take() {
440 if inner.undo_stack.len() >= UNDO_CAPACITY {
441 inner.undo_stack.pop_front();
442 }
443 inner.undo_stack.push_back(pending);
444 }
445 inner.pending_undo_snapshot.replace(snapshot());
446 } else if inner.pending_undo_snapshot.borrow().is_none() {
447 inner.pending_undo_snapshot.replace(snapshot());
448 }
449 inner.redo_stack.clear();
450 inner.last_edit_time.set(Some(now));
451 }
452
453 self.value.set(buffer.into_value());
454 if change.text_changed {
455 self.note_text_change();
456 }
457
458 drop(guard);
459
460 let listener_count = inner.borrow().listeners.len();
461 if listener_count > 0 {
462 let value = self.value.get_non_reactive();
463 for i in 0..listener_count {
464 let inner = inner.borrow();
465 if i < inner.listeners.len() {
466 (inner.listeners[i])(&value);
467 }
468 }
469 }
470 true
471 }
472
473 pub fn flush_undo_group(&self) {
476 let inner_state = self.inner();
477 let inner = inner_state.borrow();
478 if let Some(snapshot) = inner.pending_undo_snapshot.take() {
479 drop(inner);
480 let inner_state = self.inner();
481 let mut inner = inner_state.borrow_mut();
482 if inner.undo_stack.len() >= UNDO_CAPACITY {
483 inner.undo_stack.pop_front();
484 }
485 inner.undo_stack.push_back(snapshot);
486 }
487 }
488
489 pub fn set_text(&self, text: impl Into<String>) -> bool {
491 let text = text.into();
492 self.edit(|buffer| {
493 buffer.clear();
494 buffer.insert(&text);
495 })
496 }
497}
498
499impl Default for TextFieldState {
500 fn default() -> Self {
501 Self::new("")
502 }
503}
504
505impl PartialEq for TextFieldState {
506 fn eq(&self, other: &Self) -> bool {
507 self.inner == other.inner
508 }
509}
510
511#[cfg(test)]
512#[path = "tests/state_tests.rs"]
513mod tests;