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