repose_platform/
lib.rs

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