use std::sync::atomic::Ordering;
use std::time::Duration;
use crossterm::event::{self, Event, KeyCode, KeyEventKind, MouseButton, MouseEventKind};
use crate::events::HitMap;
use crate::layout::layoutbox::geometry::select_item_indices;
use crate::script::ScriptMsg;
use crate::tdom::node::{NodeArena, NodeId};
use super::InputState;
use super::cmd::RenderCmd;
use super::edit::{
apply_ctrl_shortcut, apply_input_key, apply_input_word_key, apply_shift_navigation,
is_input_field_key, paste_into_field,
};
use super::field::{
KeyGate, apply_range_key, apply_textarea_key, focused_form, is_disabled_control,
is_number_input, is_readonly_input, number_input_allows_char, textarea_content_width,
};
use super::focus::{blur_input, push_input_render};
use super::key_info::key_event_info;
use super::mouse::{
canvas_pos, handle_mouse_down, handle_mouse_move, handle_mouse_up, reveal_scrollbar_at,
scroll_at_wheel, scroll_at_wheel_x,
};
use super::page::page_level_builtins;
use super::scroll_anim::{ScrollTarget, active_scroll_target, step_target};
use super::select::{OverlayKind, close_select, refresh_datalist};
use super::{SCROLL_LINES_PER_TICK, reapply_console_modes};
fn is_edit_blocked(arena: Option<&NodeArena>, node_id: NodeId) -> bool {
is_readonly_input(arena, node_id) || is_disabled_control(arena, node_id)
}
fn commit_field_edit(
state: &mut InputState,
focused: NodeId,
hit_map: Option<&HitMap>,
arena: Option<&NodeArena>,
msgs: &mut Vec<ScriptMsg>,
cmds: &mut Vec<RenderCmd>,
) {
push_input_render(state, focused, cmds);
if arena.is_some() {
msgs.push(ScriptMsg::InputKeystroke { node_id: focused });
}
refresh_datalist(state, hit_map, arena, cmds);
}
pub fn trigger_refresh(
state: &mut InputState,
rows: u16,
max_scroll: u32,
hit_map: Option<&HitMap>,
arena: Option<&NodeArena>,
msgs: &mut Vec<ScriptMsg>,
timeout: Option<Duration>,
) -> Vec<RenderCmd> {
let mut cmds = Vec::new();
if let Some(t) = timeout {
if !event::poll(t).unwrap_or(false) {
return cmds;
}
}
match event::read() {
Ok(Event::Key(key)) => {
if key.kind == KeyEventKind::Release {
if arena.is_some() {
msgs.push(ScriptMsg::KeyUp {
target: state.focused_input,
key: key_event_info(&key),
});
}
return cmds;
}
if arena.is_some() {
state.next_key_id = state.next_key_id.wrapping_add(1);
msgs.push(ScriptMsg::KeyDown {
key_id: state.next_key_id,
target: state.focused_input,
key: key_event_info(&key),
});
}
if let Some(open) = state.open_select.as_ref() {
let Some(arena) = arena else {
return cmds;
};
let node_id = open.node_id;
let kind = open.kind;
match key.code {
KeyCode::Up | KeyCode::Down => {
let dir: i32 = if key.code == KeyCode::Up { -1 } else { 1 };
if let Some(open) = &mut state.open_select {
let items = select_item_indices(&open.rows);
if !items.is_empty() {
let pos =
items.iter().position(|&i| i == open.highlight).unwrap_or(0);
let next = items[(((pos as i32) + dir)
.rem_euclid(items.len() as i32))
as usize];
open.highlight = next;
cmds.push(RenderCmd::SelectMove {
node_id,
highlight: next,
});
}
}
return cmds;
}
KeyCode::Enter => {
if kind == OverlayKind::Select {
let pick = state.open_select.as_ref().map(|o| o.highlight);
close_select(state, arena, pick, msgs, &mut cmds);
} else {
let pick_value = state.open_select.as_ref().and_then(|o| {
o.options.get(o.highlight).map(|opt| opt.value.clone())
});
if let Some(open) = state.open_select.take() {
cmds.push(RenderCmd::SelectClose {
node_id: open.node_id,
});
}
if let Some(value) = pick_value {
state.input_value = value;
state.input_cursor = state.input_value.chars().count();
state.input_sel_anchor = None;
push_input_render(state, node_id, &mut cmds);
msgs.push(ScriptMsg::InputKeystroke { node_id });
}
}
return cmds;
}
KeyCode::Esc => {
close_select(state, arena, None, msgs, &mut cmds);
return cmds;
}
_ => {
close_select(state, arena, None, msgs, &mut cmds);
if kind == OverlayKind::Select {
return cmds;
}
}
}
}
if state.focused_input.is_some() {
let Some(focused) = state.focused_input else {
return cmds;
};
if key.code == KeyCode::Esc
|| (key.code == KeyCode::Enter && !state.focused_is_textarea)
{
if key.code == KeyCode::Enter
&& let Some(form_id) = focused_form(arena, focused)
{
msgs.push(ScriptMsg::FormSubmit { form_id });
}
blur_input(state, msgs, &mut cmds);
return cmds;
}
let content_width = textarea_content_width(arena, focused);
if state.focused_is_range {
let Some(arena) = arena else {
return cmds;
};
if apply_range_key(state, arena, focused, key.code) {
state.input_sel_anchor = None;
push_input_render(state, focused, &mut cmds);
msgs.push(ScriptMsg::RangeInput {
node_id: focused,
value: state.input_value.clone(),
});
}
return cmds;
}
if (key.modifiers.contains(event::KeyModifiers::CONTROL)
|| key.modifiers.contains(event::KeyModifiers::ALT))
&& matches!(
key.code,
KeyCode::Left
| KeyCode::Right
| KeyCode::Home
| KeyCode::End
| KeyCode::Backspace
| KeyCode::Delete
)
{
let mut code = key.code;
if is_edit_blocked(arena, focused)
&& matches!(code, KeyCode::Backspace | KeyCode::Delete)
{
code = KeyCode::Null;
}
if apply_input_word_key(
&mut state.input_value,
&mut state.input_cursor,
&mut state.input_sel_anchor,
code,
key.modifiers,
) {
commit_field_edit(state, focused, hit_map, arena, msgs, &mut cmds);
}
return cmds;
}
if key.modifiers.contains(event::KeyModifiers::CONTROL) {
let mut code = key.code;
if is_edit_blocked(arena, focused)
&& matches!(code, KeyCode::Char('x') | KeyCode::Char('v'))
{
code = KeyCode::Null;
}
if apply_ctrl_shortcut(state, code) {
commit_field_edit(state, focused, hit_map, arena, msgs, &mut cmds);
}
return cmds;
}
if key.modifiers.contains(event::KeyModifiers::SHIFT) {
if apply_shift_navigation(state, key.code) {
push_input_render(state, focused, &mut cmds);
}
return cmds;
}
if state.focused_is_textarea
&& matches!(key.code, KeyCode::Enter | KeyCode::Up | KeyCode::Down)
{
if apply_textarea_key(
&mut state.input_value,
&mut state.input_cursor,
content_width,
key.code,
) {
state.input_sel_anchor = None;
commit_field_edit(state, focused, hit_map, arena, msgs, &mut cmds);
}
return cmds;
}
if is_input_field_key(key.code) {
let mut code = key.code;
if !state.focused_is_textarea
&& is_number_input(arena, focused)
&& let KeyCode::Char(c) = code
&& !number_input_allows_char(c)
{
code = KeyCode::Null;
}
if is_edit_blocked(arena, focused)
&& matches!(
code,
KeyCode::Char(_) | KeyCode::Backspace | KeyCode::Delete
)
{
code = KeyCode::Null;
}
let changed = if state.focused_is_textarea {
apply_textarea_key(
&mut state.input_value,
&mut state.input_cursor,
content_width,
code,
)
} else {
apply_input_key(&mut state.input_value, &mut state.input_cursor, code)
};
if changed {
state.input_sel_anchor = None;
commit_field_edit(state, focused, hit_map, arena, msgs, &mut cmds);
}
return cmds;
}
}
let builtin = page_level_builtins(state, key, rows, max_scroll, arena, msgs);
if arena.is_some()
&& !builtin.is_empty()
&& !matches!(key.code, KeyCode::Tab | KeyCode::BackTab)
{
state.key_gate = Some(KeyGate {
id: state.next_key_id,
cmds: builtin,
});
return cmds;
}
cmds.extend(builtin);
cmds
}
Ok(Event::Mouse(mouse)) => {
let ctrl = mouse.modifiers.contains(event::KeyModifiers::CONTROL);
state.last_mouse = Some((mouse.column, mouse.row));
if let Some(hit_map) = hit_map {
let (x, y) = canvas_pos(state, mouse.column, mouse.row);
reveal_scrollbar_at(state, hit_map, x, y, &mut cmds);
}
match mouse.kind {
MouseEventKind::ScrollUp | MouseEventKind::ScrollDown => {
let up = matches!(mouse.kind, MouseEventKind::ScrollUp);
if state.open_select.is_some()
&& let Some(arena) = arena
{
close_select(state, arena, None, msgs, &mut cmds);
}
if ctrl {
return cmds;
}
let (x, y) = canvas_pos(state, mouse.column, mouse.row);
if state
.ui
.as_ref()
.is_some_and(|ui| ui.has_wheel_listeners.load(Ordering::Relaxed))
{
state.next_key_id = state.next_key_id.wrapping_add(1);
let gate_id = state.next_key_id;
msgs.push(ScriptMsg::Wheel { gate_id, x, y, up });
state.pending_wheel = Some((x, y, up));
state.wheel_gate = Some(KeyGate {
id: gate_id,
cmds: Vec::new(),
});
return cmds;
}
default_wheel_scroll(state, hit_map, arena, max_scroll, x, y, up, &mut cmds);
return cmds;
}
MouseEventKind::ScrollLeft | MouseEventKind::ScrollRight => {
let (x, y) = canvas_pos(state, mouse.column, mouse.row);
if let (Some(hit_map), Some(arena)) = (hit_map, arena) {
let left = matches!(mouse.kind, MouseEventKind::ScrollLeft);
let wheel_listeners = state
.ui
.as_ref()
.is_some_and(|ui| ui.has_wheel_listeners.load(Ordering::Relaxed));
if wheel_listeners {
state.next_key_id = state.next_key_id.wrapping_add(1);
let gate_id = state.next_key_id;
msgs.push(ScriptMsg::Wheel {
gate_id,
x,
y,
up: false,
});
state.pending_wheel = None;
state.wheel_gate = Some(KeyGate {
id: gate_id,
cmds: Vec::new(),
});
} else if let Some(cmd) =
scroll_at_wheel_x(state, hit_map, arena, x, y, left)
{
cmds.push(cmd);
}
}
return cmds;
}
MouseEventKind::Down(button) => {
if let (Some(hit_map), Some(arena)) = (hit_map, arena) {
state.pressed = Some((mouse.column, mouse.row));
let (x, y) = canvas_pos(state, mouse.column, mouse.row);
handle_mouse_down(state, hit_map, arena, x, y, button, msgs, &mut cmds);
}
}
MouseEventKind::Up(MouseButton::Left) => {
if let (Some(hit_map), Some(arena)) = (hit_map, arena) {
handle_mouse_up(
state,
hit_map,
arena,
(mouse.column, mouse.row),
MouseButton::Left,
msgs,
&mut cmds,
);
} else {
state.clear_press_tracking();
}
}
MouseEventKind::Up(btn @ (MouseButton::Right | MouseButton::Middle)) => {
if let (Some(hit_map), Some(arena)) = (hit_map, arena) {
handle_mouse_up(
state,
hit_map,
arena,
(mouse.column, mouse.row),
btn,
msgs,
&mut cmds,
);
} else {
state.clear_press_tracking();
}
}
MouseEventKind::Drag(_) | MouseEventKind::Moved => {
if let (Some(hit_map), Some(arena)) = (hit_map, arena) {
if !matches!(mouse.kind, MouseEventKind::Drag(_)) {
state.drop_stale_press();
}
let (x, y) = canvas_pos(state, mouse.column, mouse.row);
handle_mouse_move(state, hit_map, arena, x, y, msgs, &mut cmds);
}
}
}
cmds
}
Ok(Event::Resize(_, _new_rows)) => {
reapply_console_modes();
state.invalidate_viewport(&mut cmds);
state.request_resize_revalidation();
state.clamp_scroll_to(max_scroll);
cmds.push(RenderCmd::Redraw {
scroll_y: state.scroll_y,
zoom: state.zoom,
});
cmds
}
Ok(Event::Paste(pasted)) => {
if let Some(focused) = state.focused_input
&& !is_edit_blocked(arena, focused)
&& paste_into_field(state, &pasted, focused, &mut cmds)
&& arena.is_some()
{
msgs.push(ScriptMsg::InputKeystroke { node_id: focused });
}
cmds
}
_ => cmds,
}
}
pub fn default_wheel_scroll(
state: &mut InputState,
hit_map: Option<&HitMap>,
arena: Option<&NodeArena>,
max_scroll: u32,
x: u32,
y: u32,
up: bool,
cmds: &mut Vec<RenderCmd>,
) {
if let (Some(hit_map), Some(arena)) = (hit_map, arena)
&& let Some(cmd) = scroll_at_wheel(state, hit_map, arena, x, y, up)
{
cmds.push(cmd);
return;
}
if state.smooth_scroll {
let base = active_scroll_target(state, ScrollTarget::Page).unwrap_or(state.scroll_y);
let delta = if up {
-(SCROLL_LINES_PER_TICK as i32)
} else {
SCROLL_LINES_PER_TICK as i32
};
if let Some(to) = step_target(base, delta, 0, max_scroll) {
cmds.push(RenderCmd::ScrollAnimate {
target: ScrollTarget::Page,
to,
});
}
return;
}
let at_edge = if up {
state.scroll_y == 0
} else {
state.scroll_y >= max_scroll
};
if at_edge {
return;
}
state.scroll_y = if up {
state.scroll_y.saturating_sub(SCROLL_LINES_PER_TICK)
} else {
state.scroll_y.saturating_add(SCROLL_LINES_PER_TICK)
};
cmds.push(RenderCmd::Rerender {
scroll_y: state.scroll_y,
});
}