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