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