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) {
309 let font_px = dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().0;
310 let m = measure_text(&st.text, font_px);
311 let caret_x_px = m.positions.get(st.caret_index()).copied().unwrap_or(0.0);
312 let iw = st.inner_width;
313 st.ensure_caret_visible(caret_x_px, iw, dp_to_px(2.0));
314 }
315
316 fn copy_to_clipboard(&mut self, text: String) {
317 if let Some(cb) = &mut self.clipboard {
318 let _ = pollster::block_on(cb.set_text(&text));
319 }
320 }
321
322 fn paste_from_clipboard(&mut self) -> Option<String> {
323 if let Some(cb) = &mut self.clipboard {
324 match pollster::block_on(cb.get_text()) {
325 Ok(t) => Some(t),
326 Err(e) => {
327 eprintln!("Paste error: {}", e);
328 None
329 }
330 }
331 } else {
332 None
333 }
334 }
335
336 fn process_render_commands(&mut self) {
337 let Some(backend) = &mut self.backend else {
338 return;
339 };
340
341 for cmd in self.render.drain() {
342 match cmd {
343 RenderCommand::SetImageEncoded {
344 handle,
345 bytes,
346 srgb,
347 } => {
348 let _ = backend.set_image_from_bytes(handle, &bytes, srgb);
349 }
350 RenderCommand::SetImageRgba8 {
351 handle,
352 w,
353 h,
354 rgba,
355 srgb,
356 } => {
357 let _ = backend.set_image_rgba8(handle, w, h, &rgba, srgb);
358 }
359 RenderCommand::SetImageNv12 {
360 handle,
361 w,
362 h,
363 y,
364 uv,
365 full_range,
366 } => {
367 let _ = backend.set_image_nv12(handle, w, h, &y, &uv, full_range);
368 }
369 RenderCommand::RemoveImage { handle } => {
370 backend.remove_image(handle);
371 }
372 }
373 }
374 }
375
376 fn reset_pointer_state(&mut self) {
377 self.capture_id = None;
378 self.pressed_ids.clear();
379 self.mouse_down_pos_px = None;
380 self.drag = None;
381 self.hover_id = None;
382 }
383
384 fn overlay_drag_indicator(&self, scene: &mut Scene) {
385 let dragging_internal = self.drag.is_some();
386 let dragging_files = self.external_file_drag;
387
388 if !(dragging_internal || dragging_files) {
389 return;
390 }
391
392 let pos = Vec2 {
393 x: self.mouse_pos_px.0,
394 y: self.mouse_pos_px.1,
395 };
396
397 if let Some(f) = &self.frame_cache {
399 if let Some(tid) = rc::dnd_target_id_at(f, pos) {
400 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == tid) {
401 let color = if dragging_files {
402 Color::from_hex("#FFAA00")
403 } else {
404 Color::from_hex("#44AAFF")
405 };
406 scene.nodes.push(SceneNode::Border {
407 rect: hit.rect,
408 color,
409 width: dp_to_px(2.0),
410 radius: dp_to_px(8.0),
411 });
412 }
413 }
414 }
415
416 let label = if dragging_files {
418 "FILE DROP"
419 } else {
420 "DRAGGING"
421 };
422 let bg = if dragging_files {
423 Color::from_hex("#FFAA0077")
424 } else {
425 Color::from_hex("#44AAFF77")
426 };
427
428 let badge = Rect {
429 x: pos.x + dp_to_px(12.0),
430 y: pos.y + dp_to_px(12.0),
431 w: dp_to_px(110.0),
432 h: dp_to_px(24.0),
433 };
434
435 scene.nodes.push(SceneNode::Rect {
436 rect: badge,
437 brush: Brush::Solid(bg),
438 radius: dp_to_px(8.0),
439 });
440 scene.nodes.push(SceneNode::Text {
441 rect: Rect {
442 x: badge.x + dp_to_px(8.0),
443 y: badge.y + dp_to_px(6.0),
444 w: 0.0,
445 h: dp_to_px(14.0),
446 },
447 text: Arc::<str>::from(label),
448 color: Color::WHITE,
449 size: dp_to_px(12.0),
450 });
451 }
452
453 fn is_textfield(&self, id: u64) -> bool {
454 if let Some(f) = &self.frame_cache {
455 f.semantics_nodes
456 .iter()
457 .any(|n| n.id == id && n.role == Role::TextField)
458 } else {
459 false
460 }
461 }
462
463 fn is_multiline_id(&self, id: u64) -> bool {
464 if let Some(f) = &self.frame_cache {
465 f.hit_regions
466 .iter()
467 .find(|h| h.id == id)
468 .map(|h| h.tf_multiline)
469 .unwrap_or(false)
470 } else {
471 false
472 }
473 }
474
475 fn hit_by_id(f: &Frame, id: u64) -> Option<&HitRegion> {
476 f.hit_regions.iter().find(|h| h.id == id)
477 }
478
479 fn padding_px(&self) -> f32 {
480 dp_to_px(TF_PADDING_X_DP)
481 }
482
483 fn dp_px(&self, dp: f32) -> f32 {
484 dp_to_px(dp)
485 }
486 }
487
488 impl ApplicationHandler<()> for App {
489 fn resumed(&mut self, el: &winit::event_loop::ActiveEventLoop) {
490 self.clipboard = clipawl::Clipboard::new().ok();
491
492 if self.window.is_none() {
493 match el.create_window(
494 WindowAttributes::default()
495 .with_title("Repose")
496 .with_inner_size(PhysicalSize::new(1280, 800))
497 .with_visible(false),
498 ) {
499 Ok(win) => {
500 let w = Arc::new(win);
501
502 let activation_handler = ReposeActivationHandler {
503 initial_tree: Some(A11yTree::initial_tree()),
504 };
505
506 let action_handler = ReposeActionHandler {
507 pending_actions: self.a11y_actions.clone(),
508 };
509
510 let deactivation_handler = ReposeDeactivationHandler;
511
512 let adapter = Adapter::with_direct_handlers(
513 el,
514 &w,
515 activation_handler,
516 action_handler,
517 deactivation_handler,
518 );
519
520 self.accesskit_adapter = Some(adapter);
521
522 w.set_visible(true);
523
524 let size = w.inner_size();
525 self.sched.size = (size.width, size.height);
526
527 match repose_render_wgpu::WgpuBackend::new(w.clone()) {
528 Ok(b) => {
529 self.backend = Some(b);
530 self.window = Some(w);
531 self.request_redraw();
532 }
533 Err(e) => {
534 log::error!("Failed to create WGPU backend: {e:?}");
535 el.exit();
536 }
537 }
538 }
539 Err(e) => {
540 log::error!("Failed to create window: {e:?}");
541 el.exit();
542 }
543 }
544 }
545 }
546
547 fn window_event(
548 &mut self,
549 el: &winit::event_loop::ActiveEventLoop,
550 _id: winit::window::WindowId,
551 event: WindowEvent,
552 ) {
553 if let Some(adapter) = &mut self.accesskit_adapter {
555 adapter.process_event(self.window.as_ref().unwrap(), &event);
556 }
557
558 match event {
559 WindowEvent::CloseRequested => {
560 el.exit();
561 }
562
563 WindowEvent::Focused(false) => {
564 self.external_file_drag = false;
566 self.hovered_files.clear();
567 self.reset_pointer_state();
568
569 if let Some(w) = &self.window {
570 rc_web::set_ime_for_textfield(w, false);
571 }
572 self.ime_preedit = false;
573
574 self.request_redraw();
575 }
576
577 WindowEvent::HoveredFile(path) => {
578 self.external_file_drag = true;
580 if self.hovered_files.len() < 32 {
581 self.hovered_files.push(path);
582 }
583 if self.pending_drop_pos_px.is_none() {
585 self.pending_drop_pos_px = Some(self.mouse_pos_px);
586 }
587 self.request_redraw();
588 }
589
590 WindowEvent::HoveredFileCancelled => {
591 self.external_file_drag = false;
592 self.hovered_files.clear();
593
594 self.reset_pointer_state();
596
597 self.request_redraw();
598 }
599
600 WindowEvent::DroppedFile(path) => {
601 self.pending_dropped_files.push(path);
603 if self.pending_drop_pos_px.is_none() {
604 self.pending_drop_pos_px = Some(self.mouse_pos_px);
605 }
606
607 self.external_file_drag = false;
609 self.hovered_files.clear();
610
611 self.request_redraw();
612 }
613
614 WindowEvent::Resized(size) => {
615 self.sched.size = (size.width, size.height);
616 if let Some(b) = &mut self.backend {
617 b.configure_surface(size.width, size.height);
618 }
619 if let Some(w) = &self.window {
620 let sf = w.scale_factor() as f32;
621 let dp_w = size.width as f32 / sf;
622 let dp_h = size.height as f32 / sf;
623 log::info!(
624 "Resized: fb={}x{} px, scale_factor={}, ~{}x{} dp",
625 size.width,
626 size.height,
627 sf,
628 dp_w as i32,
629 dp_h as i32
630 );
631 }
632 self.request_redraw();
633 }
634
635 WindowEvent::CursorMoved { position, .. } => {
636 self.mouse_pos_px = (position.x as f32, position.y as f32);
637
638 let pos = Vec2 {
639 x: self.mouse_pos_px.0,
640 y: self.mouse_pos_px.1,
641 };
642
643 if self.drag.is_some() {
644 self.dnd_update_over(pos);
645 self.request_redraw();
646 return;
647 }
648
649 if self.dnd_try_begin(pos) {
650 self.dnd_update_over(pos);
651 return;
652 }
653
654 if self.inspector.hud.inspector_enabled
656 && let Some(f) = &self.frame_cache
657 {
658 let hover_rect = f
659 .hit_regions
660 .iter()
661 .find(|h| {
662 h.rect.contains(Vec2 {
663 x: self.mouse_pos_px.0,
664 y: self.mouse_pos_px.1,
665 })
666 })
667 .map(|h| h.rect);
668 self.inspector.hud.set_hovered(hover_rect);
669 self.request_redraw();
670 }
671
672 if let (Some(f), Some(cid)) = (&self.frame_cache, self.capture_id)
674 && self.is_textfield(cid)
675 {
676 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid) {
677 let key = self.tf_key_of(cid);
678 if let Some(state_rc) = self.textfield_states.get(&key) {
679 let mut st = state_rc.borrow_mut();
680
681 let pad_x = dp_to_px(TF_PADDING_X_DP);
682 let inner_x = hit.rect.x + pad_x;
683 let inner_y = hit.rect.y + dp_to_px(8.0);
684 let inner_w = (hit.rect.w - 2.0 * pad_x).max(1.0);
685 let inner_h = (hit.rect.h - dp_to_px(16.0)).max(1.0);
686
687 st.set_inner_width(inner_w);
688 st.set_inner_height(inner_h);
689
690 let content_x =
691 (self.mouse_pos_px.0 - inner_x + st.scroll_offset).max(0.0);
692 let content_y =
693 (self.mouse_pos_px.1 - inner_y + st.scroll_offset_y).max(0.0);
694
695 let font_px =
696 dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().0;
697
698 let idx = if hit.tf_multiline {
699 index_for_xy_bytes(
700 &st.text, font_px, inner_w, content_x, content_y,
701 )
702 } else {
703 index_for_x_bytes(&st.text, font_px, content_x)
704 };
705
706 st.drag_to(idx);
707
708 if hit.tf_multiline {
710 let (cx, cy, _) = caret_xy_for_byte(
711 &st.text,
712 font_px,
713 inner_w,
714 st.caret_index(),
715 );
716 st.ensure_caret_visible_xy(
717 cx,
718 cy,
719 inner_w,
720 inner_h,
721 dp_to_px(2.0),
722 );
723 } else {
724 let m = measure_text(&st.text, font_px);
725 let cx =
726 m.positions.get(st.caret_index()).copied().unwrap_or(0.0);
727 st.ensure_caret_visible(cx, inner_w, dp_to_px(2.0));
728 }
729
730 self.request_redraw();
731 }
732 }
733 }
734
735 if let Some(f) = &self.frame_cache {
737 let pos = Vec2 {
739 x: self.mouse_pos_px.0,
740 y: self.mouse_pos_px.1,
741 };
742 let top = f.hit_regions.iter().rev().find(|h| h.rect.contains(pos));
743
744 if let Some(win) = &self.window {
746 let c = top
747 .and_then(|h| h.cursor)
748 .unwrap_or(repose_core::CursorIcon::Default);
749 win.set_cursor(winit::window::Cursor::Icon(map_cursor(c)));
750 }
751
752 let new_hover = top.map(|h| h.id);
753
754 if new_hover != self.hover_id {
756 if let Some(prev_id) = self.hover_id
757 && let Some(prev) = f.hit_regions.iter().find(|h| h.id == prev_id)
758 && let Some(cb) = &prev.on_pointer_leave
759 {
760 let pe = repose_core::input::PointerEvent {
761 id: repose_core::input::PointerId(0),
762 kind: repose_core::input::PointerKind::Mouse,
763 event: repose_core::input::PointerEventKind::Leave,
764 position: pos,
765 pressure: 1.0,
766 modifiers: self.modifiers,
767 };
768 cb(pe);
769 }
770 if let Some(h) = top
771 && let Some(cb) = &h.on_pointer_enter
772 {
773 let pe = repose_core::input::PointerEvent {
774 id: repose_core::input::PointerId(0),
775 kind: repose_core::input::PointerKind::Mouse,
776 event: repose_core::input::PointerEventKind::Enter,
777 position: pos,
778 pressure: 1.0,
779 modifiers: self.modifiers,
780 };
781 cb(pe);
782 }
783 self.hover_id = new_hover;
784 }
785
786 let pe = repose_core::input::PointerEvent {
788 id: repose_core::input::PointerId(0),
789 kind: repose_core::input::PointerKind::Mouse,
790 event: repose_core::input::PointerEventKind::Move,
791 position: pos,
792 pressure: 1.0,
793 modifiers: self.modifiers,
794 };
795
796 if let Some(cid) = self.capture_id {
798 if let Some(h) = f.hit_regions.iter().find(|h| h.id == cid)
799 && let Some(cb) = &h.on_pointer_move
800 {
801 cb(pe.clone());
802 }
803 } else if let Some(h) = &top
804 && let Some(cb) = &h.on_pointer_move
805 {
806 cb(pe);
807 }
808 }
809 }
810
811 WindowEvent::MouseWheel { delta, .. } => {
812 let (dx_px, dy_px) = match delta {
814 MouseScrollDelta::LineDelta(x, y) => {
815 let unit_px = dp_to_px(60.0);
816 (-(x * unit_px), -(y * unit_px))
817 }
818 MouseScrollDelta::PixelDelta(lp) => (-(lp.x as f32), -(lp.y as f32)),
819 };
820 log::debug!("MouseWheel: dx={}, dy={}", dx_px, dy_px);
821
822 if let Some(f) = &self.frame_cache {
823 let pos = Vec2 {
824 x: self.mouse_pos_px.0,
825 y: self.mouse_pos_px.1,
826 };
827
828 for hit in f.hit_regions.iter().rev().filter(|h| h.rect.contains(pos)) {
829 if let Some(cb) = &hit.on_scroll {
830 log::debug!("Calling on_scroll for hit region id={}", hit.id);
831 let before = Vec2 { x: dx_px, y: dy_px };
832 let leftover = cb(before);
833 let consumed_x = (before.x - leftover.x).abs() > 0.001;
834 let consumed_y = (before.y - leftover.y).abs() > 0.001;
835 if consumed_x || consumed_y {
836 self.request_redraw();
837 break; }
839 }
840 }
841 }
842 }
843
844 WindowEvent::MouseInput {
845 state: ElementState::Pressed,
846 button: MouseButton::Left,
847 ..
848 } => {
849 let mut need_announce = false;
850 if let Some(f) = &self.frame_cache {
851 let pos = Vec2 {
852 x: self.mouse_pos_px.0,
853 y: self.mouse_pos_px.1,
854 };
855 if let Some(hit) = f.hit_regions.iter().rev().find(|h| h.rect.contains(pos))
856 {
857 self.mouse_down_pos_px = Some(self.mouse_pos_px);
858 self.drag = None;
859
860 self.capture_id = Some(hit.id);
862
863 if self.is_textfield(hit.id) {
865 let key = self.tf_key_of(hit.id);
866 self.textfield_states.entry(key).or_insert_with(|| {
867 Rc::new(RefCell::new(TextFieldState::new()))
868 });
869 if let Some(st_rc) = self.textfield_states.get(&key) {
870 let mut st = st_rc.borrow_mut();
871 let pad = self.padding_px();
872 let inner_x = hit.rect.x + pad;
873 let inner_y = hit.rect.y + self.dp_px(8.0);
874 let content_x =
875 (self.mouse_pos_px.0 - inner_x + st.scroll_offset).max(0.0);
876 let content_y = (self.mouse_pos_px.1 - inner_y
877 + st.scroll_offset_y)
878 .max(0.0);
879 let font_px = self.dp_px(TF_FONT_DP)
880 * repose_core::locals::text_scale().0;
881
882 let idx = if hit.tf_multiline {
883 textfield::index_for_xy_bytes(
884 &st.text,
885 font_px,
886 hit.rect.w - 2.0 * pad,
887 content_x,
888 content_y,
889 )
890 } else {
891 textfield::index_for_x_bytes(&st.text, font_px, content_x)
892 };
893
894 st.begin_drag(idx, self.modifiers.shift);
895
896 let caret_idx = st.caret_index();
898 let iw = st.inner_width;
899 let ih = st.inner_height;
900 let wrap_w = hit.rect.w - 2.0 * pad;
901 if hit.tf_multiline {
902 let (cx, cy, _) = textfield::caret_xy_for_byte(
903 &st.text, font_px, wrap_w, caret_idx,
904 );
905 st.ensure_caret_visible_xy(cx, cy, iw, ih, self.dp_px(2.0));
906 } else {
907 let m = measure_text(&st.text, font_px);
908 let cx = m.positions.get(caret_idx).copied().unwrap_or(0.0);
909 st.ensure_caret_visible(cx, iw, self.dp_px(2.0));
910 }
911 }
912 }
913 self.pressed_ids.insert(hit.id);
915 self.request_redraw();
917
918 if hit.focusable {
920 self.sched.focused = Some(hit.id);
921 need_announce = true;
922 let key = self.tf_key_of(hit.id);
923 self.textfield_states.entry(key).or_insert_with(|| {
924 Rc::new(RefCell::new(TextFieldState::new()))
925 });
926 if let Some(win) = &self.window {
927 let sf = win.scale_factor();
928 rc_web::set_ime_for_textfield(win, true);
929 win.set_ime_cursor_area(
930 LogicalPosition::new(
931 hit.rect.x as f64 / sf,
932 hit.rect.y as f64 / sf,
933 ),
934 LogicalSize::new(
935 hit.rect.w as f64 / sf,
936 hit.rect.h as f64 / sf,
937 ),
938 );
939 }
940 }
941
942 if let Some(cb) = &hit.on_pointer_down {
944 let pe = repose_core::input::PointerEvent {
945 id: repose_core::input::PointerId(0),
946 kind: repose_core::input::PointerKind::Mouse,
947 event: repose_core::input::PointerEventKind::Down(
948 repose_core::input::PointerButton::Primary,
949 ),
950 position: pos,
951 pressure: 1.0,
952 modifiers: self.modifiers,
953 };
954 cb(pe);
955 }
956
957 if need_announce {
958 self.announce_focus_change();
959 }
960
961 self.request_redraw();
962 } else {
963 if self.ime_preedit {
965 if let Some(win) = &self.window {
966 rc_web::set_ime_for_textfield(win, false);
967 }
968 self.ime_preedit = false;
969 }
970 self.sched.focused = None;
971 self.request_redraw();
972 }
973 }
974 }
975
976 WindowEvent::MouseInput {
977 state: ElementState::Released,
978 button: MouseButton::Left,
979 ..
980 } => {
981 let pos = Vec2 {
982 x: self.mouse_pos_px.0,
983 y: self.mouse_pos_px.1,
984 };
985
986 if self.drag.is_some() {
987 self.dnd_finish(pos, true);
988 self.capture_id = None;
989 self.pressed_ids.clear();
990 repose_core::request_frame();
991 return;
992 }
993
994 if let Some(cid) = self.capture_id {
995 self.pressed_ids.remove(&cid);
996 self.request_redraw();
997 }
998
999 if let (Some(f), Some(cid)) = (&self.frame_cache, self.capture_id) {
1000 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid) {
1001 if let Some(cb) = &hit.on_pointer_up {
1002 let pos = Vec2 {
1003 x: self.mouse_pos_px.0,
1004 y: self.mouse_pos_px.1,
1005 };
1006 let pe = repose_core::input::PointerEvent {
1007 id: repose_core::input::PointerId(0),
1008 kind: repose_core::input::PointerKind::Mouse,
1009 event: repose_core::input::PointerEventKind::Up(
1010 repose_core::input::PointerButton::Primary,
1011 ),
1012 position: pos,
1013 pressure: 1.0,
1014 modifiers: self.modifiers,
1015 };
1016 cb(pe);
1017 }
1018 }
1019 }
1020
1021 if let (Some(f), Some(cid)) = (&self.frame_cache, self.capture_id) {
1023 let pos = Vec2 {
1024 x: self.mouse_pos_px.0,
1025 y: self.mouse_pos_px.1,
1026 };
1027 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid)
1028 && hit.rect.contains(pos)
1029 && let Some(cb) = &hit.on_click
1030 {
1031 cb();
1032 if let Some(node) = f.semantics_nodes.iter().find(|n| n.id == cid) {
1034 let label = node.label.as_deref().unwrap_or("");
1035 self.a11y.announce(&format!("Activated {}", label));
1036 }
1037 }
1038 }
1039 if let (Some(f), Some(cid)) = (&self.frame_cache, self.capture_id)
1041 && let Some(_sem) = f
1042 .semantics_nodes
1043 .iter()
1044 .find(|n| n.id == cid && n.role == Role::TextField)
1045 {
1046 let key = self.tf_key_of(cid);
1047 if let Some(state_rc) = self.textfield_states.get(&key) {
1048 state_rc.borrow_mut().end_drag();
1049 }
1050 }
1051
1052 self.capture_id = None;
1053
1054 repose_core::request_frame();
1055 }
1056
1057 WindowEvent::ModifiersChanged(new_mods) => {
1058 self.modifiers.shift = new_mods.state().shift_key();
1059 self.modifiers.ctrl = new_mods.state().control_key();
1060 self.modifiers.alt = new_mods.state().alt_key();
1061 self.modifiers.meta = new_mods.state().super_key();
1062 self.modifiers.command = if cfg!(target_os = "macos") {
1063 self.modifiers.meta
1064 } else {
1065 self.modifiers.ctrl
1066 };
1067 }
1068
1069 WindowEvent::KeyboardInput {
1070 event: key_event, ..
1071 } => {
1072 if key_event.state == ElementState::Pressed && !key_event.repeat {
1073 match key_event.physical_key {
1074 PhysicalKey::Code(KeyCode::BrowserBack)
1075 | PhysicalKey::Code(KeyCode::Escape) => {
1076 use repose_navigation::back;
1077
1078 if self.drag.is_some() {
1079 self.dnd_cancel();
1080 return;
1081 }
1082
1083 if !back::handle() {
1084 }
1086 return;
1087 }
1088 _ => {}
1089 }
1090 }
1091 if matches!(key_event.physical_key, PhysicalKey::Code(KeyCode::Tab)) {
1093 if key_event.state == ElementState::Pressed
1095 && !key_event.repeat
1096 && let Some(f) = &self.frame_cache
1097 {
1098 if let Some(next) = rc::focus_next_in_chain(
1099 &f.focus_chain,
1100 self.sched.focused,
1101 self.modifiers.shift,
1102 ) {
1103 if let Some(active) = self.key_pressed_active.take() {
1105 self.pressed_ids.remove(&active);
1106 }
1107
1108 self.sched.focused = Some(next);
1109
1110 if let Some(win) = &self.window {
1112 let is_textfield = f
1113 .semantics_nodes
1114 .iter()
1115 .any(|n| n.id == next && n.role == Role::TextField);
1116 rc_web::set_ime_for_textfield(win, is_textfield);
1117 }
1118 self.announce_focus_change();
1119 self.request_redraw();
1120 }
1121 }
1122 return; }
1124
1125 if key_event.state == ElementState::Pressed && !key_event.repeat {
1126 if let Some(action) = repose_core::shortcuts::resolve_action(
1127 repose_core::shortcuts::KeyChord::new(
1128 rc::map_key(key_event.physical_key),
1129 self.modifiers,
1130 ),
1131 ) {
1132 if self.dispatch_action(action) {
1133 self.request_redraw();
1134 return;
1135 }
1136 }
1137 }
1138
1139 if let Some(fid) = self.sched.focused {
1140 let is_textfield = if let Some(f) = &self.frame_cache {
1142 f.semantics_nodes
1143 .iter()
1144 .any(|n| n.id == fid && n.role == Role::TextField)
1145 } else {
1146 false
1147 };
1148
1149 if !is_textfield {
1150 match key_event.physical_key {
1151 PhysicalKey::Code(KeyCode::Space)
1152 | PhysicalKey::Code(KeyCode::Enter) => {
1153 if key_event.state == ElementState::Pressed && !key_event.repeat
1154 {
1155 self.pressed_ids.insert(fid);
1156 self.key_pressed_active = Some(fid);
1157 self.request_redraw();
1158 return;
1159 }
1160 }
1161 _ => {}
1162 }
1163 }
1164 }
1165
1166 if key_event.state == ElementState::Pressed
1170 && !key_event.repeat
1171 && let PhysicalKey::Code(KeyCode::Enter) = key_event.physical_key
1172 && let Some(focused_id) = self.sched.focused
1173 && let Some(f) = &self.frame_cache
1174 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == focused_id)
1175 {
1176 let is_multiline = hit.tf_multiline;
1177 let should_submit = if is_multiline {
1178 self.modifiers.ctrl || self.modifiers.meta
1180 } else {
1181 true
1183 };
1184
1185 if should_submit {
1186 if let Some(on_submit) = &hit.on_text_submit {
1187 let key = self.tf_key_of(focused_id);
1188 if let Some(state) = self.textfield_states.get(&key) {
1189 let text = state.borrow().text.clone();
1190 on_submit(text);
1191 self.request_redraw();
1192 return;
1193 }
1194 }
1195 } else {
1196 let key = self.tf_key_of(focused_id);
1198 if let Some(state_rc) = self.textfield_states.get(&key) {
1199 let mut st = state_rc.borrow_mut();
1200 st.insert_text("\n");
1201 let new_text = st.text.clone();
1202 self.notify_text_change(focused_id, new_text);
1203 App::tf_ensure_caret_visible(&mut st);
1204 self.request_redraw();
1205 return;
1206 }
1207 }
1208 }
1209
1210 if key_event.state == ElementState::Pressed {
1211 if self.modifiers.ctrl
1213 && self.modifiers.shift
1214 && let PhysicalKey::Code(KeyCode::KeyI) = key_event.physical_key
1215 {
1216 self.inspector.hud.toggle_inspector();
1217 self.request_redraw();
1218 return;
1219 }
1220
1221 if let Some(focused_id) = self.sched.focused {
1223 let key = self.tf_key_of(focused_id);
1224 if let Some(state_rc) = self.textfield_states.get(&key) {
1225 let mut state = state_rc.borrow_mut();
1226 match key_event.physical_key {
1227 PhysicalKey::Code(KeyCode::Backspace) => {
1228 state.delete_backward();
1229 let new_text = state.text.clone();
1230 self.notify_text_change(focused_id, new_text);
1231 App::tf_ensure_caret_visible(&mut state);
1232 self.request_redraw();
1233 }
1234 PhysicalKey::Code(KeyCode::Delete) => {
1235 state.delete_forward();
1236 let new_text = state.text.clone();
1237 self.notify_text_change(focused_id, new_text);
1238 App::tf_ensure_caret_visible(&mut state);
1239 self.request_redraw();
1240 }
1241 PhysicalKey::Code(KeyCode::ArrowLeft) => {
1242 state.move_cursor(-1, self.modifiers.shift);
1243 state.preferred_x_px = None; App::tf_ensure_caret_visible(&mut state);
1245 self.request_redraw();
1246 }
1247 PhysicalKey::Code(KeyCode::ArrowRight) => {
1248 state.move_cursor(1, self.modifiers.shift);
1249 state.preferred_x_px = None; App::tf_ensure_caret_visible(&mut state);
1251 self.request_redraw();
1252 }
1253 PhysicalKey::Code(KeyCode::ArrowUp) => {
1254 if self.is_multiline_id(focused_id) {
1255 if let Some(f) = &self.frame_cache {
1256 if let Some(hit) = f
1257 .hit_regions
1258 .iter()
1259 .find(|h| h.id == focused_id)
1260 {
1261 let font_px = dp_to_px(TF_FONT_DP);
1262 let pad = self.padding_px();
1263 let wrap_w = hit.rect.w - 2.0 * pad;
1264 let cur = state.caret_index();
1265 let (new_pos, px) =
1266 repose_ui::textfield::move_caret_vertical(
1267 &state.text,
1268 font_px,
1269 wrap_w,
1270 cur,
1271 -1,
1272 state.preferred_x_px,
1273 );
1274 if self.modifiers.shift {
1275 state.selection.end = new_pos;
1276 } else {
1277 state.selection = new_pos..new_pos;
1278 }
1279 state.preferred_x_px = Some(px);
1280 let (cx, cy, _) = caret_xy_for_byte(
1282 &state.text,
1283 font_px,
1284 wrap_w,
1285 state.caret_index(),
1286 );
1287 let iw = state.inner_width;
1288 let ih = state.inner_height;
1289 state.ensure_caret_visible_xy(
1290 cx,
1291 cy,
1292 iw,
1293 ih,
1294 self.dp_px(2.0),
1295 );
1296 self.request_redraw();
1297 }
1298 }
1299 }
1300 }
1301 PhysicalKey::Code(KeyCode::ArrowDown) => {
1302 if self.is_multiline_id(focused_id) {
1303 if let Some(f) = &self.frame_cache {
1304 if let Some(hit) = f
1305 .hit_regions
1306 .iter()
1307 .find(|h| h.id == focused_id)
1308 {
1309 let font_px = dp_to_px(TF_FONT_DP);
1310 let pad = self.padding_px();
1311 let wrap_w = hit.rect.w - 2.0 * pad;
1312 let cur = state.caret_index();
1313 let (new_pos, px) =
1314 repose_ui::textfield::move_caret_vertical(
1315 &state.text,
1316 font_px,
1317 wrap_w,
1318 cur,
1319 1,
1320 state.preferred_x_px,
1321 );
1322 if self.modifiers.shift {
1323 state.selection.end = new_pos;
1324 } else {
1325 state.selection = new_pos..new_pos;
1326 }
1327 state.preferred_x_px = Some(px);
1328 let (cx, cy, _) = caret_xy_for_byte(
1330 &state.text,
1331 font_px,
1332 wrap_w,
1333 state.caret_index(),
1334 );
1335 let iw = state.inner_width;
1336 let ih = state.inner_height;
1337 state.ensure_caret_visible_xy(
1338 cx,
1339 cy,
1340 iw,
1341 ih,
1342 self.dp_px(2.0),
1343 );
1344 self.request_redraw();
1345 }
1346 }
1347 }
1348 }
1349 PhysicalKey::Code(KeyCode::Home) => {
1350 state.selection = 0..0;
1351 App::tf_ensure_caret_visible(&mut state);
1352 self.request_redraw();
1353 }
1354 PhysicalKey::Code(KeyCode::End) => {
1355 {
1356 let end = state.text.len();
1357 state.selection = end..end;
1358 }
1359 App::tf_ensure_caret_visible(&mut state);
1360 self.request_redraw();
1361 }
1362 PhysicalKey::Code(KeyCode::KeyA) if self.modifiers.ctrl => {
1363 state.selection = 0..state.text.len();
1364 App::tf_ensure_caret_visible(&mut state);
1365 self.request_redraw();
1366 }
1367 _ => {}
1368 }
1369 }
1370 if self.modifiers.ctrl {
1371 match key_event.physical_key {
1372 PhysicalKey::Code(KeyCode::KeyC) => {
1373 if let Some(fid) = self.sched.focused {
1374 let key = self.tf_key_of(fid);
1375 if let Some(state) = self.textfield_states.get(&key) {
1376 let txt = state.borrow().selected_text();
1377 if !txt.is_empty() {
1378 let _ = self.copy_to_clipboard(txt);
1379 }
1380 }
1381 }
1382 return;
1383 }
1384 PhysicalKey::Code(KeyCode::KeyX) => {
1385 if let Some(fid) = self.sched.focused {
1386 let key = self.tf_key_of(fid);
1387 if let Some(state_rc) =
1388 self.textfield_states.get(&key).cloned()
1389 {
1390 let txt = state_rc.borrow().selected_text();
1392 if !txt.is_empty() {
1393 {
1394 let _ = self.copy_to_clipboard(txt.clone());
1395 }
1396 {
1398 let mut st = state_rc.borrow_mut();
1399 st.insert_text(""); let new_text = st.text.clone();
1401 self.notify_text_change(
1402 focused_id, new_text,
1403 );
1404 App::tf_ensure_caret_visible(&mut st);
1405 }
1406 self.request_redraw();
1407 }
1408 }
1409 }
1410 return;
1411 }
1412 PhysicalKey::Code(KeyCode::KeyV) => {
1413 if let Some(fid) = self.sched.focused {
1414 let key = self.tf_key_of(fid);
1415 let is_multiline = self.is_multiline_id(fid);
1416 if let Some(state_rc) =
1417 self.textfield_states.get(&key).cloned()
1418 && let Some(mut txt) = self.paste_from_clipboard()
1419 {
1420 if is_multiline {
1423 txt.retain(|c| {
1424 c == '\n' || (!c.is_control() && c != '\r')
1425 });
1426 } else {
1427 txt.retain(|c| {
1428 !c.is_control() && c != '\n' && c != '\r'
1429 });
1430 }
1431 if !txt.is_empty() {
1432 let mut st = state_rc.borrow_mut();
1433 st.insert_text(&txt);
1434 let new_text = st.text.clone();
1435 self.notify_text_change(focused_id, new_text);
1436 App::tf_ensure_caret_visible(&mut st);
1437 self.request_redraw();
1438 }
1439 }
1440 }
1441 return;
1442 }
1443 _ => {}
1444 }
1445 }
1446 }
1447
1448 if !self.ime_preedit
1450 && !self.modifiers.ctrl
1451 && !self.modifiers.alt
1452 && !self.modifiers.meta
1453 && let Some(raw) = key_event.text.as_deref()
1454 {
1455 let text: String = raw
1456 .chars()
1457 .filter(|c| !c.is_control() && *c != '\n' && *c != '\r')
1458 .collect();
1459 if !text.is_empty()
1460 && let Some(fid) = self.sched.focused
1461 {
1462 let key = self.tf_key_of(fid);
1463 if let Some(state_rc) = self.textfield_states.get(&key) {
1464 let mut st = state_rc.borrow_mut();
1465 st.insert_text(&text);
1466 self.notify_text_change(fid, st.text.clone());
1467 App::tf_ensure_caret_visible(&mut st);
1468 self.request_redraw();
1469 }
1470 }
1471 }
1472 } else if key_event.state == ElementState::Released {
1473 if let Some(active_id) = self.key_pressed_active {
1475 match key_event.physical_key {
1476 PhysicalKey::Code(KeyCode::Space)
1477 | PhysicalKey::Code(KeyCode::Enter) => {
1478 self.pressed_ids.remove(&active_id);
1479 self.key_pressed_active = None;
1480
1481 if let Some(f) = &self.frame_cache
1482 && let Some(hit) =
1483 f.hit_regions.iter().find(|h| h.id == active_id)
1484 && let Some(cb) = &hit.on_click
1485 {
1486 cb();
1487 if let Some(node) =
1488 f.semantics_nodes.iter().find(|n| n.id == active_id)
1489 {
1490 let label = node.label.as_deref().unwrap_or("");
1491 self.a11y.announce(&format!("Activated {}", label));
1492 }
1493 }
1494 self.request_redraw();
1495 }
1496 _ => {}
1497 }
1498 }
1499 }
1500 }
1501
1502 WindowEvent::Ime(ime) => {
1503 if let Some(focused_id) = self.sched.focused {
1504 let key = self.tf_key_of(focused_id);
1505 if let Some(state_rc) = self.textfield_states.get(&key) {
1506 if let Some(f) = &self.frame_cache
1507 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == focused_id)
1508 {
1509 let mut state = state_rc.borrow_mut();
1510 let hit_rect = hit.rect;
1511 let on_text_change = hit.on_text_change.clone();
1512 let mut notify = |text: String| {
1513 if let Some(cb) = &on_text_change {
1514 cb(text);
1515 }
1516 };
1517 rc_android::handle_ime_event(
1518 ime,
1519 &mut state,
1520 hit_rect,
1521 &mut notify,
1522 &mut self.ime_preedit,
1523 );
1524 self.request_redraw();
1525 }
1526 }
1527 }
1528 }
1529
1530 WindowEvent::RedrawRequested => {
1531 self.process_a11y_actions();
1533 self.dispatch_file_drop_now();
1534 self.process_render_commands();
1535
1536 let Some(win) = self.window.as_ref() else {
1537 return;
1538 };
1539 if self.backend.is_none() {
1540 return;
1541 }
1542
1543 let t0 = Instant::now();
1544 let scale = win.scale_factor() as f32;
1545 let size_px_u32 = self.sched.size;
1546 let focused = self.sched.focused;
1547
1548 let rc = self.render.clone();
1549 let root_fn = &mut self.root;
1550 let mut composed_root = |s: &mut Scheduler| (root_fn)(s, &rc);
1551
1552 let frame = compose_frame(
1553 &mut self.sched,
1554 &mut composed_root,
1555 scale,
1556 size_px_u32,
1557 self.hover_id,
1558 &self.pressed_ids,
1559 &self.textfield_states,
1560 focused,
1561 );
1562
1563 let build_layout_ms = (Instant::now() - t0).as_secs_f32() * 1000.0;
1564
1565 if let Some(adapter) = &mut self.accesskit_adapter {
1567 let win = self.window.as_ref().unwrap();
1568 let scale = win.scale_factor();
1569 if let Some(update) =
1570 self.a11y_tree
1571 .update(&frame.semantics_nodes, scale, self.sched.focused)
1572 {
1573 adapter.update_if_active(|| update);
1574 }
1575 }
1576
1577 let mut scene = frame.scene.clone();
1579 self.inspector.hud.metrics = Some(repose_devtools::Metrics {
1581 build_layout_ms,
1582 scene_nodes: scene.nodes.len(),
1583 });
1584 self.inspector.frame(&mut scene);
1585
1586 self.overlay_drag_indicator(&mut scene);
1588
1589 let win = self.window.as_ref().unwrap();
1591 let scale = win.scale_factor() as f32;
1592 if let Some(backend) = self.backend.as_mut() {
1593 backend.frame(&scene, GlyphRasterConfig { px: 18.0 * scale });
1594 }
1595
1596 self.frame_cache = Some(frame);
1597 self.tick_snackbar();
1598 self.last_redraw = Instant::now();
1599 }
1600
1601 _ => {}
1602 }
1603 }
1604
1605 fn about_to_wait(&mut self, el: &winit::event_loop::ActiveEventLoop) {
1606 if take_frame_request() {
1607 self.pending_redraw = true;
1608 }
1609 if !self.pending_redraw {
1610 return;
1611 }
1612
1613 let now = Instant::now();
1614 let interval = web_time::Duration::from_millis(16);
1615
1616 if now.saturating_duration_since(self.last_redraw) >= interval {
1617 self.pending_redraw = false;
1618 self.request_redraw();
1619 self.last_redraw = now;
1620 } else {
1621 el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
1622 self.last_redraw + interval,
1623 ));
1624 }
1625 }
1626
1627 fn new_events(
1628 &mut self,
1629 _: &winit::event_loop::ActiveEventLoop,
1630 _: winit::event::StartCause,
1631 ) {
1632 }
1633 fn user_event(&mut self, _: &winit::event_loop::ActiveEventLoop, _: ()) {}
1634 fn device_event(
1635 &mut self,
1636 _: &winit::event_loop::ActiveEventLoop,
1637 _: winit::event::DeviceId,
1638 _: winit::event::DeviceEvent,
1639 ) {
1640 }
1641 fn suspended(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1642 fn exiting(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1643 fn memory_warning(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1644 }
1645
1646 impl App {
1647 fn announce_focus_change(&mut self) {
1648 if let Some(f) = &self.frame_cache {
1649 let focused_node = self
1650 .sched
1651 .focused
1652 .and_then(|id| f.semantics_nodes.iter().find(|n| n.id == id));
1653 self.a11y.focus_changed(focused_node);
1654 }
1655 }
1656
1657 fn notify_text_change(&self, id: u64, text: String) {
1658 if let Some(f) = &self.frame_cache
1659 && let Some(h) = f.hit_regions.iter().find(|h| h.id == id)
1660 && let Some(cb) = &h.on_text_change
1661 {
1662 cb(text);
1663 }
1664 }
1665
1666 fn tf_key_of(&self, visual_id: u64) -> u64 {
1667 if let Some(f) = &self.frame_cache {
1668 return rc::tf_key_of(f, visual_id);
1669 }
1670 visual_id
1671 }
1672
1673 fn dispatch_action(&mut self, action: repose_core::shortcuts::Action) -> bool {
1674 use repose_core::shortcuts;
1675
1676 if let (Some(f), Some(fid)) = (&self.frame_cache, self.sched.focused) {
1677 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid) {
1678 if let Some(cb) = &hit.on_action {
1679 if cb(action.clone()) {
1680 return true;
1681 }
1682 }
1683 }
1684 }
1685
1686 if shortcuts::handle(action.clone()) {
1687 return true;
1688 }
1689
1690 self.dispatch_default_action(action)
1691 }
1692
1693 fn dispatch_default_action(&mut self, action: repose_core::shortcuts::Action) -> bool {
1694 use repose_core::shortcuts::Action;
1695
1696 let Some(fid) = self.sched.focused else {
1697 return false;
1698 };
1699 let key = self.tf_key_of(fid);
1700 let Some(state_rc) = self.textfield_states.get(&key).cloned() else {
1701 return false;
1702 };
1703
1704 match action {
1705 Action::Copy => {
1706 let txt = state_rc.borrow().selected_text();
1707 if txt.is_empty() {
1708 return false;
1709 }
1710 self.copy_to_clipboard(txt);
1711 true
1712 }
1713 Action::Cut => {
1714 let txt = state_rc.borrow().selected_text();
1715 if txt.is_empty() {
1716 return false;
1717 }
1718 self.copy_to_clipboard(txt);
1719 {
1720 let mut st = state_rc.borrow_mut();
1721 st.insert_text("");
1722 self.notify_text_change(fid, st.text.clone());
1723 App::tf_ensure_caret_visible(&mut st);
1724 }
1725 true
1726 }
1727 Action::Paste => {
1728 let Some(mut txt) = self.paste_from_clipboard() else {
1729 return false;
1730 };
1731 txt.retain(|c| !c.is_control() && c != '\n' && c != '\r');
1732 if txt.is_empty() {
1733 return false;
1734 }
1735 {
1736 let mut st = state_rc.borrow_mut();
1737 st.insert_text(&txt);
1738 self.notify_text_change(fid, st.text.clone());
1739 App::tf_ensure_caret_visible(&mut st);
1740 }
1741 true
1742 }
1743 Action::SelectAll => {
1744 {
1745 let mut st = state_rc.borrow_mut();
1746 st.selection = 0..st.text.len();
1747 App::tf_ensure_caret_visible(&mut st);
1748 }
1749 true
1750 }
1751 _ => false,
1752 }
1753 }
1754
1755 fn dnd_slop_px(&self) -> f32 {
1756 dp_to_px(6.0)
1757 }
1758
1759 fn dnd_update_over(&mut self, pos: Vec2) {
1760 let Some(f) = &self.frame_cache else {
1761 return;
1762 };
1763 let Some(session) = self.drag.as_mut() else {
1764 return;
1765 };
1766
1767 let new_over = rc::dnd_target_id_at(f, pos);
1768
1769 if new_over != session.over_id {
1770 if let Some(prev) = session.over_id {
1771 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == prev) {
1772 if let Some(cb) = &hit.on_drag_leave {
1773 cb(repose_core::dnd::DragOver {
1774 source_id: session.source_id,
1775 target_id: prev,
1776 position: pos,
1777 modifiers: self.modifiers,
1778 payload: session.payload.clone(),
1779 });
1780 }
1781 }
1782 }
1783
1784 if let Some(now) = new_over {
1785 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == now) {
1786 if let Some(cb) = &hit.on_drag_enter {
1787 cb(repose_core::dnd::DragOver {
1788 source_id: session.source_id,
1789 target_id: now,
1790 position: pos,
1791 modifiers: self.modifiers,
1792 payload: session.payload.clone(),
1793 });
1794 }
1795 }
1796 }
1797
1798 session.over_id = new_over;
1799 }
1800
1801 if let Some(over) = session.over_id {
1802 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == over) {
1803 if let Some(cb) = &hit.on_drag_over {
1804 cb(repose_core::dnd::DragOver {
1805 source_id: session.source_id,
1806 target_id: over,
1807 position: pos,
1808 modifiers: self.modifiers,
1809 payload: session.payload.clone(),
1810 });
1811 }
1812 }
1813 }
1814 }
1815
1816 fn dnd_try_begin(&mut self, pos: Vec2) -> bool {
1817 if self.drag.is_some() {
1818 return true;
1819 }
1820
1821 let Some((sx, sy)) = self.mouse_down_pos_px else {
1822 return false;
1823 };
1824 let Some(cid) = self.capture_id else {
1825 return false;
1826 };
1827 if !self.pressed_ids.contains(&cid) {
1828 return false;
1829 }
1830
1831 let dx = pos.x - sx;
1832 let dy = pos.y - sy;
1833 let dist = (dx * dx + dy * dy).sqrt();
1834 if dist < self.dnd_slop_px() {
1835 return false;
1836 }
1837
1838 let Some(f) = &self.frame_cache else {
1839 return false;
1840 };
1841 let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid) else {
1842 return false;
1843 };
1844
1845 let Some(cb) = &hit.on_drag_start else {
1846 return false;
1847 };
1848
1849 let payload = cb(repose_core::dnd::DragStart {
1850 source_id: cid,
1851 position: pos,
1852 modifiers: self.modifiers,
1853 });
1854 let Some(payload) = payload else {
1855 return false;
1856 };
1857
1858 self.drag = Some(DragSession {
1859 source_id: cid,
1860 payload,
1861 start_px: (sx, sy),
1862 over_id: None,
1863 });
1864
1865 self.pressed_ids.remove(&cid);
1867 self.request_redraw();
1868 true
1869 }
1870
1871 fn dnd_finish(&mut self, pos: Vec2, accept_if_possible: bool) {
1872 let Some(f) = &self.frame_cache else {
1873 self.drag = None;
1874 self.capture_id = None;
1875 self.mouse_down_pos_px = None;
1876 self.request_redraw();
1877 return;
1878 };
1879
1880 let Some(session) = self.drag.take() else {
1881 return;
1882 };
1883
1884 let mut accepted = false;
1885
1886 if accept_if_possible {
1887 let drop_target = rc::dnd_target_id_at(f, pos);
1888 if let Some(tid) = drop_target {
1889 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == tid) {
1890 if let Some(cb) = &hit.on_drop {
1891 accepted = cb(repose_core::dnd::DropEvent {
1892 source_id: session.source_id,
1893 target_id: tid,
1894 position: pos,
1895 modifiers: self.modifiers,
1896 payload: session.payload.clone(),
1897 });
1898 }
1899 }
1900 }
1901 }
1902
1903 if let Some(source_hit) = f.hit_regions.iter().find(|h| h.id == session.source_id) {
1905 if let Some(cb) = &source_hit.on_drag_end {
1906 cb(repose_core::dnd::DragEnd { accepted });
1907 }
1908 }
1909
1910 self.capture_id = None;
1911 self.mouse_down_pos_px = None;
1912 self.request_redraw();
1913 }
1914
1915 fn dnd_cancel(&mut self) {
1916 let pos = Vec2 {
1917 x: self.mouse_pos_px.0,
1918 y: self.mouse_pos_px.1,
1919 };
1920 self.dnd_finish(pos, false);
1921 }
1922
1923 fn dispatch_file_drop_now(&mut self) {
1924 let Some(f) = &self.frame_cache else {
1925 self.pending_dropped_files.clear();
1926 self.pending_drop_pos_px = None;
1927 return;
1928 };
1929
1930 if self.pending_dropped_files.is_empty() {
1931 return;
1932 }
1933
1934 let pos_px = self.pending_drop_pos_px.unwrap_or(self.mouse_pos_px);
1935 let pos = Vec2 {
1936 x: pos_px.0,
1937 y: pos_px.1,
1938 };
1939
1940 let mut files = Vec::new();
1941 for p in self.pending_dropped_files.drain(..) {
1942 let name = p
1943 .file_name()
1944 .and_then(|s| s.to_str())
1945 .unwrap_or("file")
1946 .to_string();
1947 files.push(repose_core::dnd::DroppedFile {
1948 name,
1949 path: Some(p),
1950 });
1951 }
1952
1953 let payload: repose_core::dnd::DragPayload =
1954 std::rc::Rc::new(repose_core::dnd::DroppedFiles { files });
1955
1956 let Some(target_id) = rc::dnd_target_id_at(f, pos) else {
1957 return;
1958 };
1959
1960 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id) {
1961 if let Some(cb) = &hit.on_drop {
1962 let accepted = cb(repose_core::dnd::DropEvent {
1963 source_id: 0, target_id,
1965 position: pos,
1966 modifiers: self.modifiers,
1967 payload: payload.clone(),
1968 });
1969
1970 if accepted {
1971 if let Some(node) = f.semantics_nodes.iter().find(|n| n.id == target_id) {
1972 let label = node.label.as_deref().unwrap_or("");
1973 self.a11y.announce(&format!("Dropped files on {}", label));
1974 }
1975 }
1976 }
1977 }
1978
1979 self.pending_drop_pos_px = None;
1980 self.request_redraw();
1981 }
1982 }
1983
1984 let event_loop = EventLoop::new()?;
1985 let mut app = App::new_with_snackbar(Box::new(root), snackbar_tick);
1986 repose_core::animation::set_clock(Box::new(repose_core::animation::SystemClock));
1988 event_loop.run_app(&mut app)?;
1989 Ok(())
1990}
1991
1992#[cfg(feature = "desktop")]
1993pub fn run_app_with_snackbar(
1994 root: impl FnMut(&mut Scheduler, &RenderContext) -> View + 'static,
1995 snackbar_tick: Rc<dyn Fn(u32)>,
1996) -> anyhow::Result<()> {
1997 run_desktop_app_with_snackbar(root, Some(snackbar_tick))
1998}
1999
2000pub trait A11yBridge: Send {
2008 fn publish_tree(&mut self, nodes: &[repose_core::runtime::SemNode]);
2010
2011 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>);
2013
2014 fn announce(&mut self, msg: &str);
2016}
2017
2018struct NoopA11y;
2019impl A11yBridge for NoopA11y {
2020 fn publish_tree(&mut self, _nodes: &[repose_core::runtime::SemNode]) {
2021 }
2023 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>) {
2024 if let Some(n) = node {
2025 log::info!("A11y focus: {:?} {:?}", n.role, n.label);
2026 } else {
2027 log::info!("A11y focus: None");
2028 }
2029 }
2030 fn announce(&mut self, msg: &str) {
2031 log::info!("A11y announce: {msg}");
2032 }
2033}
2034
2035#[cfg(target_os = "linux")]
2036struct LinuxAtspiStub;
2037#[cfg(target_os = "linux")]
2038impl A11yBridge for LinuxAtspiStub {
2039 fn publish_tree(&mut self, nodes: &[repose_core::runtime::SemNode]) {
2040 log::debug!("AT-SPI stub: publish {} nodes", nodes.len());
2041 }
2042 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>) {
2043 if let Some(n) = node {
2044 log::info!("AT-SPI stub focus: {:?} {:?}", n.role, n.label);
2045 } else {
2046 log::info!("AT-SPI stub focus: None");
2047 }
2048 }
2049 fn announce(&mut self, msg: &str) {
2050 log::info!("AT-SPI stub announce: {msg}");
2051 }
2052}