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