repose_platform/lib.rs
1//! Platform runners (desktop via winit; Android soon-to-be-in-alpha)
2//!
3
4use repose_core::*;
5use repose_ui::layout_and_paint;
6use repose_ui::textfield::{
7 TF_FONT_PX, TF_PADDING_X, byte_to_char_index, index_for_x_bytes, measure_text,
8};
9use std::time::Instant;
10
11#[cfg(feature = "desktop")]
12pub fn run_desktop_app(root: impl FnMut(&mut Scheduler) -> View + 'static) -> anyhow::Result<()> {
13 use std::cell::RefCell;
14 use std::collections::{HashMap, HashSet};
15 use std::rc::Rc;
16 use std::sync::Arc;
17
18 use repose_ui::TextFieldState;
19 use winit::application::ApplicationHandler;
20 use winit::dpi::{LogicalPosition, LogicalSize, PhysicalSize};
21 use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent};
22 use winit::event_loop::EventLoop;
23 use winit::keyboard::{KeyCode, PhysicalKey};
24 use winit::window::{ImePurpose, Window, WindowAttributes};
25
26 struct App {
27 // App state
28 root: Box<dyn FnMut(&mut Scheduler) -> View>,
29 window: Option<Arc<Window>>,
30 backend: Option<repose_render_wgpu::WgpuBackend>,
31 sched: Scheduler,
32 inspector: repose_devtools::Inspector,
33 frame_cache: Option<Frame>,
34 mouse_pos: (f32, f32),
35 modifiers: Modifiers,
36 textfield_states: HashMap<u64, Rc<RefCell<TextFieldState>>>,
37 ime_preedit: bool,
38 hover_id: Option<u64>,
39 capture_id: Option<u64>,
40 pressed_ids: HashSet<u64>,
41 key_pressed_active: Option<u64>, // for Space/Enter press/release activation
42 clipboard: Option<arboard::Clipboard>,
43 a11y: Box<dyn A11yBridge>,
44 last_focus: Option<u64>,
45 }
46
47 impl App {
48 fn new(root: Box<dyn FnMut(&mut Scheduler) -> View>) -> Self {
49 Self {
50 root,
51 window: None,
52 backend: None,
53 sched: Scheduler::new(),
54 inspector: repose_devtools::Inspector::new(),
55 frame_cache: None,
56 mouse_pos: (0.0, 0.0),
57 modifiers: Modifiers::default(),
58 textfield_states: HashMap::new(),
59 ime_preedit: false,
60 hover_id: None,
61 capture_id: None,
62 pressed_ids: HashSet::new(),
63 key_pressed_active: None,
64 clipboard: None,
65 a11y: {
66 #[cfg(target_os = "linux")]
67 {
68 Box::new(LinuxAtspiStub) as Box<dyn A11yBridge>
69 }
70 #[cfg(not(target_os = "linux"))]
71 {
72 Box::new(NoopA11y) as Box<dyn A11yBridge>
73 }
74 },
75 last_focus: None,
76 }
77 }
78
79 fn request_redraw(&self) {
80 if let Some(w) = &self.window {
81 w.request_redraw();
82 }
83 }
84 fn tf_ensure_caret_visible(st: &mut TextFieldState) {
85 let px = TF_FONT_PX as u32;
86 let m = measure_text(&st.text, px);
87 let i0 = byte_to_char_index(&m, st.selection.start);
88 let i1 = byte_to_char_index(&m, st.selection.end);
89 let caret_x = m.positions.get(st.caret_index()).copied().unwrap_or(0.0);
90 st.ensure_caret_visible(caret_x, st.inner_width);
91 }
92 }
93
94 impl ApplicationHandler<()> for App {
95 fn resumed(&mut self, el: &winit::event_loop::ActiveEventLoop) {
96 self.clipboard = arboard::Clipboard::new().ok();
97 // Create the window once when app resumes.
98 if self.window.is_none() {
99 match el.create_window(
100 WindowAttributes::default()
101 .with_title("Repose Example")
102 .with_inner_size(PhysicalSize::new(1280, 800)),
103 ) {
104 Ok(win) => {
105 let w = Arc::new(win);
106 let size = w.inner_size();
107 self.sched.size = (size.width, size.height);
108 // Create WGPU backend
109 match repose_render_wgpu::WgpuBackend::new(w.clone()) {
110 Ok(b) => {
111 self.backend = Some(b);
112 self.window = Some(w);
113 self.request_redraw();
114 }
115 Err(e) => {
116 log::error!("Failed to create WGPU backend: {e:?}");
117 el.exit();
118 }
119 }
120 }
121 Err(e) => {
122 log::error!("Failed to create window: {e:?}");
123 el.exit();
124 }
125 }
126 }
127 }
128
129 fn window_event(
130 &mut self,
131 el: &winit::event_loop::ActiveEventLoop,
132 _id: winit::window::WindowId,
133 event: WindowEvent,
134 ) {
135 match event {
136 WindowEvent::CloseRequested => {
137 log::info!("Window close requested");
138 el.exit();
139 }
140 WindowEvent::Resized(size) => {
141 self.sched.size = (size.width, size.height);
142 if let Some(b) = &mut self.backend {
143 b.configure_surface(size.width, size.height);
144 }
145 self.request_redraw();
146 }
147 WindowEvent::CursorMoved { position, .. } => {
148 self.mouse_pos = (position.x as f32, position.y as f32);
149
150 // Inspector hover
151 if self.inspector.hud.inspector_enabled {
152 if let Some(f) = &self.frame_cache {
153 let hover_rect = f
154 .hit_regions
155 .iter()
156 .find(|h| {
157 h.rect.contains(Vec2 {
158 x: self.mouse_pos.0,
159 y: self.mouse_pos.1,
160 })
161 })
162 .map(|h| h.rect);
163 self.inspector.hud.set_hovered(hover_rect);
164 self.request_redraw();
165 }
166 }
167
168 if let (Some(f), Some(cid)) = (&self.frame_cache, self.capture_id) {
169 if let Some(_sem) = f
170 .semantics_nodes
171 .iter()
172 .find(|n| n.id == cid && n.role == Role::TextField)
173 {
174 if let Some(state_rc) = self.textfield_states.get(&cid) {
175 let mut state = state_rc.borrow_mut();
176 let inner_x = f
177 .hit_regions
178 .iter()
179 .find(|h| h.id == cid)
180 .map(|h| h.rect.x + TF_PADDING_X)
181 .unwrap_or(0.0);
182 let content_x = self.mouse_pos.0 - inner_x + state.scroll_offset;
183 let px = TF_FONT_PX as u32;
184 let idx = index_for_x_bytes(&state.text, px, content_x.max(0.0));
185 state.drag_to(idx);
186
187 // Scroll caret into view
188 let px = TF_FONT_PX as u32;
189 let m = measure_text(&state.text, px);
190 let i0 = byte_to_char_index(&m, state.selection.start);
191 let i1 = byte_to_char_index(&m, state.selection.end);
192 let caret_x =
193 m.positions.get(state.caret_index()).copied().unwrap_or(0.0);
194 // We also need inner width; get rect
195 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid) {
196 state.ensure_caret_visible(
197 caret_x,
198 hit.rect.w - 2.0 * TF_PADDING_X,
199 );
200 }
201 self.request_redraw();
202 }
203 }
204 }
205
206 // Pointer routing: hover + move/capture
207 if let Some(f) = &self.frame_cache {
208 // Determine topmost hit
209 let pos = Vec2 {
210 x: self.mouse_pos.0,
211 y: self.mouse_pos.1,
212 };
213 let top = f.hit_regions.iter().rev().find(|h| h.rect.contains(pos));
214 let new_hover = top.map(|h| h.id);
215
216 // Enter/Leave
217 if new_hover != self.hover_id {
218 if let Some(prev_id) = self.hover_id {
219 if let Some(prev) = f.hit_regions.iter().find(|h| h.id == prev_id) {
220 if let Some(cb) = &prev.on_pointer_leave {
221 let pe = repose_core::input::PointerEvent {
222 id: repose_core::input::PointerId(0),
223 kind: repose_core::input::PointerKind::Mouse,
224 event: repose_core::input::PointerEventKind::Leave,
225 position: pos,
226 pressure: 1.0,
227 modifiers: self.modifiers,
228 };
229 cb(pe);
230 }
231 }
232 }
233 if let Some(h) = top {
234 if let Some(cb) = &h.on_pointer_enter {
235 let pe = repose_core::input::PointerEvent {
236 id: repose_core::input::PointerId(0),
237 kind: repose_core::input::PointerKind::Mouse,
238 event: repose_core::input::PointerEventKind::Enter,
239 position: pos,
240 pressure: 1.0,
241 modifiers: self.modifiers,
242 };
243 cb(pe);
244 }
245 }
246 self.hover_id = new_hover;
247 }
248
249 // Build PointerEvent
250 let pe = repose_core::input::PointerEvent {
251 id: repose_core::input::PointerId(0),
252 kind: repose_core::input::PointerKind::Mouse,
253 event: repose_core::input::PointerEventKind::Move,
254 position: pos,
255 pressure: 1.0,
256 modifiers: self.modifiers,
257 };
258
259 // Move delivery (captured first)
260 if let Some(cid) = self.capture_id {
261 if let Some(h) = f.hit_regions.iter().find(|h| h.id == cid) {
262 if let Some(cb) = &h.on_pointer_move {
263 cb(pe.clone());
264 }
265 }
266 } else if let Some(h) = &top {
267 if let Some(cb) = &h.on_pointer_move {
268 cb(pe);
269 }
270 }
271 }
272 }
273 WindowEvent::MouseInput {
274 state: ElementState::Pressed,
275 button: MouseButton::Left,
276 ..
277 } => {
278 let mut need_announce = false;
279 if let Some(f) = &self.frame_cache {
280 let pos = Vec2 {
281 x: self.mouse_pos.0,
282 y: self.mouse_pos.1,
283 };
284 if let Some(hit) = f.hit_regions.iter().rev().find(|h| h.rect.contains(pos))
285 {
286 // Capture starts on press
287 self.capture_id = Some(hit.id);
288 // Pressed visual for mouse
289 self.pressed_ids.insert(hit.id);
290 // Repaint for pressed state
291 self.request_redraw();
292
293 // Focus & IME first for focusables (so state exists)
294 if hit.focusable {
295 self.sched.focused = Some(hit.id);
296 need_announce = true;
297 self.textfield_states.entry(hit.id).or_insert_with(|| {
298 Rc::new(RefCell::new(
299 repose_ui::textfield::TextFieldState::new(),
300 ))
301 });
302 if let Some(win) = &self.window {
303 let sf = win.scale_factor();
304 win.set_ime_allowed(true);
305 win.set_ime_purpose(ImePurpose::Normal);
306 win.set_ime_cursor_area(
307 LogicalPosition::new(
308 hit.rect.x as f64 / sf,
309 hit.rect.y as f64 / sf,
310 ),
311 LogicalSize::new(
312 hit.rect.w as f64 / sf,
313 hit.rect.h as f64 / sf,
314 ),
315 );
316 }
317 }
318
319 // PointerDown callback (legacy)
320 if let Some(cb) = &hit.on_pointer_down {
321 let pe = repose_core::input::PointerEvent {
322 id: repose_core::input::PointerId(0),
323 kind: repose_core::input::PointerKind::Mouse,
324 event: repose_core::input::PointerEventKind::Down(
325 repose_core::input::PointerButton::Primary,
326 ),
327 position: pos,
328 pressure: 1.0,
329 modifiers: self.modifiers,
330 };
331 cb(pe);
332 }
333
334 // TextField: place caret and start drag selection
335 if let Some(_sem) = f
336 .semantics_nodes
337 .iter()
338 .find(|n| n.id == hit.id && n.role == Role::TextField)
339 {
340 if let Some(state_rc) = self.textfield_states.get(&hit.id) {
341 let mut state = state_rc.borrow_mut();
342 let inner_x = hit.rect.x + TF_PADDING_X;
343 let content_x =
344 self.mouse_pos.0 - inner_x + state.scroll_offset;
345 let px = TF_FONT_PX as u32;
346 let idx =
347 index_for_x_bytes(&state.text, px, content_x.max(0.0));
348 state.begin_drag(idx, self.modifiers.shift);
349
350 // Scroll caret into view
351 let px = TF_FONT_PX as u32;
352 let m = measure_text(&state.text, px);
353 let i0 = byte_to_char_index(&m, state.selection.start);
354 let i1 = byte_to_char_index(&m, state.selection.end);
355 let caret_x = m
356 .positions
357 .get(state.caret_index())
358 .copied()
359 .unwrap_or(0.0);
360 state.ensure_caret_visible(
361 caret_x,
362 hit.rect.w - 2.0 * TF_PADDING_X,
363 );
364 }
365 }
366 if need_announce {
367 self.announce_focus_change();
368 }
369
370 self.request_redraw();
371 } else {
372 // Click outside: drop focus/IME
373 if self.ime_preedit {
374 if let Some(win) = &self.window {
375 win.set_ime_allowed(false);
376 }
377 self.ime_preedit = false;
378 }
379 self.sched.focused = None;
380 self.request_redraw();
381 }
382 }
383 }
384 WindowEvent::MouseInput {
385 state: ElementState::Released,
386 button: MouseButton::Left,
387 ..
388 } => {
389 if let Some(cid) = self.capture_id {
390 self.pressed_ids.remove(&cid);
391 self.request_redraw();
392 }
393
394 // Click on release if pointer is still over the captured hit region
395 if let (Some(f), Some(cid)) = (&self.frame_cache, self.capture_id) {
396 let pos = Vec2 {
397 x: self.mouse_pos.0,
398 y: self.mouse_pos.1,
399 };
400 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid) {
401 if hit.rect.contains(pos) {
402 if let Some(cb) = &hit.on_click {
403 cb();
404 // A11y: announce activation (mouse)
405 if let Some(node) =
406 f.semantics_nodes.iter().find(|n| n.id == cid)
407 {
408 let label = node.label.as_deref().unwrap_or("");
409 self.a11y.announce(&format!("Activated {}", label));
410 }
411 }
412 }
413 }
414 }
415 // TextField drag end
416 if let (Some(f), Some(cid)) = (&self.frame_cache, self.capture_id) {
417 if let Some(_sem) = f
418 .semantics_nodes
419 .iter()
420 .find(|n| n.id == cid && n.role == Role::TextField)
421 {
422 if let Some(state_rc) = self.textfield_states.get(&cid) {
423 state_rc.borrow_mut().end_drag();
424 }
425 }
426 }
427 self.capture_id = None;
428 }
429 WindowEvent::MouseWheel { delta, .. } => {
430 let dy = match delta {
431 MouseScrollDelta::LineDelta(_x, y) => -y * 40.0,
432 MouseScrollDelta::PixelDelta(lp) => -(lp.y as f32),
433 };
434
435 log::debug!("MouseWheel: dy={}", dy);
436
437 if let Some(f) = &self.frame_cache {
438 let pos = Vec2 {
439 x: self.mouse_pos.0,
440 y: self.mouse_pos.1,
441 };
442
443 // Find scrollable regions
444 for hit in f.hit_regions.iter().rev().filter(|h| h.rect.contains(pos)) {
445 if let Some(cb) = &hit.on_scroll {
446 log::debug!("Calling on_scroll for hit region id={}", hit.id);
447 let before = dy;
448 let leftover = cb(dy);
449 log::debug!(
450 "on_scroll consumed {} (leftover={})",
451 before - leftover,
452 leftover
453 );
454
455 if (before - leftover).abs() > 0.001 {
456 self.request_redraw();
457 break; // Stop after first handler consumes some
458 }
459 }
460 }
461 }
462 }
463 WindowEvent::ModifiersChanged(new_mods) => {
464 self.modifiers.shift = new_mods.state().shift_key();
465 self.modifiers.ctrl = new_mods.state().control_key();
466 self.modifiers.alt = new_mods.state().alt_key();
467 self.modifiers.meta = new_mods.state().super_key();
468 }
469 WindowEvent::KeyboardInput {
470 event: key_event, ..
471 } => {
472 // Focus traversal: Tab / Shift+Tab
473 if matches!(key_event.physical_key, PhysicalKey::Code(KeyCode::Tab)) {
474 // Only act on initial press, ignore repeats
475 if key_event.state == ElementState::Pressed && !key_event.repeat {
476 if let Some(f) = &self.frame_cache {
477 let chain = &f.focus_chain;
478 if !chain.is_empty() {
479 // If a button was “pressed” via keyboard, clear it when we move focus
480 if let Some(active) = self.key_pressed_active.take() {
481 self.pressed_ids.remove(&active);
482 }
483
484 let shift = self.modifiers.shift;
485 let current = self.sched.focused;
486 let next = if let Some(cur) = current {
487 if let Some(idx) = chain.iter().position(|&id| id == cur) {
488 if shift {
489 if idx == 0 {
490 chain[chain.len() - 1]
491 } else {
492 chain[idx - 1]
493 }
494 } else {
495 chain[(idx + 1) % chain.len()]
496 }
497 } else {
498 chain[0]
499 }
500 } else {
501 chain[0]
502 };
503 self.sched.focused = Some(next);
504
505 // IME only for TextField
506 if let Some(win) = &self.window {
507 if f.semantics_nodes
508 .iter()
509 .any(|n| n.id == next && n.role == Role::TextField)
510 {
511 win.set_ime_allowed(true);
512 win.set_ime_purpose(ImePurpose::Normal);
513 } else {
514 win.set_ime_allowed(false);
515 }
516 }
517 self.announce_focus_change();
518 self.request_redraw();
519 }
520 }
521 }
522 return; // swallow Tab
523 }
524
525 if let Some(fid) = self.sched.focused {
526 // If focused is NOT a TextField, allow Space/Enter activation
527 let is_textfield = if let Some(f) = &self.frame_cache {
528 f.semantics_nodes
529 .iter()
530 .any(|n| n.id == fid && n.role == Role::TextField)
531 } else {
532 false
533 };
534
535 if !is_textfield {
536 match key_event.physical_key {
537 PhysicalKey::Code(KeyCode::Space)
538 | PhysicalKey::Code(KeyCode::Enter) => {
539 if key_event.state == ElementState::Pressed && !key_event.repeat
540 {
541 self.pressed_ids.insert(fid);
542 self.key_pressed_active = Some(fid);
543 self.request_redraw();
544 return;
545 }
546 }
547 _ => {}
548 }
549 }
550 }
551
552 // Keyboard activation for focused widgets (Space/Enter)
553 if key_event.state == ElementState::Pressed && !key_event.repeat {
554 if let PhysicalKey::Code(KeyCode::Enter) = key_event.physical_key {
555 if let Some(focused_id) = self.sched.focused {
556 if let Some(f) = &self.frame_cache {
557 if let Some(hit) =
558 f.hit_regions.iter().find(|h| h.id == focused_id)
559 {
560 if let Some(on_submit) = &hit.on_text_submit {
561 if let Some(state) =
562 self.textfield_states.get(&focused_id)
563 {
564 let text = state.borrow().text.clone();
565 on_submit(text);
566 self.request_redraw();
567 return; // don’t continue as button activation
568 }
569 }
570 }
571 }
572 }
573 }
574 }
575
576 if key_event.state == ElementState::Pressed {
577 // Inspector hotkey: Ctrl+Shift+I
578 if self.modifiers.ctrl && self.modifiers.shift {
579 if let PhysicalKey::Code(KeyCode::KeyI) = key_event.physical_key {
580 self.inspector.hud.toggle_inspector();
581 self.request_redraw();
582 return;
583 }
584 }
585
586 // TextField navigation/edit
587 if let Some(focused_id) = self.sched.focused {
588 if let Some(state) = self.textfield_states.get(&focused_id) {
589 let mut state = state.borrow_mut();
590 match key_event.physical_key {
591 PhysicalKey::Code(KeyCode::Backspace) => {
592 state.delete_backward();
593 let new_text = state.text.clone();
594 self.notify_text_change(focused_id, new_text);
595 App::tf_ensure_caret_visible(&mut state);
596 self.request_redraw();
597 }
598 PhysicalKey::Code(KeyCode::Delete) => {
599 state.delete_forward();
600 let new_text = state.text.clone();
601 self.notify_text_change(focused_id, new_text);
602 App::tf_ensure_caret_visible(&mut state);
603 self.request_redraw();
604 }
605 PhysicalKey::Code(KeyCode::ArrowLeft) => {
606 state.move_cursor(-1, self.modifiers.shift);
607 App::tf_ensure_caret_visible(&mut state);
608 self.request_redraw();
609 }
610 PhysicalKey::Code(KeyCode::ArrowRight) => {
611 state.move_cursor(1, self.modifiers.shift);
612 App::tf_ensure_caret_visible(&mut state);
613 self.request_redraw();
614 }
615 PhysicalKey::Code(KeyCode::Home) => {
616 state.selection = 0..0;
617 App::tf_ensure_caret_visible(&mut state);
618 self.request_redraw();
619 }
620 PhysicalKey::Code(KeyCode::End) => {
621 {
622 let end = state.text.len();
623 state.selection = end..end;
624 }
625 App::tf_ensure_caret_visible(&mut state);
626 self.request_redraw();
627 }
628 PhysicalKey::Code(KeyCode::KeyA) if self.modifiers.ctrl => {
629 state.selection = 0..state.text.len();
630 App::tf_ensure_caret_visible(&mut state);
631 self.request_redraw();
632 }
633 _ => {}
634 }
635 }
636 if self.modifiers.ctrl {
637 match key_event.physical_key {
638 PhysicalKey::Code(KeyCode::KeyC) => {
639 if let Some(fid) = self.sched.focused {
640 if let Some(state) = self.textfield_states.get(&fid) {
641 let txt = state.borrow().selected_text();
642 if !txt.is_empty() {
643 if let Some(cb) = self.clipboard.as_mut() {
644 let _ = cb.set_text(txt);
645 }
646 }
647 }
648 }
649 return;
650 }
651 PhysicalKey::Code(KeyCode::KeyX) => {
652 if let Some(fid) = self.sched.focused {
653 if let Some(state_rc) = self.textfield_states.get(&fid)
654 {
655 // Copy
656 let txt = state_rc.borrow().selected_text();
657 if !txt.is_empty() {
658 if let Some(cb) = self.clipboard.as_mut() {
659 let _ = cb.set_text(txt.clone());
660 }
661 // Cut (delete selection)
662 {
663 let mut st = state_rc.borrow_mut();
664 st.insert_text(""); // replace selection with empty
665 let new_text = st.text.clone();
666 self.notify_text_change(
667 focused_id, new_text,
668 );
669 App::tf_ensure_caret_visible(&mut st);
670 }
671 self.request_redraw();
672 }
673 }
674 }
675 return;
676 }
677 PhysicalKey::Code(KeyCode::KeyV) => {
678 if let Some(fid) = self.sched.focused {
679 if let Some(state_rc) = self.textfield_states.get(&fid)
680 {
681 if let Some(cb) = self.clipboard.as_mut() {
682 if let Ok(mut txt) = cb.get_text() {
683 // Single-line TextField: strip control/newlines
684 txt.retain(|c| {
685 !c.is_control()
686 && c != '\n'
687 && c != '\r'
688 });
689 if !txt.is_empty() {
690 let mut st = state_rc.borrow_mut();
691 st.insert_text(&txt);
692 let new_text = st.text.clone();
693 self.notify_text_change(
694 focused_id, new_text,
695 );
696 App::tf_ensure_caret_visible(&mut st);
697 self.request_redraw();
698 }
699 }
700 }
701 }
702 }
703 return;
704 }
705 _ => {}
706 }
707 }
708 }
709
710 // Plain text input when IME is not active
711 if !self.ime_preedit
712 && !self.modifiers.ctrl
713 && !self.modifiers.alt
714 && !self.modifiers.meta
715 {
716 if let Some(raw) = key_event.text.as_deref() {
717 let text: String = raw
718 .chars()
719 .filter(|c| !c.is_control() && *c != '\n' && *c != '\r')
720 .collect();
721 if !text.is_empty() {
722 if let Some(fid) = self.sched.focused {
723 if let Some(state_rc) = self.textfield_states.get(&fid) {
724 let mut st = state_rc.borrow_mut();
725 st.insert_text(&text);
726 self.notify_text_change(fid, text.clone());
727 App::tf_ensure_caret_visible(&mut st);
728 self.request_redraw();
729 }
730 }
731 }
732 }
733 }
734 } else if key_event.state == ElementState::Released {
735 // Finish keyboard activation on release (Space/Enter)
736 if let Some(active_id) = self.key_pressed_active {
737 match key_event.physical_key {
738 PhysicalKey::Code(KeyCode::Space)
739 | PhysicalKey::Code(KeyCode::Enter) => {
740 self.pressed_ids.remove(&active_id);
741 self.key_pressed_active = None;
742
743 if let Some(f) = &self.frame_cache {
744 if let Some(hit) =
745 f.hit_regions.iter().find(|h| h.id == active_id)
746 {
747 if let Some(cb) = &hit.on_click {
748 cb();
749 if let Some(node) = f
750 .semantics_nodes
751 .iter()
752 .find(|n| n.id == active_id)
753 {
754 let label = node.label.as_deref().unwrap_or("");
755 self.a11y
756 .announce(&format!("Activated {}", label));
757 }
758 }
759 }
760 }
761 self.request_redraw();
762 return;
763 }
764 _ => {}
765 }
766 }
767 }
768 }
769
770 WindowEvent::Ime(ime) => {
771 use winit::event::Ime;
772 if let Some(focused_id) = self.sched.focused {
773 if let Some(state) = self.textfield_states.get(&focused_id) {
774 let mut state = state.borrow_mut();
775 match ime {
776 Ime::Enabled => {
777 // IME allowed, but not necessarily composing
778 self.ime_preedit = false;
779 }
780 Ime::Preedit(text, cursor) => {
781 let cursor_usize =
782 cursor.map(|(a, b)| (a as usize, b as usize));
783 state.set_composition(text.clone(), cursor_usize);
784 self.ime_preedit = !text.is_empty();
785 App::tf_ensure_caret_visible(&mut state);
786 // notify on-change if you wired it:
787 self.notify_text_change(focused_id, state.text.clone());
788 self.request_redraw();
789 }
790 Ime::Commit(text) => {
791 state.commit_composition(text);
792 self.ime_preedit = false;
793 App::tf_ensure_caret_visible(&mut state);
794 self.notify_text_change(focused_id, state.text.clone());
795 self.request_redraw();
796 }
797 Ime::Disabled => {
798 self.ime_preedit = false;
799 if state.composition.is_some() {
800 state.cancel_composition();
801 App::tf_ensure_caret_visible(&mut state);
802 self.notify_text_change(focused_id, state.text.clone());
803 }
804 self.request_redraw();
805 }
806 }
807 }
808 }
809 }
810 WindowEvent::RedrawRequested => {
811 if let (Some(backend), Some(_win)) =
812 (self.backend.as_mut(), self.window.as_ref())
813 {
814 let t0 = Instant::now();
815 // Compose
816 let focused = self.sched.focused;
817 let hover_id = self.hover_id;
818 let pressed_ids = self.pressed_ids.clone();
819 let tf_states = &self.textfield_states;
820
821 let frame = self.sched.repose(&mut self.root, move |view, size| {
822 let interactions = repose_ui::Interactions {
823 hover: hover_id,
824 pressed: pressed_ids.clone(),
825 };
826
827 layout_and_paint(view, size, tf_states, &interactions, focused)
828 });
829
830 let build_layout_ms = (Instant::now() - t0).as_secs_f32() * 1000.0;
831
832 // A11y: publish semantics tree each frame (cheap for now)
833 self.a11y.publish_tree(&frame.semantics_nodes);
834 // If focus id changed since last publish, send focused node
835 if self.last_focus != self.sched.focused {
836 let focused_node = self
837 .sched
838 .focused
839 .and_then(|id| frame.semantics_nodes.iter().find(|n| n.id == id));
840 self.a11y.focus_changed(focused_node);
841 self.last_focus = self.sched.focused;
842 }
843
844 // Render
845 let mut scene = frame.scene.clone();
846 // Update HUD metrics before overlay draws
847 self.inspector.hud.metrics = Some(repose_devtools::Metrics {
848 build_layout_ms,
849 scene_nodes: scene.nodes.len(),
850 });
851 self.inspector.frame(&mut scene);
852 backend.frame(&scene, GlyphRasterConfig { px: 18.0 });
853 self.frame_cache = Some(frame);
854 }
855 }
856 _ => {}
857 }
858 }
859
860 fn about_to_wait(&mut self, _el: &winit::event_loop::ActiveEventLoop) {
861 self.request_redraw();
862 }
863
864 fn new_events(
865 &mut self,
866 _: &winit::event_loop::ActiveEventLoop,
867 _: winit::event::StartCause,
868 ) {
869 }
870 fn user_event(&mut self, _: &winit::event_loop::ActiveEventLoop, _: ()) {}
871 fn device_event(
872 &mut self,
873 _: &winit::event_loop::ActiveEventLoop,
874 _: winit::event::DeviceId,
875 _: winit::event::DeviceEvent,
876 ) {
877 }
878 fn suspended(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
879 fn exiting(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
880 fn memory_warning(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
881 }
882
883 impl App {
884 fn announce_focus_change(&mut self) {
885 if let Some(f) = &self.frame_cache {
886 let focused_node = self
887 .sched
888 .focused
889 .and_then(|id| f.semantics_nodes.iter().find(|n| n.id == id));
890 self.a11y.focus_changed(focused_node);
891 }
892 }
893 fn notify_text_change(&self, id: u64, text: String) {
894 if let Some(f) = &self.frame_cache {
895 if let Some(h) = f.hit_regions.iter().find(|h| h.id == id) {
896 if let Some(cb) = &h.on_text_change {
897 cb(text);
898 }
899 }
900 }
901 }
902 }
903
904 let event_loop = EventLoop::new()?;
905 let mut app = App::new(Box::new(root));
906 // Install system clock once
907 repose_core::animation::set_clock(Box::new(repose_core::animation::SystemClock));
908 event_loop.run_app(&mut app)?;
909 Ok(())
910}
911
912// #[cfg(feature = "android")]
913// pub mod android {
914// use super::*;
915// use std::rc::Rc;
916// use std::sync::Arc;
917// use winit::application::ApplicationHandler;
918// use winit::dpi::PhysicalSize;
919// use winit::event::{ElementState, MouseScrollDelta, WindowEvent};
920// use winit::event_loop::EventLoop;
921// use winit::keyboard::{KeyCode, PhysicalKey};
922// use winit::platform::android::activity::AndroidApp;
923// use winit::window::{ImePurpose, Window, WindowAttributes};
924
925// pub fn run_android_app(
926// app: AndroidApp,
927// mut root: impl FnMut(&mut Scheduler) -> View + 'static,
928// ) -> anyhow::Result<()> {
929// repose_core::animation::set_clock(Box::new(repose_core::animation::SystemClock));
930// let event_loop = winit::event_loop::EventLoopBuilder::new()
931// .with_android_app(app)
932// .build()?;
933
934// struct A {
935// root: Box<dyn FnMut(&mut Scheduler) -> View>,
936// window: Option<Arc<Window>>,
937// backend: Option<repose_render_wgpu::WgpuBackend>,
938// sched: Scheduler,
939// inspector: repose_devtools::Inspector,
940// frame_cache: Option<Frame>,
941// mouse_pos: (f32, f32),
942// modifiers: Modifiers,
943// textfield_states: HashMap<u64, Rc<std::cell::RefCell<TextFieldState>>>,
944// ime_preedit: bool,
945// hover_id: Option<u64>,
946// capture_id: Option<u64>,
947// pressed_ids: HashSet<u64>,
948// key_pressed_active: Option<u64>,
949// last_scale: f64,
950// }
951// impl A {
952// fn new(root: Box<dyn FnMut(&mut Scheduler) -> View>) -> Self {
953// Self {
954// root,
955// window: None,
956// backend: None,
957// sched: Scheduler::new(),
958// inspector: repose_devtools::Inspector::new(),
959// frame_cache: None,
960// mouse_pos: (0.0, 0.0),
961// modifiers: Modifiers::default(),
962// textfield_states: HashMap::new(),
963// ime_preedit: false,
964// hover_id: None,
965// capture_id: None,
966// pressed_ids: HashSet::new(),
967// key_pressed_active: None,
968// last_scale: 1.0,
969// }
970// }
971// fn request_redraw(&self) {
972// if let Some(w) = &self.window {
973// w.request_redraw();
974// }
975// }
976// }
977// impl ApplicationHandler<()> for A {
978// fn resumed(&mut self, el: &winit::event_loop::ActiveEventLoop) {
979// if self.window.is_none() {
980// match el.create_window(WindowAttributes::default().with_title("Repose android")) {
981// Ok(win) => {
982// let w = Arc::new(win);
983// let size = w.inner_size();
984// self.sched.size = (size.width, size.height);
985// self.last_scale = w.scale_factor();
986// match repose_render_wgpu::WgpuBackend::new(w.clone()) {
987// Ok(b) => {
988// self.backend = Some(b);
989// self.window = Some(w);
990// self.request_redraw();
991// }
992// Err(e) => {
993// log::error!("WGPU backend init failed: {e:?}");
994// el.exit();
995// }
996// }
997// }
998// Err(e) => {
999// log::error!("Window create failed: {e:?}");
1000// el.exit();
1001// }
1002// }
1003// }
1004// }
1005// fn window_event(
1006// &mut self,
1007// el: &winit::event_loop::ActiveEventLoop,
1008// _id: winit::window::WindowId,
1009// event: WindowEvent,
1010// ) {
1011// match event {
1012// WindowEvent::Ime(ime) => {
1013// use winit::event::Ime;
1014// if let Some(focused_id) = self.sched.focused {
1015// if let Some(state) = self.textfield_states.get(&focused_id) {
1016// let mut state = state.borrow_mut();
1017// match ime {
1018// Ime::Enabled => {
1019// self.ime_preedit = false;
1020// }
1021// Ime::Preedit(text, cursor) => {
1022// state.set_composition(text.clone(), cursor);
1023// self.ime_preedit = !text.is_empty();
1024// self.request_redraw();
1025// }
1026// Ime::Commit(text) => {
1027// state.commit_composition(text);
1028// self.ime_preedit = false;
1029// self.request_redraw();
1030// }
1031// Ime::Disabled => {
1032// self.ime_preedit = false;
1033// if state.composition.is_some() {
1034// state.cancel_composition();
1035// }
1036// self.request_redraw();
1037// }
1038// }
1039// }
1040// }
1041// }
1042// WindowEvent::CloseRequested => el.exit(),
1043// WindowEvent::Resized(size) => {
1044// self.sched.size = (size.width, size.height);
1045// if let Some(b) = &mut self.backend {
1046// b.configure_surface(size.width, size.height);
1047// }
1048// self.request_redraw();
1049// }
1050// WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
1051// self.last_scale = scale_factor;
1052// self.request_redraw();
1053// }
1054// WindowEvent::CursorMoved { position, .. } => {
1055// self.mouse_pos = (position.x as f32, position.y as f32);
1056// // hover/move same as desktop (omitted for brevity; reuse desktop branch)
1057// if let Some(f) = &self.frame_cache {
1058// let pos = Vec2 {
1059// x: self.mouse_pos.0,
1060// y: self.mouse_pos.1,
1061// };
1062// let top = f
1063// .hit_regions
1064// .iter()
1065// .rev()
1066// .find(|h| h.rect.contains(pos))
1067// .cloned();
1068// let new_hover = top.as_ref().map(|h| h.id);
1069// if new_hover != self.hover_id {
1070// if let Some(prev_id) = self.hover_id {
1071// if let Some(prev) =
1072// f.hit_regions.iter().find(|h| h.id == prev_id)
1073// {
1074// if let Some(cb) = &prev.on_pointer_leave {
1075// cb(repose_core::input::PointerEvent {
1076// id: repose_core::input::PointerId(0),
1077// kind: repose_core::input::PointerKind::Touch,
1078// event: repose_core::input::PointerEventKind::Leave,
1079// position: pos,
1080// pressure: 1.0,
1081// modifiers: self.modifiers,
1082// });
1083// }
1084// }
1085// }
1086// if let Some(h) = &top {
1087// if let Some(cb) = &h.on_pointer_enter {
1088// cb(repose_core::input::PointerEvent {
1089// id: repose_core::input::PointerId(0),
1090// kind: repose_core::input::PointerKind::Touch,
1091// event: repose_core::input::PointerEventKind::Enter,
1092// position: pos,
1093// pressure: 1.0,
1094// modifiers: self.modifiers,
1095// });
1096// }
1097// }
1098// self.hover_id = new_hover;
1099// }
1100// let pe = repose_core::input::PointerEvent {
1101// id: repose_core::input::PointerId(0),
1102// kind: repose_core::input::PointerKind::Touch,
1103// event: repose_core::input::PointerEventKind::Move,
1104// position: pos,
1105// pressure: 1.0,
1106// modifiers: self.modifiers,
1107// };
1108// if let Some(cid) = self.capture_id {
1109// if let Some(h) = f.hit_regions.iter().find(|h| h.id == cid) {
1110// if let Some(cb) = &h.on_pointer_move {
1111// cb(pe.clone());
1112// }
1113// }
1114// } else if let Some(h) = top {
1115// if let Some(cb) = &h.on_pointer_move {
1116// cb(pe);
1117// }
1118// }
1119// }
1120// }
1121// WindowEvent::MouseInput {
1122// state,
1123// button: winit::event::MouseButton::Left,
1124// ..
1125// } => {
1126// if state == ElementState::Pressed {
1127// if let Some(f) = &self.frame_cache {
1128// let pos = Vec2 {
1129// x: self.mouse_pos.0,
1130// y: self.mouse_pos.1,
1131// };
1132// if let Some(hit) =
1133// f.hit_regions.iter().rev().find(|h| h.rect.contains(pos))
1134// {
1135// self.capture_id = Some(hit.id);
1136// self.pressed_ids.insert(hit.id);
1137// if hit.focusable {
1138// self.sched.focused = Some(hit.id);
1139// if let Some(win) = &self.window {
1140// win.set_ime_allowed(true);
1141// win.set_ime_purpose(ImePurpose::Normal);
1142// }
1143// }
1144// if let Some(cb) = &hit.on_pointer_down {
1145// cb(repose_core::input::PointerEvent {
1146// id: repose_core::input::PointerId(0),
1147// kind: repose_core::input::PointerKind::Touch,
1148// event: repose_core::input::PointerEventKind::Down(
1149// repose_core::input::PointerButton::Primary,
1150// ),
1151// position: pos,
1152// pressure: 1.0,
1153// modifiers: self.modifiers,
1154// });
1155// }
1156// self.request_redraw();
1157// }
1158// }
1159// } else {
1160// if let (Some(f), Some(cid)) = (&self.frame_cache, self.capture_id) {
1161// self.pressed_ids.remove(&cid);
1162// let pos = Vec2 {
1163// x: self.mouse_pos.0,
1164// y: self.mouse_pos.1,
1165// };
1166// if let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid) {
1167// if hit.rect.contains(pos) {
1168// if let Some(cb) = &hit.on_click {
1169// cb();
1170// }
1171// }
1172// }
1173// }
1174// self.capture_id = None;
1175// self.request_redraw();
1176// }
1177// }
1178// WindowEvent::MouseWheel { delta, .. } => {
1179// let mut dy = match delta {
1180// MouseScrollDelta::LineDelta(_x, y) => -y * 40.0,
1181// MouseScrollDelta::PixelDelta(lp) => -(lp.y as f32),
1182// };
1183// if let Some(f) = &self.frame_cache {
1184// let pos = Vec2 {
1185// x: self.mouse_pos.0,
1186// y: self.mouse_pos.1,
1187// };
1188// for hit in f.hit_regions.iter().rev().filter(|h| h.rect.contains(pos)) {
1189// if let Some(cb) = &hit.on_scroll {
1190// dy = cb(dy);
1191// if dy.abs() <= 0.001 {
1192// break;
1193// }
1194// }
1195// }
1196// self.request_redraw();
1197// }
1198// }
1199// WindowEvent::RedrawRequested => {
1200// if let (Some(backend), Some(win)) =
1201// (self.backend.as_mut(), self.window.as_ref())
1202// {
1203// let scale = win.scale_factor();
1204// self.last_scale = scale;
1205// let t0 = Instant::now();
1206// let frame = self.sched.repose(&mut self.root, |view, size| {
1207// let interactions = repose_ui::Interactions {
1208// hover: self.hover_id,
1209// pressed: self.pressed_ids.clone(),
1210// };
1211// // Density from scale factor (Android DPI / 160 roughly equals scale)
1212// with_density(
1213// Density {
1214// scale: scale as f32,
1215// },
1216// || {
1217// layout_and_paint(
1218// view,
1219// size,
1220// &self.textfield_states,
1221// &interactions,
1222// self.sched.focused,
1223// )
1224// },
1225// )
1226// });
1227// let build_layout_ms = (Instant::now() - t0).as_secs_f32() * 1000.0;
1228// let mut scene = frame.scene.clone();
1229// // HUD (opt-in via inspector hotkey; on Android you can toggle via programmatic flag later)
1230// super::App::new(Box::new(|_| View::new(0, ViewKind::Surface))); // no-op; placeholder to keep structure similar
1231// backend.frame(&scene, GlyphRasterConfig { px: 18.0 });
1232// self.frame_cache = Some(frame);
1233// }
1234// }
1235
1236// _ => {}
1237// }
1238// }
1239// }
1240// let mut app_state = A::new(Box::new(root));
1241// event_loop.run_app(&mut app_state)?;
1242// Ok(())
1243// }
1244// }
1245
1246// Accessibility bridge stub (Noop by default; logs on Linux for now)
1247pub trait A11yBridge: Send {
1248 fn publish_tree(&mut self, nodes: &[repose_core::runtime::SemNode]);
1249 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>);
1250 fn announce(&mut self, msg: &str);
1251}
1252
1253struct NoopA11y;
1254impl A11yBridge for NoopA11y {
1255 fn publish_tree(&mut self, _nodes: &[repose_core::runtime::SemNode]) {
1256 // no-op
1257 }
1258 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>) {
1259 if let Some(n) = node {
1260 log::info!("A11y focus: {:?} {:?}", n.role, n.label);
1261 } else {
1262 log::info!("A11y focus: None");
1263 }
1264 }
1265 fn announce(&mut self, msg: &str) {
1266 log::info!("A11y announce: {msg}");
1267 }
1268}
1269
1270#[cfg(target_os = "linux")]
1271struct LinuxAtspiStub;
1272#[cfg(target_os = "linux")]
1273impl A11yBridge for LinuxAtspiStub {
1274 fn publish_tree(&mut self, nodes: &[repose_core::runtime::SemNode]) {
1275 log::debug!("AT-SPI stub: publish {} nodes", nodes.len());
1276 }
1277 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>) {
1278 if let Some(n) = node {
1279 log::info!("AT-SPI stub focus: {:?} {:?}", n.role, n.label);
1280 } else {
1281 log::info!("AT-SPI stub focus: None");
1282 }
1283 }
1284 fn announce(&mut self, msg: &str) {
1285 log::info!("AT-SPI stub announce: {msg}");
1286 }
1287}