use std::{
cell::{Cell, RefCell},
rc::{Rc, Weak},
};
use crate::key_event::KeyEvent;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ImeEditorState {
pub text: String,
pub selection_start: usize,
pub selection_end: usize,
pub composition: Option<(usize, usize)>,
pub single_line: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ImeCaretGeometry {
pub caret_xs: Vec<f32>,
pub top: f32,
pub line_height: f32,
}
pub trait FocusedTextFieldHandler {
fn node_id(&self) -> Option<cranpose_core::NodeId> {
None
}
fn handle_key(&self, event: &KeyEvent) -> bool;
fn insert_text(&self, text: &str);
fn delete_surrounding(&self, before_bytes: usize, after_bytes: usize);
fn copy_selection(&self) -> Option<String>;
fn cut_selection(&self) -> Option<String>;
fn select_all(&self) {}
fn set_composition(&self, text: &str, cursor: Option<(usize, usize)>);
fn finish_composition(&self) {}
fn set_composing_region(&self, start_bytes: usize, end_bytes: usize) {
let _ = (start_bytes, end_bytes);
}
fn set_selection(&self, start_bytes: usize, end_bytes: usize) {
let _ = (start_bytes, end_bytes);
}
fn editor_state(&self) -> Option<ImeEditorState> {
None
}
fn caret_geometry(&self) -> Option<ImeCaretGeometry> {
None
}
}
pub(crate) struct TextFieldFocusState {
focused_field: RefCell<Option<Weak<RefCell<bool>>>>,
focused_handler: RefCell<Option<Rc<dyn FocusedTextFieldHandler>>>,
focused_modal_depth: Cell<usize>,
}
impl TextFieldFocusState {
pub(crate) fn new() -> Self {
Self {
focused_field: RefCell::new(None),
focused_handler: RefCell::new(None),
focused_modal_depth: Cell::new(0),
}
}
fn request_focus(
&self,
is_focused: Rc<RefCell<bool>>,
handler: Rc<dyn FocusedTextFieldHandler>,
modal_depth: usize,
) {
let mut current = self.focused_field.borrow_mut();
if let Some(ref weak) = *current
&& let Some(old_focused) = weak.upgrade()
{
*old_focused.borrow_mut() = false;
}
*is_focused.borrow_mut() = true;
*current = Some(Rc::downgrade(&is_focused));
*self.focused_handler.borrow_mut() = Some(handler);
self.focused_modal_depth.set(modal_depth);
}
fn clear_focus(&self) {
let mut current = self.focused_field.borrow_mut();
if let Some(ref weak) = *current
&& let Some(focused) = weak.upgrade()
{
*focused.borrow_mut() = false;
}
*current = None;
*self.focused_handler.borrow_mut() = None;
self.focused_modal_depth.set(0);
}
fn focused_at_depth(&self, depth: usize) -> bool {
self.has_focused_field() && self.focused_modal_depth.get() == depth
}
fn has_focused_field(&self) -> bool {
if self.focused_field_is_live() {
return true;
}
self.clear_stale_focus();
false
}
fn focused_field_is_live(&self) -> bool {
self.focused_field
.borrow()
.as_ref()
.is_some_and(|weak| weak.upgrade().is_some())
}
fn clear_stale_focus(&self) {
let stale_node = self
.focused_handler
.borrow()
.as_ref()
.and_then(|handler| handler.node_id());
let had_entry = self.focused_field.borrow_mut().take().is_some();
if !had_entry {
return;
}
self.focused_handler.borrow_mut().take();
if let Some(node_id) = stale_node {
crate::schedule_draw_repass(node_id);
}
crate::cursor_animation::stop_cursor_blink();
}
fn focused_handler(&self) -> Option<Rc<dyn FocusedTextFieldHandler>> {
if !self.has_focused_field() {
return None;
}
self.focused_handler.borrow().as_ref().cloned()
}
fn dispatch_key_event(&self, event: &KeyEvent) -> bool {
if let Some(handler) = self.focused_handler() {
handler.handle_key(event)
} else {
false
}
}
fn dispatch_paste(&self, text: &str) -> bool {
if let Some(handler) = self.focused_handler() {
handler.insert_text(text);
true
} else {
false
}
}
fn dispatch_delete_surrounding(&self, before_bytes: usize, after_bytes: usize) -> bool {
if let Some(handler) = self.focused_handler() {
handler.delete_surrounding(before_bytes, after_bytes);
true
} else {
false
}
}
fn dispatch_copy(&self) -> Option<String> {
self.focused_handler()
.and_then(|handler| handler.copy_selection())
}
fn dispatch_cut(&self) -> Option<String> {
self.focused_handler()
.and_then(|handler| handler.cut_selection())
}
fn dispatch_select_all(&self) -> bool {
if let Some(handler) = self.focused_handler() {
handler.select_all();
true
} else {
false
}
}
fn dispatch_ime_preedit(&self, text: &str, cursor: Option<(usize, usize)>) -> bool {
if let Some(handler) = self.focused_handler() {
handler.set_composition(text, cursor);
true
} else {
false
}
}
fn dispatch_ime_finish_composing(&self) -> bool {
if let Some(handler) = self.focused_handler() {
handler.finish_composition();
true
} else {
false
}
}
fn dispatch_ime_set_composing_region(&self, start_bytes: usize, end_bytes: usize) -> bool {
if let Some(handler) = self.focused_handler() {
handler.set_composing_region(start_bytes, end_bytes);
true
} else {
false
}
}
fn dispatch_ime_set_selection(&self, start_bytes: usize, end_bytes: usize) -> bool {
if let Some(handler) = self.focused_handler() {
handler.set_selection(start_bytes, end_bytes);
true
} else {
false
}
}
fn focused_editor_state(&self) -> Option<ImeEditorState> {
self.focused_handler()
.and_then(|handler| handler.editor_state())
}
fn focused_caret_geometry(&self) -> Option<ImeCaretGeometry> {
self.focused_handler()
.and_then(|handler| handler.caret_geometry())
}
}
pub fn request_focus(
is_focused: Rc<RefCell<bool>>,
handler: Rc<dyn FocusedTextFieldHandler>,
modal_depth: usize,
) {
if modal_depth < crate::modal::current_modal_depth() {
return;
}
let previous_field = focused_field_node();
let gaining_field = handler.node_id();
crate::render_state::with_text_field_focus(|state| {
state.request_focus(is_focused, handler, modal_depth)
});
for node_id in [previous_field, gaining_field].into_iter().flatten() {
crate::schedule_draw_repass(node_id);
}
crate::cursor_animation::start_cursor_blink();
crate::text_input_session::notify_text_input_focus_gained();
crate::request_render_invalidation();
}
pub(crate) fn clear_focus_for_closed_modal(depth: usize) {
let owns_focus =
crate::render_state::with_text_field_focus(|state| state.focused_at_depth(depth));
if owns_focus {
clear_focus();
}
}
pub fn clear_focus() {
if let Some(node_id) = focused_field_node() {
crate::schedule_draw_repass(node_id);
}
crate::render_state::with_text_field_focus(|state| state.clear_focus());
crate::cursor_animation::stop_cursor_blink();
crate::text_input_session::notify_text_input_focus_lost();
crate::request_render_invalidation();
}
pub fn focused_field_node() -> Option<cranpose_core::NodeId> {
crate::render_state::with_text_field_focus(|state| {
state
.focused_handler()
.and_then(|handler| handler.node_id())
})
}
pub fn has_focused_field() -> bool {
let has_focus = crate::render_state::with_text_field_focus(|state| state.has_focused_field());
if !has_focus {
crate::text_input_session::notify_text_input_focus_lost();
}
has_focus
}
pub fn dispatch_key_event(event: &KeyEvent) -> bool {
crate::render_state::with_text_field_focus(|state| state.dispatch_key_event(event))
}
pub fn dispatch_paste(text: &str) -> bool {
crate::render_state::with_text_field_focus(|state| state.dispatch_paste(text))
}
pub fn dispatch_delete_surrounding(before_bytes: usize, after_bytes: usize) -> bool {
crate::render_state::with_text_field_focus(|state| {
state.dispatch_delete_surrounding(before_bytes, after_bytes)
})
}
pub fn dispatch_copy() -> Option<String> {
crate::render_state::with_text_field_focus(|state| state.dispatch_copy())
}
pub fn dispatch_cut() -> Option<String> {
crate::render_state::with_text_field_focus(|state| state.dispatch_cut())
}
pub fn dispatch_select_all() -> bool {
crate::render_state::with_text_field_focus(|state| state.dispatch_select_all())
}
pub fn dispatch_ime_preedit(text: &str, cursor: Option<(usize, usize)>) -> bool {
crate::render_state::with_text_field_focus(|state| state.dispatch_ime_preedit(text, cursor))
}
pub fn dispatch_ime_finish_composing() -> bool {
crate::render_state::with_text_field_focus(|state| state.dispatch_ime_finish_composing())
}
pub fn dispatch_ime_set_composing_region(start_bytes: usize, end_bytes: usize) -> bool {
crate::render_state::with_text_field_focus(|state| {
state.dispatch_ime_set_composing_region(start_bytes, end_bytes)
})
}
pub fn dispatch_ime_set_selection(start_bytes: usize, end_bytes: usize) -> bool {
crate::render_state::with_text_field_focus(|state| {
state.dispatch_ime_set_selection(start_bytes, end_bytes)
})
}
pub fn focused_editor_state() -> Option<ImeEditorState> {
crate::render_state::with_text_field_focus(|state| state.focused_editor_state())
}
pub fn focused_caret_geometry() -> Option<ImeCaretGeometry> {
crate::render_state::with_text_field_focus(|state| state.focused_caret_geometry())
}
#[cfg(test)]
mod tests {
use super::*;
struct MockHandler;
impl FocusedTextFieldHandler for MockHandler {
fn handle_key(&self, _: &KeyEvent) -> bool {
false
}
fn insert_text(&self, _: &str) {}
fn delete_surrounding(&self, _: usize, _: usize) {}
fn copy_selection(&self) -> Option<String> {
None
}
fn cut_selection(&self) -> Option<String> {
None
}
fn set_composition(&self, _: &str, _: Option<(usize, usize)>) {}
}
fn mock_handler() -> Rc<dyn FocusedTextFieldHandler> {
Rc::new(MockHandler)
}
struct NodeBackedHandler(cranpose_core::NodeId);
impl FocusedTextFieldHandler for NodeBackedHandler {
fn node_id(&self) -> Option<cranpose_core::NodeId> {
Some(self.0)
}
fn handle_key(&self, _: &KeyEvent) -> bool {
false
}
fn insert_text(&self, _: &str) {}
fn delete_surrounding(&self, _: usize, _: usize) {}
fn copy_selection(&self) -> Option<String> {
None
}
fn cut_selection(&self) -> Option<String> {
None
}
fn set_composition(&self, _: &str, _: Option<(usize, usize)>) {}
}
#[test]
fn focus_transitions_schedule_scoped_draw_repasses_on_both_fields() {
let _app_context = crate::render_state::app_context_test_scope();
let _ = crate::render_state::take_draw_repass_nodes();
let first = Rc::new(RefCell::new(false));
request_focus(first.clone(), Rc::new(NodeBackedHandler(7)), 0);
assert!(
crate::render_state::take_draw_repass_nodes().contains(&7),
"gaining focus must re-record the gaining field's draws"
);
let second = Rc::new(RefCell::new(false));
request_focus(second.clone(), Rc::new(NodeBackedHandler(9)), 0);
let repasses = crate::render_state::take_draw_repass_nodes();
assert!(
repasses.contains(&7) && repasses.contains(&9),
"a focus hand-off must re-record both fields, got {repasses:?}"
);
clear_focus();
assert!(
crate::render_state::take_draw_repass_nodes().contains(&9),
"losing focus must re-record the field that had the caret"
);
}
#[test]
fn a_blink_transition_schedules_a_scoped_repass_on_the_focused_field() {
let _app_context = crate::render_state::app_context_test_scope();
let focus = Rc::new(RefCell::new(false));
request_focus(focus.clone(), Rc::new(NodeBackedHandler(21)), 0);
let _ = crate::render_state::take_draw_repass_nodes();
let past_interval = web_time::Instant::now()
+ crate::cursor_animation::CursorAnimationState::BLINK_INTERVAL
+ std::time::Duration::from_millis(1);
assert!(
crate::cursor_animation::tick_cursor_blink_at(past_interval),
"the tick past the interval must flip visibility"
);
assert!(
crate::render_state::take_draw_repass_nodes().contains(&21),
"the flip must re-record the focused field's draws"
);
clear_focus();
}
#[test]
fn request_focus_sets_flag() {
let _app_context = crate::render_state::app_context_test_scope();
let focus = Rc::new(RefCell::new(false));
request_focus(focus.clone(), mock_handler(), 0);
assert!(*focus.borrow());
clear_focus();
}
#[test]
fn request_focus_clears_previous() {
let _app_context = crate::render_state::app_context_test_scope();
let focus1 = Rc::new(RefCell::new(false));
let focus2 = Rc::new(RefCell::new(false));
request_focus(focus1.clone(), mock_handler(), 0);
assert!(*focus1.borrow());
request_focus(focus2.clone(), mock_handler(), 0);
assert!(!*focus1.borrow()); assert!(*focus2.borrow()); clear_focus();
}
#[test]
fn clear_focus_unfocuses_current() {
let _app_context = crate::render_state::app_context_test_scope();
let focus = Rc::new(RefCell::new(false));
request_focus(focus.clone(), mock_handler(), 0);
assert!(*focus.borrow());
clear_focus();
assert!(!*focus.borrow());
}
#[derive(Default)]
struct DispatchRecordingHandler {
key_count: Cell<usize>,
insert_count: Cell<usize>,
delete_count: Cell<usize>,
copy_count: Cell<usize>,
cut_count: Cell<usize>,
preedit_count: Cell<usize>,
last_delete: Cell<Option<(usize, usize)>>,
}
impl DispatchRecordingHandler {
fn bump(cell: &Cell<usize>) {
cell.set(cell.get() + 1);
}
fn total_calls(&self) -> usize {
self.key_count.get()
+ self.insert_count.get()
+ self.delete_count.get()
+ self.copy_count.get()
+ self.cut_count.get()
+ self.preedit_count.get()
}
}
impl FocusedTextFieldHandler for DispatchRecordingHandler {
fn handle_key(&self, _: &KeyEvent) -> bool {
Self::bump(&self.key_count);
true
}
fn insert_text(&self, _: &str) {
Self::bump(&self.insert_count);
}
fn delete_surrounding(&self, before_bytes: usize, after_bytes: usize) {
Self::bump(&self.delete_count);
self.last_delete.set(Some((before_bytes, after_bytes)));
}
fn copy_selection(&self) -> Option<String> {
Self::bump(&self.copy_count);
Some("copy".to_string())
}
fn cut_selection(&self) -> Option<String> {
Self::bump(&self.cut_count);
Some("cut".to_string())
}
fn set_composition(&self, _: &str, _: Option<(usize, usize)>) {
Self::bump(&self.preedit_count);
}
}
#[test]
fn dispatch_delete_surrounding_calls_handler() {
let _app_context = crate::render_state::app_context_test_scope();
let focus = Rc::new(RefCell::new(false));
let handler = Rc::new(DispatchRecordingHandler::default());
request_focus(Rc::clone(&focus), handler.clone(), 0);
assert!(dispatch_delete_surrounding(3, 1));
assert_eq!(handler.last_delete.get(), Some((3, 1)));
clear_focus();
}
#[test]
fn dispatch_clears_stale_focus_owner_before_invoking_handler() {
let _app_context = crate::render_state::app_context_test_scope();
let handler = Rc::new(DispatchRecordingHandler::default());
{
let focus = Rc::new(RefCell::new(false));
request_focus(Rc::clone(&focus), handler.clone(), 0);
assert!(has_focused_field());
}
let key_event = KeyEvent::key_down(crate::key_event::KeyCode::A, "a");
assert!(!dispatch_key_event(&key_event));
assert!(!dispatch_paste("stale paste"));
assert!(!dispatch_delete_surrounding(2, 1));
assert_eq!(dispatch_copy(), None);
assert_eq!(dispatch_cut(), None);
assert!(!dispatch_ime_preedit("preedit", Some((1, 1))));
assert!(!has_focused_field());
assert_eq!(
handler.total_calls(),
0,
"stale focused-field handlers must not receive input"
);
}
#[test]
fn stale_focus_cleanup_after_hidden_blink_does_not_reenter_the_focus_registry_borrow() {
let _app_context = crate::render_state::app_context_test_scope();
let focus = Rc::new(RefCell::new(false));
request_focus(focus.clone(), Rc::new(NodeBackedHandler(3)), 0);
let past_interval = web_time::Instant::now()
+ crate::cursor_animation::CursorAnimationState::BLINK_INTERVAL
+ std::time::Duration::from_millis(1);
assert!(
crate::cursor_animation::tick_cursor_blink_at(past_interval),
"the blink must already have toggled to hidden, matching the real \
timing where the bug's stop_cursor_blink() call is a visibility \
change and therefore reaches invalidate_focused_caret()"
);
drop(focus);
assert!(
!has_focused_field(),
"a field dropped without clear_focus must read back as unfocused \
instead of panicking on a reentrant borrow of focused_field"
);
}
#[test]
fn stale_focus_cleanup_repasses_the_node_that_lost_its_caret() {
let _app_context = crate::render_state::app_context_test_scope();
let _ = crate::render_state::take_draw_repass_nodes();
let focus = Rc::new(RefCell::new(false));
request_focus(focus.clone(), Rc::new(NodeBackedHandler(11)), 0);
let _ = crate::render_state::take_draw_repass_nodes();
drop(focus);
assert!(!has_focused_field());
assert!(
crate::render_state::take_draw_repass_nodes().contains(&11),
"discovering a stale field lazily must repass its node just like \
an explicit clear_focus does, or its caret is left stale on screen"
);
}
#[derive(Default)]
struct KeyboardProbe {
calls: RefCell<Vec<&'static str>>,
}
impl crate::text_input_session::PlatformTextInputHandler for KeyboardProbe {
fn show_keyboard(&self) {
self.calls.borrow_mut().push("show");
}
fn hide_keyboard(&self) {
self.calls.borrow_mut().push("hide");
}
}
#[test]
fn focus_transitions_drive_platform_keyboard() {
let _app_context = crate::render_state::app_context_test_scope();
let keyboard = Rc::new(KeyboardProbe::default());
crate::text_input_session::set_platform_text_input_handler(keyboard.clone());
let focus = Rc::new(RefCell::new(false));
request_focus(focus.clone(), mock_handler(), 0);
assert_eq!(*keyboard.calls.borrow(), vec!["show"]);
request_focus(focus, mock_handler(), 0);
assert_eq!(*keyboard.calls.borrow(), vec!["show", "show"]);
clear_focus();
assert_eq!(*keyboard.calls.borrow(), vec!["show", "show", "hide"]);
}
#[test]
fn stale_focus_detection_hides_platform_keyboard() {
let _app_context = crate::render_state::app_context_test_scope();
let keyboard = Rc::new(KeyboardProbe::default());
crate::text_input_session::set_platform_text_input_handler(keyboard.clone());
{
let focus = Rc::new(RefCell::new(false));
request_focus(focus, mock_handler(), 0);
}
assert!(!has_focused_field());
assert_eq!(*keyboard.calls.borrow(), vec!["show", "hide"]);
assert!(!has_focused_field());
assert_eq!(*keyboard.calls.borrow(), vec!["show", "hide"]);
}
#[test]
fn text_field_focus_is_scoped_by_app_context() {
let _app_context = crate::render_state::app_context_test_scope();
let first = crate::render_state::AppContext::new_with_density(1.0);
let second = crate::render_state::AppContext::new_with_density(1.0);
let first_focus = Rc::new(RefCell::new(false));
let second_focus = Rc::new(RefCell::new(false));
first.enter(|| {
request_focus(first_focus.clone(), mock_handler(), 0);
assert!(has_focused_field());
assert!(*first_focus.borrow());
});
second.enter(|| {
assert!(!has_focused_field());
request_focus(second_focus.clone(), mock_handler(), 0);
assert!(has_focused_field());
assert!(*second_focus.borrow());
});
first.enter(|| {
assert!(has_focused_field());
assert!(*first_focus.borrow());
clear_focus();
assert!(!has_focused_field());
assert!(!*first_focus.borrow());
});
second.enter(|| {
assert!(has_focused_field());
assert!(*second_focus.borrow());
clear_focus();
});
}
}