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