1use crate::a11y::ReposeActionHandler;
3use accesskit_winit::Adapter;
4use repose_core::locals::dp_to_px;
5use repose_core::*;
6use repose_ui::textfield::{
7 self, TF_FONT_DP, TF_PADDING_X_DP, TextFieldState, caret_xy_for_byte, index_for_x_bytes,
8 index_for_xy_bytes, measure_text,
9};
10use std::cell::RefCell;
11use std::rc::Rc;
12use std::sync::{Arc, Mutex};
13use web_time::Instant;
14
15#[cfg(all(feature = "android", target_os = "android"))]
16pub mod android;
17
18#[cfg(all(target_arch = "wasm32"))]
19pub mod web;
20
21pub mod a11y;
22#[cfg(any(feature = "desktop", feature = "android", target_arch = "wasm32"))]
23mod common;
24#[cfg(any(feature = "desktop", all(feature = "android", target_os = "android")))]
25mod common_android;
26#[cfg(any(feature = "desktop", feature = "android", target_arch = "wasm32"))]
27mod common_web;
28pub mod render;
29
30#[cfg(any(feature = "desktop", feature = "android", target_arch = "wasm32"))]
31use common as rc;
32#[cfg(any(feature = "desktop", all(feature = "android", target_os = "android")))]
33use common_android as rc_android;
34#[cfg(any(feature = "desktop", feature = "android", target_arch = "wasm32"))]
35use common_web as rc_web;
36
37pub use render::{ImageHandleGuard, RenderCommand, RenderContext};
38
39#[derive(Clone)]
40struct DragSession {
41 source_id: u64,
42 payload: repose_core::dnd::DragPayload,
43 start_px: (f32, f32),
44 over_id: Option<u64>,
45}
46
47pub fn compose_frame<F>(
49 sched: &mut Scheduler,
50 root_fn: &mut F,
51 scale: f32,
52 size_px_u32: (u32, u32),
53 hover_id: Option<u64>,
54 pressed_ids: &std::collections::HashSet<u64>,
55 tf_states: &std::collections::HashMap<u64, Rc<RefCell<repose_ui::TextFieldState>>>,
56 focused: Option<u64>,
57) -> Frame
58where
59 F: FnMut(&mut Scheduler) -> View,
60{
61 set_density_default(Density { scale });
62
63 sched.repose(
64 {
65 let scale = scale;
66 move |s: &mut Scheduler| with_density(Density { scale }, || (root_fn)(s))
67 },
68 {
69 let hover_id = hover_id;
70 let pressed_ids = pressed_ids.clone();
71 move |view, _size| {
72 let interactions = repose_ui::Interactions {
73 hover: hover_id,
74 pressed: pressed_ids.clone(),
75 };
76
77 with_density(Density { scale }, || {
78 repose_ui::layout_and_paint(
79 view,
80 size_px_u32,
81 tf_states,
82 &interactions,
83 focused,
84 )
85 })
86 }
87 },
88 )
89}
90
91pub fn tf_ensure_visible_in_rect(state: &mut repose_ui::TextFieldState, inner_rect: Rect) {
93 let font_px = dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().0;
94 let m = measure_text(&state.text, font_px);
95 let caret_x_px = m.positions.get(state.caret_index()).copied().unwrap_or(0.0);
96 state.ensure_caret_visible(
97 caret_x_px,
98 inner_rect.w - 2.0 * dp_to_px(TF_PADDING_X_DP),
99 dp_to_px(2.0),
100 );
101}
102
103#[cfg(feature = "desktop")]
104fn map_cursor(c: repose_core::CursorIcon) -> winit::window::CursorIcon {
105 use winit::window::CursorIcon as W;
106 match c {
107 repose_core::CursorIcon::Default => W::Default,
108 repose_core::CursorIcon::Pointer => W::Pointer,
109 repose_core::CursorIcon::Text => W::Text,
110 repose_core::CursorIcon::EwResize => W::EwResize,
111 repose_core::CursorIcon::NsResize => W::NsResize,
112 repose_core::CursorIcon::Grab => W::Grab,
113 repose_core::CursorIcon::Grabbing => W::Grabbing,
114 }
115}
116
117#[cfg(feature = "desktop")]
118pub fn run_desktop_app(
119 root: impl FnMut(&mut Scheduler, &RenderContext) -> View + 'static,
120) -> anyhow::Result<()> {
121 run_desktop_app_with_snackbar(root, None)
122}
123
124#[cfg(feature = "desktop")]
125pub fn run_desktop_app_with_snackbar(
126 root: impl FnMut(&mut Scheduler, &RenderContext) -> View + 'static,
127 snackbar_tick: Option<Rc<dyn Fn(u32)>>,
128) -> anyhow::Result<()> {
129 use std::collections::{HashMap, HashSet};
130 use winit::application::ApplicationHandler;
131 use winit::dpi::{LogicalPosition, LogicalSize, PhysicalSize};
132 use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent};
133 use winit::event_loop::EventLoop;
134 use winit::keyboard::{KeyCode, PhysicalKey};
135 use winit::window::{ImePurpose, Window, WindowAttributes};
136
137 use crate::a11y::A11yTree;
138
139 struct ReposeActivationHandler {
140 initial_tree: Option<accesskit::TreeUpdate>,
141 }
142
143 impl accesskit::ActivationHandler for ReposeActivationHandler {
144 fn request_initial_tree(&mut self) -> Option<accesskit::TreeUpdate> {
145 self.initial_tree.take()
146 }
147 }
148
149 struct ReposeDeactivationHandler;
150
151 impl accesskit::DeactivationHandler for ReposeDeactivationHandler {
152 fn deactivate_accessibility(&mut self) {
153 }
155 }
156
157 struct App {
158 root: Box<dyn FnMut(&mut Scheduler, &RenderContext) -> View>,
159 render: RenderContext,
160 window: Option<Arc<Window>>,
161 backend: Option<repose_render_wgpu::WgpuBackend>,
162 sched: Scheduler,
163 inspector: repose_devtools::Inspector,
164 frame_cache: Option<Frame>,
165 mouse_pos_px: (f32, f32),
166 modifiers: Modifiers,
167 textfield_states: HashMap<u64, Rc<RefCell<TextFieldState>>>,
168 ime_preedit: bool,
169 hover_id: Option<u64>,
170 capture_id: Option<u64>,
171 pressed_ids: HashSet<u64>,
172
173 mouse_down_pos_px: Option<(f32, f32)>,
175 drag: Option<DragSession>,
176
177 pending_dropped_files: Vec<std::path::PathBuf>,
179 pending_drop_pos_px: Option<(f32, f32)>,
180
181 external_file_drag: bool,
183 hovered_files: Vec<std::path::PathBuf>,
184
185 key_pressed_active: Option<u64>,
186 clipboard: Option<clipawl::Clipboard>,
187 a11y: Box<dyn A11yBridge>,
188 last_focus: Option<u64>,
189
190 accesskit_adapter: Option<Adapter>,
191 a11y_actions: Arc<Mutex<Vec<accesskit::ActionRequest>>>,
192 a11y_tree: A11yTree,
193
194 last_redraw: Instant,
195 pending_redraw: bool,
196 snackbar_tick: Option<Rc<dyn Fn(u32)>>,
197 }
198
199 impl App {
200 fn process_a11y_actions(&mut self) {
201 let mut actions = self.a11y_actions.lock().unwrap();
202 if actions.is_empty() {
203 return;
204 }
205 let pending = actions.drain(..).collect::<Vec<_>>();
206 drop(actions);
207
208 let Some(f) = &self.frame_cache else {
209 return;
210 };
211
212 for req in pending {
213 let target_id = req.target.0;
214 match req.action {
215 accesskit::Action::Click => {
216 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id) {
217 if let Some(cb) = &hit.on_click {
218 cb();
219 self.request_redraw();
220 }
221 }
222 }
223 accesskit::Action::Focus => {
224 self.sched.focused = Some(target_id);
225 self.request_redraw();
226 }
227 _ => {}
228 }
229 }
230 }
231
232 fn new(root: Box<dyn FnMut(&mut Scheduler, &RenderContext) -> View>) -> Self {
233 Self {
234 root,
235 render: RenderContext::new(),
236 window: None,
237 backend: None,
238 sched: Scheduler::new(),
239 inspector: repose_devtools::Inspector::new(),
240 frame_cache: None,
241 mouse_pos_px: (0.0, 0.0),
242 modifiers: Modifiers::default(),
243 textfield_states: HashMap::new(),
244 ime_preedit: false,
245 hover_id: None,
246 capture_id: None,
247 pressed_ids: HashSet::new(),
248 mouse_down_pos_px: None,
249 drag: None,
250 pending_dropped_files: Vec::new(),
251 pending_drop_pos_px: None,
252
253 external_file_drag: false,
254 hovered_files: Vec::new(),
255
256 key_pressed_active: None,
257 clipboard: None,
258 a11y: {
259 #[cfg(target_os = "linux")]
260 {
261 Box::new(LinuxAtspiStub) as Box<dyn A11yBridge>
262 }
263 #[cfg(not(target_os = "linux"))]
264 {
265 Box::new(NoopA11y) as Box<dyn A11yBridge>
266 }
267 },
268 last_focus: None,
269
270 accesskit_adapter: None,
271 a11y_actions: Arc::new(Mutex::new(Vec::new())),
272 a11y_tree: A11yTree::default(),
273
274 last_redraw: Instant::now(),
275 pending_redraw: false,
276 snackbar_tick: None,
277 }
278 }
279
280 fn new_with_snackbar(
281 root: Box<dyn FnMut(&mut Scheduler, &RenderContext) -> View>,
282 snackbar_tick: Option<Rc<dyn Fn(u32)>>,
283 ) -> Self {
284 let mut app = Self::new(root);
285 app.snackbar_tick = snackbar_tick;
286 app
287 }
288
289 fn request_redraw(&self) {
290 if let Some(w) = &self.window {
291 w.request_redraw();
292 }
293 }
294
295 fn tick_snackbar(&mut self) {
296 let Some(cb) = &self.snackbar_tick else {
297 return;
298 };
299 let now = Instant::now();
300 let elapsed = now.saturating_duration_since(self.last_redraw);
301 let ms = elapsed.as_millis().min(u32::MAX as u128) as u32;
302 if ms > 0 {
303 cb(ms);
304 }
305 }
306
307 fn tf_ensure_caret_visible(st: &mut TextFieldState, is_multiline: bool) {
309 rc::tf_ensure_caret_visible(st, is_multiline);
310 }
311
312 fn copy_to_clipboard(&mut self, text: String) {
313 if let Some(cb) = &mut self.clipboard {
314 let _ = pollster::block_on(cb.set_text(&text));
315 }
316 }
317
318 fn paste_from_clipboard(&mut self) -> Option<String> {
319 if let Some(cb) = &mut self.clipboard {
320 match pollster::block_on(cb.get_text()) {
321 Ok(t) => Some(t),
322 Err(e) => {
323 eprintln!("Paste error: {}", e);
324 None
325 }
326 }
327 } else {
328 None
329 }
330 }
331
332 fn process_render_commands(&mut self) {
333 let Some(backend) = &mut self.backend else {
334 return;
335 };
336
337 for cmd in self.render.drain() {
338 match cmd {
339 RenderCommand::SetImageEncoded {
340 handle,
341 bytes,
342 srgb,
343 } => {
344 let _ = backend.set_image_from_bytes(handle, &bytes, srgb);
345 }
346 RenderCommand::SetImageRgba8 {
347 handle,
348 w,
349 h,
350 rgba,
351 srgb,
352 } => {
353 let _ = backend.set_image_rgba8(handle, w, h, &rgba, srgb);
354 }
355 RenderCommand::SetImageNv12 {
356 handle,
357 w,
358 h,
359 y,
360 uv,
361 full_range,
362 } => {
363 let _ = backend.set_image_nv12(handle, w, h, &y, &uv, full_range);
364 }
365 RenderCommand::RemoveImage { handle } => {
366 backend.remove_image(handle);
367 }
368 }
369 }
370 }
371
372 fn reset_pointer_state(&mut self) {
373 self.capture_id = None;
374 self.pressed_ids.clear();
375 self.mouse_down_pos_px = None;
376 self.drag = None;
377 self.hover_id = None;
378 }
379
380 fn overlay_drag_indicator(&self, scene: &mut Scene) {
381 let dragging_internal = self.drag.is_some();
382 let dragging_files = self.external_file_drag;
383
384 if !(dragging_internal || dragging_files) {
385 return;
386 }
387
388 let pos = Vec2 {
389 x: self.mouse_pos_px.0,
390 y: self.mouse_pos_px.1,
391 };
392
393 if let Some(f) = &self.frame_cache {
395 if let Some(tid) = rc::dnd_target_id_at(f, pos) {
396 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == tid) {
397 let color = if dragging_files {
398 Color::from_hex("#FFAA00")
399 } else {
400 Color::from_hex("#44AAFF")
401 };
402 scene.nodes.push(SceneNode::Border {
403 rect: hit.rect,
404 color,
405 width: dp_to_px(2.0),
406 radius: dp_to_px(8.0),
407 });
408 }
409 }
410 }
411
412 let label = if dragging_files {
414 "" } else {
416 " " };
418 let bg = if dragging_files {
419 Color::from_hex("#FFAA0077")
420 } else {
421 Color::from_hex("#44AAFF77")
422 };
423
424 let badge = Rect {
425 x: pos.x + dp_to_px(12.0),
426 y: pos.y + dp_to_px(12.0),
427 w: dp_to_px(110.0), h: dp_to_px(24.0),
429 };
430
431 scene.nodes.push(SceneNode::Rect {
432 rect: badge,
433 brush: Brush::Solid(bg),
434 radius: dp_to_px(8.0),
435 });
436 scene.nodes.push(SceneNode::Text {
437 rect: Rect {
438 x: badge.x + dp_to_px(8.0),
439 y: badge.y + dp_to_px(6.0),
440 w: 0.0,
441 h: dp_to_px(14.0),
442 },
443 text: Arc::<str>::from(label),
444 color: Color::WHITE,
445 size: dp_to_px(12.0),
446 });
447 }
448
449 fn is_textfield(&self, id: u64) -> bool {
450 if let Some(f) = &self.frame_cache {
451 f.semantics_nodes
452 .iter()
453 .any(|n| n.id == id && n.role == Role::TextField)
454 } else {
455 false
456 }
457 }
458
459 fn is_multiline_id(&self, id: u64) -> bool {
460 if let Some(f) = &self.frame_cache {
461 f.hit_regions
462 .iter()
463 .find(|h| h.id == id)
464 .map(|h| h.tf_multiline)
465 .unwrap_or(false)
466 } else {
467 false
468 }
469 }
470
471 fn hit_by_id(f: &Frame, id: u64) -> Option<&HitRegion> {
472 f.hit_regions.iter().find(|h| h.id == id)
473 }
474
475 fn padding_px(&self) -> f32 {
476 dp_to_px(TF_PADDING_X_DP)
477 }
478
479 fn dp_px(&self, dp: f32) -> f32 {
480 dp_to_px(dp)
481 }
482 }
483
484 impl ApplicationHandler<()> for App {
485 fn resumed(&mut self, el: &winit::event_loop::ActiveEventLoop) {
486 self.clipboard = clipawl::Clipboard::new().ok();
487
488 if self.window.is_none() {
489 match el.create_window(
490 WindowAttributes::default()
491 .with_title("Repose")
492 .with_inner_size(PhysicalSize::new(1280, 800))
493 .with_visible(false),
494 ) {
495 Ok(win) => {
496 let w = Arc::new(win);
497
498 let activation_handler = ReposeActivationHandler {
499 initial_tree: Some(A11yTree::initial_tree()),
500 };
501
502 let action_handler = ReposeActionHandler {
503 pending_actions: self.a11y_actions.clone(),
504 };
505
506 let deactivation_handler = ReposeDeactivationHandler;
507
508 let adapter = Adapter::with_direct_handlers(
509 el,
510 &w,
511 activation_handler,
512 action_handler,
513 deactivation_handler,
514 );
515
516 self.accesskit_adapter = Some(adapter);
517
518 w.set_visible(true);
519
520 let size = w.inner_size();
521 self.sched.size = (size.width, size.height);
522
523 match repose_render_wgpu::WgpuBackend::new(w.clone()) {
524 Ok(b) => {
525 self.backend = Some(b);
526 self.window = Some(w);
527 self.request_redraw();
528 }
529 Err(e) => {
530 log::error!("Failed to create WGPU backend: {e:?}");
531 el.exit();
532 }
533 }
534 }
535 Err(e) => {
536 log::error!("Failed to create window: {e:?}");
537 el.exit();
538 }
539 }
540 }
541 }
542
543 fn window_event(
544 &mut self,
545 el: &winit::event_loop::ActiveEventLoop,
546 _id: winit::window::WindowId,
547 event: WindowEvent,
548 ) {
549 if let Some(adapter) = &mut self.accesskit_adapter {
551 adapter.process_event(self.window.as_ref().unwrap(), &event);
552 }
553
554 match event {
555 WindowEvent::CloseRequested => {
556 el.exit();
557 }
558
559 WindowEvent::Focused(false) => {
560 self.external_file_drag = false;
562 self.hovered_files.clear();
563 self.reset_pointer_state();
564
565 if let Some(w) = &self.window {
566 rc_web::set_ime_for_textfield(w, false);
567 }
568 self.ime_preedit = false;
569
570 self.request_redraw();
571 }
572
573 WindowEvent::HoveredFile(path) => {
574 self.external_file_drag = true;
576 if self.hovered_files.len() < 32 {
577 self.hovered_files.push(path);
578 }
579 if self.pending_drop_pos_px.is_none() {
581 self.pending_drop_pos_px = Some(self.mouse_pos_px);
582 }
583 self.request_redraw();
584 }
585
586 WindowEvent::HoveredFileCancelled => {
587 self.external_file_drag = false;
588 self.hovered_files.clear();
589
590 self.reset_pointer_state();
592
593 self.request_redraw();
594 }
595
596 WindowEvent::DroppedFile(path) => {
597 self.pending_dropped_files.push(path);
599 if self.pending_drop_pos_px.is_none() {
600 self.pending_drop_pos_px = Some(self.mouse_pos_px);
601 }
602
603 self.external_file_drag = false;
605 self.hovered_files.clear();
606
607 self.request_redraw();
608 }
609
610 WindowEvent::Resized(size) => {
611 self.sched.size = (size.width, size.height);
612 if let Some(b) = &mut self.backend {
613 b.configure_surface(size.width, size.height);
614 }
615 if let Some(w) = &self.window {
616 let sf = w.scale_factor() as f32;
617 let dp_w = size.width as f32 / sf;
618 let dp_h = size.height as f32 / sf;
619 log::info!(
620 "Resized: fb={}x{} px, scale_factor={}, ~{}x{} dp",
621 size.width,
622 size.height,
623 sf,
624 dp_w as i32,
625 dp_h as i32
626 );
627 }
628 self.request_redraw();
629 }
630
631 WindowEvent::CursorMoved { position, .. } => {
632 self.mouse_pos_px = (position.x as f32, position.y as f32);
633
634 let pos = Vec2 {
635 x: self.mouse_pos_px.0,
636 y: self.mouse_pos_px.1,
637 };
638
639 if self.drag.is_some() {
640 self.dnd_update_over(pos);
641 self.request_redraw();
642 return;
643 }
644
645 if self.dnd_try_begin(pos) {
646 self.dnd_update_over(pos);
647 return;
648 }
649
650 if self.inspector.hud.inspector_enabled
652 && let Some(f) = &self.frame_cache
653 {
654 let hit = f
655 .hit_regions
656 .iter()
657 .find(|h| {
658 h.rect.contains(Vec2 {
659 x: self.mouse_pos_px.0,
660 y: self.mouse_pos_px.1,
661 })
662 });
663 let hover_rect = hit.map(|h| h.rect);
664 let hover_info = hit.and_then(|h| {
665 f.semantics_nodes.iter().find(|s| s.id == h.id).map(|s| {
666 repose_devtools::HoveredInfo {
667 id: s.id,
668 role: format!("{:?}", s.role),
669 label: s.label.clone(),
670 }
671 })
672 });
673 self.inspector.hud.set_hovered(hover_rect, hover_info);
674 self.request_redraw();
675 }
676
677 if let (Some(f), Some(cid)) = (&self.frame_cache, self.capture_id)
679 && self.is_textfield(cid)
680 {
681 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid) {
682 let key = self.tf_key_of(cid);
683 if let Some(state_rc) = self.textfield_states.get(&key) {
684 let mut st = state_rc.borrow_mut();
685
686 let pad_x = dp_to_px(TF_PADDING_X_DP);
687 let inner_x = hit.rect.x + pad_x;
688 let inner_y = hit.rect.y + dp_to_px(8.0);
689 let inner_w = (hit.rect.w - 2.0 * pad_x).max(1.0);
690 let inner_h = (hit.rect.h - dp_to_px(16.0)).max(1.0);
691
692 st.set_inner_width(inner_w);
693 st.set_inner_height(inner_h);
694
695 let content_x =
696 (self.mouse_pos_px.0 - inner_x + st.scroll_offset).max(0.0);
697 let content_y =
698 (self.mouse_pos_px.1 - inner_y + st.scroll_offset_y).max(0.0);
699
700 let font_px =
701 dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().0;
702
703 let idx = if hit.tf_multiline {
704 index_for_xy_bytes(
705 &st.text, font_px, inner_w, content_x, content_y,
706 )
707 } else {
708 index_for_x_bytes(&st.text, font_px, content_x)
709 };
710
711 st.drag_to(idx);
712
713 if hit.tf_multiline {
715 let (cx, cy, _) = caret_xy_for_byte(
716 &st.text,
717 font_px,
718 inner_w,
719 st.caret_index(),
720 );
721 st.ensure_caret_visible_xy(
722 cx,
723 cy,
724 inner_w,
725 inner_h,
726 dp_to_px(2.0),
727 );
728 } else {
729 let m = measure_text(&st.text, font_px);
730 let cx =
731 m.positions.get(st.caret_index()).copied().unwrap_or(0.0);
732 st.ensure_caret_visible(cx, inner_w, dp_to_px(2.0));
733 }
734
735 self.request_redraw();
736 }
737 }
738 }
739
740 if let Some(f) = &self.frame_cache {
742 let pos = Vec2 {
744 x: self.mouse_pos_px.0,
745 y: self.mouse_pos_px.1,
746 };
747 let top = f.hit_regions.iter().rev().find(|h| h.rect.contains(pos));
748
749 if let Some(win) = &self.window {
751 let c = top
752 .and_then(|h| h.cursor)
753 .unwrap_or(repose_core::CursorIcon::Default);
754 win.set_cursor(winit::window::Cursor::Icon(map_cursor(c)));
755 }
756
757 let new_hover = top.map(|h| h.id);
758
759 if new_hover != self.hover_id {
761 if let Some(prev_id) = self.hover_id
762 && let Some(prev) = f.hit_regions.iter().find(|h| h.id == prev_id)
763 && let Some(cb) = &prev.on_pointer_leave
764 {
765 let pe = repose_core::input::PointerEvent {
766 id: repose_core::input::PointerId(0),
767 kind: repose_core::input::PointerKind::Mouse,
768 event: repose_core::input::PointerEventKind::Leave,
769 position: pos,
770 pressure: 1.0,
771 modifiers: self.modifiers,
772 };
773 cb(pe);
774 }
775 if let Some(h) = top
776 && let Some(cb) = &h.on_pointer_enter
777 {
778 let pe = repose_core::input::PointerEvent {
779 id: repose_core::input::PointerId(0),
780 kind: repose_core::input::PointerKind::Mouse,
781 event: repose_core::input::PointerEventKind::Enter,
782 position: pos,
783 pressure: 1.0,
784 modifiers: self.modifiers,
785 };
786 cb(pe);
787 }
788 self.hover_id = new_hover;
789 }
790
791 let pe = repose_core::input::PointerEvent {
793 id: repose_core::input::PointerId(0),
794 kind: repose_core::input::PointerKind::Mouse,
795 event: repose_core::input::PointerEventKind::Move,
796 position: pos,
797 pressure: 1.0,
798 modifiers: self.modifiers,
799 };
800
801 if let Some(cid) = self.capture_id {
803 if let Some(h) = f.hit_regions.iter().find(|h| h.id == cid)
804 && let Some(cb) = &h.on_pointer_move
805 {
806 cb(pe.clone());
807 }
808 } else if let Some(h) = &top
809 && let Some(cb) = &h.on_pointer_move
810 {
811 cb(pe);
812 }
813 }
814 }
815
816 WindowEvent::MouseWheel { delta, .. } => {
817 let (dx_px, dy_px) = match delta {
819 MouseScrollDelta::LineDelta(x, y) => {
820 let unit_px = dp_to_px(60.0);
821 (-(x * unit_px), -(y * unit_px))
822 }
823 MouseScrollDelta::PixelDelta(lp) => (-(lp.x as f32), -(lp.y as f32)),
824 };
825 log::debug!("MouseWheel: dx={}, dy={}", dx_px, dy_px);
826
827 if let Some(f) = &self.frame_cache {
828 let pos = Vec2 {
829 x: self.mouse_pos_px.0,
830 y: self.mouse_pos_px.1,
831 };
832
833 for hit in f.hit_regions.iter().rev().filter(|h| h.rect.contains(pos)) {
834 if let Some(cb) = &hit.on_scroll {
835 log::debug!("Calling on_scroll for hit region id={}", hit.id);
836 let before = Vec2 { x: dx_px, y: dy_px };
837 let leftover = cb(before);
838 let consumed_x = (before.x - leftover.x).abs() > 0.001;
839 let consumed_y = (before.y - leftover.y).abs() > 0.001;
840 if consumed_x || consumed_y {
841 self.request_redraw();
842 break; }
844 }
845 }
846 }
847 }
848
849 WindowEvent::MouseInput {
850 state: ElementState::Pressed,
851 button: MouseButton::Left,
852 ..
853 } => {
854 let mut need_announce = false;
855 if let Some(f) = &self.frame_cache {
856 let pos = Vec2 {
857 x: self.mouse_pos_px.0,
858 y: self.mouse_pos_px.1,
859 };
860 if let Some(hit) = f.hit_regions.iter().rev().find(|h| h.rect.contains(pos))
861 {
862 self.mouse_down_pos_px = Some(self.mouse_pos_px);
863 self.drag = None;
864
865 self.capture_id = Some(hit.id);
867
868 if self.is_textfield(hit.id) {
870 let key = self.tf_key_of(hit.id);
871 self.textfield_states.entry(key).or_insert_with(|| {
872 Rc::new(RefCell::new(TextFieldState::new()))
873 });
874 if let Some(st_rc) = self.textfield_states.get(&key) {
875 let mut st = st_rc.borrow_mut();
876 let pad = self.padding_px();
877 let inner_x = hit.rect.x + pad;
878 let inner_y = hit.rect.y + self.dp_px(8.0);
879 let content_x =
880 (self.mouse_pos_px.0 - inner_x + st.scroll_offset).max(0.0);
881 let content_y = (self.mouse_pos_px.1 - inner_y
882 + st.scroll_offset_y)
883 .max(0.0);
884 let font_px = self.dp_px(TF_FONT_DP)
885 * repose_core::locals::text_scale().0;
886
887 let idx = if hit.tf_multiline {
888 textfield::index_for_xy_bytes(
889 &st.text,
890 font_px,
891 hit.rect.w - 2.0 * pad,
892 content_x,
893 content_y,
894 )
895 } else {
896 textfield::index_for_x_bytes(&st.text, font_px, content_x)
897 };
898
899 st.begin_drag(idx, self.modifiers.shift);
900
901 let caret_idx = st.caret_index();
903 let iw = st.inner_width;
904 let ih = st.inner_height;
905 let wrap_w = hit.rect.w - 2.0 * pad;
906 if hit.tf_multiline {
907 let (cx, cy, _) = textfield::caret_xy_for_byte(
908 &st.text, font_px, wrap_w, caret_idx,
909 );
910 st.ensure_caret_visible_xy(cx, cy, iw, ih, self.dp_px(2.0));
911 } else {
912 let m = measure_text(&st.text, font_px);
913 let cx = m.positions.get(caret_idx).copied().unwrap_or(0.0);
914 st.ensure_caret_visible(cx, iw, self.dp_px(2.0));
915 }
916 }
917 }
918 self.pressed_ids.insert(hit.id);
920 self.request_redraw();
922
923 if hit.focusable {
925 self.sched.focused = Some(hit.id);
926 need_announce = true;
927 let key = self.tf_key_of(hit.id);
928 self.textfield_states.entry(key).or_insert_with(|| {
929 Rc::new(RefCell::new(TextFieldState::new()))
930 });
931 if let Some(win) = &self.window {
932 let sf = win.scale_factor();
933 rc_web::set_ime_for_textfield(win, true);
934 win.set_ime_cursor_area(
935 LogicalPosition::new(
936 hit.rect.x as f64 / sf,
937 hit.rect.y as f64 / sf,
938 ),
939 LogicalSize::new(
940 hit.rect.w as f64 / sf,
941 hit.rect.h as f64 / sf,
942 ),
943 );
944 }
945 }
946
947 if let Some(cb) = &hit.on_pointer_down {
949 let pe = repose_core::input::PointerEvent {
950 id: repose_core::input::PointerId(0),
951 kind: repose_core::input::PointerKind::Mouse,
952 event: repose_core::input::PointerEventKind::Down(
953 repose_core::input::PointerButton::Primary,
954 ),
955 position: pos,
956 pressure: 1.0,
957 modifiers: self.modifiers,
958 };
959 cb(pe);
960 }
961
962 if need_announce {
963 self.announce_focus_change();
964 }
965
966 self.request_redraw();
967 } else {
968 if self.ime_preedit {
970 if let Some(win) = &self.window {
971 rc_web::set_ime_for_textfield(win, false);
972 }
973 self.ime_preedit = false;
974 }
975 self.sched.focused = None;
976 self.request_redraw();
977 }
978 }
979 }
980
981 WindowEvent::MouseInput {
982 state: ElementState::Released,
983 button: MouseButton::Left,
984 ..
985 } => {
986 let pos = Vec2 {
987 x: self.mouse_pos_px.0,
988 y: self.mouse_pos_px.1,
989 };
990
991 if self.drag.is_some() {
992 self.dnd_finish(pos, true);
993 self.capture_id = None;
994 self.pressed_ids.clear();
995 repose_core::request_frame();
996 return;
997 }
998
999 if let Some(cid) = self.capture_id {
1000 self.pressed_ids.remove(&cid);
1001 self.request_redraw();
1002 }
1003
1004 if let (Some(f), Some(cid)) = (&self.frame_cache, self.capture_id) {
1005 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid) {
1006 if let Some(cb) = &hit.on_pointer_up {
1007 let pos = Vec2 {
1008 x: self.mouse_pos_px.0,
1009 y: self.mouse_pos_px.1,
1010 };
1011 let pe = repose_core::input::PointerEvent {
1012 id: repose_core::input::PointerId(0),
1013 kind: repose_core::input::PointerKind::Mouse,
1014 event: repose_core::input::PointerEventKind::Up(
1015 repose_core::input::PointerButton::Primary,
1016 ),
1017 position: pos,
1018 pressure: 1.0,
1019 modifiers: self.modifiers,
1020 };
1021 cb(pe);
1022 }
1023 }
1024 }
1025
1026 if let (Some(f), Some(cid)) = (&self.frame_cache, self.capture_id) {
1028 let pos = Vec2 {
1029 x: self.mouse_pos_px.0,
1030 y: self.mouse_pos_px.1,
1031 };
1032 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid)
1033 && hit.rect.contains(pos)
1034 && let Some(cb) = &hit.on_click
1035 {
1036 cb();
1037 if let Some(node) = f.semantics_nodes.iter().find(|n| n.id == cid) {
1039 let label = node.label.as_deref().unwrap_or("");
1040 self.a11y.announce(&format!("Activated {}", label));
1041 }
1042 }
1043 }
1044 if let (Some(f), Some(cid)) = (&self.frame_cache, self.capture_id)
1046 && let Some(_sem) = f
1047 .semantics_nodes
1048 .iter()
1049 .find(|n| n.id == cid && n.role == Role::TextField)
1050 {
1051 let key = self.tf_key_of(cid);
1052 if let Some(state_rc) = self.textfield_states.get(&key) {
1053 state_rc.borrow_mut().end_drag();
1054 }
1055 }
1056
1057 self.capture_id = None;
1058
1059 repose_core::request_frame();
1060 }
1061
1062 WindowEvent::ModifiersChanged(new_mods) => {
1063 self.modifiers.shift = new_mods.state().shift_key();
1064 self.modifiers.ctrl = new_mods.state().control_key();
1065 self.modifiers.alt = new_mods.state().alt_key();
1066 self.modifiers.meta = new_mods.state().super_key();
1067 self.modifiers.command = if cfg!(target_os = "macos") {
1068 self.modifiers.meta
1069 } else {
1070 self.modifiers.ctrl
1071 };
1072 }
1073
1074 WindowEvent::KeyboardInput {
1075 event: key_event, ..
1076 } => {
1077 if key_event.state == ElementState::Pressed && !key_event.repeat {
1078 match key_event.physical_key {
1079 PhysicalKey::Code(KeyCode::BrowserBack)
1080 | PhysicalKey::Code(KeyCode::Escape) => {
1081 use repose_navigation::back;
1082
1083 if self.drag.is_some() {
1084 self.dnd_cancel();
1085 return;
1086 }
1087
1088 if !back::handle() {
1089 }
1091 return;
1092 }
1093 _ => {}
1094 }
1095 }
1096 if matches!(key_event.physical_key, PhysicalKey::Code(KeyCode::Tab)) {
1098 if key_event.state == ElementState::Pressed
1100 && !key_event.repeat
1101 && let Some(f) = &self.frame_cache
1102 {
1103 if let Some(next) = rc::focus_next_in_chain(
1104 &f.focus_chain,
1105 self.sched.focused,
1106 self.modifiers.shift,
1107 ) {
1108 if let Some(active) = self.key_pressed_active.take() {
1110 self.pressed_ids.remove(&active);
1111 }
1112
1113 self.sched.focused = Some(next);
1114
1115 if let Some(win) = &self.window {
1117 let is_textfield = f
1118 .semantics_nodes
1119 .iter()
1120 .any(|n| n.id == next && n.role == Role::TextField);
1121 rc_web::set_ime_for_textfield(win, is_textfield);
1122 }
1123 self.announce_focus_change();
1124 self.request_redraw();
1125 }
1126 }
1127 return; }
1129
1130 if key_event.state == ElementState::Pressed && !key_event.repeat {
1131 if let Some(action) = repose_core::shortcuts::resolve_action(
1132 repose_core::shortcuts::KeyChord::new(
1133 rc::map_key(key_event.physical_key),
1134 self.modifiers,
1135 ),
1136 ) {
1137 if self.dispatch_action(action) {
1138 self.request_redraw();
1139 return;
1140 }
1141 }
1142 }
1143
1144 if let Some(fid) = self.sched.focused {
1145 let is_textfield = if let Some(f) = &self.frame_cache {
1147 f.semantics_nodes
1148 .iter()
1149 .any(|n| n.id == fid && n.role == Role::TextField)
1150 } else {
1151 false
1152 };
1153
1154 if !is_textfield {
1155 match key_event.physical_key {
1156 PhysicalKey::Code(KeyCode::Space)
1157 | PhysicalKey::Code(KeyCode::Enter) => {
1158 if key_event.state == ElementState::Pressed && !key_event.repeat
1159 {
1160 self.pressed_ids.insert(fid);
1161 self.key_pressed_active = Some(fid);
1162 self.request_redraw();
1163 return;
1164 }
1165 }
1166 _ => {}
1167 }
1168 }
1169 }
1170
1171 if key_event.state == ElementState::Pressed
1175 && !key_event.repeat
1176 && let PhysicalKey::Code(KeyCode::Enter) = key_event.physical_key
1177 && let Some(focused_id) = self.sched.focused
1178 && let Some(f) = &self.frame_cache
1179 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == focused_id)
1180 {
1181 let is_multiline = hit.tf_multiline;
1182 let should_submit = if is_multiline {
1183 self.modifiers.ctrl || self.modifiers.meta
1185 } else {
1186 true
1188 };
1189
1190 if should_submit {
1191 if let Some(on_submit) = &hit.on_text_submit {
1192 let key = self.tf_key_of(focused_id);
1193 if let Some(state) = self.textfield_states.get(&key) {
1194 let text = state.borrow().text.clone();
1195 on_submit(text);
1196 self.request_redraw();
1197 return;
1198 }
1199 }
1200 } else {
1201 let key = self.tf_key_of(focused_id);
1203 if let Some(state_rc) = self.textfield_states.get(&key) {
1204 let mut st = state_rc.borrow_mut();
1205 st.insert_text("\n");
1206 let new_text = st.text.clone();
1207 self.notify_text_change(focused_id, new_text);
1208 App::tf_ensure_caret_visible(&mut st, hit.tf_multiline);
1209 self.request_redraw();
1210 return;
1211 }
1212 }
1213 }
1214
1215 if key_event.state == ElementState::Pressed {
1216 if self.modifiers.ctrl
1218 && self.modifiers.shift
1219 && let PhysicalKey::Code(KeyCode::KeyI) = key_event.physical_key
1220 {
1221 self.inspector.hud.toggle_inspector();
1222 self.request_redraw();
1223 return;
1224 }
1225
1226 if let Some(focused_id) = self.sched.focused {
1228 let key = self.tf_key_of(focused_id);
1229 if let Some(state_rc) = self.textfield_states.get(&key) {
1230 let mut state = state_rc.borrow_mut();
1231 match key_event.physical_key {
1232 PhysicalKey::Code(KeyCode::Backspace) => {
1233 state.delete_backward();
1234 let new_text = state.text.clone();
1235 self.notify_text_change(focused_id, new_text);
1236 App::tf_ensure_caret_visible(
1237 &mut state,
1238 self.is_multiline_id(focused_id),
1239 );
1240 self.request_redraw();
1241 }
1242 PhysicalKey::Code(KeyCode::Delete) => {
1243 state.delete_forward();
1244 let new_text = state.text.clone();
1245 self.notify_text_change(focused_id, new_text);
1246 App::tf_ensure_caret_visible(
1247 &mut state,
1248 self.is_multiline_id(focused_id),
1249 );
1250 self.request_redraw();
1251 }
1252 PhysicalKey::Code(KeyCode::ArrowLeft) => {
1253 state.move_cursor(-1, self.modifiers.shift);
1254 state.preferred_x_px = None; App::tf_ensure_caret_visible(
1256 &mut state,
1257 self.is_multiline_id(focused_id),
1258 );
1259 self.request_redraw();
1260 }
1261 PhysicalKey::Code(KeyCode::ArrowRight) => {
1262 state.move_cursor(1, self.modifiers.shift);
1263 state.preferred_x_px = None; App::tf_ensure_caret_visible(
1265 &mut state,
1266 self.is_multiline_id(focused_id),
1267 );
1268 self.request_redraw();
1269 }
1270 PhysicalKey::Code(KeyCode::ArrowUp) => {
1271 if self.is_multiline_id(focused_id) {
1272 if let Some(f) = &self.frame_cache {
1273 if let Some(hit) = f
1274 .hit_regions
1275 .iter()
1276 .find(|h| h.id == focused_id)
1277 {
1278 let font_px = dp_to_px(TF_FONT_DP);
1279 let pad = self.padding_px();
1280 let wrap_w = hit.rect.w - 2.0 * pad;
1281 let cur = state.caret_index();
1282 let (new_pos, px) =
1283 repose_ui::textfield::move_caret_vertical(
1284 &state.text,
1285 font_px,
1286 wrap_w,
1287 cur,
1288 -1,
1289 state.preferred_x_px,
1290 );
1291 if self.modifiers.shift {
1292 state.selection.end = new_pos;
1293 } else {
1294 state.selection = new_pos..new_pos;
1295 }
1296 state.preferred_x_px = Some(px);
1297 let (cx, cy, _) = caret_xy_for_byte(
1299 &state.text,
1300 font_px,
1301 wrap_w,
1302 state.caret_index(),
1303 );
1304 let iw = state.inner_width;
1305 let ih = state.inner_height;
1306 state.ensure_caret_visible_xy(
1307 cx,
1308 cy,
1309 iw,
1310 ih,
1311 self.dp_px(2.0),
1312 );
1313 self.request_redraw();
1314 }
1315 }
1316 }
1317 }
1318 PhysicalKey::Code(KeyCode::ArrowDown) => {
1319 if self.is_multiline_id(focused_id) {
1320 if let Some(f) = &self.frame_cache {
1321 if let Some(hit) = f
1322 .hit_regions
1323 .iter()
1324 .find(|h| h.id == focused_id)
1325 {
1326 let font_px = dp_to_px(TF_FONT_DP);
1327 let pad = self.padding_px();
1328 let wrap_w = hit.rect.w - 2.0 * pad;
1329 let cur = state.caret_index();
1330 let (new_pos, px) =
1331 repose_ui::textfield::move_caret_vertical(
1332 &state.text,
1333 font_px,
1334 wrap_w,
1335 cur,
1336 1,
1337 state.preferred_x_px,
1338 );
1339 if self.modifiers.shift {
1340 state.selection.end = new_pos;
1341 } else {
1342 state.selection = new_pos..new_pos;
1343 }
1344 state.preferred_x_px = Some(px);
1345 let (cx, cy, _) = caret_xy_for_byte(
1347 &state.text,
1348 font_px,
1349 wrap_w,
1350 state.caret_index(),
1351 );
1352 let iw = state.inner_width;
1353 let ih = state.inner_height;
1354 state.ensure_caret_visible_xy(
1355 cx,
1356 cy,
1357 iw,
1358 ih,
1359 self.dp_px(2.0),
1360 );
1361 self.request_redraw();
1362 }
1363 }
1364 }
1365 }
1366 PhysicalKey::Code(KeyCode::Home) => {
1367 state.selection = 0..0;
1368 App::tf_ensure_caret_visible(
1369 &mut state,
1370 self.is_multiline_id(focused_id),
1371 );
1372 self.request_redraw();
1373 }
1374 PhysicalKey::Code(KeyCode::End) => {
1375 {
1376 let end = state.text.len();
1377 state.selection = end..end;
1378 }
1379 App::tf_ensure_caret_visible(
1380 &mut state,
1381 self.is_multiline_id(focused_id),
1382 );
1383 self.request_redraw();
1384 }
1385 PhysicalKey::Code(KeyCode::KeyA) if self.modifiers.ctrl => {
1386 state.selection = 0..state.text.len();
1387 App::tf_ensure_caret_visible(
1388 &mut state,
1389 self.is_multiline_id(focused_id),
1390 );
1391 self.request_redraw();
1392 }
1393 _ => {}
1394 }
1395 }
1396 if self.modifiers.ctrl {
1397 match key_event.physical_key {
1398 PhysicalKey::Code(KeyCode::KeyC) => {
1399 if let Some(fid) = self.sched.focused {
1400 let key = self.tf_key_of(fid);
1401 if let Some(state) = self.textfield_states.get(&key) {
1402 let txt = state.borrow().selected_text();
1403 if !txt.is_empty() {
1404 let _ = self.copy_to_clipboard(txt);
1405 }
1406 }
1407 }
1408 return;
1409 }
1410 PhysicalKey::Code(KeyCode::KeyX) => {
1411 if let Some(fid) = self.sched.focused {
1412 let key = self.tf_key_of(fid);
1413 if let Some(state_rc) =
1414 self.textfield_states.get(&key).cloned()
1415 {
1416 let txt = state_rc.borrow().selected_text();
1418 if !txt.is_empty() {
1419 {
1420 let _ = self.copy_to_clipboard(txt.clone());
1421 }
1422 {
1424 let mut st = state_rc.borrow_mut();
1425 st.insert_text(""); let new_text = st.text.clone();
1427 self.notify_text_change(
1428 focused_id, new_text,
1429 );
1430 App::tf_ensure_caret_visible(
1431 &mut st,
1432 self.is_multiline_id(focused_id),
1433 );
1434 }
1435 self.request_redraw();
1436 }
1437 }
1438 }
1439 return;
1440 }
1441 PhysicalKey::Code(KeyCode::KeyV) => {
1442 if let Some(fid) = self.sched.focused {
1443 let key = self.tf_key_of(fid);
1444 let is_multiline = self.is_multiline_id(fid);
1445 if let Some(state_rc) =
1446 self.textfield_states.get(&key).cloned()
1447 && let Some(mut txt) = self.paste_from_clipboard()
1448 {
1449 if is_multiline {
1452 txt.retain(|c| {
1453 c == '\n' || (!c.is_control() && c != '\r')
1454 });
1455 } else {
1456 txt.retain(|c| {
1457 !c.is_control() && c != '\n' && c != '\r'
1458 });
1459 }
1460 if !txt.is_empty() {
1461 let mut st = state_rc.borrow_mut();
1462 st.insert_text(&txt);
1463 let new_text = st.text.clone();
1464 self.notify_text_change(focused_id, new_text);
1465 App::tf_ensure_caret_visible(
1466 &mut st,
1467 is_multiline,
1468 );
1469 self.request_redraw();
1470 }
1471 }
1472 }
1473 return;
1474 }
1475 _ => {}
1476 }
1477 }
1478 }
1479
1480 if !self.ime_preedit
1482 && !self.modifiers.ctrl
1483 && !self.modifiers.alt
1484 && !self.modifiers.meta
1485 && let Some(raw) = key_event.text.as_deref()
1486 {
1487 let text: String = raw
1488 .chars()
1489 .filter(|c| !c.is_control() && *c != '\n' && *c != '\r')
1490 .collect();
1491 if !text.is_empty()
1492 && let Some(fid) = self.sched.focused
1493 {
1494 let key = self.tf_key_of(fid);
1495 if let Some(state_rc) = self.textfield_states.get(&key) {
1496 let mut st = state_rc.borrow_mut();
1497 st.insert_text(&text);
1498 self.notify_text_change(fid, st.text.clone());
1499 if let Some(f) = &self.frame_cache
1500 && let Some(hit) =
1501 f.hit_regions.iter().find(|h| h.id == fid)
1502 {
1503 App::tf_ensure_caret_visible(&mut st, hit.tf_multiline);
1504 }
1505 self.request_redraw();
1506 }
1507 }
1508 }
1509 } else if key_event.state == ElementState::Released {
1510 if let Some(active_id) = self.key_pressed_active {
1512 match key_event.physical_key {
1513 PhysicalKey::Code(KeyCode::Space)
1514 | PhysicalKey::Code(KeyCode::Enter) => {
1515 self.pressed_ids.remove(&active_id);
1516 self.key_pressed_active = None;
1517
1518 if let Some(f) = &self.frame_cache
1519 && let Some(hit) =
1520 f.hit_regions.iter().find(|h| h.id == active_id)
1521 && let Some(cb) = &hit.on_click
1522 {
1523 cb();
1524 if let Some(node) =
1525 f.semantics_nodes.iter().find(|n| n.id == active_id)
1526 {
1527 let label = node.label.as_deref().unwrap_or("");
1528 self.a11y.announce(&format!("Activated {}", label));
1529 }
1530 }
1531 self.request_redraw();
1532 }
1533 _ => {}
1534 }
1535 }
1536 }
1537 }
1538
1539 WindowEvent::Ime(ime) => {
1540 if let Some(focused_id) = self.sched.focused {
1541 let key = self.tf_key_of(focused_id);
1542 if let Some(state_rc) = self.textfield_states.get(&key) {
1543 if let Some(f) = &self.frame_cache
1544 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == focused_id)
1545 {
1546 let mut state = state_rc.borrow_mut();
1547 let hit_rect = hit.rect;
1548 let on_text_change = hit.on_text_change.clone();
1549 let mut notify = |text: String| {
1550 if let Some(cb) = &on_text_change {
1551 cb(text);
1552 }
1553 };
1554 rc_android::handle_ime_event(
1555 ime,
1556 &mut state,
1557 hit_rect,
1558 &mut notify,
1559 &mut self.ime_preedit,
1560 );
1561 self.request_redraw();
1562 }
1563 }
1564 }
1565 }
1566
1567 WindowEvent::RedrawRequested => {
1568 self.process_a11y_actions();
1570 self.dispatch_file_drop_now();
1571 self.process_render_commands();
1572
1573 let Some(win) = self.window.as_ref() else {
1574 return;
1575 };
1576 if self.backend.is_none() {
1577 return;
1578 }
1579
1580 let t0 = Instant::now();
1581 let scale = win.scale_factor() as f32;
1582 let size_px_u32 = self.sched.size;
1583 let focused = self.sched.focused;
1584
1585 let rc = self.render.clone();
1586 let root_fn = &mut self.root;
1587 let mut composed_root = |s: &mut Scheduler| (root_fn)(s, &rc);
1588
1589 let frame = compose_frame(
1590 &mut self.sched,
1591 &mut composed_root,
1592 scale,
1593 size_px_u32,
1594 self.hover_id,
1595 &self.pressed_ids,
1596 &self.textfield_states,
1597 focused,
1598 );
1599
1600 let build_layout_ms = (Instant::now() - t0).as_secs_f32() * 1000.0;
1601
1602 if let Some(adapter) = &mut self.accesskit_adapter {
1604 let win = self.window.as_ref().unwrap();
1605 let scale = win.scale_factor();
1606 if let Some(update) =
1607 self.a11y_tree
1608 .update(&frame.semantics_nodes, scale, self.sched.focused)
1609 {
1610 adapter.update_if_active(|| update);
1611 }
1612 }
1613
1614 let mut scene = frame.scene.clone();
1616 let widget_count = frame.semantics_nodes.len() + frame.hit_regions.len();
1618 let signal_count = self.sched.id_count() as usize;
1619 self.inspector.hud.metrics = Some(repose_devtools::Metrics {
1620 build_ms: build_layout_ms,
1621 layout_ms: build_layout_ms * 0.5,
1622 scene_nodes: scene.nodes.len(),
1623 widget_count,
1624 signal_count,
1625 });
1626 self.inspector.frame(&mut scene);
1627
1628 self.overlay_drag_indicator(&mut scene);
1630
1631 let win = self.window.as_ref().unwrap();
1633 let scale = win.scale_factor() as f32;
1634 if let Some(backend) = self.backend.as_mut() {
1635 backend.frame(&scene, GlyphRasterConfig { px: 18.0 * scale });
1636 }
1637
1638 self.frame_cache = Some(frame);
1639 self.tick_snackbar();
1640 self.last_redraw = Instant::now();
1641 }
1642
1643 _ => {}
1644 }
1645 }
1646
1647 fn about_to_wait(&mut self, el: &winit::event_loop::ActiveEventLoop) {
1648 if take_frame_request() {
1649 self.pending_redraw = true;
1650 }
1651 if !self.pending_redraw {
1652 return;
1653 }
1654
1655 let now = Instant::now();
1656 let interval = web_time::Duration::from_millis(16);
1657
1658 if now.saturating_duration_since(self.last_redraw) >= interval {
1659 self.pending_redraw = false;
1660 self.request_redraw();
1661 self.last_redraw = now;
1662 } else {
1663 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
1664 self.last_redraw + interval,
1665 ));
1666 }
1667 }
1668
1669 fn new_events(
1670 &mut self,
1671 _: &winit::event_loop::ActiveEventLoop,
1672 _: winit::event::StartCause,
1673 ) {
1674 }
1675 fn user_event(&mut self, _: &winit::event_loop::ActiveEventLoop, _: ()) {}
1676 fn device_event(
1677 &mut self,
1678 _: &winit::event_loop::ActiveEventLoop,
1679 _: winit::event::DeviceId,
1680 _: winit::event::DeviceEvent,
1681 ) {
1682 }
1683 fn suspended(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1684 fn exiting(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1685 fn memory_warning(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1686 }
1687
1688 impl App {
1689 fn announce_focus_change(&mut self) {
1690 if let Some(f) = &self.frame_cache {
1691 let focused_node = self
1692 .sched
1693 .focused
1694 .and_then(|id| f.semantics_nodes.iter().find(|n| n.id == id));
1695 self.a11y.focus_changed(focused_node);
1696 }
1697 }
1698
1699 fn notify_text_change(&self, id: u64, text: String) {
1700 if let Some(f) = &self.frame_cache
1701 && let Some(h) = f.hit_regions.iter().find(|h| h.id == id)
1702 && let Some(cb) = &h.on_text_change
1703 {
1704 cb(text);
1705 }
1706 }
1707
1708 fn tf_key_of(&self, visual_id: u64) -> u64 {
1709 if let Some(f) = &self.frame_cache {
1710 return rc::tf_key_of(f, visual_id);
1711 }
1712 visual_id
1713 }
1714
1715 fn dispatch_action(&mut self, action: repose_core::shortcuts::Action) -> bool {
1716 use repose_core::shortcuts;
1717
1718 if let (Some(f), Some(fid)) = (&self.frame_cache, self.sched.focused) {
1719 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid) {
1720 if let Some(cb) = &hit.on_action {
1721 if cb(action.clone()) {
1722 return true;
1723 }
1724 }
1725 }
1726 }
1727
1728 if shortcuts::handle(action.clone()) {
1729 return true;
1730 }
1731
1732 self.dispatch_default_action(action)
1733 }
1734
1735 fn dispatch_default_action(&mut self, action: repose_core::shortcuts::Action) -> bool {
1736 use repose_core::shortcuts::Action;
1737
1738 let Some(fid) = self.sched.focused else {
1739 return false;
1740 };
1741 let key = self.tf_key_of(fid);
1742 let Some(state_rc) = self.textfield_states.get(&key).cloned() else {
1743 return false;
1744 };
1745
1746 match action {
1747 Action::Copy => {
1748 let txt = state_rc.borrow().selected_text();
1749 if txt.is_empty() {
1750 return false;
1751 }
1752 self.copy_to_clipboard(txt);
1753 true
1754 }
1755 Action::Cut => {
1756 let txt = state_rc.borrow().selected_text();
1757 if txt.is_empty() {
1758 return false;
1759 }
1760 self.copy_to_clipboard(txt);
1761 {
1762 let mut st = state_rc.borrow_mut();
1763 st.insert_text("");
1764 self.notify_text_change(fid, st.text.clone());
1765 if let Some(f) = &self.frame_cache
1766 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid)
1767 {
1768 App::tf_ensure_caret_visible(&mut st, hit.tf_multiline);
1769 }
1770 }
1771 true
1772 }
1773 Action::Paste => {
1774 let Some(mut txt) = self.paste_from_clipboard() else {
1775 return false;
1776 };
1777 txt.retain(|c| !c.is_control() && c != '\n' && c != '\r');
1778 if txt.is_empty() {
1779 return false;
1780 }
1781 {
1782 let mut st = state_rc.borrow_mut();
1783 st.insert_text(&txt);
1784 self.notify_text_change(fid, st.text.clone());
1785 if let Some(f) = &self.frame_cache
1786 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid)
1787 {
1788 App::tf_ensure_caret_visible(&mut st, hit.tf_multiline);
1789 }
1790 }
1791 true
1792 }
1793 Action::SelectAll => {
1794 {
1795 let mut st = state_rc.borrow_mut();
1796 st.selection = 0..st.text.len();
1797 if let Some(f) = &self.frame_cache
1798 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid)
1799 {
1800 App::tf_ensure_caret_visible(&mut st, hit.tf_multiline);
1801 }
1802 }
1803 true
1804 }
1805 _ => false,
1806 }
1807 }
1808
1809 fn dnd_slop_px(&self) -> f32 {
1810 dp_to_px(6.0)
1811 }
1812
1813 fn dnd_update_over(&mut self, pos: Vec2) {
1814 let Some(f) = &self.frame_cache else {
1815 return;
1816 };
1817 let Some(session) = self.drag.as_mut() else {
1818 return;
1819 };
1820
1821 let new_over = rc::dnd_target_id_at(f, pos);
1822
1823 if new_over != session.over_id {
1824 if let Some(prev) = session.over_id {
1825 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == prev) {
1826 if let Some(cb) = &hit.on_drag_leave {
1827 cb(repose_core::dnd::DragOver {
1828 source_id: session.source_id,
1829 target_id: prev,
1830 position: pos,
1831 modifiers: self.modifiers,
1832 payload: session.payload.clone(),
1833 });
1834 }
1835 }
1836 }
1837
1838 if let Some(now) = new_over {
1839 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == now) {
1840 if let Some(cb) = &hit.on_drag_enter {
1841 cb(repose_core::dnd::DragOver {
1842 source_id: session.source_id,
1843 target_id: now,
1844 position: pos,
1845 modifiers: self.modifiers,
1846 payload: session.payload.clone(),
1847 });
1848 }
1849 }
1850 }
1851
1852 session.over_id = new_over;
1853 }
1854
1855 if let Some(over) = session.over_id {
1856 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == over) {
1857 if let Some(cb) = &hit.on_drag_over {
1858 cb(repose_core::dnd::DragOver {
1859 source_id: session.source_id,
1860 target_id: over,
1861 position: pos,
1862 modifiers: self.modifiers,
1863 payload: session.payload.clone(),
1864 });
1865 }
1866 }
1867 }
1868 }
1869
1870 fn dnd_try_begin(&mut self, pos: Vec2) -> bool {
1871 if self.drag.is_some() {
1872 return true;
1873 }
1874
1875 let Some((sx, sy)) = self.mouse_down_pos_px else {
1876 return false;
1877 };
1878 let Some(cid) = self.capture_id else {
1879 return false;
1880 };
1881 if !self.pressed_ids.contains(&cid) {
1882 return false;
1883 }
1884
1885 let dx = pos.x - sx;
1886 let dy = pos.y - sy;
1887 let dist = (dx * dx + dy * dy).sqrt();
1888 if dist < self.dnd_slop_px() {
1889 return false;
1890 }
1891
1892 let Some(f) = &self.frame_cache else {
1893 return false;
1894 };
1895 let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid) else {
1896 return false;
1897 };
1898
1899 let Some(cb) = &hit.on_drag_start else {
1900 return false;
1901 };
1902
1903 let payload = cb(repose_core::dnd::DragStart {
1904 source_id: cid,
1905 position: pos,
1906 modifiers: self.modifiers,
1907 });
1908 let Some(payload) = payload else {
1909 return false;
1910 };
1911
1912 self.drag = Some(DragSession {
1913 source_id: cid,
1914 payload,
1915 start_px: (sx, sy),
1916 over_id: None,
1917 });
1918
1919 self.pressed_ids.remove(&cid);
1921 self.request_redraw();
1922 true
1923 }
1924
1925 fn dnd_finish(&mut self, pos: Vec2, accept_if_possible: bool) {
1926 let Some(f) = &self.frame_cache else {
1927 self.drag = None;
1928 self.capture_id = None;
1929 self.mouse_down_pos_px = None;
1930 self.request_redraw();
1931 return;
1932 };
1933
1934 let Some(session) = self.drag.take() else {
1935 return;
1936 };
1937
1938 let mut accepted = false;
1939
1940 if accept_if_possible {
1941 let drop_target = rc::dnd_target_id_at(f, pos);
1942 if let Some(tid) = drop_target {
1943 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == tid) {
1944 if let Some(cb) = &hit.on_drop {
1945 accepted = cb(repose_core::dnd::DropEvent {
1946 source_id: session.source_id,
1947 target_id: tid,
1948 position: pos,
1949 modifiers: self.modifiers,
1950 payload: session.payload.clone(),
1951 });
1952 }
1953 }
1954 }
1955 }
1956
1957 if let Some(source_hit) = f.hit_regions.iter().find(|h| h.id == session.source_id) {
1959 if let Some(cb) = &source_hit.on_drag_end {
1960 cb(repose_core::dnd::DragEnd { accepted });
1961 }
1962 }
1963
1964 self.capture_id = None;
1965 self.mouse_down_pos_px = None;
1966 self.request_redraw();
1967 }
1968
1969 fn dnd_cancel(&mut self) {
1970 let pos = Vec2 {
1971 x: self.mouse_pos_px.0,
1972 y: self.mouse_pos_px.1,
1973 };
1974 self.dnd_finish(pos, false);
1975 }
1976
1977 fn dispatch_file_drop_now(&mut self) {
1978 let Some(f) = &self.frame_cache else {
1979 self.pending_dropped_files.clear();
1980 self.pending_drop_pos_px = None;
1981 return;
1982 };
1983
1984 if self.pending_dropped_files.is_empty() {
1985 return;
1986 }
1987
1988 let pos_px = self.pending_drop_pos_px.unwrap_or(self.mouse_pos_px);
1989 let pos = Vec2 {
1990 x: pos_px.0,
1991 y: pos_px.1,
1992 };
1993
1994 let mut files = Vec::new();
1995 for p in self.pending_dropped_files.drain(..) {
1996 let name = p
1997 .file_name()
1998 .and_then(|s| s.to_str())
1999 .unwrap_or("file")
2000 .to_string();
2001 files.push(repose_core::dnd::DroppedFile {
2002 name,
2003 path: Some(p),
2004 });
2005 }
2006
2007 let payload: repose_core::dnd::DragPayload =
2008 std::rc::Rc::new(repose_core::dnd::DroppedFiles { files });
2009
2010 let Some(target_id) = rc::dnd_target_id_at(f, pos) else {
2011 return;
2012 };
2013
2014 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id) {
2015 if let Some(cb) = &hit.on_drop {
2016 let accepted = cb(repose_core::dnd::DropEvent {
2017 source_id: 0, target_id,
2019 position: pos,
2020 modifiers: self.modifiers,
2021 payload: payload.clone(),
2022 });
2023
2024 if accepted {
2025 if let Some(node) = f.semantics_nodes.iter().find(|n| n.id == target_id) {
2026 let label = node.label.as_deref().unwrap_or("");
2027 self.a11y.announce(&format!("Dropped files on {}", label));
2028 }
2029 }
2030 }
2031 }
2032
2033 self.pending_drop_pos_px = None;
2034 self.request_redraw();
2035 }
2036 }
2037
2038 let event_loop = EventLoop::new()?;
2039 let mut app = App::new_with_snackbar(Box::new(root), snackbar_tick);
2040 repose_core::animation::set_clock(Box::new(repose_core::animation::SystemClock));
2042 event_loop.run_app(&mut app)?;
2043 Ok(())
2044}
2045
2046#[cfg(feature = "desktop")]
2047pub fn run_app_with_snackbar(
2048 root: impl FnMut(&mut Scheduler, &RenderContext) -> View + 'static,
2049 snackbar_tick: Rc<dyn Fn(u32)>,
2050) -> anyhow::Result<()> {
2051 run_desktop_app_with_snackbar(root, Some(snackbar_tick))
2052}
2053
2054pub trait A11yBridge: Send {
2062 fn publish_tree(&mut self, nodes: &[repose_core::runtime::SemNode]);
2064
2065 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>);
2067
2068 fn announce(&mut self, msg: &str);
2070}
2071
2072struct NoopA11y;
2073impl A11yBridge for NoopA11y {
2074 fn publish_tree(&mut self, _nodes: &[repose_core::runtime::SemNode]) {
2075 }
2077 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>) {
2078 if let Some(n) = node {
2079 log::info!("A11y focus: {:?} {:?}", n.role, n.label);
2080 } else {
2081 log::info!("A11y focus: None");
2082 }
2083 }
2084 fn announce(&mut self, msg: &str) {
2085 log::info!("A11y announce: {msg}");
2086 }
2087}
2088
2089#[cfg(target_os = "linux")]
2090struct LinuxAtspiStub;
2091#[cfg(target_os = "linux")]
2092impl A11yBridge for LinuxAtspiStub {
2093 fn publish_tree(&mut self, nodes: &[repose_core::runtime::SemNode]) {
2094 log::debug!("AT-SPI stub: publish {} nodes", nodes.len());
2095 }
2096 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>) {
2097 if let Some(n) = node {
2098 log::info!("AT-SPI stub focus: {:?} {:?}", n.role, n.label);
2099 } else {
2100 log::info!("AT-SPI stub focus: None");
2101 }
2102 }
2103 fn announce(&mut self, msg: &str) {
2104 log::info!("AT-SPI stub announce: {msg}");
2105 }
2106}