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