use std::collections::HashMap;
use kurbo::Point;
use crate::element::{Cursor, Element, Kind};
use crate::frame::Frame;
use crate::frame_state::FrameState;
use crate::id::WidgetId;
use crate::input;
use crate::text::Fonts;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Key {
Enter,
Space,
Escape,
ArrowLeft,
ArrowRight,
ArrowUp,
ArrowDown,
Home,
End,
Backspace,
Delete,
PageUp,
PageDown,
Char(char),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KeyInput {
pub key: Key,
pub shift: bool,
pub ctrl: bool,
pub alt: bool,
pub meta: bool,
}
impl KeyInput {
pub fn plain(key: Key) -> Self {
Self {
key,
shift: false,
ctrl: false,
alt: false,
meta: false,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum InputEvent {
PointerMove {
x: f32,
y: f32,
},
PointerDown,
PointerUp,
RightDown,
RightUp,
FileDrop(std::path::PathBuf),
Wheel {
dy: f32,
},
Modifiers {
shift: bool,
ctrl: bool,
alt: bool,
meta: bool,
},
PointerLeave,
Tab,
ShiftTab,
Key(KeyInput),
Text(String),
ImePreedit {
text: String,
cursor: Option<(usize, usize)>,
},
}
pub struct Dispatch<Msg> {
pub msgs: Vec<Msg>,
pub redraw: bool,
pub cursor: Option<Cursor>,
}
impl<Msg> Default for Dispatch<Msg> {
fn default() -> Self {
Self {
msgs: Vec::new(),
redraw: false,
cursor: None,
}
}
}
enum ElemRef<'a, Msg> {
Borrowed(&'a Element<Msg>),
Owned {
row: std::rc::Rc<Element<Msg>>,
path: Vec<usize>,
},
}
impl<Msg> ElemRef<'_, Msg> {
fn resolve(&self) -> &Element<Msg> {
match self {
Self::Borrowed(el) => el,
Self::Owned { row, path } => {
let mut el: &Element<Msg> = row;
for &i in path {
el = &el.children[i];
}
el
}
}
}
}
struct Handlers<'a, Msg> {
map: HashMap<WidgetId, ElemRef<'a, Msg>>,
}
impl<'a, Msg> Handlers<'a, Msg> {
fn collect(root: &'a Element<Msg>, state: &FrameState, viewport: f32) -> Self {
let mut handlers = Self {
map: HashMap::new(),
};
handlers.walk_borrowed(root, WidgetId::ROOT, state, viewport);
handlers
}
fn walk_borrowed(
&mut self,
el: &'a Element<Msg>,
id: WidgetId,
state: &FrameState,
viewport: f32,
) {
self.expand_virtual(el, id, state, viewport);
self.map.insert(id, ElemRef::Borrowed(el));
for (i, child) in el.children.iter().enumerate() {
self.walk_borrowed(child, id.child(i, child.key.as_deref()), state, viewport);
}
}
fn expand_virtual(
&mut self,
el: &Element<Msg>,
id: WidgetId,
state: &FrameState,
viewport: f32,
) {
let Some(v) = &el.virtual_rows else {
return;
};
let window =
crate::frame::virtual_window(v.count, v.row_height, state.scroll_offset(id), viewport);
for (j, i) in window.enumerate() {
let row = std::rc::Rc::new(crate::frame::materialize_virtual_row(v, i));
let rid = id.child(1 + j, row.key.as_deref());
self.walk_owned(&row, Vec::new(), rid, state, viewport);
}
}
fn walk_owned(
&mut self,
row: &std::rc::Rc<Element<Msg>>,
path: Vec<usize>,
id: WidgetId,
state: &FrameState,
viewport: f32,
) {
let el = {
let mut el: &Element<Msg> = row;
for &i in &path {
el = &el.children[i];
}
el
};
self.expand_virtual(el, id, state, viewport);
let child_ids: Vec<WidgetId> = el
.children
.iter()
.enumerate()
.map(|(i, c)| id.child(i, c.key.as_deref()))
.collect();
self.map.insert(
id,
ElemRef::Owned {
row: std::rc::Rc::clone(row),
path: path.clone(),
},
);
for (i, child_id) in child_ids.into_iter().enumerate() {
let mut child_path = path.clone();
child_path.push(i);
self.walk_owned(row, child_path, child_id, state, viewport);
}
}
fn get(&self, id: WidgetId) -> Option<&Element<Msg>> {
self.map.get(&id).map(ElemRef::resolve)
}
}
fn update_hover<Msg: Clone>(
handlers: &Handlers<'_, Msg>,
frame: &Frame,
state: &mut FrameState,
point: Point,
out: &mut Dispatch<Msg>,
) {
let chain = frame.hit_chain(point);
let now = state.now();
let hovered: HashMap<WidgetId, f64> = chain
.iter()
.copied()
.filter(|id| {
handlers.get(*id).is_some_and(|el| {
!el.disabled
&& (el.hover_style.is_some()
|| el.on_click.is_some()
|| el.on_hover.is_some()
|| frame.toggle_overlay_of(*id).is_some()
|| has_hover_overlay(el))
})
})
.map(|id| (id, state.hovered.get(&id).copied().unwrap_or(now)))
.collect();
for id in &chain {
if hovered.contains_key(id)
&& !state.hovered.contains_key(id)
&& let Some(el) = handlers.get(*id)
&& let Some(msg) = &el.on_hover
{
out.msgs.push(msg.clone());
}
}
let changed = hovered.len() != state.hovered.len()
|| hovered.keys().any(|id| !state.hovered.contains_key(id));
if changed {
state.hovered = hovered;
out.redraw = true;
}
out.cursor = Some(cursor_of(handlers, &chain));
}
fn has_hover_overlay<Msg>(el: &Element<Msg>) -> bool {
el.children.iter().any(|c| {
matches!(
c.overlay,
Some(crate::element::Overlay {
mode: crate::element::OverlayMode::Hover { .. },
..
})
)
})
}
pub fn refresh_hover<Msg>(root: &Element<Msg>, frame: &Frame, state: &mut FrameState) -> bool {
let Some((x, y)) = state.pointer else {
return false;
};
let handlers = Handlers::collect(root, state, frame.canvas_height());
let chain = frame.hit_chain(Point::new(f64::from(x), f64::from(y)));
let now = state.now();
let hovered: HashMap<WidgetId, f64> = chain
.iter()
.copied()
.filter(|id| {
handlers.get(*id).is_some_and(|el| {
!el.disabled
&& (el.hover_style.is_some()
|| el.on_click.is_some()
|| el.on_hover.is_some()
|| frame.toggle_overlay_of(*id).is_some()
|| has_hover_overlay(el))
})
})
.map(|id| (id, state.hovered.get(&id).copied().unwrap_or(now)))
.collect();
let changed = hovered.len() != state.hovered.len()
|| hovered.keys().any(|id| !state.hovered.contains_key(id));
if changed {
state.hovered = hovered;
true
} else {
false
}
}
pub fn click_msg_of<Msg: Clone>(
root: &Element<Msg>,
frame: &Frame,
state: &FrameState,
id: WidgetId,
) -> Option<Msg> {
let handlers = Handlers::collect(root, state, frame.canvas_height());
handlers
.get(id)
.filter(|el| !el.disabled)
.and_then(|el| el.on_click.clone())
}
fn first_file_drop<Msg>(el: &Element<Msg>) -> Option<&(dyn Fn(&std::path::Path) -> Msg + '_)> {
if let Some(f) = &el.on_file_drop {
return Some(&**f);
}
el.children.iter().find_map(first_file_drop)
}
pub fn dispatch<Msg: Clone>(
root: &Element<Msg>,
frame: &Frame,
state: &mut FrameState,
fonts: &mut Fonts,
event: InputEvent,
) -> Dispatch<Msg> {
let handlers = Handlers::collect(root, state, frame.canvas_height());
let mut out = Dispatch::default();
match event {
InputEvent::PointerMove { x, y } => {
let point = Point::new(f64::from(x), f64::from(y));
state.pointer = Some((x, y));
if let Some(active) = state.active {
if let Some(el) = handlers.get(active)
&& !el.disabled
{
if matches!(el.kind, Kind::Input(_)) {
let now = state.now();
if let Some((lx, ly)) = input_local(frame, state, el, active, point) {
if let Some(editor) = state.editors.get_mut(&active) {
input::pointer_drag(editor, fonts, lx, ly);
editor.last_activity = now;
}
out.redraw = true;
}
} else if let Some(f) = &el.on_drag
&& let Some((fx, fy)) = frame.fraction_in(active, point)
&& let Some(msg) = f(fx, fy)
{
out.msgs.push(msg);
}
}
out.cursor = Some(cursor_of(&handlers, &[active]));
return out;
}
update_hover(&handlers, frame, state, point, &mut out);
}
InputEvent::Modifiers {
shift,
ctrl,
alt,
meta,
} => {
state.mods = (shift, ctrl, alt, meta);
}
InputEvent::PointerLeave => {
state.pointer = None;
if !state.hovered.is_empty() {
state.hovered.clear();
out.redraw = true;
}
out.cursor = Some(Cursor::Default);
}
InputEvent::PointerDown => {
let Some((px, py)) = state.pointer else {
return out;
};
let point = Point::new(f64::from(px), f64::from(py));
let chain = frame.hit_chain(point);
state.dragging = chain.iter().rev().find_map(|id| {
handlers
.get(*id)
.filter(|el| !el.disabled)
.and_then(|el| el.drag_source.clone())
});
let hit_overlay = chain
.first()
.and_then(|deepest| frame.overlay_containing(*deepest));
let mut closed_any = false;
for (overlay_id, mode) in frame.open_overlays_top_down() {
if matches!(mode, crate::element::OverlayMode::Toggle)
&& hit_overlay != Some(overlay_id)
&& !chain
.iter()
.any(|id| frame.toggle_overlay_of(*id) == Some(overlay_id))
{
state.close_overlay(overlay_id);
closed_any = true;
}
}
if closed_any {
out.redraw = true;
out.cursor = Some(cursor_of(&handlers, &chain));
return out;
}
if chain.is_empty()
&& let Some(modal) = frame.top_overlay_is_modal()
{
if let Some(el) = handlers.get(modal)
&& let Some(msg) = &el.on_close
{
out.msgs.push(msg.clone());
}
out.redraw = true;
return out;
}
let target = chain.iter().rev().copied().find(|id| {
handlers.get(*id).is_some_and(|el| {
!el.disabled
&& (el.on_click.is_some()
|| el.on_drag.is_some()
|| el.focusable
|| frame.toggle_overlay_of(*id).is_some())
})
});
if let Some(id) = target {
state.active = Some(id);
if let Some(el) = handlers.get(id) {
if el.focusable && state.focus != Some(id) {
state.focus = Some(id);
state.focus_visible = false;
} else if el.focusable {
state.focus_visible = false;
}
if let Some(overlay_id) = frame.toggle_overlay_of(id) {
if state.overlay_open(overlay_id) {
state.close_overlay(overlay_id);
} else {
state.open_overlay(overlay_id);
}
}
if matches!(el.kind, Kind::Input(_)) {
let now = state.now();
let count = match state.last_press {
Some((pid, at, c)) if pid == id && now - at <= 0.4 => (c % 3) + 1,
_ => 1,
};
state.last_press = Some((id, now, count));
if let Some((lx, ly)) = input_local(frame, state, el, id, point)
&& let Some(editor) = state.editors.get_mut(&id)
{
match count {
2 => input::select_word_at(editor, fonts, lx, ly),
3 => input::select_line_at(editor, fonts, lx, ly),
_ => input::pointer_down(editor, fonts, lx, ly, state.mods.0),
}
editor.last_activity = now;
}
} else if let Some(f) = &el.on_drag
&& let Some((fx, fy)) = frame.fraction_in(id, point)
&& let Some(msg) = f(fx, fy)
{
out.msgs.push(msg);
}
}
out.redraw = true;
} else if state.focus.is_some() {
state.focus = None;
state.focus_visible = false;
out.redraw = true;
}
out.cursor = Some(cursor_of(&handlers, &chain));
}
InputEvent::FileDrop(path) => {
let hit = state.pointer.and_then(|(px, py)| {
frame
.hit_chain(Point::new(f64::from(px), f64::from(py)))
.iter()
.rev()
.find_map(|id| {
handlers
.get(*id)
.filter(|el| !el.disabled && el.on_file_drop.is_some())
.map(|_| *id)
})
});
let msg = match hit {
Some(id) => handlers
.get(id)
.and_then(|el| el.on_file_drop.as_ref())
.map(|f| f(&path)),
None => first_file_drop(root).map(|f| f(&path)),
};
if let Some(msg) = msg {
out.msgs.push(msg);
out.redraw = true;
}
}
InputEvent::RightDown => {
if let Some((px, py)) = state.pointer {
let chain = frame.hit_chain(Point::new(f64::from(px), f64::from(py)));
if let Some(msg) = chain.iter().rev().find_map(|id| {
handlers
.get(*id)
.filter(|el| !el.disabled)
.and_then(|el| el.on_right_click.clone())
}) {
out.msgs.push(msg);
out.redraw = true;
}
}
}
InputEvent::RightUp => {}
InputEvent::PointerUp => {
if let Some(payload) = state.dragging.take()
&& let Some((px, py)) = state.pointer
&& let Some(msg) = frame
.hit_chain(Point::new(f64::from(px), f64::from(py)))
.iter()
.rev()
.find_map(|id| {
handlers
.get(*id)
.filter(|el| !el.disabled)
.and_then(|el| el.on_drop.as_ref())
.and_then(|f| f(&payload))
})
{
out.msgs.push(msg);
out.redraw = true;
}
if let Some(active) = state.active.take() {
if let Some((px, py)) = state.pointer
&& frame
.hit_chain(Point::new(f64::from(px), f64::from(py)))
.contains(&active)
&& let Some(el) = handlers.get(active)
&& !el.disabled
{
if let Some(msg) = &el.on_click {
out.msgs.push(msg.clone());
if let Some(overlay_id) = frame.overlay_containing(active) {
state.close_overlay(overlay_id);
}
}
let now = state.now();
let doubled = state
.last_click
.is_some_and(|(id, at)| id == active && now - at <= 0.4);
if doubled {
if let Some(msg) = &el.on_double_click {
out.msgs.push(msg.clone());
}
state.last_click = None;
} else {
state.last_click = Some((active, now));
}
}
out.redraw = true;
}
if let Some((x, y)) = state.pointer {
update_hover(
&handlers,
frame,
state,
Point::new(f64::from(x), f64::from(y)),
&mut out,
);
}
}
InputEvent::Wheel { dy } => {
if let Some((x, y)) = state.pointer
&& let Some(id) = frame.scrollable_at(Point::new(f64::from(x), f64::from(y)))
{
state.scroll_by(id, -dy);
out.redraw = true;
}
}
InputEvent::Tab | InputEvent::ShiftTab => {
let order = frame.focusables();
if !order.is_empty() {
let next = match state
.focus
.and_then(|f| order.iter().position(|id| *id == f))
{
Some(i) if matches!(event, InputEvent::Tab) => order[(i + 1) % order.len()],
Some(i) => order[(i + order.len() - 1) % order.len()],
None if matches!(event, InputEvent::Tab) => order[0],
None => order[order.len() - 1],
};
state.focus = Some(next);
state.focus_visible = true;
out.redraw = true;
}
}
InputEvent::Key(key) => {
let mut key_handled = false;
if let Some(focus) = state.focus
&& let Some(el) = handlers.get(focus)
&& !el.disabled
{
if matches!(el.kind, Kind::Input(_)) {
let now = state.now();
let st = &mut *state;
if let Some(editor) = st.editors.get_mut(&focus) {
let outcome = input::handle_key(editor, fonts, st.clipboard.as_mut(), &key);
editor.last_activity = now;
if outcome.changed {
if let Some(f) = &el.on_input {
out.msgs.push(f(editor.editor.raw_text()));
}
out.redraw = true;
return out;
}
if outcome.consumed {
out.redraw = true;
return out;
}
}
}
if let Some(f) = &el.on_key
&& let Some(msg) = f(&key)
{
out.msgs.push(msg);
out.redraw = true;
key_handled = true;
}
if matches!(key.key, Key::Enter | Key::Space) {
if let Some(msg) = &el.on_click {
out.msgs.push(msg.clone());
out.redraw = true;
}
if let Some(overlay_id) = frame.toggle_overlay_of(focus) {
if state.overlay_open(overlay_id) {
state.close_overlay(overlay_id);
} else {
state.open_overlay(overlay_id);
}
out.redraw = true;
}
}
}
if !key_handled
&& matches!(key.key, Key::PageUp | Key::PageDown | Key::Home | Key::End)
&& let Some((target, rect)) = frame.scroll_target_for(state.focus)
{
#[expect(clippy::cast_possible_truncation, reason = "viewports fit in f32")]
let page = (rect.height() * 0.9) as f32;
match key.key {
Key::PageDown => state.scroll_by(target, page),
Key::PageUp => state.scroll_by(target, -page),
Key::End => state.scroll_to(target, f32::MAX),
Key::Home => state.scroll_to(target, 0.0),
_ => {}
}
out.redraw = true;
}
if matches!(key.key, Key::Escape)
&& let Some((overlay_id, mode)) = frame.open_overlays_top_down().first().copied()
{
match mode {
crate::element::OverlayMode::Toggle => {
state.close_overlay(overlay_id);
out.redraw = true;
}
crate::element::OverlayMode::Open => {
if let Some(el) = handlers.get(overlay_id)
&& let Some(msg) = &el.on_close
{
out.msgs.push(msg.clone());
out.redraw = true;
}
}
crate::element::OverlayMode::Hover { .. } => {}
}
}
}
InputEvent::Text(text) => {
if let Some(focus) = state.focus
&& let Some(el) = handlers.get(focus)
&& !el.disabled
{
if matches!(el.kind, Kind::Input(_)) {
let now = state.now();
if let Some(editor) = state.editors.get_mut(&focus) {
let outcome = input::handle_text(editor, fonts, &text);
editor.last_activity = now;
if outcome.changed {
if let Some(f) = &el.on_input {
out.msgs.push(f(editor.editor.raw_text()));
}
out.redraw = true;
}
}
} else if text == " "
&& let Some(msg) = &el.on_click
{
out.msgs.push(msg.clone());
out.redraw = true;
}
}
}
InputEvent::ImePreedit { text, cursor } => {
let now = state.now();
if let Some(focus) = state.focus
&& let Some(el) = handlers.get(focus)
&& !el.disabled
&& matches!(el.kind, Kind::Input(_))
&& let Some(editor) = state.editors.get_mut(&focus)
{
input::handle_preedit(editor, fonts, &text, cursor);
editor.last_activity = now;
out.redraw = true;
}
}
}
out
}
fn cursor_of<Msg>(handlers: &Handlers<'_, Msg>, chain: &[WidgetId]) -> Cursor {
chain
.iter()
.rev()
.find_map(|id| {
handlers.get(*id).and_then(|el| {
if el.disabled && el.cursor.is_some() {
Some(Cursor::NotAllowed)
} else {
el.cursor
}
})
})
.unwrap_or(Cursor::Default)
}
fn input_local<Msg>(
frame: &Frame,
state: &FrameState,
el: &Element<Msg>,
id: WidgetId,
point: Point,
) -> Option<(f64, f64)> {
let rect = frame.rect_of(id)?;
let scroll_x = state.editors.get(&id).map_or(0.0, |e| e.scroll_x);
let pad = f64::from(el.style().padding.left);
Some((point.x - rect.x0 - pad + scroll_x, rect.height() * 0.5))
}