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