use std::{
cell::{Cell, RefCell},
collections::VecDeque,
hash::{Hash, Hasher},
rc::Rc,
};
use cranpose_core::MutableState;
use super::{TextFieldBuffer, TextRange};
#[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;
const EDIT_HEADROOM_BYTES: usize = 64;
struct EditChange {
text_changed: bool,
continues_typing: bool,
}
impl EditChange {
fn between(before: &TextFieldValue, after: &TextFieldBuffer) -> Option<Self> {
let selection = after.selection();
let text_changed = after.text() != before.text;
if !text_changed
&& selection == before.selection
&& after.composition() == before.composition
{
return None;
}
let single_byte_insert = after.len() == before.text.len() + 1;
let cursor_followed = selection.start == before.selection.start + 1
|| selection.start == before.selection.end + 1;
Some(Self {
text_changed,
continues_typing: single_byte_insert
&& cursor_followed
&& !after.text().ends_with(char::is_whitespace),
})
}
}
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>>,
}
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, Copy)]
pub struct TextFieldState {
pub inner: MutableState<Rc<RefCell<TextFieldStateInner>>>,
value: MutableState<TextFieldValue>,
}
impl std::fmt::Debug for TextFieldState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.value.read(|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);
let runtime = cranpose_core::current_runtime_handle()
.expect("TextFieldState::new requires an active runtime");
Self {
inner: MutableState::with_runtime(
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),
})),
runtime.clone(),
),
value: MutableState::with_runtime(initial_value, runtime),
}
}
pub fn with_selection(initial_text: impl Into<String>, selection: TextRange) -> Self {
let initial_value = TextFieldValue::with_selection(initial_text, selection);
let runtime = cranpose_core::current_runtime_handle()
.expect("TextFieldState::with_selection requires an active runtime");
Self {
inner: MutableState::with_runtime(
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),
})),
runtime.clone(),
),
value: MutableState::with_runtime(initial_value, runtime),
}
}
fn inner(&self) -> Rc<RefCell<TextFieldStateInner>> {
self.inner.get_non_reactive()
}
pub fn id(&self) -> u64 {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
self.inner.runtime_state_id().hash(&mut hasher);
hasher.finish()
}
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.read(|v| v.text.clone())
}
pub fn selection(&self) -> TextRange {
self.value.read(|v| v.selection)
}
pub fn composition(&self) -> Option<TextRange> {
self.value.read(|v| v.composition)
}
pub fn copy_selection(&self) -> Option<String> {
self.value.read(|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.read(Clone::clone)
}
pub fn add_listener(&self, listener: impl Fn(&TextFieldValue) + 'static) -> usize {
let inner_state = self.inner();
let mut inner = inner_state.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.read(|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 inner_state = self.inner();
let mut inner = inner_state.borrow_mut();
if let Some(previous_state) = inner.undo_stack.pop_back() {
let current = self.value.read(Clone::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 inner_state = self.inner();
let mut inner = inner_state.borrow_mut();
if let Some(redo_state) = inner.redo_stack.pop_back() {
let current = self.value.read(Clone::clone);
inner.undo_stack.push_back(current);
drop(inner);
self.value.set(redo_state);
true
} else {
false
}
}
pub fn edit<F>(&self, f: F) -> bool
where
F: FnOnce(&mut TextFieldBuffer),
{
let inner = self.inner();
let Ok(guard) = EditGuard::new(&inner) else {
return false;
};
let mut buffer = self.value.read(|current| {
let mut text = String::with_capacity(current.text.len() + EDIT_HEADROOM_BYTES);
text.push_str(¤t.text);
let mut buffer = TextFieldBuffer::with_selection(text, current.selection);
buffer.set_composition(current.composition);
buffer
});
f(&mut buffer);
let Some(change) = self
.value
.read(|current| EditChange::between(current, &buffer))
else {
return true;
};
if !change.text_changed {
inner.borrow().last_edit_time.set(None);
} else {
let now = web_time::Instant::now();
let mut inner = inner.borrow_mut();
let timeout_expired = inner
.last_edit_time
.get()
.is_none_or(|last| now.duration_since(last).as_millis() > UNDO_COALESCE_MS);
let snapshot = || Some(self.value.read(Clone::clone));
if timeout_expired || !change.continues_typing {
if let Some(pending) = inner.pending_undo_snapshot.take() {
if inner.undo_stack.len() >= UNDO_CAPACITY {
inner.undo_stack.pop_front();
}
inner.undo_stack.push_back(pending);
}
inner.pending_undo_snapshot.replace(snapshot());
} else if inner.pending_undo_snapshot.borrow().is_none() {
inner.pending_undo_snapshot.replace(snapshot());
}
inner.redo_stack.clear();
inner.last_edit_time.set(Some(now));
}
self.value.set(buffer.into_value());
drop(guard);
let listener_count = inner.borrow().listeners.len();
if listener_count > 0 {
let value = self.value.get_non_reactive();
for i in 0..listener_count {
let inner = inner.borrow();
if i < inner.listeners.len() {
(inner.listeners[i])(&value);
}
}
}
true
}
pub fn flush_undo_group(&self) {
let inner_state = self.inner();
let inner = inner_state.borrow();
if let Some(snapshot) = inner.pending_undo_snapshot.take() {
drop(inner);
let inner_state = self.inner();
let mut inner = inner_state.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>) -> bool {
let text = text.into();
self.edit(|buffer| {
buffer.clear();
buffer.insert(&text);
})
}
}
impl Default for TextFieldState {
fn default() -> Self {
Self::new("")
}
}
impl PartialEq for TextFieldState {
fn eq(&self, other: &Self) -> bool {
self.inner == other.inner
}
}
#[cfg(test)]
#[path = "tests/state_tests.rs"]
mod tests;