1use crate::a11y::ReposeActionHandler;
3use accesskit_winit::Adapter;
4use repose_core::locals::dp_to_px;
5use repose_core::*;
6use repose_ui::textfield::{TF_FONT_DP, TF_PADDING_X_DP, TextFieldState, measure_text};
7use std::cell::RefCell;
8use std::rc::Rc;
9use std::sync::{Arc, Mutex};
10use web_time::Instant;
11
12#[cfg(all(feature = "android", target_os = "android"))]
13pub mod android;
14
15#[cfg(all(target_arch = "wasm32"))]
16pub mod web;
17
18pub mod a11y;
19mod common;
20pub mod render;
21
22pub use render::{ImageHandleGuard, RenderCommand, RenderContext};
23
24#[derive(Clone)]
25struct DragSession {
26 source_id: u64,
27 payload: repose_core::dnd::DragPayload,
28 start_px: (f32, f32),
29 over_id: Option<u64>,
30}
31
32pub fn compose_frame<F>(
34 sched: &mut Scheduler,
35 root_fn: &mut F,
36 scale: f32,
37 size_px_u32: (u32, u32),
38 hover_id: Option<u64>,
39 pressed_ids: &std::collections::HashSet<u64>,
40 tf_states: &std::collections::HashMap<u64, Rc<RefCell<repose_ui::TextFieldState>>>,
41 focused: Option<u64>,
42) -> Frame
43where
44 F: FnMut(&mut Scheduler) -> View,
45{
46 set_density_default(Density { scale });
47
48 sched.repose(
49 {
50 let scale = scale;
51 move |s: &mut Scheduler| with_density(Density { scale }, || (root_fn)(s))
52 },
53 {
54 let hover_id = hover_id;
55 let pressed_ids = pressed_ids.clone();
56 move |view, _size| {
57 let interactions = repose_ui::Interactions {
58 hover: hover_id,
59 pressed: pressed_ids.clone(),
60 };
61
62 with_density(Density { scale }, || {
63 repose_ui::layout_and_paint(
64 view,
65 size_px_u32,
66 tf_states,
67 &interactions,
68 focused,
69 )
70 })
71 }
72 },
73 )
74}
75
76pub fn tf_ensure_visible_in_rect(state: &mut repose_ui::TextFieldState, inner_rect: Rect) {
78 let font_px = dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().0;
79 let m = measure_text(&state.text, font_px);
80 let caret_x_px = m.positions.get(state.caret_index()).copied().unwrap_or(0.0);
81 state.ensure_caret_visible(
82 caret_x_px,
83 inner_rect.w - 2.0 * dp_to_px(TF_PADDING_X_DP),
84 dp_to_px(2.0),
85 );
86}
87
88#[cfg(feature = "desktop")]
89pub fn run_desktop_app(
90 root: impl FnMut(&mut Scheduler, &RenderContext) -> View + 'static,
91) -> anyhow::Result<()> {
92 use std::collections::{HashMap, HashSet};
93 use winit::application::ApplicationHandler;
94 use winit::dpi::{LogicalPosition, LogicalSize, PhysicalSize};
95 use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent};
96 use winit::event_loop::EventLoop;
97 use winit::keyboard::{KeyCode, PhysicalKey};
98 use winit::window::{ImePurpose, Window, WindowAttributes};
99
100 use crate::a11y::A11yTree;
101
102 struct ReposeActivationHandler {
103 initial_tree: Option<accesskit::TreeUpdate>,
104 }
105
106 impl accesskit::ActivationHandler for ReposeActivationHandler {
107 fn request_initial_tree(&mut self) -> Option<accesskit::TreeUpdate> {
108 self.initial_tree.take()
109 }
110 }
111
112 struct ReposeDeactivationHandler;
113
114 impl accesskit::DeactivationHandler for ReposeDeactivationHandler {
115 fn deactivate_accessibility(&mut self) {
116 }
118 }
119
120 struct App {
121 root: Box<dyn FnMut(&mut Scheduler, &RenderContext) -> View>,
122 render: RenderContext,
123 window: Option<Arc<Window>>,
124 backend: Option<repose_render_wgpu::WgpuBackend>,
125 sched: Scheduler,
126 inspector: repose_devtools::Inspector,
127 frame_cache: Option<Frame>,
128 mouse_pos_px: (f32, f32),
129 modifiers: Modifiers,
130 textfield_states: HashMap<u64, Rc<RefCell<TextFieldState>>>,
131 ime_preedit: bool,
132 hover_id: Option<u64>,
133 capture_id: Option<u64>,
134 pressed_ids: HashSet<u64>,
135
136 mouse_down_pos_px: Option<(f32, f32)>,
138 drag: Option<DragSession>,
139
140 pending_dropped_files: Vec<std::path::PathBuf>,
142 pending_drop_pos_px: Option<(f32, f32)>,
143
144 key_pressed_active: Option<u64>,
145 clipboard: Option<clipawl::Clipboard>,
146 a11y: Box<dyn A11yBridge>,
147 last_focus: Option<u64>,
148
149 accesskit_adapter: Option<Adapter>,
150 a11y_actions: Arc<Mutex<Vec<accesskit::ActionRequest>>>,
151 a11y_tree: A11yTree,
152
153 last_redraw: Instant,
154 pending_redraw: bool,
155 }
156
157 impl App {
158 fn process_a11y_actions(&mut self) {
159 let mut actions = self.a11y_actions.lock().unwrap();
160 if actions.is_empty() {
161 return;
162 }
163 let pending = actions.drain(..).collect::<Vec<_>>();
164 drop(actions);
165
166 let Some(f) = &self.frame_cache else {
167 return;
168 };
169
170 for req in pending {
171 let target_id = req.target.0;
172 match req.action {
173 accesskit::Action::Click => {
174 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id) {
175 if let Some(cb) = &hit.on_click {
176 cb();
177 self.request_redraw();
178 }
179 }
180 }
181 accesskit::Action::Focus => {
182 self.sched.focused = Some(target_id);
183 self.request_redraw();
184 }
185 _ => {}
186 }
187 }
188 }
189
190 fn new(root: Box<dyn FnMut(&mut Scheduler, &RenderContext) -> View>) -> Self {
191 Self {
192 root,
193 render: RenderContext::new(),
194 window: None,
195 backend: None,
196 sched: Scheduler::new(),
197 inspector: repose_devtools::Inspector::new(),
198 frame_cache: None,
199 mouse_pos_px: (0.0, 0.0),
200 modifiers: Modifiers::default(),
201 textfield_states: HashMap::new(),
202 ime_preedit: false,
203 hover_id: None,
204 capture_id: None,
205 pressed_ids: HashSet::new(),
206 mouse_down_pos_px: None,
207 drag: None,
208 pending_dropped_files: Vec::new(),
209 pending_drop_pos_px: None,
210 key_pressed_active: None,
211 clipboard: None,
212 a11y: {
213 #[cfg(target_os = "linux")]
214 {
215 Box::new(LinuxAtspiStub) as Box<dyn A11yBridge>
216 }
217 #[cfg(not(target_os = "linux"))]
218 {
219 Box::new(NoopA11y) as Box<dyn A11yBridge>
220 }
221 },
222 last_focus: None,
223
224 accesskit_adapter: None,
225 a11y_actions: Arc::new(Mutex::new(Vec::new())),
226 a11y_tree: A11yTree::default(),
227
228 last_redraw: Instant::now(),
229 pending_redraw: false,
230 }
231 }
232
233 fn request_redraw(&self) {
234 if let Some(w) = &self.window {
235 w.request_redraw();
236 }
237 }
238
239 fn tf_ensure_caret_visible(st: &mut TextFieldState) {
241 let font_px = dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().0;
242 let m = measure_text(&st.text, font_px);
243 let caret_x_px = m.positions.get(st.caret_index()).copied().unwrap_or(0.0);
244 st.ensure_caret_visible(caret_x_px, st.inner_width, dp_to_px(2.0));
245 }
246
247 fn copy_to_clipboard(&mut self, text: String) {
248 if let Some(cb) = &mut self.clipboard {
249 let _ = pollster::block_on(cb.set_text(&text));
250 }
251 }
252
253 fn paste_from_clipboard(&mut self) -> Option<String> {
254 if let Some(cb) = &mut self.clipboard {
255 match pollster::block_on(cb.get_text()) {
256 Ok(t) => Some(t),
257 Err(e) => {
258 eprintln!("Paste error: {}", e);
259 None
260 }
261 }
262 } else {
263 None
264 }
265 }
266
267 fn process_render_commands(&mut self) {
268 let Some(backend) = &mut self.backend else {
269 return;
270 };
271
272 for cmd in self.render.drain() {
273 match cmd {
274 RenderCommand::SetImageEncoded {
275 handle,
276 bytes,
277 srgb,
278 } => {
279 let _ = backend.set_image_from_bytes(handle, &bytes, srgb);
280 }
281 RenderCommand::SetImageRgba8 {
282 handle,
283 w,
284 h,
285 rgba,
286 srgb,
287 } => {
288 let _ = backend.set_image_rgba8(handle, w, h, &rgba, srgb);
289 }
290 RenderCommand::SetImageNv12 {
291 handle,
292 w,
293 h,
294 y,
295 uv,
296 full_range,
297 } => {
298 let _ = backend.set_image_nv12(handle, w, h, &y, &uv, full_range);
299 }
300 RenderCommand::RemoveImage { handle } => {
301 backend.remove_image(handle);
302 }
303 }
304 }
305 }
306 }
307
308 impl ApplicationHandler<()> for App {
309 fn resumed(&mut self, el: &winit::event_loop::ActiveEventLoop) {
310 self.clipboard = clipawl::Clipboard::new().ok();
311
312 if self.window.is_none() {
313 match el.create_window(
314 WindowAttributes::default()
315 .with_title("Repose")
316 .with_inner_size(PhysicalSize::new(1280, 800))
317 .with_visible(false),
318 ) {
319 Ok(win) => {
320 let w = Arc::new(win);
321
322 let activation_handler = ReposeActivationHandler {
323 initial_tree: Some(A11yTree::initial_tree()),
324 };
325
326 let action_handler = ReposeActionHandler {
327 pending_actions: self.a11y_actions.clone(),
328 };
329
330 let deactivation_handler = ReposeDeactivationHandler;
331
332 let adapter = Adapter::with_direct_handlers(
333 el,
334 &w,
335 activation_handler,
336 action_handler,
337 deactivation_handler,
338 );
339
340 self.accesskit_adapter = Some(adapter);
341
342 w.set_visible(true);
343
344 let size = w.inner_size();
345 self.sched.size = (size.width, size.height);
346
347 match repose_render_wgpu::WgpuBackend::new(w.clone()) {
348 Ok(b) => {
349 self.backend = Some(b);
350 self.window = Some(w);
351 self.request_redraw();
352 }
353 Err(e) => {
354 log::error!("Failed to create WGPU backend: {e:?}");
355 el.exit();
356 }
357 }
358 }
359 Err(e) => {
360 log::error!("Failed to create window: {e:?}");
361 el.exit();
362 }
363 }
364 }
365 }
366
367 fn window_event(
368 &mut self,
369 el: &winit::event_loop::ActiveEventLoop,
370 _id: winit::window::WindowId,
371 event: WindowEvent,
372 ) {
373 if let Some(adapter) = &mut self.accesskit_adapter {
375 adapter.process_event(self.window.as_ref().unwrap(), &event);
376 }
377
378 match event {
379 WindowEvent::CloseRequested => {
380 el.exit();
381 }
382 WindowEvent::DroppedFile(path) => {
383 self.pending_dropped_files.push(path);
384 if self.pending_drop_pos_px.is_none() {
385 self.pending_drop_pos_px = Some(self.mouse_pos_px);
386 }
387 self.request_redraw();
388 }
389 WindowEvent::Resized(size) => {
390 self.sched.size = (size.width, size.height);
391 if let Some(b) = &mut self.backend {
392 b.configure_surface(size.width, size.height);
393 }
394 if let Some(w) = &self.window {
395 let sf = w.scale_factor() as f32;
396 let dp_w = size.width as f32 / sf;
397 let dp_h = size.height as f32 / sf;
398 log::info!(
399 "Resized: fb={}x{} px, scale_factor={}, ~{}x{} dp",
400 size.width,
401 size.height,
402 sf,
403 dp_w as i32,
404 dp_h as i32
405 );
406 }
407 self.request_redraw();
408 }
409 WindowEvent::CursorMoved { position, .. } => {
410 self.mouse_pos_px = (position.x as f32, position.y as f32);
411
412 let pos = Vec2 {
413 x: self.mouse_pos_px.0,
414 y: self.mouse_pos_px.1,
415 };
416
417 if self.drag.is_some() {
418 self.dnd_update_over(pos);
419 self.request_redraw();
420 return;
421 }
422
423 if self.dnd_try_begin(pos) {
424 self.dnd_update_over(pos);
425 return;
426 }
427
428 if self.inspector.hud.inspector_enabled
430 && let Some(f) = &self.frame_cache
431 {
432 let hover_rect = f
433 .hit_regions
434 .iter()
435 .find(|h| {
436 h.rect.contains(Vec2 {
437 x: self.mouse_pos_px.0,
438 y: self.mouse_pos_px.1,
439 })
440 })
441 .map(|h| h.rect);
442 self.inspector.hud.set_hovered(hover_rect);
443 self.request_redraw();
444 }
445
446 if let (Some(f), Some(cid)) = (&self.frame_cache, self.capture_id)
447 && let Some(_sem) = f
448 .semantics_nodes
449 .iter()
450 .find(|n| n.id == cid && n.role == Role::TextField)
451 {
452 let key = self.tf_key_of(cid);
453 if let Some(state_rc) = self.textfield_states.get(&key) {
454 use repose_ui::textfield::index_for_x_bytes;
455
456 let mut state = state_rc.borrow_mut();
457 let inner_x_px = f
459 .hit_regions
460 .iter()
461 .find(|h| h.id == cid)
462 .map(|h| h.rect.x + dp_to_px(TF_PADDING_X_DP))
463 .unwrap_or(0.0);
464 let content_x_px =
465 self.mouse_pos_px.0 - inner_x_px + state.scroll_offset;
466 let font_px =
467 dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().0;
468 let idx =
469 index_for_x_bytes(&state.text, font_px, content_x_px.max(0.0));
470 state.drag_to(idx);
471
472 let m = measure_text(&state.text, font_px);
474 let caret_x_px =
475 m.positions.get(state.caret_index()).copied().unwrap_or(0.0);
476 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid) {
477 state.ensure_caret_visible(
478 caret_x_px,
479 hit.rect.w - 2.0 * dp_to_px(TF_PADDING_X_DP),
480 dp_to_px(2.0),
481 );
482 }
483 self.request_redraw();
484 }
485 }
486
487 if let Some(f) = &self.frame_cache {
489 let pos = Vec2 {
491 x: self.mouse_pos_px.0,
492 y: self.mouse_pos_px.1,
493 };
494 let top = f.hit_regions.iter().rev().find(|h| h.rect.contains(pos));
495 let new_hover = top.map(|h| h.id);
496
497 if new_hover != self.hover_id {
499 if let Some(prev_id) = self.hover_id
500 && let Some(prev) = f.hit_regions.iter().find(|h| h.id == prev_id)
501 && let Some(cb) = &prev.on_pointer_leave
502 {
503 let pe = repose_core::input::PointerEvent {
504 id: repose_core::input::PointerId(0),
505 kind: repose_core::input::PointerKind::Mouse,
506 event: repose_core::input::PointerEventKind::Leave,
507 position: pos,
508 pressure: 1.0,
509 modifiers: self.modifiers,
510 };
511 cb(pe);
512 }
513 if let Some(h) = top
514 && let Some(cb) = &h.on_pointer_enter
515 {
516 let pe = repose_core::input::PointerEvent {
517 id: repose_core::input::PointerId(0),
518 kind: repose_core::input::PointerKind::Mouse,
519 event: repose_core::input::PointerEventKind::Enter,
520 position: pos,
521 pressure: 1.0,
522 modifiers: self.modifiers,
523 };
524 cb(pe);
525 }
526 self.hover_id = new_hover;
527 }
528
529 let pe = repose_core::input::PointerEvent {
531 id: repose_core::input::PointerId(0),
532 kind: repose_core::input::PointerKind::Mouse,
533 event: repose_core::input::PointerEventKind::Move,
534 position: pos,
535 pressure: 1.0,
536 modifiers: self.modifiers,
537 };
538
539 if let Some(cid) = self.capture_id {
541 if let Some(h) = f.hit_regions.iter().find(|h| h.id == cid)
542 && let Some(cb) = &h.on_pointer_move
543 {
544 cb(pe.clone());
545 }
546 } else if let Some(h) = &top
547 && let Some(cb) = &h.on_pointer_move
548 {
549 cb(pe);
550 }
551 }
552 }
553 WindowEvent::MouseWheel { delta, .. } => {
554 let (dx_px, dy_px) = match delta {
556 MouseScrollDelta::LineDelta(x, y) => {
557 let unit_px = dp_to_px(60.0);
558 (-(x * unit_px), -(y * unit_px))
559 }
560 MouseScrollDelta::PixelDelta(lp) => (-(lp.x as f32), -(lp.y as f32)),
561 };
562 log::debug!("MouseWheel: dx={}, dy={}", dx_px, dy_px);
563
564 if let Some(f) = &self.frame_cache {
565 let pos = Vec2 {
566 x: self.mouse_pos_px.0,
567 y: self.mouse_pos_px.1,
568 };
569
570 for hit in f.hit_regions.iter().rev().filter(|h| h.rect.contains(pos)) {
571 if let Some(cb) = &hit.on_scroll {
572 log::debug!("Calling on_scroll for hit region id={}", hit.id);
573 let before = Vec2 { x: dx_px, y: dy_px };
574 let leftover = cb(before);
575 let consumed_x = (before.x - leftover.x).abs() > 0.001;
576 let consumed_y = (before.y - leftover.y).abs() > 0.001;
577 if consumed_x || consumed_y {
578 self.request_redraw();
579 break; }
581 }
582 }
583 }
584 }
585 WindowEvent::MouseInput {
586 state: ElementState::Pressed,
587 button: MouseButton::Left,
588 ..
589 } => {
590 let mut need_announce = false;
591 if let Some(f) = &self.frame_cache {
592 let pos = Vec2 {
593 x: self.mouse_pos_px.0,
594 y: self.mouse_pos_px.1,
595 };
596 if let Some(hit) = f.hit_regions.iter().rev().find(|h| h.rect.contains(pos))
597 {
598 self.mouse_down_pos_px = Some(self.mouse_pos_px);
599 self.drag = None;
600
601 self.capture_id = Some(hit.id);
603 self.pressed_ids.insert(hit.id);
605 self.request_redraw();
607
608 if hit.focusable {
610 self.sched.focused = Some(hit.id);
611 need_announce = true;
612 let key = self.tf_key_of(hit.id);
613 self.textfield_states.entry(key).or_insert_with(|| {
614 Rc::new(RefCell::new(TextFieldState::new()))
615 });
616 if let Some(win) = &self.window {
617 let sf = win.scale_factor();
618 win.set_ime_allowed(true);
619 win.set_ime_purpose(ImePurpose::Normal);
620 win.set_ime_cursor_area(
621 LogicalPosition::new(
622 hit.rect.x as f64 / sf,
623 hit.rect.y as f64 / sf,
624 ),
625 LogicalSize::new(
626 hit.rect.w as f64 / sf,
627 hit.rect.h as f64 / sf,
628 ),
629 );
630 }
631 }
632
633 if let Some(cb) = &hit.on_pointer_down {
635 let pe = repose_core::input::PointerEvent {
636 id: repose_core::input::PointerId(0),
637 kind: repose_core::input::PointerKind::Mouse,
638 event: repose_core::input::PointerEventKind::Down(
639 repose_core::input::PointerButton::Primary,
640 ),
641 position: pos,
642 pressure: 1.0,
643 modifiers: self.modifiers,
644 };
645 cb(pe);
646 }
647
648 if let Some(_sem) = f
650 .semantics_nodes
651 .iter()
652 .find(|n| n.id == hit.id && n.role == Role::TextField)
653 {
654 let key = self.tf_key_of(hit.id);
655 if let Some(state_rc) = self.textfield_states.get(&key) {
656 use repose_ui::textfield::index_for_x_bytes;
657
658 let mut state = state_rc.borrow_mut();
659 let inner_x_px = hit.rect.x + dp_to_px(TF_PADDING_X_DP);
660 let content_x_px =
661 self.mouse_pos_px.0 - inner_x_px + state.scroll_offset;
662 let font_px =
663 dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().0;
664 let idx = index_for_x_bytes(
665 &state.text,
666 font_px,
667 content_x_px.max(0.0),
668 );
669 state.begin_drag(idx, self.modifiers.shift);
670 let m = measure_text(&state.text, font_px);
671 let caret_x_px = m
672 .positions
673 .get(state.caret_index())
674 .copied()
675 .unwrap_or(0.0);
676 state.ensure_caret_visible(
677 caret_x_px,
678 hit.rect.w - 2.0 * dp_to_px(TF_PADDING_X_DP),
679 dp_to_px(2.0),
680 );
681 }
682 }
683 if need_announce {
684 self.announce_focus_change();
685 }
686
687 self.request_redraw();
688 } else {
689 if self.ime_preedit {
691 if let Some(win) = &self.window {
692 win.set_ime_allowed(false);
693 }
694 self.ime_preedit = false;
695 }
696 self.sched.focused = None;
697 self.request_redraw();
698 }
699 }
700 }
701 WindowEvent::MouseInput {
702 state: ElementState::Released,
703 button: MouseButton::Left,
704 ..
705 } => {
706 let pos = Vec2 {
707 x: self.mouse_pos_px.0,
708 y: self.mouse_pos_px.1,
709 };
710
711 if self.drag.is_some() {
712 self.dnd_finish(pos, true);
713 self.capture_id = None;
714 self.pressed_ids.clear();
715 repose_core::request_frame();
716 return;
717 }
718
719 if let Some(cid) = self.capture_id {
720 self.pressed_ids.remove(&cid);
721 self.request_redraw();
722 }
723
724 if let (Some(f), Some(cid)) = (&self.frame_cache, self.capture_id) {
725 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid) {
726 if let Some(cb) = &hit.on_pointer_up {
727 let pos = Vec2 {
728 x: self.mouse_pos_px.0,
729 y: self.mouse_pos_px.1,
730 };
731 let pe = repose_core::input::PointerEvent {
732 id: repose_core::input::PointerId(0),
733 kind: repose_core::input::PointerKind::Mouse,
734 event: repose_core::input::PointerEventKind::Up(
735 repose_core::input::PointerButton::Primary,
736 ),
737 position: pos,
738 pressure: 1.0,
739 modifiers: self.modifiers,
740 };
741 cb(pe);
742 }
743 }
744 }
745
746 if let (Some(f), Some(cid)) = (&self.frame_cache, self.capture_id) {
748 let pos = Vec2 {
749 x: self.mouse_pos_px.0,
750 y: self.mouse_pos_px.1,
751 };
752 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid)
753 && hit.rect.contains(pos)
754 && let Some(cb) = &hit.on_click
755 {
756 cb();
757 if let Some(node) = f.semantics_nodes.iter().find(|n| n.id == cid) {
759 let label = node.label.as_deref().unwrap_or("");
760 self.a11y.announce(&format!("Activated {}", label));
761 }
762 }
763 }
764 if let (Some(f), Some(cid)) = (&self.frame_cache, self.capture_id)
766 && let Some(_sem) = f
767 .semantics_nodes
768 .iter()
769 .find(|n| n.id == cid && n.role == Role::TextField)
770 {
771 let key = self.tf_key_of(cid);
772 if let Some(state_rc) = self.textfield_states.get(&key) {
773 state_rc.borrow_mut().end_drag();
774 }
775 }
776 self.capture_id = None;
777
778 repose_core::request_frame();
779 }
780 WindowEvent::ModifiersChanged(new_mods) => {
781 self.modifiers.shift = new_mods.state().shift_key();
782 self.modifiers.ctrl = new_mods.state().control_key();
783 self.modifiers.alt = new_mods.state().alt_key();
784 self.modifiers.meta = new_mods.state().super_key();
785 self.modifiers.command = if cfg!(target_os = "macos") {
786 self.modifiers.meta
787 } else {
788 self.modifiers.ctrl
789 };
790 }
791 WindowEvent::KeyboardInput {
792 event: key_event, ..
793 } => {
794 if key_event.state == ElementState::Pressed && !key_event.repeat {
795 match key_event.physical_key {
796 PhysicalKey::Code(KeyCode::BrowserBack)
797 | PhysicalKey::Code(KeyCode::Escape) => {
798 use repose_navigation::back;
799
800 if self.drag.is_some() {
801 self.dnd_cancel();
802 return;
803 }
804
805 if !back::handle() {
806 }
808 return;
809 }
810 _ => {}
811 }
812 }
813 if matches!(key_event.physical_key, PhysicalKey::Code(KeyCode::Tab)) {
815 if key_event.state == ElementState::Pressed
817 && !key_event.repeat
818 && let Some(f) = &self.frame_cache
819 {
820 let chain = &f.focus_chain;
821 if !chain.is_empty() {
822 if let Some(active) = self.key_pressed_active.take() {
824 self.pressed_ids.remove(&active);
825 }
826
827 let shift = self.modifiers.shift;
828 let current = self.sched.focused;
829 let next = if let Some(cur) = current {
830 if let Some(idx) = chain.iter().position(|&id| id == cur) {
831 if shift {
832 if idx == 0 {
833 chain[chain.len() - 1]
834 } else {
835 chain[idx - 1]
836 }
837 } else {
838 chain[(idx + 1) % chain.len()]
839 }
840 } else {
841 chain[0]
842 }
843 } else {
844 chain[0]
845 };
846 self.sched.focused = Some(next);
847
848 if let Some(win) = &self.window {
850 if f.semantics_nodes
851 .iter()
852 .any(|n| n.id == next && n.role == Role::TextField)
853 {
854 win.set_ime_allowed(true);
855 win.set_ime_purpose(ImePurpose::Normal);
856 } else {
857 win.set_ime_allowed(false);
858 }
859 }
860 self.announce_focus_change();
861 self.request_redraw();
862 }
863 }
864 return; }
866
867 if key_event.state == ElementState::Pressed
868 && !key_event.repeat
869 && self.modifiers.command
870 {
871 use repose_core::shortcuts::Action;
872
873 let handled = match key_event.physical_key {
874 PhysicalKey::Code(KeyCode::KeyC) => self.dispatch_action(Action::Copy),
875 PhysicalKey::Code(KeyCode::KeyX) => self.dispatch_action(Action::Cut),
876 PhysicalKey::Code(KeyCode::KeyV) => self.dispatch_action(Action::Paste),
877 PhysicalKey::Code(KeyCode::KeyA) => {
878 use repose_core::shortcuts::Action;
879
880 self.dispatch_action(Action::SelectAll)
881 }
882 PhysicalKey::Code(KeyCode::KeyZ) => {
883 self.dispatch_action(if self.modifiers.shift {
884 Action::Redo
885 } else {
886 Action::Undo
887 })
888 }
889 PhysicalKey::Code(KeyCode::KeyF) => self.dispatch_action(Action::Find),
890 PhysicalKey::Code(KeyCode::KeyS) => self.dispatch_action(Action::Save),
891 _ => false,
892 };
893
894 if handled {
895 self.request_redraw();
896 return;
897 }
898 }
899
900 if let Some(fid) = self.sched.focused {
901 let is_textfield = if let Some(f) = &self.frame_cache {
903 f.semantics_nodes
904 .iter()
905 .any(|n| n.id == fid && n.role == Role::TextField)
906 } else {
907 false
908 };
909
910 if !is_textfield {
911 match key_event.physical_key {
912 PhysicalKey::Code(KeyCode::Space)
913 | PhysicalKey::Code(KeyCode::Enter) => {
914 if key_event.state == ElementState::Pressed && !key_event.repeat
915 {
916 self.pressed_ids.insert(fid);
917 self.key_pressed_active = Some(fid);
918 self.request_redraw();
919 return;
920 }
921 }
922 _ => {}
923 }
924 }
925 }
926
927 if key_event.state == ElementState::Pressed
929 && !key_event.repeat
930 && let PhysicalKey::Code(KeyCode::Enter) = key_event.physical_key
931 && let Some(focused_id) = self.sched.focused
932 && let Some(f) = &self.frame_cache
933 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == focused_id)
934 && let Some(on_submit) = &hit.on_text_submit
935 {
936 let key = self.tf_key_of(focused_id);
937
938 if let Some(state) = self.textfield_states.get(&key) {
939 let text = state.borrow().text.clone();
940 on_submit(text);
941 self.request_redraw();
942 return; }
944 }
945
946 if key_event.state == ElementState::Pressed {
947 if self.modifiers.ctrl
949 && self.modifiers.shift
950 && let PhysicalKey::Code(KeyCode::KeyI) = key_event.physical_key
951 {
952 self.inspector.hud.toggle_inspector();
953 self.request_redraw();
954 return;
955 }
956
957 if let Some(focused_id) = self.sched.focused {
959 let key = self.tf_key_of(focused_id);
960 if let Some(state_rc) = self.textfield_states.get(&key) {
961 let mut state = state_rc.borrow_mut();
962 match key_event.physical_key {
963 PhysicalKey::Code(KeyCode::Backspace) => {
964 state.delete_backward();
965 let new_text = state.text.clone();
966 self.notify_text_change(focused_id, new_text);
967 App::tf_ensure_caret_visible(&mut state);
968 self.request_redraw();
969 }
970 PhysicalKey::Code(KeyCode::Delete) => {
971 state.delete_forward();
972 let new_text = state.text.clone();
973 self.notify_text_change(focused_id, new_text);
974 App::tf_ensure_caret_visible(&mut state);
975 self.request_redraw();
976 }
977 PhysicalKey::Code(KeyCode::ArrowLeft) => {
978 state.move_cursor(-1, self.modifiers.shift);
979 App::tf_ensure_caret_visible(&mut state);
980 self.request_redraw();
981 }
982 PhysicalKey::Code(KeyCode::ArrowRight) => {
983 state.move_cursor(1, self.modifiers.shift);
984 App::tf_ensure_caret_visible(&mut state);
985 self.request_redraw();
986 }
987 PhysicalKey::Code(KeyCode::Home) => {
988 state.selection = 0..0;
989 App::tf_ensure_caret_visible(&mut state);
990 self.request_redraw();
991 }
992 PhysicalKey::Code(KeyCode::End) => {
993 {
994 let end = state.text.len();
995 state.selection = end..end;
996 }
997 App::tf_ensure_caret_visible(&mut state);
998 self.request_redraw();
999 }
1000 PhysicalKey::Code(KeyCode::KeyA) if self.modifiers.ctrl => {
1001 state.selection = 0..state.text.len();
1002 App::tf_ensure_caret_visible(&mut state);
1003 self.request_redraw();
1004 }
1005 _ => {}
1006 }
1007 }
1008 if self.modifiers.ctrl {
1009 match key_event.physical_key {
1010 PhysicalKey::Code(KeyCode::KeyC) => {
1011 if let Some(fid) = self.sched.focused {
1012 let key = self.tf_key_of(fid);
1013 if let Some(state) = self.textfield_states.get(&key) {
1014 let txt = state.borrow().selected_text();
1015 if !txt.is_empty() {
1016 let _ = self.copy_to_clipboard(txt);
1017 }
1018 }
1019 }
1020 return;
1021 }
1022 PhysicalKey::Code(KeyCode::KeyX) => {
1023 if let Some(fid) = self.sched.focused {
1024 let key = self.tf_key_of(fid);
1025 if let Some(state_rc) =
1026 self.textfield_states.get(&key).cloned()
1027 {
1028 let txt = state_rc.borrow().selected_text();
1030 if !txt.is_empty() {
1031 {
1032 let _ = self.copy_to_clipboard(txt.clone());
1033 }
1034 {
1036 let mut st = state_rc.borrow_mut();
1037 st.insert_text(""); let new_text = st.text.clone();
1039 self.notify_text_change(
1040 focused_id, new_text,
1041 );
1042 App::tf_ensure_caret_visible(&mut st);
1043 }
1044 self.request_redraw();
1045 }
1046 }
1047 }
1048 return;
1049 }
1050 PhysicalKey::Code(KeyCode::KeyV) => {
1051 if let Some(fid) = self.sched.focused {
1052 let key = self.tf_key_of(fid);
1053 if let Some(state_rc) =
1054 self.textfield_states.get(&key).cloned()
1055 && let Some(mut txt) = self.paste_from_clipboard()
1056 {
1057 txt.retain(|c| {
1059 !c.is_control() && c != '\n' && c != '\r'
1060 });
1061 if !txt.is_empty() {
1062 let mut st = state_rc.borrow_mut();
1063 st.insert_text(&txt);
1064 let new_text = st.text.clone();
1065 self.notify_text_change(focused_id, new_text);
1066 App::tf_ensure_caret_visible(&mut st);
1067 self.request_redraw();
1068 }
1069 }
1070 }
1071 return;
1072 }
1073 _ => {}
1074 }
1075 }
1076 }
1077
1078 if !self.ime_preedit
1080 && !self.modifiers.ctrl
1081 && !self.modifiers.alt
1082 && !self.modifiers.meta
1083 && let Some(raw) = key_event.text.as_deref()
1084 {
1085 let text: String = raw
1086 .chars()
1087 .filter(|c| !c.is_control() && *c != '\n' && *c != '\r')
1088 .collect();
1089 if !text.is_empty()
1090 && let Some(fid) = self.sched.focused
1091 {
1092 let key = self.tf_key_of(fid);
1093 if let Some(state_rc) = self.textfield_states.get(&key) {
1094 let mut st = state_rc.borrow_mut();
1095 st.insert_text(&text);
1096 self.notify_text_change(fid, st.text.clone());
1097 App::tf_ensure_caret_visible(&mut st);
1098 self.request_redraw();
1099 }
1100 }
1101 }
1102 } else if key_event.state == ElementState::Released {
1103 if let Some(active_id) = self.key_pressed_active {
1105 match key_event.physical_key {
1106 PhysicalKey::Code(KeyCode::Space)
1107 | PhysicalKey::Code(KeyCode::Enter) => {
1108 self.pressed_ids.remove(&active_id);
1109 self.key_pressed_active = None;
1110
1111 if let Some(f) = &self.frame_cache
1112 && let Some(hit) =
1113 f.hit_regions.iter().find(|h| h.id == active_id)
1114 && let Some(cb) = &hit.on_click
1115 {
1116 cb();
1117 if let Some(node) =
1118 f.semantics_nodes.iter().find(|n| n.id == active_id)
1119 {
1120 let label = node.label.as_deref().unwrap_or("");
1121 self.a11y.announce(&format!("Activated {}", label));
1122 }
1123 }
1124 self.request_redraw();
1125 }
1126 _ => {}
1127 }
1128 }
1129 }
1130 }
1131
1132 WindowEvent::Ime(ime) => {
1140 use winit::event::Ime;
1141 if let Some(focused_id) = self.sched.focused {
1142 let key = self.tf_key_of(focused_id);
1143 if let Some(state_rc) = self.textfield_states.get(&key) {
1144 let mut state = state_rc.borrow_mut();
1145 match ime {
1146 Ime::Enabled => {
1147 self.ime_preedit = false;
1149 }
1150 Ime::Preedit(text, cursor) => {
1151 let cursor_usize = cursor.map(|(a, b)| (a, b));
1152 state.set_composition(text.clone(), cursor_usize);
1153 self.ime_preedit = !text.is_empty();
1154 if let Some(f) = &self.frame_cache
1155 && let Some(hit) =
1156 f.hit_regions.iter().find(|h| h.id == focused_id)
1157 {
1158 let inner = Rect {
1159 x: hit.rect.x + dp_to_px(TF_PADDING_X_DP),
1160 y: hit.rect.y,
1161 w: hit.rect.w,
1162 h: hit.rect.h,
1163 };
1164 tf_ensure_visible_in_rect(&mut state, inner);
1165 }
1166 self.notify_text_change(focused_id, state.text.clone());
1168 self.request_redraw();
1169 }
1170 Ime::Commit(text) => {
1171 state.commit_composition(text);
1172 self.ime_preedit = false;
1173 if let Some(f) = &self.frame_cache
1174 && let Some(hit) =
1175 f.hit_regions.iter().find(|h| h.id == focused_id)
1176 {
1177 let inner = Rect {
1178 x: hit.rect.x + dp_to_px(TF_PADDING_X_DP),
1179 y: hit.rect.y,
1180 w: hit.rect.w,
1181 h: hit.rect.h,
1182 };
1183 tf_ensure_visible_in_rect(&mut state, inner);
1184 }
1185 self.notify_text_change(focused_id, state.text.clone());
1186 self.request_redraw();
1187 }
1188 Ime::Disabled => {
1189 self.ime_preedit = false;
1190 if state.composition.is_some() {
1191 state.cancel_composition();
1192 if let Some(f) = &self.frame_cache
1193 && let Some(hit) =
1194 f.hit_regions.iter().find(|h| h.id == focused_id)
1195 {
1196 let inner = Rect {
1197 x: hit.rect.x + dp_to_px(TF_PADDING_X_DP),
1198 y: hit.rect.y,
1199 w: hit.rect.w,
1200 h: hit.rect.h,
1201 };
1202 tf_ensure_visible_in_rect(&mut state, inner);
1203 }
1204 self.notify_text_change(focused_id, state.text.clone());
1205 }
1206 self.request_redraw();
1207 }
1208 }
1209 }
1210 }
1211 }
1212 WindowEvent::RedrawRequested => {
1213 self.process_a11y_actions();
1215 self.dispatch_file_drop_now();
1216 self.process_render_commands();
1217
1218 if let (Some(backend), Some(win)) =
1219 (self.backend.as_mut(), self.window.as_ref())
1220 {
1221 let t0 = Instant::now();
1222 let scale = win.scale_factor() as f32;
1223 let size_px_u32 = self.sched.size;
1224 let focused = self.sched.focused;
1225
1226 let rc = self.render.clone();
1227 let root_fn = &mut self.root;
1228 let mut composed_root = |s: &mut Scheduler| (root_fn)(s, &rc);
1229
1230 let frame = compose_frame(
1231 &mut self.sched,
1232 &mut composed_root,
1233 scale,
1234 size_px_u32,
1235 self.hover_id,
1236 &self.pressed_ids,
1237 &self.textfield_states,
1238 focused,
1239 );
1240
1241 let build_layout_ms = (Instant::now() - t0).as_secs_f32() * 1000.0;
1242
1243 if let Some(adapter) = &mut self.accesskit_adapter {
1245 let scale = win.scale_factor();
1246 if let Some(update) = self.a11y_tree.update(
1247 &frame.semantics_nodes,
1248 scale,
1249 self.sched.focused,
1250 ) {
1251 adapter.update_if_active(|| update);
1252 }
1253 }
1254
1255 let mut scene = frame.scene.clone();
1257 self.inspector.hud.metrics = Some(repose_devtools::Metrics {
1259 build_layout_ms,
1260 scene_nodes: scene.nodes.len(),
1261 });
1262 self.inspector.frame(&mut scene);
1263 backend
1264 .frame(&scene, GlyphRasterConfig { px: 18.0 * scale });
1266 self.frame_cache = Some(frame);
1267
1268 self.last_redraw = Instant::now();
1269 }
1270 }
1271 _ => {}
1272 }
1273 }
1274
1275 fn about_to_wait(&mut self, el: &winit::event_loop::ActiveEventLoop) {
1276 if take_frame_request() {
1277 self.pending_redraw = true;
1278 }
1279 if !self.pending_redraw {
1280 return;
1281 }
1282
1283 let now = Instant::now();
1284 let interval = web_time::Duration::from_millis(16);
1285
1286 if now.saturating_duration_since(self.last_redraw) >= interval {
1287 self.pending_redraw = false;
1288 self.request_redraw();
1289 self.last_redraw = now;
1290 } else {
1291 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
1292 self.last_redraw + interval,
1293 ));
1294 }
1295 }
1296
1297 fn new_events(
1298 &mut self,
1299 _: &winit::event_loop::ActiveEventLoop,
1300 _: winit::event::StartCause,
1301 ) {
1302 }
1303 fn user_event(&mut self, _: &winit::event_loop::ActiveEventLoop, _: ()) {}
1304 fn device_event(
1305 &mut self,
1306 _: &winit::event_loop::ActiveEventLoop,
1307 _: winit::event::DeviceId,
1308 _: winit::event::DeviceEvent,
1309 ) {
1310 }
1311 fn suspended(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1312 fn exiting(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1313 fn memory_warning(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1314 }
1315
1316 impl App {
1317 fn announce_focus_change(&mut self) {
1318 if let Some(f) = &self.frame_cache {
1319 let focused_node = self
1320 .sched
1321 .focused
1322 .and_then(|id| f.semantics_nodes.iter().find(|n| n.id == id));
1323 self.a11y.focus_changed(focused_node);
1324 }
1325 }
1326 fn notify_text_change(&self, id: u64, text: String) {
1327 if let Some(f) = &self.frame_cache
1328 && let Some(h) = f.hit_regions.iter().find(|h| h.id == id)
1329 && let Some(cb) = &h.on_text_change
1330 {
1331 cb(text);
1332 }
1333 }
1334 fn tf_key_of(&self, visual_id: u64) -> u64 {
1335 if let Some(f) = &self.frame_cache
1336 && let Some(hr) = f.hit_regions.iter().find(|h| h.id == visual_id)
1337 {
1338 return hr.tf_state_key.unwrap_or(hr.id);
1339 }
1340 visual_id
1341 }
1342 fn dispatch_action(&mut self, action: repose_core::shortcuts::Action) -> bool {
1343 use repose_core::shortcuts;
1344
1345 if let (Some(f), Some(fid)) = (&self.frame_cache, self.sched.focused) {
1346 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid) {
1347 if let Some(cb) = &hit.on_action {
1348 if cb(action.clone()) {
1349 return true;
1350 }
1351 }
1352 }
1353 }
1354
1355 if shortcuts::handle(action.clone()) {
1356 return true;
1357 }
1358
1359 self.dispatch_default_action(action)
1360 }
1361
1362 fn dispatch_default_action(&mut self, action: repose_core::shortcuts::Action) -> bool {
1363 use repose_core::shortcuts::Action;
1364
1365 let Some(fid) = self.sched.focused else {
1366 return false;
1367 };
1368 let key = self.tf_key_of(fid);
1369 let Some(state_rc) = self.textfield_states.get(&key).cloned() else {
1370 return false;
1371 };
1372
1373 match action {
1374 Action::Copy => {
1375 let txt = state_rc.borrow().selected_text();
1376 if txt.is_empty() {
1377 return false;
1378 }
1379 self.copy_to_clipboard(txt);
1380 true
1381 }
1382 Action::Cut => {
1383 let txt = state_rc.borrow().selected_text();
1384 if txt.is_empty() {
1385 return false;
1386 }
1387 self.copy_to_clipboard(txt);
1388 {
1389 let mut st = state_rc.borrow_mut();
1390 st.insert_text("");
1391 self.notify_text_change(fid, st.text.clone());
1392 App::tf_ensure_caret_visible(&mut st);
1393 }
1394 true
1395 }
1396 Action::Paste => {
1397 let Some(mut txt) = self.paste_from_clipboard() else {
1398 return false;
1399 };
1400 txt.retain(|c| !c.is_control() && c != '\n' && c != '\r');
1401 if txt.is_empty() {
1402 return false;
1403 }
1404 {
1405 let mut st = state_rc.borrow_mut();
1406 st.insert_text(&txt);
1407 self.notify_text_change(fid, st.text.clone());
1408 App::tf_ensure_caret_visible(&mut st);
1409 }
1410 true
1411 }
1412 Action::SelectAll => {
1413 {
1414 let mut st = state_rc.borrow_mut();
1415 st.selection = 0..st.text.len();
1416 App::tf_ensure_caret_visible(&mut st);
1417 }
1418 true
1419 }
1420 _ => false,
1421 }
1422 }
1423 fn is_dnd_target(hit: &HitRegion) -> bool {
1424 hit.on_drop.is_some()
1425 || hit.on_drag_enter.is_some()
1426 || hit.on_drag_over.is_some()
1427 || hit.on_drag_leave.is_some()
1428 }
1429
1430 fn dnd_slop_px(&self) -> f32 {
1431 dp_to_px(6.0)
1432 }
1433
1434 fn dnd_target_id_at(f: &Frame, pos: Vec2) -> Option<u64> {
1435 f.hit_regions
1436 .iter()
1437 .rev()
1438 .filter(|h| h.rect.contains(pos))
1439 .find(|h| Self::is_dnd_target(h))
1440 .map(|h| h.id)
1441 }
1442
1443 fn dnd_update_over(&mut self, pos: Vec2) {
1444 let Some(f) = &self.frame_cache else {
1445 return;
1446 };
1447 let Some(session) = self.drag.as_mut() else {
1448 return;
1449 };
1450
1451 let new_over = Self::dnd_target_id_at(f, pos);
1452
1453 if new_over != session.over_id {
1454 if let Some(prev) = session.over_id {
1455 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == prev) {
1456 if let Some(cb) = &hit.on_drag_leave {
1457 cb(repose_core::dnd::DragOver {
1458 source_id: session.source_id,
1459 target_id: prev,
1460 position: pos,
1461 modifiers: self.modifiers,
1462 payload: session.payload.clone(),
1463 });
1464 }
1465 }
1466 }
1467
1468 if let Some(now) = new_over {
1469 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == now) {
1470 if let Some(cb) = &hit.on_drag_enter {
1471 cb(repose_core::dnd::DragOver {
1472 source_id: session.source_id,
1473 target_id: now,
1474 position: pos,
1475 modifiers: self.modifiers,
1476 payload: session.payload.clone(),
1477 });
1478 }
1479 }
1480 }
1481
1482 session.over_id = new_over;
1483 }
1484
1485 if let Some(over) = session.over_id {
1486 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == over) {
1487 if let Some(cb) = &hit.on_drag_over {
1488 cb(repose_core::dnd::DragOver {
1489 source_id: session.source_id,
1490 target_id: over,
1491 position: pos,
1492 modifiers: self.modifiers,
1493 payload: session.payload.clone(),
1494 });
1495 }
1496 }
1497 }
1498 }
1499
1500 fn dnd_try_begin(&mut self, pos: Vec2) -> bool {
1501 if self.drag.is_some() {
1502 return true;
1503 }
1504
1505 let Some((sx, sy)) = self.mouse_down_pos_px else {
1506 return false;
1507 };
1508 let Some(cid) = self.capture_id else {
1509 return false;
1510 };
1511 if !self.pressed_ids.contains(&cid) {
1512 return false;
1513 }
1514
1515 let dx = pos.x - sx;
1516 let dy = pos.y - sy;
1517 let dist = (dx * dx + dy * dy).sqrt();
1518 if dist < self.dnd_slop_px() {
1519 return false;
1520 }
1521
1522 let Some(f) = &self.frame_cache else {
1523 return false;
1524 };
1525 let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid) else {
1526 return false;
1527 };
1528
1529 let Some(cb) = &hit.on_drag_start else {
1530 return false;
1531 };
1532
1533 let payload = cb(repose_core::dnd::DragStart {
1534 source_id: cid,
1535 position: pos,
1536 modifiers: self.modifiers,
1537 });
1538 let Some(payload) = payload else {
1539 return false;
1540 };
1541
1542 self.drag = Some(DragSession {
1543 source_id: cid,
1544 payload,
1545 start_px: (sx, sy),
1546 over_id: None,
1547 });
1548
1549 self.pressed_ids.remove(&cid);
1551 self.request_redraw();
1552 true
1553 }
1554
1555 fn dnd_finish(&mut self, pos: Vec2, accept_if_possible: bool) {
1556 let Some(f) = &self.frame_cache else {
1557 self.drag = None;
1558 self.capture_id = None;
1559 self.mouse_down_pos_px = None;
1560 self.request_redraw();
1561 return;
1562 };
1563
1564 let Some(session) = self.drag.take() else {
1565 return;
1566 };
1567
1568 let mut accepted = false;
1569
1570 if accept_if_possible {
1571 let drop_target = Self::dnd_target_id_at(f, pos);
1572 if let Some(tid) = drop_target {
1573 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == tid) {
1574 if let Some(cb) = &hit.on_drop {
1575 accepted = cb(repose_core::dnd::DropEvent {
1576 source_id: session.source_id,
1577 target_id: tid,
1578 position: pos,
1579 modifiers: self.modifiers,
1580 payload: session.payload.clone(),
1581 });
1582 }
1583 }
1584 }
1585 }
1586
1587 if let Some(source_hit) = f.hit_regions.iter().find(|h| h.id == session.source_id) {
1589 if let Some(cb) = &source_hit.on_drag_end {
1590 cb(repose_core::dnd::DragEnd { accepted });
1591 }
1592 }
1593
1594 self.capture_id = None;
1595 self.mouse_down_pos_px = None;
1596 self.request_redraw();
1597 }
1598
1599 fn dnd_cancel(&mut self) {
1600 let pos = Vec2 {
1601 x: self.mouse_pos_px.0,
1602 y: self.mouse_pos_px.1,
1603 };
1604 self.dnd_finish(pos, false);
1605 }
1606
1607 fn dispatch_file_drop_now(&mut self) {
1608 let Some(f) = &self.frame_cache else {
1609 self.pending_dropped_files.clear();
1610 self.pending_drop_pos_px = None;
1611 return;
1612 };
1613
1614 if self.pending_dropped_files.is_empty() {
1615 return;
1616 }
1617
1618 let pos_px = self.pending_drop_pos_px.unwrap_or(self.mouse_pos_px);
1619 let pos = Vec2 {
1620 x: pos_px.0,
1621 y: pos_px.1,
1622 };
1623
1624 let mut files = Vec::new();
1625 for p in self.pending_dropped_files.drain(..) {
1626 let name = p
1627 .file_name()
1628 .and_then(|s| s.to_str())
1629 .unwrap_or("file")
1630 .to_string();
1631 files.push(repose_core::dnd::DroppedFile {
1632 name,
1633 path: Some(p),
1634 });
1635 }
1636
1637 let payload: repose_core::dnd::DragPayload =
1638 std::rc::Rc::new(repose_core::dnd::DroppedFiles { files });
1639
1640 let Some(target_id) = Self::dnd_target_id_at(f, pos) else {
1641 self.pending_drop_pos_px = None;
1642 return;
1643 };
1644
1645 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id) {
1646 if let Some(cb) = &hit.on_drop {
1647 let accepted = cb(repose_core::dnd::DropEvent {
1648 source_id: 0, target_id,
1650 position: pos,
1651 modifiers: self.modifiers,
1652 payload: payload.clone(),
1653 });
1654
1655 if accepted {
1656 if let Some(node) = f.semantics_nodes.iter().find(|n| n.id == target_id) {
1657 let label = node.label.as_deref().unwrap_or("");
1658 self.a11y.announce(&format!("Dropped files on {}", label));
1659 }
1660 }
1661 }
1662 }
1663
1664 self.pending_drop_pos_px = None;
1665 self.request_redraw();
1666 }
1667 }
1668
1669 let event_loop = EventLoop::new()?;
1670 let mut app = App::new(Box::new(root));
1671 repose_core::animation::set_clock(Box::new(repose_core::animation::SystemClock));
1673 event_loop.run_app(&mut app)?;
1674 Ok(())
1675}
1676
1677pub trait A11yBridge: Send {
1685 fn publish_tree(&mut self, nodes: &[repose_core::runtime::SemNode]);
1687
1688 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>);
1690
1691 fn announce(&mut self, msg: &str);
1693}
1694
1695struct NoopA11y;
1696impl A11yBridge for NoopA11y {
1697 fn publish_tree(&mut self, _nodes: &[repose_core::runtime::SemNode]) {
1698 }
1700 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>) {
1701 if let Some(n) = node {
1702 log::info!("A11y focus: {:?} {:?}", n.role, n.label);
1703 } else {
1704 log::info!("A11y focus: None");
1705 }
1706 }
1707 fn announce(&mut self, msg: &str) {
1708 log::info!("A11y announce: {msg}");
1709 }
1710}
1711
1712#[cfg(target_os = "linux")]
1713struct LinuxAtspiStub;
1714#[cfg(target_os = "linux")]
1715impl A11yBridge for LinuxAtspiStub {
1716 fn publish_tree(&mut self, nodes: &[repose_core::runtime::SemNode]) {
1717 log::debug!("AT-SPI stub: publish {} nodes", nodes.len());
1718 }
1719 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>) {
1720 if let Some(n) = node {
1721 log::info!("AT-SPI stub focus: {:?} {:?}", n.role, n.label);
1722 } else {
1723 log::info!("AT-SPI stub focus: None");
1724 }
1725 }
1726 fn announce(&mut self, msg: &str) {
1727 log::info!("AT-SPI stub announce: {msg}");
1728 }
1729}