use std::collections::HashMap;
use velox_dom::VNode;
use crate::RenderTree;
type EventHandler = Box<dyn FnMut(Option<&str>)>;
pub struct EventRegistry {
handlers: HashMap<String, EventHandler>,
}
impl Default for EventRegistry {
fn default() -> Self {
Self::new()
}
}
impl EventRegistry {
pub fn new() -> Self {
Self {
handlers: HashMap::new(),
}
}
pub fn on<F: FnMut(Option<&str>) + 'static>(&mut self, name: impl Into<String>, f: F) {
self.handlers.insert(name.into(), Box::new(f));
}
pub fn remove(&mut self, name: &str) {
self.handlers.remove(name);
}
pub fn has(&self, name: &str) -> bool {
self.handlers.contains_key(name)
}
}
#[derive(Debug, Clone)]
pub struct ClickTarget {
pub rect: velox_dom::layout::Rect,
pub handler: String,
pub payload: Option<String>,
pub z_index: i32,
pub order: i32,
pub clip: Option<velox_dom::layout::Rect>,
pub sc: StackCtx,
}
#[derive(Debug, Clone)]
pub struct HoverTarget {
pub rect: velox_dom::layout::Rect,
pub id: u32,
pub z_index: i32,
pub order: i32,
pub clip: Option<velox_dom::layout::Rect>,
pub sc: StackCtx,
}
#[derive(Debug, Clone)]
pub struct InputTarget {
pub rect: velox_dom::layout::Rect,
pub path: Vec<usize>,
pub z_index: i32,
pub order: i32,
pub clip: Option<velox_dom::layout::Rect>,
pub sc: StackCtx,
pub focused: bool,
pub cursor: usize,
pub anchor: Option<usize>,
pub blink_on: bool,
}
impl InputTarget {
pub fn selection(&self) -> Option<(usize, usize)> {
let anchor = self.anchor?;
if anchor == self.cursor {
return None;
}
Some(if anchor < self.cursor {
(anchor, self.cursor)
} else {
(self.cursor, anchor)
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StackCtx {
pub group_z: i32,
pub depth: usize,
}
impl StackCtx {
pub const ROOT: Self = Self {
group_z: 0,
depth: 0,
};
fn enter(&self, node: &velox_dom::layout::LayoutNode) -> Self {
let creates = node.stacking_context;
Self {
group_z: if self.depth == 0 && creates {
node.z_index
} else {
self.group_z
},
depth: self.depth + usize::from(creates),
}
}
}
pub fn is_hoverable(tag: &str, props: &velox_dom::Props) -> bool {
if props.attrs.contains_key("on:click") || tag == "button" {
return true;
}
props
.attrs
.get("class")
.map(|s| s.split_whitespace().any(|c| c == "btn"))
.unwrap_or(false)
}
fn intersect(
a: velox_dom::layout::Rect,
b: velox_dom::layout::Rect,
) -> Option<velox_dom::layout::Rect> {
let x0 = a.x.max(b.x);
let y0 = a.y.max(b.y);
let x1 = (a.x + a.w).min(b.x + b.w);
let y1 = (a.y + a.h).min(b.y + b.h);
if x1 <= x0 || y1 <= y0 {
return None;
}
Some(velox_dom::layout::Rect {
x: x0,
y: y0,
w: x1 - x0,
h: y1 - y0,
})
}
fn clip_stack_next(
parent: Option<velox_dom::layout::Rect>,
node: &velox_dom::layout::LayoutNode,
) -> Option<Option<velox_dom::layout::Rect>> {
match (parent, node.clip) {
(Some(c), Some(lc)) => intersect(c, lc).map(Some),
(None, Some(lc)) => Some(Some(lc)),
(Some(c), None) => Some(Some(c)),
(None, None) => Some(None),
}
}
pub fn collect_click_targets(
vnode: &VNode,
layout: &velox_dom::layout::LayoutNode,
clip: Option<velox_dom::layout::Rect>,
sc: StackCtx,
order: &mut i32,
out: &mut Vec<ClickTarget>,
) {
let sc = sc.enter(layout);
let Some(next_clip) = clip_stack_next(clip, layout) else {
return;
};
match vnode {
VNode::Text(_) => {}
VNode::Element {
props, children, ..
} => {
if let Some(handler) = props.attrs.get("on:click").cloned() {
let payload = props.attrs.get("on:click-payload").cloned();
if next_clip
.map(|c| rects_intersect(layout.rect, c))
.unwrap_or(true)
{
let ord = *order;
*order += 1;
out.push(ClickTarget {
rect: layout.rect,
handler,
payload,
z_index: layout.z_index,
order: ord,
clip: next_clip,
sc,
});
}
}
let mut ordered: Vec<(i32, usize)> = layout
.children
.iter()
.enumerate()
.filter_map(|(i, ln)| {
if ln.display_none {
None
} else {
Some((ln.z_index, i))
}
})
.collect();
ordered.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
for (_, idx) in ordered {
if let Some(child_layout) = layout.children.get(idx)
&& let Some(src_idx) = child_layout.source_index
&& let Some(child) = children.get(src_idx)
{
collect_click_targets(child, child_layout, next_clip, sc, order, out);
}
}
}
}
}
pub fn collect_hover_targets(
vnode: &VNode,
layout: &velox_dom::layout::LayoutNode,
clip: Option<velox_dom::layout::Rect>,
sc: StackCtx,
order: &mut i32,
out: &mut Vec<HoverTarget>,
) {
let sc = sc.enter(layout);
let Some(next_clip) = clip_stack_next(clip, layout) else {
return;
};
match vnode {
VNode::Text(_) => {}
VNode::Element {
tag,
props,
children,
..
} => {
if is_hoverable(tag, props) {
let id = props
.attrs
.get("data-hover-id")
.and_then(|v| v.parse::<u32>().ok())
.unwrap_or(0);
if next_clip
.map(|c| rects_intersect(layout.rect, c))
.unwrap_or(true)
{
let ord = *order;
*order += 1;
out.push(HoverTarget {
rect: layout.rect,
id,
z_index: layout.z_index,
order: ord,
clip: next_clip,
sc,
});
}
}
let mut ordered: Vec<(i32, usize)> = layout
.children
.iter()
.enumerate()
.filter_map(|(i, ln)| {
if ln.display_none {
None
} else {
Some((ln.z_index, i))
}
})
.collect();
ordered.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
for (_, idx) in ordered {
if let Some(child_layout) = layout.children.get(idx)
&& let Some(src_idx) = child_layout.source_index
&& let Some(child) = children.get(src_idx)
{
collect_hover_targets(child, child_layout, next_clip, sc, order, out);
}
}
}
}
}
fn rects_intersect(a: velox_dom::layout::Rect, b: velox_dom::layout::Rect) -> bool {
let x0 = a.x.max(b.x);
let y0 = a.y.max(b.y);
let x1 = (a.x + a.w).min(b.x + b.w);
let y1 = (a.y + a.h).min(b.y + b.h);
x1 > x0 && y1 > y0
}
fn rect_contains_point(r: velox_dom::layout::Rect, x: f32, y: f32) -> bool {
let x0 = r.x as f32;
let y0 = r.y as f32;
let x1 = (r.x + r.w) as f32;
let y1 = (r.y + r.h) as f32;
x >= x0 && x <= x1 && y >= y0 && y <= y1
}
fn stack_order_desc<T>(
targets: &[T],
z_index: impl Fn(&T) -> i32,
order: impl Fn(&T) -> i32,
sc: impl Fn(&T) -> StackCtx,
) -> Vec<usize> {
let mut ordered: Vec<(i32, usize, i32, i32, usize)> = targets
.iter()
.enumerate()
.map(|(i, t)| (sc(t).group_z, sc(t).depth, z_index(t), order(t), i))
.collect();
ordered.sort_by(|a, b| {
b.0.cmp(&a.0)
.then(b.1.cmp(&a.1))
.then(b.2.cmp(&a.2))
.then(b.3.cmp(&a.3))
});
ordered.into_iter().map(|(_, _, _, _, i)| i).collect()
}
pub fn hit_test_click(targets: &[ClickTarget], x: f32, y: f32) -> Option<(&str, Option<&str>)> {
for idx in stack_order_desc(targets, |t| t.z_index, |t| t.order, |t| t.sc) {
let target = &targets[idx];
if let Some(c) = target.clip
&& !rect_contains_point(c, x, y)
{
continue;
}
if rect_contains_point(target.rect, x, y) {
return Some((target.handler.as_str(), target.payload.as_deref()));
}
}
None
}
pub fn is_text_input(tag: &str, props: &velox_dom::Props) -> bool {
if tag != "input" {
return false;
}
match props.attrs.get("type") {
None => true,
Some(t) => {
let t = t.trim();
t.is_empty() || t.eq_ignore_ascii_case("text")
}
}
}
pub fn collect_input_targets(
vnode: &VNode,
layout: &velox_dom::layout::LayoutNode,
clip: Option<velox_dom::layout::Rect>,
sc: StackCtx,
path: &mut Vec<usize>,
order: &mut i32,
out: &mut Vec<InputTarget>,
) {
let sc = sc.enter(layout);
let Some(next_clip) = clip_stack_next(clip, layout) else {
return;
};
match vnode {
VNode::Text(_) => {}
VNode::Element {
tag,
props,
children,
..
} => {
if is_text_input(tag, props)
&& next_clip
.map(|c| rects_intersect(layout.rect, c))
.unwrap_or(true)
{
let ord = *order;
*order += 1;
out.push(InputTarget {
rect: layout.rect,
path: path.clone(),
z_index: layout.z_index,
order: ord,
clip: next_clip,
sc,
focused: false,
cursor: 0,
anchor: None,
blink_on: true,
});
}
let mut ordered: Vec<(i32, usize)> = layout
.children
.iter()
.enumerate()
.filter_map(|(i, ln)| {
if ln.display_none {
None
} else {
Some((ln.z_index, i))
}
})
.collect();
ordered.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
for (_, idx) in ordered {
if let Some(child_layout) = layout.children.get(idx)
&& let Some(src_idx) = child_layout.source_index
&& let Some(child) = children.get(src_idx)
{
path.push(src_idx);
collect_input_targets(child, child_layout, next_clip, sc, path, order, out);
path.pop();
}
}
}
}
}
pub fn hit_test_input(targets: &[InputTarget], x: f32, y: f32) -> Option<&InputTarget> {
hit_test_input_index(targets, x, y).map(|i| &targets[i])
}
pub fn hit_test_input_index(targets: &[InputTarget], x: f32, y: f32) -> Option<usize> {
for idx in stack_order_desc(targets, |t| t.z_index, |t| t.order, |t| t.sc) {
let target = &targets[idx];
if let Some(c) = target.clip
&& !rect_contains_point(c, x, y)
{
continue;
}
if rect_contains_point(target.rect, x, y) {
return Some(idx);
}
}
None
}
pub fn focused_input_index(targets: &[InputTarget]) -> Option<usize> {
targets.iter().position(|t| t.focused)
}
pub fn any_input_focused(targets: &[InputTarget]) -> bool {
targets.iter().any(|t| t.focused)
}
pub fn focused_input_path(targets: &[InputTarget]) -> Option<Vec<usize>> {
focused_input_index(targets).map(|i| targets[i].path.clone())
}
pub fn focus_input(targets: &mut [InputTarget], index: usize) -> bool {
if index >= targets.len() {
return false;
}
let mut changed = false;
for (i, t) in targets.iter_mut().enumerate() {
let want = i == index;
if t.focused != want {
t.focused = want;
changed = true;
}
}
changed
}
pub fn input_index_by_path(targets: &[InputTarget], path: &[usize]) -> Option<usize> {
targets.iter().position(|t| t.path == path)
}
pub fn find_focus_id_path(node: &VNode, path: &[usize], id: &str) -> Option<Vec<usize>> {
let VNode::Element {
props, children, ..
} = node
else {
return None;
};
if props
.attrs
.get("data-focus-id")
.is_some_and(|v| v.trim() == id)
{
return Some(path.to_vec());
}
for (i, child) in children.iter().enumerate() {
let mut child_path = path.to_vec();
child_path.push(i);
if let Some(found) = find_focus_id_path(child, &child_path, id) {
return Some(found);
}
}
None
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct KeydownPlan {
pub handlers: Vec<String>,
pub focus_paths: Vec<Vec<usize>>,
}
pub fn plan_keydown(vnode: &VNode, key_name: &str) -> KeydownPlan {
let mut plan = KeydownPlan::default();
collect_keydown(vnode, &[], key_name, &mut plan);
plan
}
fn collect_keydown(node: &VNode, path: &[usize], key_name: &str, plan: &mut KeydownPlan) {
let VNode::Element {
props, children, ..
} = node
else {
return;
};
if let Some(handler) = props.attrs.get("on:keydown")
&& !handler.is_empty()
{
plan.handlers.push(handler.clone());
}
if props
.attrs
.get("data-focus-on")
.is_some_and(|v| v.trim().eq_ignore_ascii_case(key_name))
{
plan.focus_paths.push(path.to_vec());
}
for (i, child) in children.iter().enumerate() {
let mut child_path = path.to_vec();
child_path.push(i);
collect_keydown(child, &child_path, key_name, plan);
}
}
pub fn apply_keydown(
plan: &KeydownPlan,
key_name: &str,
input_targets: &mut [InputTarget],
focused_input: &mut Option<Vec<usize>>,
value_len: &dyn Fn(&[usize]) -> usize,
on_event: &mut impl FnMut(&str, Option<&str>),
) -> bool {
let mut focus_changed = false;
for path in &plan.focus_paths {
let Some(idx) = input_index_by_path(input_targets, path) else {
continue;
};
if focus_input(input_targets, idx) {
input_targets[idx].blink_on = true;
input_targets[idx].anchor = None;
input_targets[idx].cursor = value_len(path);
*focused_input = Some(input_targets[idx].path.clone());
focus_changed = true;
}
}
for handler in &plan.handlers {
on_event(handler, Some(key_name));
}
focus_changed || !plan.handlers.is_empty()
}
pub fn blur_focused_input(targets: &mut [InputTarget]) -> bool {
let mut changed = false;
for t in targets.iter_mut() {
if t.focused || t.anchor.is_some() {
t.focused = false;
t.anchor = None;
t.blink_on = true;
changed = true;
}
}
changed
}
pub fn preserve_input_state(
targets: &mut [InputTarget],
previous: &[InputTarget],
value_len: &dyn Fn(&[usize]) -> usize,
) {
for t in targets.iter_mut() {
if let Some(prev) = previous.iter().find(|p| p.path == t.path) {
let len = value_len(&t.path);
t.focused = prev.focused;
t.blink_on = prev.blink_on;
t.cursor = prev.cursor.min(len);
t.anchor = prev.anchor.map(|a| a.min(len));
}
}
}
pub fn click_to_char_index(
value: &str,
rect: velox_dom::layout::Rect,
click_x: f32,
text_origin_x: f32,
font_size: f32,
measure: &dyn Fn(&str) -> f32,
) -> usize {
let char_count = value.chars().count();
if char_count == 0 {
return 0;
}
let origin = text_origin_x.max(rect.x as f32);
let local = (click_x - origin).max(0.0);
let fallback = |s: &str| font_size * 0.5 * s.chars().count() as f32;
let width_of = |s: &str| {
let w = measure(s);
if w.is_finite() && w >= 0.0 {
w
} else {
fallback(s)
}
};
let mut best_left = 0usize;
let mut left_x = 0.0f32;
let mut best_right: Option<usize> = None;
let mut right_x = 0.0f32;
for (char_i, (byte_i, _)) in value.char_indices().enumerate() {
let w = width_of(&value[..byte_i]);
if w <= local {
best_left = char_i;
left_x = w;
} else {
best_right = Some(char_i);
right_x = w;
break;
}
}
match best_right {
None => char_count,
Some(right) => {
let d_left = (local - left_x).abs();
let d_right = (right_x - local).abs();
if d_left <= d_right { best_left } else { right }
}
}
}
pub fn snap_to_char_boundary(value: &str, index: usize) -> usize {
let mut i = index.min(value.len());
while i > 0 && !value.is_char_boundary(i) {
i -= 1;
}
i
}
fn byte_of_char(value: &str, char_i: usize) -> usize {
value
.char_indices()
.nth(char_i)
.map(|(b, _)| b)
.unwrap_or(value.len())
}
pub fn delete_char_range(value: &str, a: usize, b: usize) -> String {
let (a, b) = if a <= b { (a, b) } else { (b, a) };
let ba = snap_to_char_boundary(value, byte_of_char(value, a));
let bb = snap_to_char_boundary(value, byte_of_char(value, b));
let (ba, bb) = if ba <= bb { (ba, bb) } else { (bb, ba) };
let mut out = String::with_capacity(value.len().saturating_sub(bb - ba));
out.push_str(&value[..ba]);
out.push_str(&value[bb..]);
out
}
pub fn insert_char_at(value: &str, char_i: usize, ch: char) -> String {
let b = snap_to_char_boundary(value, byte_of_char(value, char_i));
let mut out = String::with_capacity(value.len() + ch.len_utf8());
out.push_str(&value[..b]);
out.push(ch);
out.push_str(&value[b..]);
out
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EditAction {
MoveLeft { shift: bool },
MoveRight { shift: bool },
Home { shift: bool },
End { shift: bool },
Backspace,
Delete,
Insert(char),
Submit,
Blur,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct EditResult {
pub value: Option<String>,
pub submit: bool,
pub moved: bool,
}
impl EditResult {
pub fn needs_repaint(&self) -> bool {
self.value.is_some() || self.submit || self.moved
}
}
pub fn apply_edit(target: &mut InputTarget, value: &str, action: EditAction) -> EditResult {
let len = value.chars().count();
let mut cursor = target.cursor.min(len);
let anchor = target.anchor.map(|a| a.min(len));
let sel = match anchor {
Some(a) if a != cursor => Some(if a < cursor { (a, cursor) } else { (cursor, a) }),
_ => None,
};
let mut res = EditResult::default();
let mut touched = false;
match action {
EditAction::MoveLeft { shift } => {
if cursor > 0 {
let old = cursor;
cursor -= 1;
if shift {
target.anchor = Some(anchor.unwrap_or(old));
} else {
target.anchor = None;
}
touched = true;
}
}
EditAction::MoveRight { shift } => {
if cursor < len {
let old = cursor;
cursor += 1;
if shift {
target.anchor = Some(anchor.unwrap_or(old));
} else {
target.anchor = None;
}
touched = true;
}
}
EditAction::Home { shift } => {
if shift {
target.anchor = Some(anchor.unwrap_or(cursor));
} else {
target.anchor = None;
}
if cursor != 0 {
cursor = 0;
touched = true;
} else {
res.moved = sel.is_some();
}
}
EditAction::End { shift } => {
if shift {
target.anchor = Some(anchor.unwrap_or(cursor));
} else {
target.anchor = None;
}
if cursor != len {
cursor = len;
touched = true;
} else {
res.moved = sel.is_some();
}
}
EditAction::Backspace => {
if let Some((s, e)) = sel {
res.value = Some(delete_char_range(value, s, e));
cursor = s;
target.anchor = None;
touched = true;
} else if cursor > 0 {
res.value = Some(delete_char_range(value, cursor - 1, cursor));
cursor -= 1;
target.anchor = None;
touched = true;
}
}
EditAction::Delete => {
if let Some((s, e)) = sel {
res.value = Some(delete_char_range(value, s, e));
cursor = s;
target.anchor = None;
touched = true;
} else if cursor < len {
res.value = Some(delete_char_range(value, cursor, cursor + 1));
target.anchor = None;
touched = true;
}
}
EditAction::Insert(ch) => {
if let Some((s, e)) = sel {
let cleared = delete_char_range(value, s, e);
res.value = Some(insert_char_at(&cleared, s, ch));
cursor = s + 1;
} else {
res.value = Some(insert_char_at(value, cursor, ch));
cursor += 1;
}
target.anchor = None;
touched = true;
}
EditAction::Submit => {
if let Some((s, e)) = sel {
res.value = Some(delete_char_range(value, s, e));
cursor = s;
touched = true;
}
target.anchor = None;
res.submit = true;
}
EditAction::Blur => {
res.moved = target.focused || sel.is_some();
target.focused = false;
target.anchor = None;
cursor = 0;
}
}
target.cursor = cursor;
if touched {
target.blink_on = true;
res.moved = true;
}
res
}
pub fn hit_test_hover(targets: &[HoverTarget], x: f32, y: f32) -> Option<u32> {
for idx in stack_order_desc(targets, |t| t.z_index, |t| t.order, |t| t.sc) {
let target = &targets[idx];
if let Some(c) = target.clip
&& !rect_contains_point(c, x, y)
{
continue;
}
if rect_contains_point(target.rect, x, y) {
return Some(target.id);
}
}
None
}
pub fn hit_test_scrollable(
layout: &velox_dom::layout::LayoutNode,
x: f32,
y: f32,
) -> Option<Vec<usize>> {
fn dfs(
node: &velox_dom::layout::LayoutNode,
x: f32,
y: f32,
path: &mut Vec<usize>,
best: &mut Option<(Vec<usize>, usize)>,
depth: usize,
) {
if node.scrollable && rect_contains_point(node.rect, x, y) {
let candidate = path.clone();
match best {
Some((_, best_depth)) if depth <= *best_depth => {}
_ => *best = Some((candidate, depth)),
}
}
for child in &node.children {
if let Some(idx) = child.source_index {
path.push(idx);
dfs(child, x, y, path, best, depth + 1);
path.pop();
} else {
dfs(child, x, y, path, best, depth + 1);
}
}
}
let mut best: Option<(Vec<usize>, usize)> = None;
let mut path = Vec::new();
dfs(layout, x, y, &mut path, &mut best, 0);
best.map(|(p, _)| p)
}
pub fn node_at_path<'a>(
layout: &'a velox_dom::layout::LayoutNode,
path: &[usize],
) -> Option<&'a velox_dom::layout::LayoutNode> {
let mut node = layout;
for &idx in path {
node = node.children.iter().find(|c| c.source_index == Some(idx))?;
}
Some(node)
}
fn shift_subtree_y(node: &mut velox_dom::layout::LayoutNode, dy: i32) {
node.rect.y += dy;
if let Some(clip) = node.clip.as_mut() {
clip.y += dy;
}
for child in &mut node.children {
shift_subtree_y(child, dy);
}
}
pub fn apply_scroll_offsets(
layout: &mut velox_dom::layout::LayoutNode,
offsets: &std::collections::HashMap<Vec<usize>, f32>,
path: &mut Vec<usize>,
) {
if layout.scrollable
&& let Some(&off) = offsets.get(path)
{
let max = (layout.max_scroll_y as f32).max(0.0);
let target = off.clamp(0.0, max).round() as i32;
let dy = layout.scroll_y - target;
if dy != 0 {
for child in &mut layout.children {
shift_subtree_y(child, dy);
}
}
layout.scroll_y = target;
}
for child in &mut layout.children {
if let Some(idx) = child.source_index {
path.push(idx);
apply_scroll_offsets(child, offsets, path);
path.pop();
} else {
apply_scroll_offsets(child, offsets, path);
}
}
}
pub fn apply_wheel_scroll(
layout: &velox_dom::layout::LayoutNode,
x: f32,
y: f32,
offsets: &mut std::collections::HashMap<Vec<usize>, f32>,
delta_y: f32,
) -> bool {
let Some(path) = hit_test_scrollable(layout, x, y) else {
return false;
};
let Some(node) = node_at_path(layout, &path) else {
return false;
};
let entry = offsets.entry(path).or_insert(node.scroll_y as f32);
let before = *entry;
let mut state = velox_dom::layout::ScrollState::new(node.max_scroll_y as f32);
state.offset_y = before;
state.on_wheel(delta_y);
*entry = state.offset_y;
state.offset_y != before
}
pub fn dispatch(event: &str, tree: &RenderTree, registry: &mut EventRegistry) -> usize {
let mut invoked = 0;
let key = format!("on:{}", event);
let payload_key = format!("on:{}-payload", event);
fn walk(node: &VNode, key: &str, payload_key: &str, out: &mut Vec<(String, Option<String>)>) {
match node {
VNode::Text(_) => {}
VNode::Element {
props, children, ..
} => {
if let Some(v) = props.attrs.get(key) {
let payload = props.attrs.get(payload_key).cloned();
out.push((v.clone(), payload));
}
for c in children {
walk(c, key, payload_key, out);
}
}
}
}
let mut targets = Vec::new();
walk(&tree.root, &key, &payload_key, &mut targets);
for (name, payload) in targets {
if let Some(cb) = registry.handlers.get_mut(&name) {
cb(payload.as_deref());
invoked += 1;
}
}
invoked
}
use std::time::{Duration, Instant};
pub struct Runtime {
pub tree: RenderTree,
pub registry: EventRegistry,
last_click: Option<Instant>,
hover_sent: bool,
}
impl Runtime {
pub fn new(tree: RenderTree) -> Self {
Self {
tree,
registry: EventRegistry::new(),
last_click: None,
hover_sent: false,
}
}
pub fn mouse_click(&mut self) -> usize {
let now = Instant::now();
if let Some(prev) = self.last_click {
if now.duration_since(prev) <= Duration::from_millis(400) {
self.last_click = None;
dispatch("dblclick", &self.tree, &mut self.registry)
} else {
self.last_click = Some(now);
dispatch("click", &self.tree, &mut self.registry)
}
} else {
self.last_click = Some(now);
dispatch("click", &self.tree, &mut self.registry)
}
}
pub fn cursor_moved(&mut self) -> usize {
self.hover_sent = true;
dispatch("hover", &self.tree, &mut self.registry)
}
pub fn reset_hover(&mut self) {
self.hover_sent = false;
}
}