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