use crossterm::event::{KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
use rdom_core::{ListenerOptions, NodeId};
use crate::TuiDom;
use crate::runtime::builtins::toggle;
use crate::tui_event::{TuiDispatchExt, 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(label) = closest_label(ctx.dom, target) else {
return;
};
let Some(control) = associated_control(ctx.dom, label) else {
return;
};
crate::runtime::focus::focus_node(ctx.dom, Some(control));
if toggle::is_toggle(ctx.dom, control) && target != control {
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(control, &mut click);
}
})
.expect("root label click listener install");
}
fn closest_label(dom: &TuiDom, id: NodeId) -> Option<NodeId> {
let mut cur = Some(id);
while let Some(n) = cur {
if dom.node(n).tag_name() == Some("label") {
return Some(n);
}
cur = dom.node(n).parent_node().map(|p| p.id());
}
None
}
pub fn associated_control(dom: &TuiDom, label: NodeId) -> Option<NodeId> {
if let Some(id_ref) = dom.node(label).get_attribute("for")
&& !id_ref.is_empty()
{
let root = dom.root();
let id_ref_owned = id_ref.to_string();
if let Some(found) = find_by_id(dom, root, &id_ref_owned) {
return Some(found);
}
return None;
}
find_labelable_descendant(dom, label)
}
fn find_by_id(dom: &TuiDom, root: NodeId, needle: &str) -> Option<NodeId> {
if dom.node(root).get_attribute("id") == Some(needle) {
return Some(root);
}
for child in dom.node(root).child_nodes() {
if let Some(found) = find_by_id(dom, child.id(), needle) {
return Some(found);
}
}
None
}
fn find_labelable_descendant(dom: &TuiDom, start: NodeId) -> Option<NodeId> {
for child in dom.node(start).child_nodes() {
let id = child.id();
if is_labelable(dom, id) {
return Some(id);
}
if let Some(found) = find_labelable_descendant(dom, id) {
return Some(found);
}
}
None
}
fn is_labelable(dom: &TuiDom, id: NodeId) -> bool {
let node = dom.node(id);
let Some(tag) = node.tag_name() else {
return false;
};
match tag {
"button" | "meter" | "output" | "progress" | "select" | "textarea" => true,
"input" => !matches!(node.get_attribute("type"), Some("hidden")),
_ => false,
}
}
#[cfg(test)]
mod tests;