repose_platform/
lib.rs

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