use std::{cell::RefCell, rc::Rc};
use cranpose_app_shell::AppShell;
use cranpose_core::{location_key, remember};
use cranpose_testing::robot::TestRenderer;
use cranpose_ui::{
BasicTextFieldOptions, BasicTextFieldWithOptions, FocusRequester, KeyCode, KeyEvent, Modifier,
Modifiers, PlatformTextInputHandler, TextFieldState,
};
pub(crate) const SHIFT: Modifiers = Modifiers {
shift: true,
..Modifiers::NONE
};
pub(crate) const COMMAND: Modifiers = if cfg!(target_os = "macos") {
Modifiers {
meta: true,
..Modifiers::NONE
}
} else {
Modifiers {
ctrl: true,
..Modifiers::NONE
}
};
#[derive(Default)]
pub(crate) struct RecordingKeyboard {
pub(crate) calls: RefCell<Vec<&'static str>>,
}
impl PlatformTextInputHandler for RecordingKeyboard {
fn show_keyboard(&self) {
self.calls.borrow_mut().push("show");
}
fn hide_keyboard(&self) {
self.calls.borrow_mut().push("hide");
}
}
#[track_caller]
pub(crate) fn shell_with_keyboard(
content: impl FnMut() + 'static,
) -> (AppShell<TestRenderer>, Rc<RecordingKeyboard>) {
let caller = std::panic::Location::caller();
let mut shell = AppShell::new(
TestRenderer::default(),
location_key(caller.file(), caller.line(), caller.column()),
content,
);
shell.set_viewport(320.0, 200.0);
shell.set_buffer_size(320, 200);
let keyboard = Rc::new(RecordingKeyboard::default());
shell.set_platform_text_input(keyboard.clone());
shell.update();
(shell, keyboard)
}
pub(crate) struct FocusedField {
pub(crate) shell: AppShell<TestRenderer>,
state: Rc<std::cell::Cell<Option<TextFieldState>>>,
}
impl FocusedField {
#[track_caller]
pub(crate) fn new(text: &'static str, options: BasicTextFieldOptions) -> Self {
Self::composed(text, move |state, modifier| {
BasicTextFieldWithOptions(state, modifier, options.clone());
})
}
#[track_caller]
pub(crate) fn composed(
text: &'static str,
field: impl Fn(TextFieldState, Modifier) + 'static,
) -> Self {
let requester = FocusRequester::new();
let state = Rc::new(std::cell::Cell::new(None));
let (content_requester, content_state) = (requester.clone(), state.clone());
let (shell, _keyboard) = shell_with_keyboard(move || {
let text_state = remember(|| TextFieldState::new(text)).with(|state| *state);
content_state.set(Some(text_state));
field(
text_state,
Modifier::empty()
.width(300.0)
.height(80.0)
.focus_requester(&content_requester),
);
});
requester.request_focus().expect("field focus");
let mut field = Self { shell, state };
field.press(KeyCode::End, Modifiers::NONE);
field
}
pub(crate) fn state(&self) -> TextFieldState {
self.state.get().expect("composed field")
}
pub(crate) fn press(&mut self, key: KeyCode, modifiers: Modifiers) -> &mut Self {
assert!(
self.shell
.on_key_event(&KeyEvent::key_down_with_modifiers(key, "", modifiers)),
"the focused field takes {key:?}"
);
self
}
pub(crate) fn type_text(&mut self, text: &str) -> &mut Self {
for character in text.chars() {
self.shell
.on_key_event(&KeyEvent::key_down(KeyCode::Unknown, character.to_string()));
}
self
}
pub(crate) fn text(&mut self) -> String {
self.shell.ime_editor_state().expect("focused field").text
}
pub(crate) fn shown_text(&mut self) -> String {
self.shell.update();
self.shell.with_layout_tree(|tree| {
fn find(node: &cranpose_ui::LayoutBox) -> Option<String> {
node.node_data
.modifier_slices()
.text_content()
.map(|text| text.to_string())
.or_else(|| node.children.iter().find_map(find))
}
tree.and_then(|tree| find(tree.root())).unwrap_or_default()
})
}
pub(crate) fn tap_after(&mut self, shown_prefix: &str) -> &mut Self {
let x = self.shell.app_context().enter(|| {
cranpose_ui::text::measure_text(
&cranpose_ui::text::AnnotatedString::from(shown_prefix),
&cranpose_ui::TextStyle::default(),
)
.width
});
assert!(self.shell.set_cursor(x, 10.0));
assert!(self.shell.pointer_pressed());
assert!(self.shell.pointer_released());
self
}
pub(crate) fn selection(&mut self) -> (usize, usize) {
let editor = self.shell.ime_editor_state().expect("focused field");
(editor.selection_start, editor.selection_end)
}
}