repose_platform/
lib.rs

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