use super::{TextFieldBuffer, TextRange};
use std::cell::{Cell, RefCell};
use std::collections::VecDeque;
use std::rc::Rc;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct TextFieldValue {
pub text: String,
pub selection: TextRange,
pub composition: Option<TextRange>,
}
impl TextFieldValue {
pub fn new(text: impl Into<String>) -> Self {
let text = text.into();
let len = text.len();
Self {
text,
selection: TextRange::cursor(len),
composition: None,
}
}
pub fn with_selection(text: impl Into<String>, selection: TextRange) -> Self {
let text = text.into();
let selection = selection.coerce_in(text.len());
Self {
text,
selection,
composition: None,
}
}
}
type ChangeListener = Box<dyn Fn(&TextFieldValue)>;
const UNDO_CAPACITY: usize = 100;
const UNDO_COALESCE_MS: u128 = 1000;
pub struct TextFieldStateInner {
is_editing: bool,
listeners: Vec<ChangeListener>,
undo_stack: VecDeque<TextFieldValue>,
redo_stack: VecDeque<TextFieldValue>,
desired_column: Cell<Option<usize>>,
last_edit_time: Cell<Option<web_time::Instant>>,
pending_undo_snapshot: RefCell<Option<TextFieldValue>>,
line_offsets_cache: RefCell<Option<Vec<usize>>>,
}
struct EditGuard<'a> {
inner: &'a RefCell<TextFieldStateInner>,
}
impl<'a> EditGuard<'a> {
fn new(inner: &'a RefCell<TextFieldStateInner>) -> Result<Self, ()> {
{
let borrowed = inner.borrow();
if borrowed.is_editing {
return Err(()); }
}
inner.borrow_mut().is_editing = true;
Ok(Self { inner })
}
}
impl Drop for EditGuard<'_> {
fn drop(&mut self) {
self.inner.borrow_mut().is_editing = false;
}
}
#[derive(Clone)]
pub struct TextFieldState {
pub inner: Rc<RefCell<TextFieldStateInner>>,
value: Rc<cranpose_core::OwnedMutableState<TextFieldValue>>,
}
impl std::fmt::Debug for TextFieldState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.value.with(|v| {
f.debug_struct("TextFieldState")
.field("text", &v.text)
.field("selection", &v.selection)
.finish()
})
}
}
impl TextFieldState {
pub fn new(initial_text: impl Into<String>) -> Self {
let initial_value = TextFieldValue::new(initial_text);
Self {
inner: Rc::new(RefCell::new(TextFieldStateInner {
is_editing: false,
listeners: Vec::new(),
undo_stack: VecDeque::new(),
redo_stack: VecDeque::new(),
desired_column: Cell::new(None),
last_edit_time: Cell::new(None),
pending_undo_snapshot: RefCell::new(None),
line_offsets_cache: RefCell::new(None),
})),
value: Rc::new(cranpose_core::ownedMutableStateOf(initial_value)),
}
}
pub fn with_selection(initial_text: impl Into<String>, selection: TextRange) -> Self {
let initial_value = TextFieldValue::with_selection(initial_text, selection);
Self {
inner: Rc::new(RefCell::new(TextFieldStateInner {
is_editing: false,
listeners: Vec::new(),
undo_stack: VecDeque::new(),
redo_stack: VecDeque::new(),
desired_column: Cell::new(None),
last_edit_time: Cell::new(None),
pending_undo_snapshot: RefCell::new(None),
line_offsets_cache: RefCell::new(None),
})),
value: Rc::new(cranpose_core::ownedMutableStateOf(initial_value)),
}
}
pub fn desired_column(&self) -> Option<usize> {
self.inner.borrow().desired_column.get()
}
pub fn set_desired_column(&self, col: Option<usize>) {
self.inner.borrow().desired_column.set(col);
}
pub fn text(&self) -> String {
self.value.with(|v| v.text.clone())
}
pub fn selection(&self) -> TextRange {
self.value.with(|v| v.selection)
}
pub fn composition(&self) -> Option<TextRange> {
self.value.with(|v| v.composition)
}
pub fn line_offsets(&self) -> Vec<usize> {
let inner = self.inner.borrow();
if let Some(ref offsets) = *inner.line_offsets_cache.borrow() {
return offsets.clone();
}
let text = self.text();
let mut offsets = vec![0];
for (i, c) in text.char_indices() {
if c == '\n' {
offsets.push(i + 1);
}
}
*inner.line_offsets_cache.borrow_mut() = Some(offsets.clone());
offsets
}
fn invalidate_line_cache(&self) {
self.inner.borrow().line_offsets_cache.borrow_mut().take();
}
pub fn copy_selection(&self) -> Option<String> {
self.value.with(|v| {
let selection = v.selection;
if selection.collapsed() {
return None;
}
let start = selection.min();
let end = selection.max();
Some(v.text[start..end].to_string())
})
}
pub fn value(&self) -> TextFieldValue {
self.value.with(|v| v.clone())
}
pub fn add_listener(&self, listener: impl Fn(&TextFieldValue) + 'static) -> usize {
let mut inner = self.inner.borrow_mut();
let index = inner.listeners.len();
inner.listeners.push(Box::new(listener));
index
}
pub fn set_selection(&self, selection: TextRange) {
let new_value = self.value.with(|v| {
let len = v.text.len();
TextFieldValue {
text: v.text.clone(),
selection: selection.coerce_in(len),
composition: v.composition,
}
});
self.value.set(new_value);
}
pub fn can_undo(&self) -> bool {
!self.inner.borrow().undo_stack.is_empty()
}
pub fn can_redo(&self) -> bool {
!self.inner.borrow().redo_stack.is_empty()
}
pub fn undo(&self) -> bool {
self.flush_undo_group();
let mut inner = self.inner.borrow_mut();
if let Some(previous_state) = inner.undo_stack.pop_back() {
let current = self.value.with(|v| v.clone());
inner.redo_stack.push_back(current);
inner.last_edit_time.set(None);
drop(inner);
self.value.set(previous_state);
true
} else {
false
}
}
pub fn redo(&self) -> bool {
let mut inner = self.inner.borrow_mut();
if let Some(redo_state) = inner.redo_stack.pop_back() {
let current = self.value.with(|v| v.clone());
inner.undo_stack.push_back(current);
drop(inner);
self.value.set(redo_state);
true
} else {
false
}
}
pub fn edit<F>(&self, f: F)
where
F: FnOnce(&mut TextFieldBuffer),
{
let _guard = EditGuard::new(&self.inner)
.expect("TextFieldState does not support concurrent or nested editing");
let current = self.value();
let mut buffer = TextFieldBuffer::with_selection(¤t.text, current.selection);
if let Some(comp) = current.composition {
buffer.set_composition(Some(comp));
}
f(&mut buffer);
let new_value = TextFieldValue {
text: buffer.text().to_string(),
selection: buffer.selection(),
composition: buffer.composition(),
};
let changed = new_value != current;
let text_changed = new_value.text != current.text;
if text_changed {
self.invalidate_line_cache();
}
if changed {
let now = web_time::Instant::now();
let should_break_group = {
let inner = self.inner.borrow();
let timeout_expired = inner
.last_edit_time
.get()
.map(|last| now.duration_since(last).as_millis() > UNDO_COALESCE_MS)
.unwrap_or(true);
if timeout_expired {
true
} else {
let text_delta = new_value.text.len() as i64 - current.text.len() as i64;
let is_single_char_insert = text_delta == 1;
let ends_with_whitespace = new_value.text.ends_with(char::is_whitespace);
let cursor_jumped = new_value.selection.start != current.selection.start + 1
&& new_value.selection.start != current.selection.end + 1;
!is_single_char_insert || ends_with_whitespace || cursor_jumped
}
};
{
let inner = self.inner.borrow();
if should_break_group {
let pending = inner.pending_undo_snapshot.take();
drop(inner);
let mut inner = self.inner.borrow_mut();
if let Some(snapshot) = pending {
if inner.undo_stack.len() >= UNDO_CAPACITY {
inner.undo_stack.pop_front();
}
inner.undo_stack.push_back(snapshot);
}
inner.redo_stack.clear();
drop(inner);
self.inner
.borrow()
.pending_undo_snapshot
.replace(Some(current.clone()));
} else {
if inner.pending_undo_snapshot.borrow().is_none() {
inner.pending_undo_snapshot.replace(Some(current.clone()));
}
drop(inner);
self.inner.borrow_mut().redo_stack.clear();
}
self.inner.borrow().last_edit_time.set(Some(now));
}
self.value.set(new_value.clone());
}
drop(_guard);
if changed {
let listener_count = self.inner.borrow().listeners.len();
for i in 0..listener_count {
let inner = self.inner.borrow();
if i < inner.listeners.len() {
(inner.listeners[i])(&new_value);
}
}
}
}
pub fn flush_undo_group(&self) {
let inner = self.inner.borrow();
if let Some(snapshot) = inner.pending_undo_snapshot.take() {
drop(inner);
let mut inner = self.inner.borrow_mut();
if inner.undo_stack.len() >= UNDO_CAPACITY {
inner.undo_stack.pop_front();
}
inner.undo_stack.push_back(snapshot);
}
}
pub fn set_text(&self, text: impl Into<String>) {
let text = text.into();
self.edit(|buffer| {
buffer.clear();
buffer.insert(&text);
});
}
pub fn set_text_and_select_all(&self, text: impl Into<String>) {
let text = text.into();
self.edit(|buffer| {
buffer.clear();
buffer.insert(&text);
buffer.select_all();
});
}
}
impl Default for TextFieldState {
fn default() -> Self {
Self::new("")
}
}
impl PartialEq for TextFieldState {
fn eq(&self, other: &Self) -> bool {
Rc::ptr_eq(&self.inner, &other.inner)
}
}
#[cfg(test)]
mod tests {
use super::*;
use cranpose_core::{DefaultScheduler, Runtime};
use std::sync::Arc;
fn with_test_runtime<T>(f: impl FnOnce() -> T) -> T {
let _runtime = Runtime::new(Arc::new(DefaultScheduler));
f()
}
#[test]
fn new_state_has_cursor_at_end() {
with_test_runtime(|| {
let state = TextFieldState::new("Hello");
assert_eq!(state.text(), "Hello");
assert_eq!(state.selection(), TextRange::cursor(5));
});
}
#[test]
fn edit_updates_text() {
with_test_runtime(|| {
let state = TextFieldState::new("Hello");
state.edit(|buffer| {
buffer.place_cursor_at_end();
buffer.insert(", World!");
});
assert_eq!(state.text(), "Hello, World!");
});
}
#[test]
fn edit_updates_selection() {
with_test_runtime(|| {
let state = TextFieldState::new("Hello");
state.edit(|buffer| {
buffer.select_all();
});
assert_eq!(state.selection(), TextRange::new(0, 5));
});
}
#[test]
fn set_text_replaces_content() {
with_test_runtime(|| {
let state = TextFieldState::new("Hello");
state.set_text("Goodbye");
assert_eq!(state.text(), "Goodbye");
assert_eq!(state.selection(), TextRange::cursor(7));
});
}
#[test]
#[should_panic(expected = "concurrent or nested editing")]
fn nested_edit_panics() {
with_test_runtime(|| {
let state = TextFieldState::new("Hello");
let state_clone = state.clone();
state.edit(move |_buffer| {
state_clone.edit(|_| {}); });
});
}
#[test]
fn listener_is_called_on_change() {
with_test_runtime(|| {
use std::cell::Cell;
use std::rc::Rc;
let state = TextFieldState::new("Hello");
let called = Rc::new(Cell::new(false));
let called_clone = called.clone();
state.add_listener(move |_value| {
called_clone.set(true);
});
state.edit(|buffer| {
buffer.insert("!");
});
assert!(called.get());
});
}
}