use crossterm::event::{KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
use rdom_core::{ListenerOptions, NodeId};
use crate::tui_event::TuiDispatchExt;
use crate::{TuiDom, TuiEvent};
pub fn install(dom: &mut TuiDom) {
let root = dom.root();
dom.add_event_listener(root, "click", ListenerOptions::default(), move |ctx| {
if ctx.event.default_prevented() {
return;
}
let Some(target) = ctx.event.target else {
return;
};
let Some(widget) = closest_toggle(ctx.dom, target) else {
return;
};
if ctx.dom.node(widget).has_attribute("disabled") {
return;
}
apply_toggle(ctx.dom, widget);
})
.expect("toggle click listener install");
dom.add_event_listener(root, "keydown", ListenerOptions::default(), move |ctx| {
if ctx.event.default_prevented() {
return;
}
let Some(focused) = ctx.dom.focused() else {
return;
};
if !is_toggle(ctx.dom, focused) {
return;
}
let Some(key) = ctx.event.detail.as_keyboard() else {
return;
};
if key.modifiers.ctrl || key.modifiers.meta || key.modifiers.alt {
return;
}
if key.key != " " {
return;
}
let fake_mouse = MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
column: 0,
row: 0,
modifiers: KeyModifiers::empty(),
};
let mut click = TuiEvent::click(fake_mouse);
click.event = click.event.clone().with_synthetic(true);
let _ = ctx.dom.dispatch_tui_event(focused, &mut click);
})
.expect("toggle space listener install");
dom.add_event_listener(root, "keydown", ListenerOptions::default(), move |ctx| {
if ctx.event.default_prevented() {
return;
}
let Some(focused) = ctx.dom.focused() else {
return;
};
if !is_radio(ctx.dom, focused) {
return;
}
let Some(key) = ctx.event.detail.as_keyboard() else {
return;
};
if key.modifiers.ctrl || key.modifiers.shift || key.modifiers.alt || key.modifiers.meta {
return;
}
let direction = match key.key.as_str() {
"ArrowUp" | "ArrowLeft" => -1i32,
"ArrowDown" | "ArrowRight" => 1i32,
_ => return,
};
move_focus_within_group(ctx.dom, focused, direction);
})
.expect("toggle arrow listener install");
}
fn apply_toggle(dom: &mut TuiDom, widget: NodeId) {
let was_checked = dom.node(widget).has_attribute("checked");
let is_radio_widget = is_radio(dom, widget);
if is_radio_widget {
if was_checked {
return;
}
let siblings = collect_radio_group(dom, widget);
for sib in siblings {
if sib != widget {
let _ = dom.remove_attribute(sib, "checked");
}
}
let _ = dom.set_attribute(widget, "checked", "");
} else {
if was_checked {
let _ = dom.remove_attribute(widget, "checked");
} else {
let _ = dom.set_attribute(widget, "checked", "");
}
}
let mut input_ev = TuiEvent::new("input");
let _ = dom.dispatch_tui_event(widget, &mut input_ev);
let mut change_ev = TuiEvent::new("change");
let _ = dom.dispatch_tui_event(widget, &mut change_ev);
}
fn move_focus_within_group(dom: &mut TuiDom, focused: NodeId, direction: i32) {
let group = collect_radio_group(dom, focused);
if group.len() < 2 {
return;
}
let Some(idx) = group.iter().position(|&id| id == focused) else {
return;
};
let len = group.len() as i32;
let next = ((idx as i32 + direction).rem_euclid(len)) as usize;
crate::runtime::focus::focus_node(dom, Some(group[next]));
}
fn collect_radio_group(dom: &TuiDom, widget: NodeId) -> Vec<NodeId> {
let name = dom
.node(widget)
.get_attribute("name")
.map(|s| s.to_string());
let Some(name) = name.filter(|s| !s.is_empty()) else {
return vec![widget];
};
let mut out = Vec::new();
walk_radios_with_name(dom, dom.root(), &name, &mut out);
out
}
fn walk_radios_with_name(dom: &TuiDom, id: NodeId, name: &str, out: &mut Vec<NodeId>) {
if is_radio(dom, id) && dom.node(id).get_attribute("name") == Some(name) {
out.push(id);
}
for child in dom.node(id).child_nodes() {
walk_radios_with_name(dom, child.id(), name, out);
}
}
fn closest_toggle(dom: &TuiDom, id: NodeId) -> Option<NodeId> {
let mut cur = Some(id);
while let Some(n) = cur {
if is_toggle(dom, n) {
return Some(n);
}
cur = dom.node(n).parent_node().map(|p| p.id());
}
None
}
pub(crate) fn is_toggle(dom: &TuiDom, id: NodeId) -> bool {
if dom.node(id).tag_name() != Some("input") {
return false;
}
matches!(
dom.node(id).get_attribute("type"),
Some("checkbox") | Some("radio")
)
}
fn is_radio(dom: &TuiDom, id: NodeId) -> bool {
dom.node(id).tag_name() == Some("input") && dom.node(id).get_attribute("type") == Some("radio")
}
#[cfg(test)]
mod tests;