use super::redraw::Redraw;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use super::App;
use crate::render::backend::Backend;
use crate::{TuiDom, TuiEvent};
impl<B: Backend> App<B> {
pub(super) fn handle_key_event(&mut self, key: KeyEvent) {
use crossterm::event::KeyEventKind;
crate::runtime::focus::visible::note_key(&mut self.dom, key);
if key.kind == KeyEventKind::Release {
let target = self.dom.focused().unwrap_or_else(|| self.dom.root());
let mut tui = TuiEvent::keyup(key);
crate::tui_event::dispatch_to_live(&mut self.dom, target, &mut tui);
self.redraw
.note_if(tui.event.redraw_requested(), Redraw::Cascade);
return;
}
if try_handle_clipboard_key(self, key) {
return;
}
if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) {
self.should_quit = true;
return;
}
let target = self.dom.focused().unwrap_or_else(|| self.dom.root());
let mut tui = TuiEvent::keydown(key);
crate::tui_event::dispatch_to_live(&mut self.dom, target, &mut tui);
if key.code == KeyCode::F(10) && key.modifiers.contains(KeyModifiers::SHIFT) {
let mut cm = TuiEvent::new("contextmenu");
cm.event = cm.event.clone().with_synthetic(true);
crate::tui_event::dispatch_to_live(&mut self.dom, target, &mut cm);
self.redraw
.note_if(cm.event.redraw_requested(), Redraw::Cascade);
}
if !tui.event.default_prevented() {
if crate::runtime::selection::keyboard::try_handle_key(&mut self.dom, key)
|| try_handle_editable_key(&mut self.dom, key)
|| crate::runtime::scrollbar::handle_scroll_key(&mut self.dom, key)
{
self.redraw.note(Redraw::Layout);
} else {
match key.code {
KeyCode::Tab if key.modifiers.contains(KeyModifiers::SHIFT) => {
crate::runtime::focus::tabindex::focus_prev(&mut self.dom);
self.redraw.note(Redraw::Layout);
}
KeyCode::Tab => {
crate::runtime::focus::tabindex::focus_next(&mut self.dom);
self.redraw.note(Redraw::Layout);
}
KeyCode::BackTab => {
crate::runtime::focus::tabindex::focus_prev(&mut self.dom);
self.redraw.note(Redraw::Layout);
}
_ => {}
}
}
}
self.redraw
.note_if(tui.event.redraw_requested(), Redraw::Cascade);
}
}
fn try_handle_clipboard_key<B: Backend>(app: &mut App<B>, key: crossterm::event::KeyEvent) -> bool {
let ctrl_or_super = key.modifiers.contains(KeyModifiers::CONTROL)
|| key.modifiers.contains(KeyModifiers::SUPER);
if !ctrl_or_super {
return false;
}
match key.code {
KeyCode::Char('c') | KeyCode::Char('C') => do_copy(app),
KeyCode::Char('x') | KeyCode::Char('X') => do_cut(app),
KeyCode::Char('v') | KeyCode::Char('V') => do_paste(app),
_ => false,
}
}
fn do_copy<B: Backend>(app: &mut App<B>) -> bool {
let Some((text, _range)) =
crate::runtime::selection::clipboard::current_selection_text(&app.dom)
else {
return false;
};
let target = crate::runtime::selection::clipboard::copy_target(&app.dom)
.unwrap_or_else(|| app.dom.root());
let mut tui = TuiEvent::copy(text.clone());
crate::tui_event::dispatch_to_live(&mut app.dom, target, &mut tui);
if !tui.event.default_prevented() {
app.clipboard.write_text(text);
}
true
}
fn do_cut<B: Backend>(app: &mut App<B>) -> bool {
let Some((text, _range)) =
crate::runtime::selection::clipboard::current_selection_text(&app.dom)
else {
return false;
};
let target = crate::runtime::selection::clipboard::copy_target(&app.dom)
.unwrap_or_else(|| app.dom.root());
let mut tui = TuiEvent::cut(text.clone());
crate::tui_event::dispatch_to_live(&mut app.dom, target, &mut tui);
if !tui.event.default_prevented() {
app.clipboard.write_text(text);
if crate::node::nearest_editable_ancestor(&app.dom, target).is_some() {
let _ = crate::runtime::editing::insert_at_selection_as(
&mut app.dom,
"",
rdom_core::InputType::DeleteByCut,
);
}
}
true
}
fn try_handle_history_key(dom: &mut TuiDom, key: crossterm::event::KeyEvent) -> bool {
let ctrl_or_super = key.modifiers.contains(KeyModifiers::CONTROL)
|| key.modifiers.contains(KeyModifiers::SUPER);
if !ctrl_or_super {
return false;
}
let shift = key.modifiers.contains(KeyModifiers::SHIFT);
let is_undo = matches!(key.code, KeyCode::Char('z') | KeyCode::Char('Z')) && !shift;
let is_redo = matches!(key.code, KeyCode::Char('y') | KeyCode::Char('Y'))
|| (matches!(key.code, KeyCode::Char('z') | KeyCode::Char('Z')) && shift);
if is_undo {
matches!(
crate::runtime::editing::undo_last(dom),
crate::runtime::editing::UndoOutcome::Applied
)
} else if is_redo {
matches!(
crate::runtime::editing::redo_last(dom),
crate::runtime::editing::UndoOutcome::Applied
)
} else {
false
}
}
fn do_paste<B: Backend>(app: &mut App<B>) -> bool {
let text = app.clipboard.read_text().unwrap_or_default();
let target = app.dom.focused().unwrap_or_else(|| app.dom.root());
let mut tui = TuiEvent::paste(text.clone());
crate::tui_event::dispatch_to_live(&mut app.dom, target, &mut tui);
if !tui.event.default_prevented()
&& crate::node::nearest_editable_ancestor(&app.dom, target).is_some()
{
let _ = crate::runtime::editing::insert_at_selection_as(
&mut app.dom,
&text,
rdom_core::InputType::InsertFromPaste,
);
}
true
}
fn try_handle_editable_key(dom: &mut TuiDom, key: crossterm::event::KeyEvent) -> bool {
let Some(focused) = dom.focused() else {
return false;
};
if crate::node::nearest_editable_ancestor(dom, focused).is_none() {
return false;
};
if try_handle_history_key(dom, key) {
return true;
}
if crate::runtime::editing::movement::try_handle_movement_key(dom, key) {
return true;
}
if matches!(key.code, KeyCode::Enter)
&& !key
.modifiers
.intersects(KeyModifiers::CONTROL | KeyModifiers::SUPER | KeyModifiers::ALT)
{
let editable = crate::node::nearest_editable_ancestor(dom, focused);
let is_input = editable
.map(|id| dom.node(id).tag_name() == Some("input"))
.unwrap_or(false);
if is_input {
return true;
}
let outcome = crate::runtime::editing::insert_at_selection(dom, "\n");
return matches!(
outcome,
crate::runtime::editing::EditOutcome::Applied
| crate::runtime::editing::EditOutcome::Prevented
);
}
if key.modifiers.contains(KeyModifiers::CONTROL) || key.modifiers.contains(KeyModifiers::SUPER)
{
return false;
}
let ch = match key.code {
KeyCode::Char(c) if !c.is_control() => c,
_ => return false,
};
let outcome = crate::runtime::editing::insert_at_selection_as(
dom,
&ch.to_string(),
rdom_core::InputType::InsertText,
);
matches!(
outcome,
crate::runtime::editing::EditOutcome::Applied
| crate::runtime::editing::EditOutcome::Prevented
)
}