use rdom_core::NodeId;
use super::dropdown::{close, is_dropdown, is_open, open};
use super::model::{is_multi, option_disabled, options, selected_options};
use super::state::{
anchor, extend_selection_to, fire_input_and_change, highlight, select_single, set_anchor,
set_highlight, toggle_option,
};
use super::typeahead::{single_printable_char, typeahead_search};
use crate::{TuiDom, TuiEventCtx};
pub(super) fn on_keydown(ctx: &mut TuiEventCtx<'_>) {
if ctx.event.default_prevented() {
return;
}
let Some(focused) = ctx.dom.focused() else {
return;
};
if ctx.dom.node(focused).tag_name() != Some("select") {
return;
}
if ctx.dom.is_actually_disabled(focused) {
return;
}
let Some(key) = ctx.event.detail.as_keyboard() else {
return;
};
let select = focused;
let multi = is_multi(ctx.dom, select);
let shift = key.modifiers.shift;
let ctrl = key.modifiers.ctrl || key.modifiers.meta;
let alt = key.modifiers.alt;
let no_mods = !key.modifiers.ctrl && !shift && !alt && !key.modifiers.meta;
match key.key.as_str() {
"ArrowUp" | "ArrowDown" if !ctrl && !alt => {
let dir = if key.key == "ArrowDown" { 1 } else { -1 };
step_navigation(ctx.dom, select, dir, multi, shift);
}
"Home" => {
jump_to_end(ctx.dom, select, true, multi, shift);
}
"End" => {
jump_to_end(ctx.dom, select, false, multi, shift);
}
" " if multi && !ctrl => {
toggle_highlighted(ctx.dom, select);
}
"a" | "A" if multi && ctrl => {
select_all(ctx.dom, select);
}
"Escape" if no_mods && is_dropdown(ctx.dom, select) && is_open(ctx.dom, select) => {
close(ctx.dom, select);
}
"Enter" if no_mods && is_dropdown(ctx.dom, select) => {
if is_open(ctx.dom, select) {
close(ctx.dom, select);
} else {
open(ctx.dom, select);
}
}
other if !ctrl && !alt && other != " " => {
if let Some(c) = single_printable_char(other) {
typeahead_search(ctx.dom, select, c, multi);
}
}
_ => {}
}
}
fn step_navigation(dom: &mut TuiDom, select: NodeId, dir: i32, multi: bool, shift: bool) {
let all = options(dom, select);
if all.is_empty() {
return;
}
let current = highlight(dom, select).or_else(|| {
selected_options(dom, select)
.first()
.copied()
.or_else(|| all.iter().find(|&&o| !option_disabled(dom, o)).copied())
});
let next = match current {
Some(c) => step_from(dom, &all, c, dir),
None => return,
};
let Some(next) = next else { return };
if multi {
if shift {
if anchor(dom, select).is_none()
&& let Some(old) = current
{
set_anchor(dom, select, Some(old));
}
set_highlight(dom, select, Some(next));
extend_selection_to(dom, select, next);
} else {
set_highlight(dom, select, Some(next));
set_anchor(dom, select, None);
}
} else {
set_highlight(dom, select, Some(next));
select_single(dom, select, next);
}
fire_input_and_change(dom, select);
}
fn step_from(dom: &TuiDom, list: &[NodeId], from: NodeId, dir: i32) -> Option<NodeId> {
let start = list.iter().position(|&o| o == from)?;
let mut i = start as i32;
let len = list.len() as i32;
loop {
i += dir;
if i < 0 || i >= len {
return None;
}
let candidate = list[i as usize];
if !option_disabled(dom, candidate) {
return Some(candidate);
}
}
}
fn jump_to_end(dom: &mut TuiDom, select: NodeId, home: bool, multi: bool, shift: bool) {
let all = options(dom, select);
let target = if home {
all.iter().find(|&&o| !option_disabled(dom, o)).copied()
} else {
all.iter()
.rev()
.find(|&&o| !option_disabled(dom, o))
.copied()
};
let Some(target) = target else { return };
set_highlight(dom, select, Some(target));
if multi {
if shift {
extend_selection_to(dom, select, target);
} else {
set_anchor(dom, select, None);
}
} else {
select_single(dom, select, target);
}
fire_input_and_change(dom, select);
}
fn toggle_highlighted(dom: &mut TuiDom, select: NodeId) {
let Some(h) = highlight(dom, select) else {
return;
};
if option_disabled(dom, h) {
return;
}
toggle_option(dom, select, h);
fire_input_and_change(dom, select);
}
fn select_all(dom: &mut TuiDom, select: NodeId) {
super::state::note_default_selected(dom, select);
for opt in options(dom, select) {
if !option_disabled(dom, opt) {
let _ = dom.set_attribute(opt, "selected", "");
}
}
fire_input_and_change(dom, select);
}