1use std::collections::HashMap;
6
7use kurbo::Point;
8
9use crate::element::{Cursor, Element, Kind, SwipeDir};
10use crate::frame::Frame;
11use crate::frame_state::FrameState;
12use crate::id::WidgetId;
13use crate::input;
14use crate::text::Fonts;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum Key {
19 Enter,
21 Space,
23 Escape,
25 ArrowLeft,
27 ArrowRight,
29 ArrowUp,
31 ArrowDown,
33 Home,
35 End,
37 Backspace,
39 Delete,
41 PageUp,
43 PageDown,
45 Char(char),
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct KeyInput {
52 pub key: Key,
54 pub shift: bool,
56 pub ctrl: bool,
58 pub alt: bool,
60 pub meta: bool,
62}
63
64impl KeyInput {
65 pub fn plain(key: Key) -> Self {
67 Self {
68 key,
69 shift: false,
70 ctrl: false,
71 alt: false,
72 meta: false,
73 }
74 }
75}
76
77#[derive(Debug, Clone, PartialEq)]
80pub enum InputEvent {
81 PointerMove {
83 x: f32,
85 y: f32,
87 },
88 PointerDown,
90 PointerUp,
92 RightDown,
94 RightUp,
96 FileDrop(std::path::PathBuf),
98 Wheel {
102 dx: f32,
104 dy: f32,
106 },
107 Modifiers {
110 shift: bool,
112 ctrl: bool,
114 alt: bool,
116 meta: bool,
118 },
119 PointerLeave,
121 Tab,
123 ShiftTab,
125 Key(KeyInput),
127 Text(String),
129 ImePreedit {
131 text: String,
133 cursor: Option<(usize, usize)>,
135 },
136}
137
138pub struct Dispatch<Msg> {
140 pub msgs: Vec<Msg>,
142 pub redraw: bool,
144 pub cursor: Option<Cursor>,
147}
148
149impl<Msg> Default for Dispatch<Msg> {
150 fn default() -> Self {
151 Self {
152 msgs: Vec::new(),
153 redraw: false,
154 cursor: None,
155 }
156 }
157}
158
159enum ElemRef<'a, Msg> {
162 Borrowed(&'a Element<Msg>),
163 Owned {
164 row: std::rc::Rc<Element<Msg>>,
165 path: Vec<usize>,
166 },
167}
168
169impl<Msg> ElemRef<'_, Msg> {
170 fn resolve(&self) -> &Element<Msg> {
171 match self {
172 Self::Borrowed(el) => el,
173 Self::Owned { row, path } => {
174 let mut el: &Element<Msg> = row;
175 for &i in path {
176 el = &el.children[i];
177 }
178 el
179 }
180 }
181 }
182}
183
184struct Handlers<'a, Msg> {
189 map: HashMap<WidgetId, ElemRef<'a, Msg>>,
190}
191
192impl<'a, Msg> Handlers<'a, Msg> {
193 fn collect(root: &'a Element<Msg>, state: &FrameState, viewport: f32) -> Self {
194 let mut handlers = Self {
195 map: HashMap::new(),
196 };
197 handlers.walk_borrowed(root, WidgetId::ROOT, state, viewport);
198 handlers
199 }
200
201 fn walk_borrowed(
202 &mut self,
203 el: &'a Element<Msg>,
204 id: WidgetId,
205 state: &FrameState,
206 viewport: f32,
207 ) {
208 self.expand_virtual(el, id, state, viewport);
209 self.map.insert(id, ElemRef::Borrowed(el));
210 for (i, child) in el.children.iter().enumerate() {
211 self.walk_borrowed(child, id.child(i, child.key.as_deref()), state, viewport);
212 }
213 }
214
215 fn expand_virtual(
218 &mut self,
219 el: &Element<Msg>,
220 id: WidgetId,
221 state: &FrameState,
222 viewport: f32,
223 ) {
224 let Some(v) = &el.virtual_rows else {
225 return;
226 };
227 let window = if v.variable {
230 state.virtual_windows.get(&id).cloned().unwrap_or(0..0)
231 } else {
232 crate::frame::virtual_window(v.count, v.row_height, state.scroll_offset(id), viewport)
233 };
234 for (j, i) in window.enumerate() {
235 let row = if v.variable {
236 let mut row = (v.builder)(i);
237 if row.key.is_none() {
238 row = row.id(&format!("v{i}"));
239 }
240 std::rc::Rc::new(row.shrink0())
241 } else {
242 std::rc::Rc::new(crate::frame::materialize_virtual_row(v, i))
243 };
244 let rid = id.child(1 + j, row.key.as_deref());
245 self.walk_owned(&row, Vec::new(), rid, state, viewport);
246 }
247 }
248
249 fn walk_owned(
250 &mut self,
251 row: &std::rc::Rc<Element<Msg>>,
252 path: Vec<usize>,
253 id: WidgetId,
254 state: &FrameState,
255 viewport: f32,
256 ) {
257 let el = {
258 let mut el: &Element<Msg> = row;
259 for &i in &path {
260 el = &el.children[i];
261 }
262 el
263 };
264 self.expand_virtual(el, id, state, viewport);
265 let child_ids: Vec<WidgetId> = el
266 .children
267 .iter()
268 .enumerate()
269 .map(|(i, c)| id.child(i, c.key.as_deref()))
270 .collect();
271 self.map.insert(
272 id,
273 ElemRef::Owned {
274 row: std::rc::Rc::clone(row),
275 path: path.clone(),
276 },
277 );
278 for (i, child_id) in child_ids.into_iter().enumerate() {
279 let mut child_path = path.clone();
280 child_path.push(i);
281 self.walk_owned(row, child_path, child_id, state, viewport);
282 }
283 }
284
285 fn get(&self, id: WidgetId) -> Option<&Element<Msg>> {
286 self.map.get(&id).map(ElemRef::resolve)
287 }
288}
289
290fn update_hover<Msg: Clone>(
293 handlers: &Handlers<'_, Msg>,
294 frame: &Frame,
295 state: &mut FrameState,
296 point: Point,
297 out: &mut Dispatch<Msg>,
298) {
299 let chain = frame.hit_chain(point);
300 let now = state.now();
301 let hovered: HashMap<WidgetId, f64> = chain
302 .iter()
303 .copied()
304 .filter(|id| {
305 handlers.get(*id).is_some_and(|el| {
306 !el.disabled
307 && (el.hover_style.is_some()
308 || el.state_layer.is_some()
309 || el.on_click.is_some()
310 || el.on_hover.is_some()
311 || frame.toggle_overlay_of(*id).is_some()
312 || has_hover_overlay(el))
313 })
314 })
315 .map(|id| (id, state.hovered.get(&id).copied().unwrap_or(now)))
317 .collect();
318 for id in &chain {
320 if hovered.contains_key(id)
321 && !state.hovered.contains_key(id)
322 && let Some(el) = handlers.get(*id)
323 && let Some(msg) = &el.on_hover
324 {
325 out.msgs.push(msg.clone());
326 }
327 }
328 let changed = hovered.len() != state.hovered.len()
329 || hovered.keys().any(|id| !state.hovered.contains_key(id));
330 if changed {
331 state.hovered = hovered;
332 out.redraw = true;
333 }
334 out.cursor = Some(cursor_of(handlers, &chain));
335}
336
337fn has_hover_overlay<Msg>(el: &Element<Msg>) -> bool {
340 el.children.iter().any(|c| {
341 matches!(
342 c.overlay,
343 Some(crate::element::Overlay {
344 mode: crate::element::OverlayMode::Hover { .. },
345 ..
346 })
347 )
348 })
349}
350
351pub fn refresh_hover<Msg>(root: &Element<Msg>, frame: &Frame, state: &mut FrameState) -> bool {
355 let Some((x, y)) = state.pointer else {
356 return false;
357 };
358 let handlers = Handlers::collect(root, state, frame.canvas_height());
359 let chain = frame.hit_chain(Point::new(f64::from(x), f64::from(y)));
360 let now = state.now();
361 let hovered: HashMap<WidgetId, f64> = chain
362 .iter()
363 .copied()
364 .filter(|id| {
365 handlers.get(*id).is_some_and(|el| {
366 !el.disabled
367 && (el.hover_style.is_some()
368 || el.state_layer.is_some()
369 || el.on_click.is_some()
370 || el.on_hover.is_some()
371 || frame.toggle_overlay_of(*id).is_some()
372 || has_hover_overlay(el))
373 })
374 })
375 .map(|id| (id, state.hovered.get(&id).copied().unwrap_or(now)))
376 .collect();
377 let changed = hovered.len() != state.hovered.len()
378 || hovered.keys().any(|id| !state.hovered.contains_key(id));
379 if changed {
380 state.hovered = hovered;
381 true
382 } else {
383 false
384 }
385}
386
387pub fn click_msg_of<Msg: Clone>(
392 root: &Element<Msg>,
393 frame: &Frame,
394 state: &FrameState,
395 id: WidgetId,
396) -> Option<Msg> {
397 let handlers = Handlers::collect(root, state, frame.canvas_height());
398 handlers
399 .get(id)
400 .filter(|el| !el.disabled)
401 .and_then(|el| el.on_click.clone())
402}
403
404fn first_file_drop<Msg>(el: &Element<Msg>) -> Option<&(dyn Fn(&std::path::Path) -> Msg + '_)> {
406 if let Some(f) = &el.on_file_drop {
407 return Some(&**f);
408 }
409 el.children.iter().find_map(first_file_drop)
410}
411
412const SWIPE_MIN_DIST: f32 = 24.0;
414const SWIPE_MAX_SECS: f64 = 0.5;
416
417fn recognize_swipe(dx: f32, dy: f32, dt: f64) -> Option<SwipeDir> {
420 if dx.hypot(dy) < SWIPE_MIN_DIST || !(0.0..=SWIPE_MAX_SECS).contains(&dt) {
421 return None;
422 }
423 Some(if dx.abs() >= dy.abs() {
424 if dx >= 0.0 {
425 SwipeDir::Right
426 } else {
427 SwipeDir::Left
428 }
429 } else if dy >= 0.0 {
430 SwipeDir::Down
431 } else {
432 SwipeDir::Up
433 })
434}
435
436pub fn dispatch<Msg: Clone>(
439 root: &Element<Msg>,
440 frame: &Frame,
441 state: &mut FrameState,
442 fonts: &mut Fonts,
443 event: InputEvent,
444) -> Dispatch<Msg> {
445 let handlers = Handlers::collect(root, state, frame.canvas_height());
446 let mut out = Dispatch::default();
447
448 match event {
449 InputEvent::PointerMove { x, y } => {
450 let point = Point::new(f64::from(x), f64::from(y));
451 state.pointer = Some((x, y));
452
453 if let Some(active) = state.active {
454 if let Some(el) = handlers.get(active)
456 && !el.disabled
457 {
458 if matches!(el.kind, Kind::Input(_)) {
459 let now = state.now();
461 if let Some((lx, ly)) = input_local(frame, state, el, active, point) {
462 if let Some(editor) = state.editors.get_mut(&active) {
463 input::pointer_drag(editor, fonts, lx, ly);
464 editor.last_activity = now;
465 }
466 out.redraw = true;
467 }
468 } else if el.selectable && matches!(el.kind, Kind::Text(_) | Kind::Rich(_)) {
469 if let Some((sid, sel, true)) = state.static_sel
470 && sid == active
471 && let Some((text, style)) = frame.static_text_of(active)
472 && let Some(rect) = frame.rect_of(active)
473 && let Some(point) = frame.to_layout_point(active, point)
474 {
475 #[expect(
476 clippy::cast_possible_truncation,
477 reason = "text coords fit in f32"
478 )]
479 let sel = fonts.static_extend(
480 &text,
481 style,
482 Some(rect.width() as f32),
483 sel,
484 (point.x - rect.x0) as f32,
485 (point.y - rect.y0) as f32,
486 );
487 state.static_sel = Some((active, sel, true));
488 out.redraw = true;
489 }
490 } else if let Some(f) = &el.on_drag
491 && let Some((fx, fy)) = frame.fraction_in(active, point)
492 && let Some(msg) = f(fx, fy)
493 {
494 out.msgs.push(msg);
495 }
496 }
497 out.cursor = Some(cursor_of(&handlers, &[active]));
498 return out;
499 }
500
501 update_hover(&handlers, frame, state, point, &mut out);
502 }
503 InputEvent::Modifiers {
504 shift,
505 ctrl,
506 alt,
507 meta,
508 } => {
509 state.mods = (shift, ctrl, alt, meta);
510 }
511 InputEvent::PointerLeave => {
512 state.pointer = None;
513 if !state.hovered.is_empty() {
514 state.hovered.clear();
515 out.redraw = true;
516 }
517 out.cursor = Some(Cursor::Default);
518 }
519 InputEvent::PointerDown => {
520 let Some((px, py)) = state.pointer else {
522 return out;
523 };
524 let point = Point::new(f64::from(px), f64::from(py));
525 let chain = frame.hit_chain(point);
526 state.dragging = chain.iter().rev().find_map(|id| {
528 handlers
529 .get(*id)
530 .filter(|el| !el.disabled)
531 .and_then(|el| el.drag_source.clone())
532 });
533
534 let hit_overlay = chain
538 .first()
539 .and_then(|deepest| frame.overlay_containing(*deepest));
540 let mut closed_any = false;
541 for (overlay_id, mode) in frame.open_overlays_top_down() {
542 if matches!(mode, crate::element::OverlayMode::Toggle)
543 && hit_overlay != Some(overlay_id)
544 && !chain
545 .iter()
546 .any(|id| frame.toggle_overlay_of(*id) == Some(overlay_id))
547 {
548 state.close_overlay(overlay_id);
549 closed_any = true;
550 }
551 }
552 if closed_any {
553 out.redraw = true;
554 out.cursor = Some(cursor_of(&handlers, &chain));
555 return out;
556 }
557 if chain.is_empty()
558 && let Some(modal) = frame.top_overlay_is_modal()
559 {
560 if let Some(el) = handlers.get(modal)
562 && let Some(msg) = &el.on_close
563 {
564 out.msgs.push(msg.clone());
565 }
566 out.redraw = true;
567 return out;
568 }
569 if state.static_sel.take().is_some() {
573 out.redraw = true;
574 }
575 let target = chain.iter().rev().copied().find(|id| {
576 handlers.get(*id).is_some_and(|el| {
577 !el.disabled
578 && (el.on_click.is_some()
579 || el.on_drag.is_some()
580 || el.on_swipe.is_some()
581 || el.focusable
582 || el.selectable
583 || frame.toggle_overlay_of(*id).is_some())
584 })
585 });
586 if let Some(id) = target {
587 state.active = Some(id);
588 state.press_origin = state.pointer.map(|(x, y)| (x, y, state.now()));
590 if let Some(el) = handlers.get(id) {
591 if el.focusable && state.focus != Some(id) {
592 state.focus = Some(id);
593 state.focus_visible = false;
594 } else if el.focusable {
595 state.focus_visible = false;
596 }
597 if let Some(overlay_id) = frame.toggle_overlay_of(id) {
599 if state.overlay_open(overlay_id) {
600 state.close_overlay(overlay_id);
601 } else {
602 state.open_overlay(overlay_id);
603 }
604 }
605 if matches!(el.kind, Kind::Input(_)) {
606 let now = state.now();
607 let count = match state.last_press {
610 Some((pid, at, c)) if pid == id && now - at <= 0.4 => (c % 3) + 1,
611 _ => 1,
612 };
613 state.last_press = Some((id, now, count));
614 if let Some((lx, ly)) = input_local(frame, state, el, id, point)
615 && let Some(editor) = state.editors.get_mut(&id)
616 {
617 match count {
618 2 => input::select_word_at(editor, fonts, lx, ly),
619 3 => input::select_line_at(editor, fonts, lx, ly),
620 _ => input::pointer_down(editor, fonts, lx, ly, state.mods.0),
621 }
622 editor.last_activity = now;
623 }
624 } else if el.selectable && matches!(el.kind, Kind::Text(_) | Kind::Rich(_)) {
625 let now = state.now();
626 let count = match state.last_press {
627 Some((pid, at, c)) if pid == id && now - at <= 0.4 => (c % 3) + 1,
628 _ => 1,
629 };
630 state.last_press = Some((id, now, count));
631 if let Some((text, style)) = frame.static_text_of(id)
632 && let Some(rect) = frame.rect_of(id)
633 && let Some(point) = frame.to_layout_point(id, point)
634 {
635 #[expect(
636 clippy::cast_possible_truncation,
637 reason = "text coords fit in f32"
638 )]
639 let sel = fonts.static_select(
640 &text,
641 style,
642 Some(rect.width() as f32),
643 count,
644 (point.x - rect.x0) as f32,
645 (point.y - rect.y0) as f32,
646 );
647 state.static_sel = Some((id, sel, true));
648 }
649 } else if let Some(f) = &el.on_drag
650 && let Some((fx, fy)) = frame.fraction_in(id, point)
651 && let Some(msg) = f(fx, fy)
652 {
653 out.msgs.push(msg);
654 }
655 }
656 out.redraw = true;
657 } else if state.focus.is_some() {
658 state.focus = None;
660 state.focus_visible = false;
661 out.redraw = true;
662 }
663 out.cursor = Some(cursor_of(&handlers, &chain));
664 }
665 InputEvent::FileDrop(path) => {
666 let hit = state.pointer.and_then(|(px, py)| {
669 frame
670 .hit_chain(Point::new(f64::from(px), f64::from(py)))
671 .iter()
672 .rev()
673 .find_map(|id| {
674 handlers
675 .get(*id)
676 .filter(|el| !el.disabled && el.on_file_drop.is_some())
677 .map(|_| *id)
678 })
679 });
680 let msg = match hit {
681 Some(id) => handlers
682 .get(id)
683 .and_then(|el| el.on_file_drop.as_ref())
684 .map(|f| f(&path)),
685 None => first_file_drop(root).map(|f| f(&path)),
686 };
687 if let Some(msg) = msg {
688 out.msgs.push(msg);
689 out.redraw = true;
690 }
691 }
692 InputEvent::RightDown => {
693 if let Some((px, py)) = state.pointer {
694 let chain = frame.hit_chain(Point::new(f64::from(px), f64::from(py)));
695 if let Some(msg) = chain.iter().rev().find_map(|id| {
697 handlers
698 .get(*id)
699 .filter(|el| !el.disabled)
700 .and_then(|el| el.on_right_click.clone())
701 }) {
702 out.msgs.push(msg);
703 out.redraw = true;
704 }
705 }
706 }
707 InputEvent::RightUp => {}
708 InputEvent::PointerUp => {
709 if let Some((sid, sel, true)) = state.static_sel {
710 state.static_sel = Some((sid, sel, false));
711 }
712 if let Some(payload) = state.dragging.take()
715 && let Some((px, py)) = state.pointer
716 && let Some(msg) = frame
717 .hit_chain(Point::new(f64::from(px), f64::from(py)))
718 .iter()
719 .rev()
720 .find_map(|id| {
721 handlers
722 .get(*id)
723 .filter(|el| !el.disabled)
724 .and_then(|el| el.on_drop.as_ref())
725 .and_then(|f| f(&payload))
726 })
727 {
728 out.msgs.push(msg);
729 out.redraw = true;
730 }
731 if let Some(active) = state.active.take() {
732 if let Some((px, py)) = state.pointer
734 && frame
735 .hit_chain(Point::new(f64::from(px), f64::from(py)))
736 .contains(&active)
737 && let Some(el) = handlers.get(active)
738 && !el.disabled
739 {
740 if let Some(msg) = &el.on_click {
741 out.msgs.push(msg.clone());
742 if let Some(overlay_id) = frame.overlay_containing(active) {
744 state.close_overlay(overlay_id);
745 }
746 }
747 let now = state.now();
750 let doubled = state
751 .last_click
752 .is_some_and(|(id, at)| id == active && now - at <= 0.4);
753 if doubled {
754 if let Some(msg) = &el.on_double_click {
755 out.msgs.push(msg.clone());
756 }
757 state.last_click = None;
758 } else {
759 state.last_click = Some((active, now));
760 }
761 }
762 if let Some(el) = handlers.get(active)
768 && !el.disabled
769 && el.on_drag.is_some()
770 && let Some(msg) = &el.on_drag_end
771 {
772 out.msgs.push(msg.clone());
773 out.redraw = true;
774 }
775 if let Some(el) = handlers.get(active)
778 && !el.disabled
779 && let Some(f) = &el.on_swipe
780 && let (Some((ox, oy, ot)), Some((px, py))) =
781 (state.press_origin, state.pointer)
782 && let Some(dir) = recognize_swipe(px - ox, py - oy, state.now() - ot)
783 {
784 out.msgs.push(f(dir));
785 out.redraw = true;
786 }
787 out.redraw = true;
788 }
789 state.press_origin = None;
790 if let Some((x, y)) = state.pointer {
793 update_hover(
794 &handlers,
795 frame,
796 state,
797 Point::new(f64::from(x), f64::from(y)),
798 &mut out,
799 );
800 }
801 }
802 InputEvent::Wheel { dx, dy } => {
803 if let Some((x, y)) = state.pointer {
804 let p = Point::new(f64::from(x), f64::from(y));
805 if dy.abs() > 1e-3
809 && let Some(id) = frame.scrollable_y_at(p)
810 {
811 state.scroll_by(id, -dy);
812 out.redraw = true;
813 }
814 if dx.abs() > 1e-3
815 && let Some(id) = frame.scrollable_x_at(p)
816 {
817 state.scroll_by_x(id, -dx);
818 out.redraw = true;
819 }
820 }
821 }
822 InputEvent::Tab | InputEvent::ShiftTab => {
823 let order = frame.focusables();
824 if !order.is_empty() {
825 let next = match state
826 .focus
827 .and_then(|f| order.iter().position(|id| *id == f))
828 {
829 Some(i) if matches!(event, InputEvent::Tab) => order[(i + 1) % order.len()],
830 Some(i) => order[(i + order.len() - 1) % order.len()],
831 None if matches!(event, InputEvent::Tab) => order[0],
832 None => order[order.len() - 1],
833 };
834 state.focus = Some(next);
835 state.focus_visible = true;
836 out.redraw = true;
837 }
838 }
839 InputEvent::Key(key) => {
840 let mut key_handled = false;
841 if let Some(focus) = state.focus
842 && let Some(el) = handlers.get(focus)
843 && !el.disabled
844 {
845 if matches!(el.kind, Kind::Input(_)) {
847 let now = state.now();
848 let st = &mut *state;
849 if let Some(editor) = st.editors.get_mut(&focus) {
850 let outcome = input::handle_key(editor, fonts, st.clipboard.as_mut(), &key);
851 editor.last_activity = now;
852 if outcome.changed {
853 if let Some(f) = &el.on_input {
854 out.msgs.push(f(editor.editor.raw_text()));
855 }
856 out.redraw = true;
857 return out;
858 }
859 if outcome.consumed {
860 out.redraw = true;
861 return out;
862 }
863 }
864 }
865 if let Some(f) = &el.on_key
866 && let Some(msg) = f(&key)
867 {
868 out.msgs.push(msg);
869 out.redraw = true;
870 key_handled = true;
871 }
872 if let Some(f) = &el.on_type_ahead {
873 if matches!(key.key, Key::Escape) {
874 state.type_ahead = None;
875 } else if let Key::Char(c) = key.key
876 && !key.ctrl
877 && !key.meta
878 && !c.is_control()
879 {
880 let now = state.now();
881 let buffer = match state.type_ahead.take() {
882 Some((id, mut b, at)) if id == focus && now - at <= 1.0 => {
884 b.push(c);
885 b
886 }
887 _ => c.to_string(),
888 };
889 if let Some(msg) = f(&buffer) {
890 out.msgs.push(msg);
891 out.redraw = true;
892 key_handled = true;
893 }
894 state.type_ahead = Some((focus, buffer, now));
895 }
896 }
897 if matches!(key.key, Key::Enter | Key::Space) {
899 if let Some(msg) = &el.on_click {
900 out.msgs.push(msg.clone());
901 out.redraw = true;
902 }
903 if let Some(overlay_id) = frame.toggle_overlay_of(focus) {
904 if state.overlay_open(overlay_id) {
905 state.close_overlay(overlay_id);
906 } else {
907 state.open_overlay(overlay_id);
908 }
909 out.redraw = true;
910 }
911 }
912 }
913 if !key_handled
916 && (key.meta || key.ctrl)
917 && matches!(key.key, Key::Char(c) if c.eq_ignore_ascii_case(&'c'))
918 && let Some((sid, sel, _)) = state.static_sel
919 {
920 let range = sel.text_range();
921 if range.start < range.end
922 && let Some((text, _)) = frame.static_text_of(sid)
923 {
924 let full = text.to_text();
925 if let Some(slice) = full.get(range) {
926 state.clipboard.as_mut().set(slice.to_owned());
927 }
928 }
929 }
930
931 if !key_handled
934 && matches!(key.key, Key::PageUp | Key::PageDown | Key::Home | Key::End)
935 && let Some((target, rect)) = frame.scroll_target_for(state.focus)
936 {
937 #[expect(clippy::cast_possible_truncation, reason = "viewports fit in f32")]
938 let page = (rect.height() * 0.9) as f32;
939 match key.key {
940 Key::PageDown => state.scroll_by(target, page),
941 Key::PageUp => state.scroll_by(target, -page),
942 Key::End => state.scroll_to(target, f32::MAX),
943 Key::Home => state.scroll_to(target, 0.0),
944 _ => {}
945 }
946 out.redraw = true;
947 }
948 if matches!(key.key, Key::Escape)
951 && let Some((overlay_id, mode)) = frame.open_overlays_top_down().first().copied()
952 {
953 match mode {
954 crate::element::OverlayMode::Toggle => {
955 state.close_overlay(overlay_id);
956 out.redraw = true;
957 }
958 crate::element::OverlayMode::Open => {
959 if let Some(el) = handlers.get(overlay_id)
960 && let Some(msg) = &el.on_close
961 {
962 out.msgs.push(msg.clone());
963 out.redraw = true;
964 }
965 }
966 crate::element::OverlayMode::Hover { .. } => {}
967 }
968 }
969 }
970 InputEvent::Text(text) => {
971 if let Some(focus) = state.focus
972 && let Some(el) = handlers.get(focus)
973 && !el.disabled
974 {
975 if matches!(el.kind, Kind::Input(_)) {
976 let now = state.now();
977 if let Some(editor) = state.editors.get_mut(&focus) {
978 let outcome = input::handle_text(editor, fonts, &text);
979 editor.last_activity = now;
980 if outcome.changed {
981 if let Some(f) = &el.on_input {
982 out.msgs.push(f(editor.editor.raw_text()));
983 }
984 out.redraw = true;
985 }
986 }
987 } else if text == " "
988 && let Some(msg) = &el.on_click
989 {
990 out.msgs.push(msg.clone());
993 out.redraw = true;
994 }
995 }
996 }
997 InputEvent::ImePreedit { text, cursor } => {
998 let now = state.now();
999 if let Some(focus) = state.focus
1000 && let Some(el) = handlers.get(focus)
1001 && !el.disabled
1002 && matches!(el.kind, Kind::Input(_))
1003 && let Some(editor) = state.editors.get_mut(&focus)
1004 {
1005 input::handle_preedit(editor, fonts, &text, cursor);
1006 editor.last_activity = now;
1007 out.redraw = true;
1008 }
1009 }
1010 }
1011 out
1012}
1013
1014fn cursor_of<Msg>(handlers: &Handlers<'_, Msg>, chain: &[WidgetId]) -> Cursor {
1016 chain
1017 .iter()
1018 .rev()
1019 .find_map(|id| {
1020 handlers.get(*id).and_then(|el| {
1021 if el.disabled && el.cursor.is_some() {
1022 Some(Cursor::NotAllowed)
1023 } else {
1024 el.cursor
1025 }
1026 })
1027 })
1028 .unwrap_or(Cursor::Default)
1029}
1030
1031fn input_local<Msg>(
1035 frame: &Frame,
1036 state: &FrameState,
1037 el: &Element<Msg>,
1038 id: WidgetId,
1039 point: Point,
1040) -> Option<(f64, f64)> {
1041 let rect = frame.rect_of(id)?;
1042 let point = frame.to_layout_point(id, point)?;
1043 let scroll_x = state.editors.get(&id).map_or(0.0, |e| e.scroll_x);
1044 let pad = f64::from(el.style().padding.left);
1045 Some((point.x - rect.x0 - pad + scroll_x, rect.height() * 0.5))
1046}