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