repose_platform/
lib.rs

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