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