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 mut dy = match delta {
431                        MouseScrollDelta::LineDelta(_x, y) => -y * 40.0,
432                        MouseScrollDelta::PixelDelta(lp) => -(lp.y as f32),
433                    };
434
435                    if let Some(f) = &self.frame_cache {
436                        let pos = Vec2 {
437                            x: self.mouse_pos.0,
438                            y: self.mouse_pos.1,
439                        };
440                        // Nested routing: from topmost to deeper ancestors under cursor
441                        let mut consumed_any = false;
442                        for hit in f.hit_regions.iter().rev().filter(|h| h.rect.contains(pos)) {
443                            if let Some(cb) = &hit.on_scroll {
444                                let before = dy;
445                                dy = cb(dy); // returns leftover
446                                if (before - dy).abs() > 0.001 {
447                                    consumed_any = true;
448                                }
449                                if dy.abs() <= 0.001 {
450                                    break;
451                                }
452                            }
453                        }
454                        if consumed_any {
455                            self.request_redraw();
456                        }
457                    }
458                }
459                WindowEvent::ModifiersChanged(new_mods) => {
460                    self.modifiers.shift = new_mods.state().shift_key();
461                    self.modifiers.ctrl = new_mods.state().control_key();
462                    self.modifiers.alt = new_mods.state().alt_key();
463                    self.modifiers.meta = new_mods.state().super_key();
464                }
465                WindowEvent::KeyboardInput {
466                    event: key_event, ..
467                } => {
468                    // Focus traversal: Tab / Shift+Tab
469                    if matches!(
470                        key_event.physical_key,
471                        winit::keyboard::PhysicalKey::Code(winit::keyboard::KeyCode::Tab)
472                    ) {
473                        if let Some(f) = &self.frame_cache {
474                            let chain = &f.focus_chain;
475                            if !chain.is_empty() {
476                                let shift = self.modifiers.shift;
477                                let current = self.sched.focused;
478                                let next = if let Some(cur) = current {
479                                    if let Some(idx) = chain.iter().position(|&id| id == cur) {
480                                        if shift {
481                                            if idx == 0 {
482                                                chain[chain.len() - 1]
483                                            } else {
484                                                chain[idx - 1]
485                                            }
486                                        } else {
487                                            chain[(idx + 1) % chain.len()]
488                                        }
489                                    } else {
490                                        chain[0]
491                                    }
492                                } else {
493                                    chain[0]
494                                };
495                                self.sched.focused = Some(next);
496                                // IME on TextField focus; off otherwise
497                                if let Some(win) = &self.window {
498                                    if f.semantics_nodes
499                                        .iter()
500                                        .any(|n| n.id == next && n.role == Role::TextField)
501                                    {
502                                        win.set_ime_allowed(true);
503                                        win.set_ime_purpose(winit::window::ImePurpose::Normal);
504                                    } else {
505                                        win.set_ime_allowed(false);
506                                    }
507                                }
508                                self.announce_focus_change();
509                                self.request_redraw();
510                            }
511                        }
512                        return;
513                    }
514
515                    // Keyboard activation for focused widgets (Space/Enter)
516                    if let Some(fid) = self.sched.focused {
517                        match key_event.physical_key {
518                            winit::keyboard::PhysicalKey::Code(winit::keyboard::KeyCode::Space)
519                            | winit::keyboard::PhysicalKey::Code(winit::keyboard::KeyCode::Enter) =>
520                            {
521                                // pressed visual and remember which to release
522                                self.pressed_ids.insert(fid);
523                                self.key_pressed_active = Some(fid);
524                                self.request_redraw();
525                                return; // don't fall through to text input path
526                            }
527                            _ => {}
528                        }
529                    }
530
531                    if key_event.state == ElementState::Pressed {
532                        // Inspector hotkey: Ctrl+Shift+I
533                        if self.modifiers.ctrl && self.modifiers.shift {
534                            if let PhysicalKey::Code(KeyCode::KeyI) = key_event.physical_key {
535                                self.inspector.hud.toggle_inspector();
536                                self.request_redraw();
537                                return;
538                            }
539                        }
540
541                        // TextField navigation/edit
542                        if let Some(focused_id) = self.sched.focused {
543                            if let Some(state) = self.textfield_states.get(&focused_id) {
544                                let mut state = state.borrow_mut();
545                                match key_event.physical_key {
546                                    PhysicalKey::Code(KeyCode::Backspace) => {
547                                        state.delete_backward();
548                                        let new_text = state.text.clone();
549                                        self.notify_text_change(focused_id, new_text);
550                                        App::tf_ensure_caret_visible(&mut state);
551                                        self.request_redraw();
552                                    }
553                                    PhysicalKey::Code(KeyCode::Delete) => {
554                                        state.delete_forward();
555                                        let new_text = state.text.clone();
556                                        self.notify_text_change(focused_id, new_text);
557                                        App::tf_ensure_caret_visible(&mut state);
558                                        self.request_redraw();
559                                    }
560                                    PhysicalKey::Code(KeyCode::ArrowLeft) => {
561                                        state.move_cursor(-1, self.modifiers.shift);
562                                        App::tf_ensure_caret_visible(&mut state);
563                                        self.request_redraw();
564                                    }
565                                    PhysicalKey::Code(KeyCode::ArrowRight) => {
566                                        state.move_cursor(1, self.modifiers.shift);
567                                        App::tf_ensure_caret_visible(&mut state);
568                                        self.request_redraw();
569                                    }
570                                    PhysicalKey::Code(KeyCode::Home) => {
571                                        state.selection = 0..0;
572                                        App::tf_ensure_caret_visible(&mut state);
573                                        self.request_redraw();
574                                    }
575                                    PhysicalKey::Code(KeyCode::End) => {
576                                        {
577                                            let end = state.text.len();
578                                            state.selection = end..end;
579                                        }
580                                        App::tf_ensure_caret_visible(&mut state);
581                                        self.request_redraw();
582                                    }
583                                    PhysicalKey::Code(KeyCode::KeyA) if self.modifiers.ctrl => {
584                                        state.selection = 0..state.text.len();
585                                        App::tf_ensure_caret_visible(&mut state);
586                                        self.request_redraw();
587                                    }
588                                    _ => {}
589                                }
590                            }
591                            if self.modifiers.ctrl {
592                                match key_event.physical_key {
593                                    PhysicalKey::Code(KeyCode::KeyC) => {
594                                        if let Some(fid) = self.sched.focused {
595                                            if let Some(state) = self.textfield_states.get(&fid) {
596                                                let txt = state.borrow().selected_text();
597                                                if !txt.is_empty() {
598                                                    if let Some(cb) = self.clipboard.as_mut() {
599                                                        let _ = cb.set_text(txt);
600                                                    }
601                                                }
602                                            }
603                                        }
604                                        return;
605                                    }
606                                    PhysicalKey::Code(KeyCode::KeyX) => {
607                                        if let Some(fid) = self.sched.focused {
608                                            if let Some(state_rc) = self.textfield_states.get(&fid)
609                                            {
610                                                // Copy
611                                                let txt = state_rc.borrow().selected_text();
612                                                if !txt.is_empty() {
613                                                    if let Some(cb) = self.clipboard.as_mut() {
614                                                        let _ = cb.set_text(txt.clone());
615                                                    }
616                                                    // Cut (delete selection)
617                                                    {
618                                                        let mut st = state_rc.borrow_mut();
619                                                        st.insert_text(""); // replace selection with empty
620                                                        let new_text = st.text.clone();
621                                                        self.notify_text_change(
622                                                            focused_id, new_text,
623                                                        );
624                                                        App::tf_ensure_caret_visible(&mut st);
625                                                    }
626                                                    self.request_redraw();
627                                                }
628                                            }
629                                        }
630                                        return;
631                                    }
632                                    PhysicalKey::Code(KeyCode::KeyV) => {
633                                        if let Some(fid) = self.sched.focused {
634                                            if let Some(state_rc) = self.textfield_states.get(&fid)
635                                            {
636                                                if let Some(cb) = self.clipboard.as_mut() {
637                                                    if let Ok(mut txt) = cb.get_text() {
638                                                        // Single-line TextField: strip control/newlines
639                                                        txt.retain(|c| {
640                                                            !c.is_control()
641                                                                && c != '\n'
642                                                                && c != '\r'
643                                                        });
644                                                        if !txt.is_empty() {
645                                                            let mut st = state_rc.borrow_mut();
646                                                            st.insert_text(&txt);
647                                                            let new_text = st.text.clone();
648                                                            self.notify_text_change(
649                                                                focused_id, new_text,
650                                                            );
651                                                            App::tf_ensure_caret_visible(&mut st);
652                                                            self.request_redraw();
653                                                        }
654                                                    }
655                                                }
656                                            }
657                                        }
658                                        return;
659                                    }
660                                    _ => {}
661                                }
662                            }
663                        }
664
665                        // Plain text input when IME is not active
666                        if !self.ime_preedit
667                            && !self.modifiers.ctrl
668                            && !self.modifiers.alt
669                            && !self.modifiers.meta
670                        {
671                            if let Some(raw) = key_event.text.as_deref() {
672                                let text: String = raw
673                                    .chars()
674                                    .filter(|c| !c.is_control() && *c != '\n' && *c != '\r')
675                                    .collect();
676                                if !text.is_empty() {
677                                    if let Some(fid) = self.sched.focused {
678                                        if let Some(state_rc) = self.textfield_states.get(&fid) {
679                                            let mut st = state_rc.borrow_mut();
680                                            st.insert_text(&text);
681                                            self.notify_text_change(fid, text.clone());
682                                            App::tf_ensure_caret_visible(&mut st);
683                                            self.request_redraw();
684                                        }
685                                    }
686                                }
687                            }
688                        }
689                    } else if key_event.state == ElementState::Released {
690                        // Finish keyboard activation on release (Space/Enter)
691                        if let Some(active) = self.key_pressed_active.take() {
692                            if matches!(
693                                key_event.physical_key,
694                                winit::keyboard::PhysicalKey::Code(winit::keyboard::KeyCode::Space)
695                                    | winit::keyboard::PhysicalKey::Code(
696                                        winit::keyboard::KeyCode::Enter
697                                    )
698                            ) {
699                                self.pressed_ids.remove(&active);
700                                // Fire on_click if the focused item has it
701                                if let Some(f) = &self.frame_cache {
702                                    if let Some(h) = f.hit_regions.iter().find(|h| h.id == active) {
703                                        if let Some(cb) = &h.on_click {
704                                            cb();
705                                            // A11y: announce activation
706                                            if let Some(node) =
707                                                f.semantics_nodes.iter().find(|n| n.id == active)
708                                            {
709                                                let label = node.label.as_deref().unwrap_or("");
710                                                self.a11y.announce(&format!("Activated {}", label));
711                                            }
712                                        }
713                                    }
714                                }
715                                self.request_redraw();
716                                return;
717                            }
718                        }
719                    }
720                }
721
722                WindowEvent::Ime(ime) => {
723                    use winit::event::Ime;
724
725                    if let Some(focused_id) = self.sched.focused {
726                        if let Some(state) = self.textfield_states.get(&focused_id) {
727                            let mut state = state.borrow_mut();
728                            match ime {
729                                Ime::Enabled => {
730                                    // IME allowed, but not necessarily composing
731                                    self.ime_preedit = false;
732                                }
733                                Ime::Preedit(text, cursor) => {
734                                    {
735                                        state.set_composition(text.clone(), cursor);
736                                    }
737                                    self.ime_preedit = !text.is_empty();
738                                    let new_text = state.text.clone();
739                                    self.notify_text_change(focused_id, new_text);
740                                    App::tf_ensure_caret_visible(&mut state);
741                                    self.request_redraw();
742                                }
743                                Ime::Commit(text) => {
744                                    {
745                                        state.commit_composition(text);
746                                    }
747                                    self.ime_preedit = false;
748                                    App::tf_ensure_caret_visible(&mut state);
749                                    let new_text = state.text.clone();
750                                    self.notify_text_change(focused_id, new_text);
751                                    self.request_redraw();
752                                }
753                                Ime::Disabled => {
754                                    self.ime_preedit = false;
755                                    if state.composition.is_some() {
756                                        {
757                                            state.cancel_composition();
758                                            let new_text = state.text.clone();
759                                            self.notify_text_change(focused_id, new_text);
760                                        }
761                                        App::tf_ensure_caret_visible(&mut state);
762                                    }
763                                    self.request_redraw();
764                                }
765                            }
766                        }
767                    }
768                }
769                WindowEvent::RedrawRequested => {
770                    if let (Some(backend), Some(_win)) =
771                        (self.backend.as_mut(), self.window.as_ref())
772                    {
773                        let t0 = Instant::now();
774                        // Compose
775                        let focused = self.sched.focused;
776                        let hover_id = self.hover_id;
777                        let pressed_ids = self.pressed_ids.clone();
778                        let tf_states = &self.textfield_states;
779
780                        let frame = self.sched.repose(&mut self.root, move |view, size| {
781                            let interactions = repose_ui::Interactions {
782                                hover: hover_id,
783                                pressed: pressed_ids.clone(),
784                            };
785
786                            layout_and_paint(view, size, tf_states, &interactions, focused)
787                        });
788
789                        let build_layout_ms = (Instant::now() - t0).as_secs_f32() * 1000.0;
790
791                        // A11y: publish semantics tree each frame (cheap for now)
792                        self.a11y.publish_tree(&frame.semantics_nodes);
793                        // If focus id changed since last publish, send focused node
794                        if self.last_focus != self.sched.focused {
795                            let focused_node = self
796                                .sched
797                                .focused
798                                .and_then(|id| frame.semantics_nodes.iter().find(|n| n.id == id));
799                            self.a11y.focus_changed(focused_node);
800                            self.last_focus = self.sched.focused;
801                        }
802
803                        // Render
804                        let mut scene = frame.scene.clone();
805                        // Update HUD metrics before overlay draws
806                        self.inspector.hud.metrics = Some(repose_devtools::Metrics {
807                            build_layout_ms,
808                            scene_nodes: scene.nodes.len(),
809                        });
810                        self.inspector.frame(&mut scene);
811                        backend.frame(&scene, GlyphRasterConfig { px: 18.0 });
812                        self.frame_cache = Some(frame);
813                    }
814                }
815                _ => {}
816            }
817        }
818
819        fn about_to_wait(&mut self, _el: &winit::event_loop::ActiveEventLoop) {
820            self.request_redraw();
821        }
822
823        fn new_events(
824            &mut self,
825            _: &winit::event_loop::ActiveEventLoop,
826            _: winit::event::StartCause,
827        ) {
828        }
829        fn user_event(&mut self, _: &winit::event_loop::ActiveEventLoop, _: ()) {}
830        fn device_event(
831            &mut self,
832            _: &winit::event_loop::ActiveEventLoop,
833            _: winit::event::DeviceId,
834            _: winit::event::DeviceEvent,
835        ) {
836        }
837        fn suspended(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
838        fn exiting(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
839        fn memory_warning(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
840    }
841
842    impl App {
843        fn announce_focus_change(&mut self) {
844            if let Some(f) = &self.frame_cache {
845                let focused_node = self
846                    .sched
847                    .focused
848                    .and_then(|id| f.semantics_nodes.iter().find(|n| n.id == id));
849                self.a11y.focus_changed(focused_node);
850            }
851        }
852        fn notify_text_change(&self, id: u64, text: String) {
853            if let Some(f) = &self.frame_cache {
854                if let Some(h) = f.hit_regions.iter().find(|h| h.id == id) {
855                    if let Some(cb) = &h.on_text_change {
856                        cb(text);
857                    }
858                }
859            }
860        }
861    }
862
863    let event_loop = EventLoop::new()?;
864    let mut app = App::new(Box::new(root));
865    // Install system clock once
866    repose_core::animation::set_clock(Box::new(repose_core::animation::SystemClock));
867    event_loop.run_app(&mut app)?;
868    Ok(())
869}
870
871// #[cfg(feature = "android")]
872// pub mod android {
873//     use super::*;
874//     use std::rc::Rc;
875//     use std::sync::Arc;
876//     use winit::application::ApplicationHandler;
877//     use winit::dpi::PhysicalSize;
878//     use winit::event::{ElementState, MouseScrollDelta, WindowEvent};
879//     use winit::event_loop::EventLoop;
880//     use winit::keyboard::{KeyCode, PhysicalKey};
881//     use winit::platform::android::activity::AndroidApp;
882//     use winit::window::{ImePurpose, Window, WindowAttributes};
883
884//     pub fn run_android_app(
885//         app: AndroidApp,
886//         mut root: impl FnMut(&mut Scheduler) -> View + 'static,
887//     ) -> anyhow::Result<()> {
888//         repose_core::animation::set_clock(Box::new(repose_core::animation::SystemClock));
889//         let event_loop = winit::event_loop::EventLoopBuilder::new()
890//             .with_android_app(app)
891//             .build()?;
892
893//         struct A {
894//             root: Box<dyn FnMut(&mut Scheduler) -> View>,
895//             window: Option<Arc<Window>>,
896//             backend: Option<repose_render_wgpu::WgpuBackend>,
897//             sched: Scheduler,
898//             inspector: repose_devtools::Inspector,
899//             frame_cache: Option<Frame>,
900//             mouse_pos: (f32, f32),
901//             modifiers: Modifiers,
902//             textfield_states: HashMap<u64, Rc<std::cell::RefCell<TextFieldState>>>,
903//             ime_preedit: bool,
904//             hover_id: Option<u64>,
905//             capture_id: Option<u64>,
906//             pressed_ids: HashSet<u64>,
907//             key_pressed_active: Option<u64>,
908//             last_scale: f64,
909//         }
910//         impl A {
911//             fn new(root: Box<dyn FnMut(&mut Scheduler) -> View>) -> Self {
912//                 Self {
913//                     root,
914//                     window: None,
915//                     backend: None,
916//                     sched: Scheduler::new(),
917//                     inspector: repose_devtools::Inspector::new(),
918//                     frame_cache: None,
919//                     mouse_pos: (0.0, 0.0),
920//                     modifiers: Modifiers::default(),
921//                     textfield_states: HashMap::new(),
922//                     ime_preedit: false,
923//                     hover_id: None,
924//                     capture_id: None,
925//                     pressed_ids: HashSet::new(),
926//                     key_pressed_active: None,
927//                     last_scale: 1.0,
928//                 }
929//             }
930//             fn request_redraw(&self) {
931//                 if let Some(w) = &self.window {
932//                     w.request_redraw();
933//                 }
934//             }
935//         }
936//         impl ApplicationHandler<()> for A {
937//             fn resumed(&mut self, el: &winit::event_loop::ActiveEventLoop) {
938//                 if self.window.is_none() {
939//                     match el.create_window(WindowAttributes::default().with_title("Repose android")) {
940//                         Ok(win) => {
941//                             let w = Arc::new(win);
942//                             let size = w.inner_size();
943//                             self.sched.size = (size.width, size.height);
944//                             self.last_scale = w.scale_factor();
945//                             match repose_render_wgpu::WgpuBackend::new(w.clone()) {
946//                                 Ok(b) => {
947//                                     self.backend = Some(b);
948//                                     self.window = Some(w);
949//                                     self.request_redraw();
950//                                 }
951//                                 Err(e) => {
952//                                     log::error!("WGPU backend init failed: {e:?}");
953//                                     el.exit();
954//                                 }
955//                             }
956//                         }
957//                         Err(e) => {
958//                             log::error!("Window create failed: {e:?}");
959//                             el.exit();
960//                         }
961//                     }
962//                 }
963//             }
964//             fn window_event(
965//                 &mut self,
966//                 el: &winit::event_loop::ActiveEventLoop,
967//                 _id: winit::window::WindowId,
968//                 event: WindowEvent,
969//             ) {
970//                 match event {
971//                     WindowEvent::Ime(ime) => {
972//                         use winit::event::Ime;
973//                         if let Some(focused_id) = self.sched.focused {
974//                             if let Some(state) = self.textfield_states.get(&focused_id) {
975//                                 let mut state = state.borrow_mut();
976//                                 match ime {
977//                                     Ime::Enabled => {
978//                                         self.ime_preedit = false;
979//                                     }
980//                                     Ime::Preedit(text, cursor) => {
981//                                         state.set_composition(text.clone(), cursor);
982//                                         self.ime_preedit = !text.is_empty();
983//                                         self.request_redraw();
984//                                     }
985//                                     Ime::Commit(text) => {
986//                                         state.commit_composition(text);
987//                                         self.ime_preedit = false;
988//                                         self.request_redraw();
989//                                     }
990//                                     Ime::Disabled => {
991//                                         self.ime_preedit = false;
992//                                         if state.composition.is_some() {
993//                                             state.cancel_composition();
994//                                         }
995//                                         self.request_redraw();
996//                                     }
997//                                 }
998//                             }
999//                         }
1000//                     }
1001//                     WindowEvent::CloseRequested => el.exit(),
1002//                     WindowEvent::Resized(size) => {
1003//                         self.sched.size = (size.width, size.height);
1004//                         if let Some(b) = &mut self.backend {
1005//                             b.configure_surface(size.width, size.height);
1006//                         }
1007//                         self.request_redraw();
1008//                     }
1009//                     WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
1010//                         self.last_scale = scale_factor;
1011//                         self.request_redraw();
1012//                     }
1013//                     WindowEvent::CursorMoved { position, .. } => {
1014//                         self.mouse_pos = (position.x as f32, position.y as f32);
1015//                         // hover/move same as desktop (omitted for brevity; reuse desktop branch)
1016//                         if let Some(f) = &self.frame_cache {
1017//                             let pos = Vec2 {
1018//                                 x: self.mouse_pos.0,
1019//                                 y: self.mouse_pos.1,
1020//                             };
1021//                             let top = f
1022//                                 .hit_regions
1023//                                 .iter()
1024//                                 .rev()
1025//                                 .find(|h| h.rect.contains(pos))
1026//                                 .cloned();
1027//                             let new_hover = top.as_ref().map(|h| h.id);
1028//                             if new_hover != self.hover_id {
1029//                                 if let Some(prev_id) = self.hover_id {
1030//                                     if let Some(prev) =
1031//                                         f.hit_regions.iter().find(|h| h.id == prev_id)
1032//                                     {
1033//                                         if let Some(cb) = &prev.on_pointer_leave {
1034//                                             cb(repose_core::input::PointerEvent {
1035//                                                 id: repose_core::input::PointerId(0),
1036//                                                 kind: repose_core::input::PointerKind::Touch,
1037//                                                 event: repose_core::input::PointerEventKind::Leave,
1038//                                                 position: pos,
1039//                                                 pressure: 1.0,
1040//                                                 modifiers: self.modifiers,
1041//                                             });
1042//                                         }
1043//                                     }
1044//                                 }
1045//                                 if let Some(h) = &top {
1046//                                     if let Some(cb) = &h.on_pointer_enter {
1047//                                         cb(repose_core::input::PointerEvent {
1048//                                             id: repose_core::input::PointerId(0),
1049//                                             kind: repose_core::input::PointerKind::Touch,
1050//                                             event: repose_core::input::PointerEventKind::Enter,
1051//                                             position: pos,
1052//                                             pressure: 1.0,
1053//                                             modifiers: self.modifiers,
1054//                                         });
1055//                                     }
1056//                                 }
1057//                                 self.hover_id = new_hover;
1058//                             }
1059//                             let pe = repose_core::input::PointerEvent {
1060//                                 id: repose_core::input::PointerId(0),
1061//                                 kind: repose_core::input::PointerKind::Touch,
1062//                                 event: repose_core::input::PointerEventKind::Move,
1063//                                 position: pos,
1064//                                 pressure: 1.0,
1065//                                 modifiers: self.modifiers,
1066//                             };
1067//                             if let Some(cid) = self.capture_id {
1068//                                 if let Some(h) = f.hit_regions.iter().find(|h| h.id == cid) {
1069//                                     if let Some(cb) = &h.on_pointer_move {
1070//                                         cb(pe.clone());
1071//                                     }
1072//                                 }
1073//                             } else if let Some(h) = top {
1074//                                 if let Some(cb) = &h.on_pointer_move {
1075//                                     cb(pe);
1076//                                 }
1077//                             }
1078//                         }
1079//                     }
1080//                     WindowEvent::MouseInput {
1081//                         state,
1082//                         button: winit::event::MouseButton::Left,
1083//                         ..
1084//                     } => {
1085//                         if state == ElementState::Pressed {
1086//                             if let Some(f) = &self.frame_cache {
1087//                                 let pos = Vec2 {
1088//                                     x: self.mouse_pos.0,
1089//                                     y: self.mouse_pos.1,
1090//                                 };
1091//                                 if let Some(hit) =
1092//                                     f.hit_regions.iter().rev().find(|h| h.rect.contains(pos))
1093//                                 {
1094//                                     self.capture_id = Some(hit.id);
1095//                                     self.pressed_ids.insert(hit.id);
1096//                                     if hit.focusable {
1097//                                         self.sched.focused = Some(hit.id);
1098//                                         if let Some(win) = &self.window {
1099//                                             win.set_ime_allowed(true);
1100//                                             win.set_ime_purpose(ImePurpose::Normal);
1101//                                         }
1102//                                     }
1103//                                     if let Some(cb) = &hit.on_pointer_down {
1104//                                         cb(repose_core::input::PointerEvent {
1105//                                             id: repose_core::input::PointerId(0),
1106//                                             kind: repose_core::input::PointerKind::Touch,
1107//                                             event: repose_core::input::PointerEventKind::Down(
1108//                                                 repose_core::input::PointerButton::Primary,
1109//                                             ),
1110//                                             position: pos,
1111//                                             pressure: 1.0,
1112//                                             modifiers: self.modifiers,
1113//                                         });
1114//                                     }
1115//                                     self.request_redraw();
1116//                                 }
1117//                             }
1118//                         } else {
1119//                             if let (Some(f), Some(cid)) = (&self.frame_cache, self.capture_id) {
1120//                                 self.pressed_ids.remove(&cid);
1121//                                 let pos = Vec2 {
1122//                                     x: self.mouse_pos.0,
1123//                                     y: self.mouse_pos.1,
1124//                                 };
1125//                                 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid) {
1126//                                     if hit.rect.contains(pos) {
1127//                                         if let Some(cb) = &hit.on_click {
1128//                                             cb();
1129//                                         }
1130//                                     }
1131//                                 }
1132//                             }
1133//                             self.capture_id = None;
1134//                             self.request_redraw();
1135//                         }
1136//                     }
1137//                     WindowEvent::MouseWheel { delta, .. } => {
1138//                         let mut dy = match delta {
1139//                             MouseScrollDelta::LineDelta(_x, y) => -y * 40.0,
1140//                             MouseScrollDelta::PixelDelta(lp) => -(lp.y as f32),
1141//                         };
1142//                         if let Some(f) = &self.frame_cache {
1143//                             let pos = Vec2 {
1144//                                 x: self.mouse_pos.0,
1145//                                 y: self.mouse_pos.1,
1146//                             };
1147//                             for hit in f.hit_regions.iter().rev().filter(|h| h.rect.contains(pos)) {
1148//                                 if let Some(cb) = &hit.on_scroll {
1149//                                     dy = cb(dy);
1150//                                     if dy.abs() <= 0.001 {
1151//                                         break;
1152//                                     }
1153//                                 }
1154//                             }
1155//                             self.request_redraw();
1156//                         }
1157//                     }
1158//                     WindowEvent::RedrawRequested => {
1159//                         if let (Some(backend), Some(win)) =
1160//                             (self.backend.as_mut(), self.window.as_ref())
1161//                         {
1162//                             let scale = win.scale_factor();
1163//                             self.last_scale = scale;
1164//                             let t0 = Instant::now();
1165//                             let frame = self.sched.repose(&mut self.root, |view, size| {
1166//                                 let interactions = repose_ui::Interactions {
1167//                                     hover: self.hover_id,
1168//                                     pressed: self.pressed_ids.clone(),
1169//                                 };
1170//                                 // Density from scale factor (Android DPI / 160 roughly equals scale)
1171//                                 with_density(
1172//                                     Density {
1173//                                         scale: scale as f32,
1174//                                     },
1175//                                     || {
1176//                                         layout_and_paint(
1177//                                             view,
1178//                                             size,
1179//                                             &self.textfield_states,
1180//                                             &interactions,
1181//                                             self.sched.focused,
1182//                                         )
1183//                                     },
1184//                                 )
1185//                             });
1186//                             let build_layout_ms = (Instant::now() - t0).as_secs_f32() * 1000.0;
1187//                             let mut scene = frame.scene.clone();
1188//                             // HUD (opt-in via inspector hotkey; on Android you can toggle via programmatic flag later)
1189//                             super::App::new(Box::new(|_| View::new(0, ViewKind::Surface))); // no-op; placeholder to keep structure similar
1190//                             backend.frame(&scene, GlyphRasterConfig { px: 18.0 });
1191//                             self.frame_cache = Some(frame);
1192//                         }
1193//                     }
1194
1195//                     _ => {}
1196//                 }
1197//             }
1198//         }
1199//         let mut app_state = A::new(Box::new(root));
1200//         event_loop.run_app(&mut app_state)?;
1201//         Ok(())
1202//     }
1203// }
1204
1205// Accessibility bridge stub (Noop by default; logs on Linux for now)
1206pub trait A11yBridge: Send {
1207    fn publish_tree(&mut self, nodes: &[repose_core::runtime::SemNode]);
1208    fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>);
1209    fn announce(&mut self, msg: &str);
1210}
1211
1212struct NoopA11y;
1213impl A11yBridge for NoopA11y {
1214    fn publish_tree(&mut self, _nodes: &[repose_core::runtime::SemNode]) {
1215        // no-op
1216    }
1217    fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>) {
1218        if let Some(n) = node {
1219            log::info!("A11y focus: {:?} {:?}", n.role, n.label);
1220        } else {
1221            log::info!("A11y focus: None");
1222        }
1223    }
1224    fn announce(&mut self, msg: &str) {
1225        log::info!("A11y announce: {msg}");
1226    }
1227}
1228
1229#[cfg(target_os = "linux")]
1230struct LinuxAtspiStub;
1231#[cfg(target_os = "linux")]
1232impl A11yBridge for LinuxAtspiStub {
1233    fn publish_tree(&mut self, nodes: &[repose_core::runtime::SemNode]) {
1234        log::debug!("AT-SPI stub: publish {} nodes", nodes.len());
1235    }
1236    fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>) {
1237        if let Some(n) = node {
1238            log::info!("AT-SPI stub focus: {:?} {:?}", n.role, n.label);
1239        } else {
1240            log::info!("AT-SPI stub focus: None");
1241        }
1242    }
1243    fn announce(&mut self, msg: &str) {
1244        log::info!("AT-SPI stub announce: {msg}");
1245    }
1246}