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::{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_column: Cell<Option<usize>>,
95 last_edit_time: Cell<Option<web_time::Instant>>,
96 pending_undo_snapshot: RefCell<Option<TextFieldValue>>,
97}
98
99struct EditGuard<'a> {
100 inner: &'a RefCell<TextFieldStateInner>,
101}
102
103impl<'a> EditGuard<'a> {
104 fn new(inner: &'a RefCell<TextFieldStateInner>) -> Result<Self, ()> {
105 {
106 let borrowed = inner.borrow();
107 if borrowed.is_editing {
108 return Err(());
109 }
110 }
111 inner.borrow_mut().is_editing = true;
112 Ok(Self { inner })
113 }
114}
115
116impl Drop for EditGuard<'_> {
117 fn drop(&mut self) {
118 self.inner.borrow_mut().is_editing = false;
119 }
120}
121
122#[derive(Clone, Copy)]
148pub struct TextFieldState {
149 pub inner: MutableState<Rc<RefCell<TextFieldStateInner>>>,
152
153 value: MutableState<TextFieldValue>,
154}
155
156impl std::fmt::Debug for TextFieldState {
157 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158 self.value.read(|v| {
159 f.debug_struct("TextFieldState")
160 .field("text", &v.text)
161 .field("selection", &v.selection)
162 .finish()
163 })
164 }
165}
166
167impl TextFieldState {
168 pub fn new(initial_text: impl Into<String>) -> Self {
170 let initial_value = TextFieldValue::new(initial_text);
171 let runtime = cranpose_core::current_runtime_handle()
172 .expect("TextFieldState::new requires an active runtime");
173 Self {
174 inner: MutableState::with_runtime(
175 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 })),
184 runtime.clone(),
185 ),
186 value: MutableState::with_runtime(initial_value, runtime),
187 }
188 }
189
190 pub fn with_selection(initial_text: impl Into<String>, selection: TextRange) -> Self {
192 let initial_value = TextFieldValue::with_selection(initial_text, selection);
193 let runtime = cranpose_core::current_runtime_handle()
194 .expect("TextFieldState::with_selection requires an active runtime");
195 Self {
196 inner: MutableState::with_runtime(
197 Rc::new(RefCell::new(TextFieldStateInner {
198 is_editing: false,
199 listeners: Vec::new(),
200 undo_stack: VecDeque::new(),
201 redo_stack: VecDeque::new(),
202 desired_column: Cell::new(None),
203 last_edit_time: Cell::new(None),
204 pending_undo_snapshot: RefCell::new(None),
205 })),
206 runtime.clone(),
207 ),
208 value: MutableState::with_runtime(initial_value, runtime),
209 }
210 }
211
212 fn inner(&self) -> Rc<RefCell<TextFieldStateInner>> {
213 self.inner.get_non_reactive()
214 }
215
216 pub fn id(&self) -> u64 {
217 let mut hasher = std::collections::hash_map::DefaultHasher::new();
218 self.inner.runtime_state_id().hash(&mut hasher);
219 hasher.finish()
220 }
221
222 pub fn desired_column(&self) -> Option<usize> {
225 self.inner().borrow().desired_column.get()
226 }
227
228 pub fn set_desired_column(&self, col: Option<usize>) {
231 self.inner().borrow().desired_column.set(col);
232 }
233
234 pub fn text(&self) -> String {
237 self.value.read(|v| v.text.clone())
238 }
239
240 pub fn selection(&self) -> TextRange {
242 self.value.read(|v| v.selection)
243 }
244
245 pub fn composition(&self) -> Option<TextRange> {
247 self.value.read(|v| v.composition)
248 }
249
250 pub fn copy_selection(&self) -> Option<String> {
253 self.value.read(|v| {
254 let selection = v.selection;
255 if selection.collapsed() {
256 return None;
257 }
258 let start = selection.min();
259 let end = selection.max();
260 Some(v.text[start..end].to_string())
261 })
262 }
263
264 pub fn value(&self) -> TextFieldValue {
267 self.value.read(Clone::clone)
268 }
269
270 pub fn add_listener(&self, listener: impl Fn(&TextFieldValue) + 'static) -> usize {
274 let inner_state = self.inner();
275 let mut inner = inner_state.borrow_mut();
276 let index = inner.listeners.len();
277 inner.listeners.push(Box::new(listener));
278 index
279 }
280
281 pub fn set_selection(&self, selection: TextRange) {
284 let new_value = self.value.read(|v| {
285 let len = v.text.len();
286 TextFieldValue {
287 text: v.text.clone(),
288 selection: selection.coerce_in(len),
289 composition: v.composition,
290 }
291 });
292 self.value.set(new_value);
293 }
294
295 pub fn can_undo(&self) -> bool {
297 !self.inner().borrow().undo_stack.is_empty()
298 }
299
300 pub fn can_redo(&self) -> bool {
302 !self.inner().borrow().redo_stack.is_empty()
303 }
304
305 pub fn undo(&self) -> bool {
308 self.flush_undo_group();
309
310 let inner_state = self.inner();
311 let mut inner = inner_state.borrow_mut();
312 if let Some(previous_state) = inner.undo_stack.pop_back() {
313 let current = self.value.read(Clone::clone);
314 inner.redo_stack.push_back(current);
315 inner.last_edit_time.set(None);
316 drop(inner);
317 self.value.set(previous_state);
318 true
319 } else {
320 false
321 }
322 }
323
324 pub fn redo(&self) -> bool {
327 let inner_state = self.inner();
328 let mut inner = inner_state.borrow_mut();
329 if let Some(redo_state) = inner.redo_stack.pop_back() {
330 let current = self.value.read(Clone::clone);
331 inner.undo_stack.push_back(current);
332 drop(inner);
333 self.value.set(redo_state);
334 true
335 } else {
336 false
337 }
338 }
339
340 pub fn edit<F>(&self, f: F) -> bool
357 where
358 F: FnOnce(&mut TextFieldBuffer),
359 {
360 let inner = self.inner();
361 let Ok(guard) = EditGuard::new(&inner) else {
362 return false;
363 };
364
365 let mut buffer = self.value.read(|current| {
366 let mut text = String::with_capacity(current.text.len() + EDIT_HEADROOM_BYTES);
367 text.push_str(¤t.text);
368 let mut buffer = TextFieldBuffer::with_selection(text, current.selection);
369 buffer.set_composition(current.composition);
370 buffer
371 });
372
373 f(&mut buffer);
374
375 let Some(change) = self
376 .value
377 .read(|current| EditChange::between(current, &buffer))
378 else {
379 return true;
380 };
381
382 if !change.text_changed {
385 inner.borrow().last_edit_time.set(None);
386 } else {
387 let now = web_time::Instant::now();
388 let mut inner = inner.borrow_mut();
389 let timeout_expired = inner
390 .last_edit_time
391 .get()
392 .is_none_or(|last| now.duration_since(last).as_millis() > UNDO_COALESCE_MS);
393 let snapshot = || Some(self.value.read(Clone::clone));
395 if timeout_expired || !change.continues_typing {
396 if let Some(pending) = inner.pending_undo_snapshot.take() {
397 if inner.undo_stack.len() >= UNDO_CAPACITY {
398 inner.undo_stack.pop_front();
399 }
400 inner.undo_stack.push_back(pending);
401 }
402 inner.pending_undo_snapshot.replace(snapshot());
403 } else if inner.pending_undo_snapshot.borrow().is_none() {
404 inner.pending_undo_snapshot.replace(snapshot());
405 }
406 inner.redo_stack.clear();
407 inner.last_edit_time.set(Some(now));
408 }
409
410 self.value.set(buffer.into_value());
411
412 drop(guard);
413
414 let listener_count = inner.borrow().listeners.len();
415 if listener_count > 0 {
416 let value = self.value.get_non_reactive();
417 for i in 0..listener_count {
418 let inner = inner.borrow();
419 if i < inner.listeners.len() {
420 (inner.listeners[i])(&value);
421 }
422 }
423 }
424 true
425 }
426
427 pub fn flush_undo_group(&self) {
430 let inner_state = self.inner();
431 let inner = inner_state.borrow();
432 if let Some(snapshot) = inner.pending_undo_snapshot.take() {
433 drop(inner);
434 let inner_state = self.inner();
435 let mut inner = inner_state.borrow_mut();
436 if inner.undo_stack.len() >= UNDO_CAPACITY {
437 inner.undo_stack.pop_front();
438 }
439 inner.undo_stack.push_back(snapshot);
440 }
441 }
442
443 pub fn set_text(&self, text: impl Into<String>) -> bool {
445 let text = text.into();
446 self.edit(|buffer| {
447 buffer.clear();
448 buffer.insert(&text);
449 })
450 }
451}
452
453impl Default for TextFieldState {
454 fn default() -> Self {
455 Self::new("")
456 }
457}
458
459impl PartialEq for TextFieldState {
460 fn eq(&self, other: &Self) -> bool {
461 self.inner == other.inner
462 }
463}
464
465#[cfg(test)]
466#[path = "tests/state_tests.rs"]
467mod tests;