Skip to main content

platform_glyph/
app.rs

1use core_glyph::{
2    clear_redraw, needs_redraw, tick_tweens, FlatView, FlatViewKind, Signal, Theme, View, ViewTree,
3};
4use std::path::PathBuf;
5use render_glyph::{GpuContext, Renderer};
6use crate::menu::{install_menu_macos, poll_menu_events, MenuBar};
7#[cfg(target_os = "windows")]
8use crate::menu::install_menu_windows;
9use std::collections::HashMap;
10use std::sync::{Arc, Mutex};
11use std::time::Instant;
12use winit::{
13    application::ApplicationHandler,
14    event::{ElementState, KeyEvent, MouseButton, MouseScrollDelta, WindowEvent},
15    event_loop::{ActiveEventLoop, EventLoop},
16    keyboard::{Key, ModifiersState, NamedKey},
17    window::{Cursor, CursorIcon, Window, WindowId},
18};
19
20
21
22type BuildViewFn = Box<dyn Fn(&WindowOpener, &WindowCloser) -> (Theme, View) + Send>;
23
24pub struct WindowRequest {
25    pub build_view: BuildViewFn,
26    pub title: String,
27    pub width: f64,
28    pub height: f64,
29    pub theme: Theme,
30}
31
32/// Clone this into button callbacks to open new windows.
33#[derive(Clone)]
34pub struct WindowOpener(Arc<Mutex<Vec<WindowRequest>>>);
35
36impl WindowOpener {
37    fn new() -> (Self, Arc<Mutex<Vec<WindowRequest>>>) {
38        let q = Arc::new(Mutex::new(Vec::new()));
39        (Self(Arc::clone(&q)), q)
40    }
41
42    pub fn open(
43        &self,
44        build_view: impl Fn(&WindowOpener, &WindowCloser) -> (Theme, View) + Send + 'static,
45        title: impl Into<String>,
46        width: f64,
47        height: f64,
48        theme: Theme,
49    ) {
50        self.0.lock().unwrap().push(WindowRequest {
51            build_view: Box::new(build_view),
52            title: title.into(),
53            width,
54            height,
55            theme,
56        });
57    }
58}
59
60/// Clone this into button callbacks to close the window it was created for.
61///
62/// The closer is bound to a specific window: calling `close()` queues
63/// that window for removal at the next event-loop tick.
64#[derive(Clone)]
65pub struct WindowCloser {
66    /// The id of the window this closer is bound to.  Populated by
67    /// `open_window` immediately after the OS window is created.
68    id: Arc<Mutex<Option<WindowId>>>,
69    /// Shared queue on `App` — draining this closes windows.
70    queue: Arc<Mutex<Vec<WindowId>>>,
71}
72
73impl WindowCloser {
74    fn new(queue: Arc<Mutex<Vec<WindowId>>>) -> Self {
75        Self {
76            id: Arc::new(Mutex::new(None)),
77            queue,
78        }
79    }
80
81    fn set_id(&self, id: WindowId) {
82        *self.id.lock().unwrap() = Some(id);
83    }
84
85    /// Request that this window be closed at the next event loop tick.
86    pub fn close(&self) {
87        if let Some(id) = *self.id.lock().unwrap() {
88            self.queue.lock().unwrap().push(id);
89        }
90    }
91}
92
93// Per-window state
94
95/// Lightweight cursor/hover info extracted from the flat list after each redraw.
96struct HitItem {
97    x: f32,
98    y: f32,
99    w: f32,
100    h: f32,
101    kind: HitKind,
102}
103
104enum HitKind {
105    Button(bool),
106    Text,
107    Slider,
108}
109
110#[derive(Default)]
111struct TextEditState {
112    focused_flat_index: Option<usize>,
113    selection_anchor: Option<usize>,
114    selection: Option<(usize, usize)>,
115    composing: Option<(usize, String)>,
116}
117
118/// A scrollable region extracted from the flat list, used for momentum scrolling.
119struct ScrollItem {
120    /// Content-space position of this scroll region.
121    cx: f32,
122    cy: f32,
123    w: f32,
124    h: f32,
125    offset_x: Signal<f32>,
126    offset_y: Signal<f32>,
127    max_x: f32,
128    max_y: f32,
129    /// Signals for each enclosing scroll region, in order from outermost to innermost.
130    /// Used to compute the live screen-space position: content_pos - sum(enclosing offsets).
131    enclosing: Vec<(Signal<f32>, Signal<f32>)>,
132}
133
134struct WindowState {
135    window: Arc<Window>,
136    renderer: Renderer,
137    build_view: BuildViewFn,
138    closer: WindowCloser,
139    theme: Theme,
140    cursor_pos: (f32, f32),
141    frame: u32,
142    hit_items: Vec<HitItem>,
143    scroll_items: Vec<ScrollItem>,
144    scroll_vx: f32,
145    scroll_vy: f32,
146    last_scroll: Option<Instant>,
147    flat_cache: Vec<FlatView>,
148    scaled_cache: Vec<FlatView>,
149    modifiers: ModifiersState,
150    text_edit: TextEditState,
151    /// Set when a scroll event arrives; cleared after each redraw. When true and
152    /// no VirtualList row range changed, we skip ViewTree::build and re-render
153    /// scaled_cache directly (scroll offsets are read live from signals by the renderer).
154    scroll_dirty: bool,
155    /// Cached VirtualList row ranges: (offset_y, first_row, last_row) per list.
156    vlist_ranges: Vec<f32>,
157    /// Active slider drag: flat index of the slider being dragged.
158    dragging_slider: Option<usize>,
159}
160
161impl WindowState {
162    /// Call `build_view`, update `self.theme` from the returned theme, return the view.
163    fn build(&mut self, opener: &WindowOpener) -> View {
164        let (theme, view) = (self.build_view)(opener, &self.closer);
165        self.theme = theme;
166        view
167    }
168
169    fn scale(&self) -> f32 {
170        self.window.scale_factor() as f32
171    }
172
173    /// Layout width in logical pixels.
174    fn lw(&self) -> f32 {
175        self.renderer.surface_cfg.width as f32 / self.scale()
176    }
177
178    /// Layout height in logical pixels.
179    fn lh(&self) -> f32 {
180        self.renderer.surface_cfg.height as f32 / self.scale()
181    }
182}
183
184// App — window manager
185
186pub struct App {
187    ctx: Option<Arc<GpuContext>>,
188    windows: HashMap<WindowId, WindowState>,
189    opener: WindowOpener,
190    queue: Arc<Mutex<Vec<WindowRequest>>>,
191    pending_close: Arc<Mutex<Vec<WindowId>>>,
192    initial: Option<WindowRequest>,
193    last_tick: Option<Instant>,
194    pending_fonts: Vec<Vec<u8>>,
195    pending_font_files: Vec<PathBuf>,
196    on_quit: Option<Box<dyn Fn() + Send>>,
197    on_open_file: Option<Box<dyn Fn(PathBuf) + Send>>,
198    on_focus: Option<Box<dyn Fn(bool) + Send>>,
199    menu: Option<MenuBar>,
200}
201
202/// Builder for configuring an `App` before running it.
203pub struct AppBuilder {
204    build_view: BuildViewFn,
205    theme: Theme,
206    title: String,
207    width: f64,
208    height: f64,
209    fonts: Vec<Vec<u8>>,
210    font_files: Vec<PathBuf>,
211    on_quit: Option<Box<dyn Fn() + Send>>,
212    on_open_file: Option<Box<dyn Fn(PathBuf) + Send>>,
213    on_focus: Option<Box<dyn Fn(bool) + Send>>,
214    menu: Option<MenuBar>,
215}
216
217impl AppBuilder {
218    /// Add a font from raw bytes.
219    pub fn font(mut self, data: Vec<u8>) -> Self {
220        self.fonts.push(data);
221        self
222    }
223
224    /// Add a font from a file path.
225    pub fn font_file(mut self, path: impl Into<PathBuf>) -> Self {
226        self.font_files.push(path.into());
227        self
228    }
229
230    /// Called just before the app exits (all windows closed).
231    pub fn on_quit(mut self, f: impl Fn() + Send + 'static) -> Self {
232        self.on_quit = Some(Box::new(f));
233        self
234    }
235
236    /// Called when the user drops a file onto the app or the OS sends an open-file event.
237    pub fn on_open_file(mut self, f: impl Fn(PathBuf) + Send + 'static) -> Self {
238        self.on_open_file = Some(Box::new(f));
239        self
240    }
241
242    /// Called when the app gains (`true`) or loses (`false`) focus.
243    pub fn on_focus(mut self, f: impl Fn(bool) + Send + 'static) -> Self {
244        self.on_focus = Some(Box::new(f));
245        self
246    }
247
248    /// Attach a native OS menu bar.
249    pub fn menu(mut self, menu: MenuBar) -> Self {
250        self.menu = Some(menu);
251        self
252    }
253
254    pub fn run(self) {
255        let (opener, queue) = WindowOpener::new();
256        let pending_close = Arc::new(Mutex::new(Vec::<WindowId>::new()));
257        let initial = WindowRequest {
258            build_view: self.build_view,
259            title: self.title,
260            width: self.width,
261            height: self.height,
262            theme: self.theme,
263        };
264        let event_loop = EventLoop::new().expect("event loop");
265        let mut app = App {
266            ctx: None,
267            windows: HashMap::new(),
268            opener,
269            queue,
270            pending_close,
271            initial: Some(initial),
272            last_tick: None,
273            pending_fonts: self.fonts,
274            pending_font_files: self.font_files,
275            on_quit: self.on_quit,
276            on_open_file: self.on_open_file,
277            on_focus: self.on_focus,
278            menu: self.menu,
279        };
280        event_loop.run_app(&mut app).expect("event loop run");
281    }
282}
283
284impl App {
285    /// Quick-start: single window, no font customization.
286    pub fn run(
287        build_view: impl Fn(&WindowOpener, &WindowCloser) -> (Theme, View) + Send + 'static,
288        theme: Theme,
289        title: impl Into<String>,
290        width: f64,
291        height: f64,
292    ) {
293        App::builder(build_view, theme, title, width, height).run();
294    }
295
296    /// Returns a builder for registering custom fonts before the event loop starts.
297    pub fn builder(
298        build_view: impl Fn(&WindowOpener, &WindowCloser) -> (Theme, View) + Send + 'static,
299        theme: Theme,
300        title: impl Into<String>,
301        width: f64,
302        height: f64,
303    ) -> AppBuilder {
304        AppBuilder {
305            build_view: Box::new(build_view),
306            theme,
307            title: title.into(),
308            width,
309            height,
310            fonts: Vec::new(),
311            font_files: Vec::new(),
312            on_quit: None,
313            on_open_file: None,
314            on_focus: None,
315            menu: None,
316        }
317    }
318
319    fn apply_pending_fonts(&self, renderer: &mut Renderer) {
320        for data in &self.pending_fonts {
321            renderer.load_font(data.clone());
322        }
323        for path in &self.pending_font_files {
324            if let Err(e) = renderer.load_font_file(path) {
325                eprintln!("glyph: failed to load font {:?}: {}", path, e);
326            }
327        }
328    }
329
330    fn open_window(&mut self, req: WindowRequest, event_loop: &ActiveEventLoop) {
331        let ctx = self
332            .ctx
333            .as_ref()
334            .expect("GpuContext not initialised")
335            .clone();
336        let window = Arc::new(
337            event_loop
338                .create_window(
339                    Window::default_attributes()
340                        .with_title(&req.title)
341                        .with_inner_size(winit::dpi::LogicalSize::new(req.width, req.height)),
342                )
343                .expect("window"),
344        );
345        let size = window.inner_size();
346        let (surface, surface_cfg) =
347            ctx.create_surface(Arc::clone(&window), size.width.max(1), size.height.max(1));
348        let mut renderer = Renderer::new(Arc::clone(&ctx), surface, surface_cfg);
349        self.apply_pending_fonts(&mut renderer);
350        let id = window.id();
351        let closer = WindowCloser::new(Arc::clone(&self.pending_close));
352        closer.set_id(id);
353        self.windows.insert(
354            id,
355            WindowState {
356                window,
357                renderer,
358                build_view: req.build_view,
359                closer,
360                theme: req.theme,
361                cursor_pos: (0.0, 0.0),
362                frame: 0,
363                hit_items: Vec::new(),
364                scroll_items: Vec::new(),
365                scroll_vx: 0.0,
366                scroll_vy: 0.0,
367                last_scroll: None,
368                flat_cache: Vec::new(),
369                scaled_cache: Vec::new(),
370                modifiers: ModifiersState::empty(),
371                text_edit: TextEditState::default(),
372                scroll_dirty: false,
373                vlist_ranges: Vec::new(),
374                    dragging_slider: None,
375            },
376        );
377    }
378}
379
380impl ApplicationHandler for App {
381    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
382        if self.ctx.is_some() {
383            return;
384        }
385        // Create the first window before the GPU context so the adapter can be
386        // selected with surface compatibility — required on Windows with multiple GPUs.
387        if let Some(req) = self.initial.take() {
388            let window = Arc::new(
389                event_loop
390                    .create_window(
391                        Window::default_attributes()
392                            .with_title(&req.title)
393                            .with_inner_size(winit::dpi::LogicalSize::new(req.width, req.height)),
394                    )
395                    .expect("window"),
396            );
397            let ctx = pollster::block_on(GpuContext::new_with_window(Arc::clone(&window)));
398            self.ctx = Some(Arc::clone(&ctx));
399            // Install native menu bar (macOS: global; Windows: per-window)
400            if let Some(ref mb) = self.menu {
401                install_menu_macos(mb);
402                #[cfg(target_os = "windows")]
403                {
404                    use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
405                    if let Ok(handle) = window.window_handle() {
406                        if let RawWindowHandle::Win32(h) = handle.as_raw() {
407                            install_menu_windows(mb, h.hwnd.get());
408                        }
409                    }
410                }
411            }
412            let size = window.inner_size();
413            let (surface, surface_cfg) =
414                ctx.create_surface(Arc::clone(&window), size.width.max(1), size.height.max(1));
415            let mut renderer = Renderer::new(ctx, surface, surface_cfg);
416            self.apply_pending_fonts(&mut renderer);
417            let id = window.id();
418            let closer = WindowCloser::new(Arc::clone(&self.pending_close));
419            closer.set_id(id);
420            self.windows.insert(
421                id,
422                WindowState {
423                    window,
424                    renderer,
425                    build_view: req.build_view,
426                    closer,
427                    theme: req.theme,
428                    cursor_pos: (0.0, 0.0),
429                    frame: 0,
430                    hit_items: Vec::new(),
431                    scroll_items: Vec::new(),
432                    scroll_vx: 0.0,
433                    scroll_vy: 0.0,
434                    last_scroll: None,
435                    flat_cache: Vec::new(),
436                    scaled_cache: Vec::new(),
437                    modifiers: ModifiersState::empty(),
438                    text_edit: TextEditState::default(),
439                    scroll_dirty: false,
440                    vlist_ranges: Vec::new(),
441                    dragging_slider: None,
442                },
443            );
444        }
445    }
446
447    fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
448        let now = Instant::now();
449        let dt = self
450            .last_tick
451            .map_or(0.0, |t| now.duration_since(t).as_secs_f32())
452            .min(0.05); // cap dt so a stalled frame doesn't cause a huge jump
453        self.last_tick = Some(now);
454
455        if tick_tweens(dt) {
456            for ws in self.windows.values() {
457                ws.window.request_redraw();
458            }
459        }
460
461        // Apply mouse-wheel momentum (LineDelta only — trackpad uses native macOS momentum).
462        // Exponential decay with tau=350ms: decay = exp(-dt/0.35)
463        let mut any_scrolling = false;
464        for ws in self.windows.values_mut() {
465            let speed = ws.scroll_vx.abs().max(ws.scroll_vy.abs());
466            if speed > 1.0 {
467                any_scrolling = true;
468                let (cur_x, cur_y) = ws.cursor_pos;
469                let only_scroll = !needs_redraw();
470                let dx = ws.scroll_vx * dt;
471                let dy = ws.scroll_vy * dt;
472                apply_scroll(&ws.scroll_items, cur_x, cur_y, dx, dy);
473                let decay = (-dt / 0.35).exp();
474                ws.scroll_vx *= decay;
475                ws.scroll_vy *= decay;
476                clear_redraw();
477                if only_scroll {
478                    ws.scroll_dirty = true;
479                }
480                ws.window.request_redraw();
481            } else {
482                ws.scroll_vx = 0.0;
483                ws.scroll_vy = 0.0;
484            }
485        }
486
487        // Stay in Poll mode while scrolling or for 100ms after the last scroll event.
488        // This ensures we render at display rate during trackpad momentum (which macOS
489        // delivers as continued PixelDelta events) rather than at event-delivery rate.
490        let recently_scrolled = self.windows.values().any(|ws| {
491            ws.last_scroll
492                .is_some_and(|t| now.duration_since(t).as_millis() < 100)
493        });
494        if any_scrolling || recently_scrolled {
495            for ws in self.windows.values() {
496                ws.window.request_redraw();
497            }
498            event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll);
499        } else {
500            event_loop.set_control_flow(winit::event_loop::ControlFlow::Wait);
501        }
502
503        // Dispatch native menu item clicks
504        if let Some(ref mb) = self.menu {
505            poll_menu_events(&mb.handlers);
506        }
507
508        let pending: Vec<WindowRequest> = self.queue.lock().unwrap().drain(..).collect();
509        for req in pending {
510            self.open_window(req, event_loop);
511        }
512
513        // Process window-close requests from WindowCloser::close().
514        let to_close: Vec<WindowId> = self.pending_close.lock().unwrap().drain(..).collect();
515        for id in to_close {
516            self.windows.remove(&id);
517        }
518
519        if self.windows.is_empty() && self.ctx.is_some() {
520            if let Some(ref f) = self.on_quit { f(); }
521            event_loop.exit();
522        }
523    }
524
525    fn window_event(&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) {
526        let opener = self.opener.clone();
527
528        match event {
529            WindowEvent::CloseRequested => {
530                self.windows.remove(&id);
531                if self.windows.is_empty() {
532                    if let Some(ref f) = self.on_quit { f(); }
533                    event_loop.exit();
534                }
535            }
536
537            WindowEvent::Focused(gained) => {
538                if let Some(ref f) = self.on_focus { f(gained); }
539            }
540
541            WindowEvent::DroppedFile(path) => {
542                if let Some(ref f) = self.on_open_file { f(path); }
543            }
544
545            WindowEvent::Resized(size) => {
546                if let Some(ws) = self.windows.get_mut(&id) {
547                    ws.renderer.resize(size.width, size.height);
548                    ws.window.request_redraw();
549                }
550            }
551
552            WindowEvent::CursorMoved { position, .. } => {
553                let Some(ws) = self.windows.get_mut(&id) else {
554                    return;
555                };
556                let scale = ws.scale();
557                let (px, py) = (position.x as f32 / scale, position.y as f32 / scale);
558                ws.cursor_pos = (px, py);
559
560                // Fast path: check if cursor is near any interactive element.
561                // Only do a full rebuild (for on_hover callbacks) if needed.
562                let mut icon = CursorIcon::Default;
563                let mut needs_rebuild = false;
564                for item in &ws.hit_items {
565                    let hit = px >= item.x
566                        && px <= item.x + item.w
567                        && py >= item.y
568                        && py <= item.y + item.h;
569                    match &item.kind {
570                        HitKind::Button(has_hover) => {
571                            if hit {
572                                icon = CursorIcon::Pointer;
573                            }
574                            if *has_hover {
575                                needs_rebuild = true;
576                            }
577                        }
578                        HitKind::Text => {
579                            if hit {
580                                icon = CursorIcon::Text;
581                            }
582                        }
583                        HitKind::Slider => {
584                            if hit { icon = CursorIcon::EwResize; }
585                        }
586                    }
587                }
588                // Fire active slider drag on mouse move.
589                if let Some(drag_idx) = ws.dragging_slider {
590                    if let Some(fv) = ws.scaled_cache.get(drag_idx) {
591                        if let FlatViewKind::Slider { on_drag, .. } = &fv.kind {
592                            let l = fv.layout.location.x;
593                            let w = fv.layout.size.width;
594                            let norm = ((px - l) / w).clamp(0.0, 1.0);
595                            on_drag(norm);
596                            ws.window.request_redraw();
597                        }
598                    }
599                }
600                ws.window.set_cursor(Cursor::Icon(icon));
601
602                if needs_rebuild {
603                    // Use flat_cache to fire hover callbacks without a full ViewTree rebuild.
604                    // flat_cache positions are content-space; subtract current scroll offsets.
605                    let mut changed = false;
606                    let mut scroll_stack: Vec<(f32, f32)> = Vec::new();
607                    let mut pending_scroll: Option<(f32, f32)> = None;
608                    for fv in &ws.flat_cache {
609                        match &fv.kind {
610                            FlatViewKind::ScrollRegion { offset_x, offset_y, .. } => {
611                                pending_scroll = Some((offset_x.get(), offset_y.get()));
612                                continue;
613                            }
614                            FlatViewKind::ClipStart { .. } => {
615                                scroll_stack.push(pending_scroll.take().unwrap_or((0.0, 0.0)));
616                                continue;
617                            }
618                            FlatViewKind::ClipEnd => { scroll_stack.pop(); continue; }
619                            FlatViewKind::OpacityStart { .. } | FlatViewKind::OpacityEnd => continue,
620                            _ => {}
621                        }
622                        let sox: f32 = scroll_stack.iter().map(|(ox, _)| ox).sum();
623                        let soy: f32 = scroll_stack.iter().map(|(_, oy)| oy).sum();
624                        let l = fv.layout.location.x - sox;
625                        let t = fv.layout.location.y - soy;
626                        let hit = px >= l
627                            && px <= l + fv.layout.size.width
628                            && py >= t
629                            && py <= t + fv.layout.size.height;
630                        if let FlatViewKind::Button {
631                            on_hover: Some(on_hover),
632                            ..
633                        } = &fv.kind
634                        {
635                            on_hover(hit);
636                            changed = true;
637                        }
638                    }
639                    // Clear the redraw flag set by hover signals so scroll frames
640                    // can still use the fast path after hover processing.
641                    clear_redraw();
642                    if changed {
643                        ws.window.request_redraw();
644                    }
645                }
646            }
647
648            WindowEvent::CursorLeft { .. } => {
649                let Some(ws) = self.windows.get_mut(&id) else {
650                    return;
651                };
652                ws.window.set_cursor(Cursor::Icon(CursorIcon::Default));
653                let has_hover_btns = ws
654                    .hit_items
655                    .iter()
656                    .any(|i| matches!(&i.kind, HitKind::Button(true)));
657                if has_hover_btns {
658                    let mut changed = false;
659                    for fv in &ws.flat_cache {
660                        if let FlatViewKind::Button {
661                            on_hover: Some(on_hover),
662                            ..
663                        } = &fv.kind
664                        {
665                            on_hover(false);
666                            changed = true;
667                        }
668                    }
669                    clear_redraw();
670                    if changed {
671                        ws.window.request_redraw();
672                    }
673                }
674            }
675
676            WindowEvent::ModifiersChanged(modifiers) => {
677                let Some(ws) = self.windows.get_mut(&id) else {
678                    return;
679                };
680                ws.modifiers = modifiers.state();
681            }
682
683            WindowEvent::MouseWheel { delta, .. } => {
684                let Some(ws) = self.windows.get_mut(&id) else {
685                    return;
686                };
687                let (cur_x, cur_y) = ws.cursor_pos;
688                // Only mark as a pure scroll frame (fast-path eligible) if no other
689                // signals changed before this event (e.g. tweens, button clicks).
690                let only_scroll = !needs_redraw();
691                match delta {
692                    // Trackpad: macOS delivers PixelDelta in logical points (not device pixels),
693                    // and includes its own momentum phase after finger lift. Apply directly.
694                    MouseScrollDelta::PixelDelta(pos) => {
695                        let dx = pos.x as f32;
696                        let dy = pos.y as f32;
697                        apply_scroll(&ws.scroll_items, cur_x, cur_y, dx, dy);
698                        ws.scroll_vx = 0.0;
699                        ws.scroll_vy = 0.0;
700                    }
701                    // Mouse wheel: discrete line steps — add our own momentum.
702                    MouseScrollDelta::LineDelta(x, y) => {
703                        let dx = x * 40.0;
704                        let dy = y * 40.0;
705                        apply_scroll(&ws.scroll_items, cur_x, cur_y, dx, dy);
706                        ws.scroll_vx = ws.scroll_vx * 0.8 + dx * 6.0;
707                        ws.scroll_vy = ws.scroll_vy * 0.8 + dy * 6.0;
708                    }
709                }
710                // apply_scroll set needs_redraw; clear it since we'll redraw anyway.
711                // Only mark scroll_dirty if no other signals were dirty beforehand.
712                clear_redraw();
713                ws.last_scroll = Some(Instant::now());
714                if only_scroll {
715                    ws.scroll_dirty = true;
716                }
717                ws.window.request_redraw();
718            }
719
720            WindowEvent::MouseInput {
721                state,
722                button: MouseButton::Left,
723                ..
724            } => {
725                let Some(ws) = self.windows.get_mut(&id) else {
726                    return;
727                };
728                let (cx, cy) = ws.cursor_pos;
729                let (w, h) = (ws.lw(), ws.lh());
730                let view = ws.build(&opener);
731                // Stay in logical pixels. Layout positions are content-space; subtract
732                // the current scroll offset (tracked via scroll_stack) to get screen coords.
733                let flat = ViewTree::build(view, &ws.theme, w, h, &mut ws.renderer.measurer());
734                let pressed = state == ElementState::Pressed;
735                let mut clicked = false;
736                let mut hit_text_input = false;
737                let mut scroll_stack: Vec<(f32, f32)> = Vec::new();
738                let mut pending_scroll: Option<(f32, f32)> = None;
739                for (idx, fv) in flat.iter().enumerate() {
740                    // Track scroll regions so we can offset hit rects correctly.
741                    match &fv.kind {
742                        FlatViewKind::ScrollRegion { offset_x, offset_y, .. } => {
743                            pending_scroll = Some((offset_x.get(), offset_y.get()));
744                            continue;
745                        }
746                        FlatViewKind::ClipStart { .. } => {
747                            scroll_stack.push(pending_scroll.take().unwrap_or((0.0, 0.0)));
748                            continue;
749                        }
750                        FlatViewKind::ClipEnd => {
751                            scroll_stack.pop();
752                            continue;
753                        }
754                        FlatViewKind::OpacityStart { .. } | FlatViewKind::OpacityEnd => continue,
755                        _ => {}
756                    }
757                    let sox: f32 = scroll_stack.iter().map(|(ox, _)| ox).sum();
758                    let soy: f32 = scroll_stack.iter().map(|(_, oy)| oy).sum();
759                    let l = fv.layout.location.x - sox;
760                    let t = fv.layout.location.y - soy;
761                    let hit = cx >= l
762                        && cx <= l + fv.layout.size.width
763                        && cy >= t
764                        && cy <= t + fv.layout.size.height;
765                    match &fv.kind {
766                        FlatViewKind::Button {
767                            on_click, on_press, disabled, ..
768                        } => {
769                            if *disabled { continue; }
770                            if hit {
771                                if let Some(op) = on_press {
772                                    op(pressed);
773                                }
774                                if pressed && !clicked {
775                                    on_click();
776                                    clicked = true;
777                                }
778                            } else if !pressed {
779                                if let Some(op) = on_press {
780                                    op(false);
781                                }
782                            }
783                        }
784                        FlatViewKind::TextInput {
785                            focused,
786                            cursor,
787                            value,
788                            font_size,
789                            disabled,
790                            ..
791                        } if pressed => {
792                            if *disabled { continue; }
793                            focused.set(hit);
794                            ws.window.set_ime_allowed(hit);
795                            if hit {
796                                hit_text_input = true;
797                                ws.frame = 0;
798                                let val = value.get();
799                                let pad = 8.0;
800                                let click_offset = (cx - l - pad).max(0.0);
801                                let byte_idx = ws.renderer.cursor_for_x(
802                                    &val,
803                                    *font_size,
804                                    click_offset,
805                                );
806                                cursor.set(byte_idx);
807                                ws.text_edit.focused_flat_index = Some(idx);
808                                ws.text_edit.composing = None;
809                                if ws.modifiers.shift_key() {
810                                    let anchor =
811                                        ws.text_edit.selection_anchor.unwrap_or(cursor.get());
812                                    ws.text_edit.selection_anchor = Some(anchor);
813                                    ws.text_edit.selection =
814                                        normalized_selection(anchor, byte_idx);
815                                } else {
816                                    ws.text_edit.selection_anchor = None;
817                                    ws.text_edit.selection = None;
818                                }
819                            }
820                        }
821                        FlatViewKind::TextArea {
822                            focused,
823                            cursor,
824                            value,
825                            font_size,
826                            scroll_y,
827                            ..
828                        } if pressed => {
829                            focused.set(hit);
830                            if hit {
831                                ws.frame = 0;
832                                let val = value.get();
833                                let line_height = font_size * 1.4;
834                                let pad = 8.0;
835                                let rel_y = cy - t - pad + scroll_y.get();
836                                let line_idx = (rel_y / line_height).floor().max(0.0) as usize;
837                                let lines: Vec<&str> = val.split('\n').collect();
838                                let line_idx = line_idx.min(lines.len().saturating_sub(1));
839                                let mut byte_offset: usize =
840                                    lines[..line_idx].iter().map(|l| l.len() + 1).sum();
841                                let rel_x = (cx - l - pad).max(0.0);
842                                let line_cursor = ws.renderer.cursor_for_x(
843                                    lines[line_idx],
844                                    *font_size,
845                                    rel_x,
846                                );
847                                byte_offset += line_cursor;
848                                cursor.set(byte_offset.min(val.len()));
849                            }
850                        }
851                        FlatViewKind::Slider { on_drag, .. } if pressed && hit => {
852                            // Start drag: compute initial value from click x position.
853                            let norm = ((cx - l) / fv.layout.size.width).clamp(0.0, 1.0);
854                            on_drag(norm);
855                            ws.dragging_slider = Some(idx);
856                        }
857                        FlatViewKind::Slider { .. } if !pressed => {
858                            ws.dragging_slider = None;
859                        }
860                        _ => {}
861                    }
862                }
863                if pressed && !hit_text_input {
864                    ws.text_edit = TextEditState::default();
865                    ws.window.set_ime_allowed(false);
866                }
867                if needs_redraw() {
868                    clear_redraw();
869                }
870                ws.window.request_redraw();
871            }
872
873            WindowEvent::Ime(ime) => {
874                let Some(ws) = self.windows.get_mut(&id) else {
875                    return;
876                };
877                let (w, h) = (ws.lw(), ws.lh());
878                let view = ws.build(&opener);
879                let flat = ViewTree::build(view, &ws.theme, w, h, &mut ws.renderer.measurer());
880                let Some(idx) = ws.text_edit.focused_flat_index else {
881                    return;
882                };
883                let Some(fv) = flat.get(idx) else {
884                    return;
885                };
886                if let FlatViewKind::TextInput {
887                    value,
888                    focused,
889                    cursor,
890                    on_change,
891                    ..
892                } = &fv.kind
893                {
894                    if !focused.get() {
895                        return;
896                    }
897                    match ime {
898                        winit::event::Ime::Preedit(text, _) => {
899                            let mut s = value.get();
900                            let mut cur = cursor.get().min(s.len());
901                            if !text.is_empty() && ws.text_edit.composing.is_none()
902                                && delete_selection(&mut s, &mut cur, ws.text_edit.selection) {
903                                    value.set(s);
904                                    cursor.set(cur);
905                                    ws.text_edit.selection = None;
906                                    ws.text_edit.selection_anchor = None;
907                                }
908                            ws.text_edit.composing = if text.is_empty() {
909                                None
910                            } else {
911                                Some((cur, text))
912                            };
913                            ws.window.request_redraw();
914                        }
915                        winit::event::Ime::Commit(text) => {
916                            let mut s = value.get();
917                            let mut cur = cursor.get().min(s.len());
918                            delete_selection(&mut s, &mut cur, ws.text_edit.selection);
919                            s.insert_str(cur, &text);
920                            cur += text.len();
921                            value.set(s.clone());
922                            cursor.set(cur);
923                            ws.text_edit.selection = None;
924                            ws.text_edit.selection_anchor = None;
925                            ws.text_edit.composing = None;
926                            if let Some(f) = on_change {
927                                f(s);
928                            }
929                            if needs_redraw() {
930                                clear_redraw();
931                            }
932                            ws.window.request_redraw();
933                        }
934                        winit::event::Ime::Enabled => {}
935                        winit::event::Ime::Disabled => {
936                            ws.text_edit.composing = None;
937                            ws.window.request_redraw();
938                        }
939                    }
940                }
941            }
942
943            WindowEvent::KeyboardInput {
944                event:
945                    KeyEvent {
946                        logical_key,
947                        state: ElementState::Pressed,
948                        ..
949                    },
950                ..
951            } => {
952                let Some(ws) = self.windows.get_mut(&id) else {
953                    return;
954                };
955                let (w, h) = (ws.lw(), ws.lh());
956                let view = ws.build(&opener);
957                let flat = ViewTree::build(view, &ws.theme, w, h, &mut ws.renderer.measurer());
958
959                // Collect text-input and text-area indices for Tab cycling.
960                let input_indices: Vec<usize> = flat
961                    .iter()
962                    .enumerate()
963                    .filter(|(_, fv)| {
964                        matches!(
965                            &fv.kind,
966                            FlatViewKind::TextInput { .. } | FlatViewKind::TextArea { .. }
967                        )
968                    })
969                    .map(|(i, _)| i)
970                    .collect();
971
972                // Check if any TextInput is focused.
973                let mut handled = false;
974                for (pos, &idx) in input_indices.iter().enumerate() {
975                    match &flat[idx].kind {
976                        FlatViewKind::TextInput {
977                            value,
978                            focused,
979                            cursor,
980                            on_change,
981                            on_submit,
982                            disabled,
983                            ..
984                        } => {
985                            if !focused.get() || *disabled {
986                                continue;
987                            }
988                            ws.text_edit.focused_flat_index = Some(idx);
989                            let mut s = value.get();
990                            let mut cur = cursor.get().min(s.len());
991                            let mut changed = false;
992                            let command = ws.modifiers.super_key() || ws.modifiers.control_key();
993                            match &logical_key {
994                                Key::Character(ch)
995                                    if command && ch.as_str().eq_ignore_ascii_case("a") =>
996                                {
997                                    ws.text_edit.selection_anchor = Some(0);
998                                    ws.text_edit.selection = normalized_selection(0, s.len());
999                                    cursor.set(s.len());
1000                                }
1001                                Key::Named(NamedKey::Backspace) => {
1002                                    if delete_selection(&mut s, &mut cur, ws.text_edit.selection) {
1003                                        value.set(s.clone());
1004                                        cursor.set(cur);
1005                                        ws.text_edit.selection = None;
1006                                        ws.text_edit.selection_anchor = None;
1007                                        changed = true;
1008                                    } else if cur > 0 {
1009                                        let prev = s[..cur]
1010                                            .char_indices()
1011                                            .next_back()
1012                                            .map(|(i, _)| i)
1013                                            .unwrap_or(0);
1014                                        s.remove(prev);
1015                                        cur = prev;
1016                                        value.set(s.clone());
1017                                        cursor.set(cur);
1018                                        changed = true;
1019                                    }
1020                                }
1021                                Key::Named(NamedKey::Delete) => {
1022                                    if delete_selection(&mut s, &mut cur, ws.text_edit.selection) {
1023                                        value.set(s.clone());
1024                                        cursor.set(cur);
1025                                        ws.text_edit.selection = None;
1026                                        ws.text_edit.selection_anchor = None;
1027                                        changed = true;
1028                                    } else if cur < s.len() {
1029                                        s.remove(cur);
1030                                        value.set(s.clone());
1031                                        changed = true;
1032                                    }
1033                                }
1034                                Key::Named(NamedKey::ArrowLeft) if cur > 0 => {
1035                                    let prev = s[..cur]
1036                                        .char_indices()
1037                                        .next_back()
1038                                        .map(|(i, _)| i)
1039                                        .unwrap_or(0);
1040                                    cursor.set(prev);
1041                                    update_selection_for_move(
1042                                        &mut ws.text_edit,
1043                                        cur,
1044                                        prev,
1045                                        ws.modifiers.shift_key(),
1046                                    );
1047                                }
1048                                Key::Named(NamedKey::ArrowRight) if cur < s.len() => {
1049                                    let next = s[cur..]
1050                                        .char_indices()
1051                                        .nth(1)
1052                                        .map(|(i, _)| cur + i)
1053                                        .unwrap_or(s.len());
1054                                    cursor.set(next);
1055                                    update_selection_for_move(
1056                                        &mut ws.text_edit,
1057                                        cur,
1058                                        next,
1059                                        ws.modifiers.shift_key(),
1060                                    );
1061                                }
1062                                Key::Named(NamedKey::Space) => {
1063                                    delete_selection(&mut s, &mut cur, ws.text_edit.selection);
1064                                    s.insert(cur, ' ');
1065                                    cur += 1;
1066                                    value.set(s.clone());
1067                                    cursor.set(cur);
1068                                    ws.text_edit.selection = None;
1069                                    ws.text_edit.selection_anchor = None;
1070                                    changed = true;
1071                                }
1072                                Key::Named(NamedKey::Home) => {
1073                                    cursor.set(0);
1074                                    update_selection_for_move(
1075                                        &mut ws.text_edit,
1076                                        cur,
1077                                        0,
1078                                        ws.modifiers.shift_key(),
1079                                    );
1080                                }
1081                                Key::Named(NamedKey::End) => {
1082                                    cursor.set(s.len());
1083                                    update_selection_for_move(
1084                                        &mut ws.text_edit,
1085                                        cur,
1086                                        s.len(),
1087                                        ws.modifiers.shift_key(),
1088                                    );
1089                                }
1090                                Key::Named(NamedKey::Escape) => {
1091                                    focused.set(false);
1092                                    ws.text_edit = TextEditState::default();
1093                                    ws.window.set_ime_allowed(false);
1094                                }
1095                                Key::Named(NamedKey::Tab) => {
1096                                    focused.set(false);
1097                                    let next_idx = input_indices[(pos + 1) % input_indices.len()];
1098                                    set_focused_at(&flat, next_idx, true);
1099                                    ws.window.set_ime_allowed(true);
1100                                    ws.text_edit = TextEditState {
1101                                        focused_flat_index: Some(next_idx),
1102                                        ..TextEditState::default()
1103                                    };
1104                                }
1105                                Key::Named(NamedKey::Enter) => {
1106                                    if let Some(f) = on_submit {
1107                                        f(value.get());
1108                                    }
1109                                    focused.set(false);
1110                                    ws.text_edit = TextEditState::default();
1111                                    ws.window.set_ime_allowed(false);
1112                                }
1113                                Key::Character(ch) if !command => {
1114                                    delete_selection(&mut s, &mut cur, ws.text_edit.selection);
1115                                    s.insert_str(cur, ch.as_str());
1116                                    cur += ch.len();
1117                                    value.set(s.clone());
1118                                    cursor.set(cur);
1119                                    ws.text_edit.selection = None;
1120                                    ws.text_edit.selection_anchor = None;
1121                                    ws.text_edit.composing = None;
1122                                    changed = true;
1123                                }
1124                                _ => {}
1125                            }
1126                            if changed {
1127                                if let Some(f) = on_change {
1128                                    f(s);
1129                                }
1130                            }
1131                            handled = true;
1132                            break;
1133                        }
1134                        FlatViewKind::TextArea {
1135                            value,
1136                            focused,
1137                            cursor,
1138                            scroll_y,
1139                            on_change,
1140                            font_size,
1141                            ..
1142                        } => {
1143                            if !focused.get() {
1144                                continue;
1145                            }
1146                            let mut s = value.get();
1147                            let mut cur = cursor.get().min(s.len());
1148                            let mut changed = false;
1149                            match &logical_key {
1150                                Key::Named(NamedKey::Backspace) if cur > 0 => {
1151                                    let prev = s[..cur]
1152                                        .char_indices()
1153                                        .next_back()
1154                                        .map(|(i, _)| i)
1155                                        .unwrap_or(0);
1156                                    s.remove(prev);
1157                                    cur = prev;
1158                                    value.set(s.clone());
1159                                    cursor.set(cur);
1160                                    changed = true;
1161                                }
1162                                Key::Named(NamedKey::Delete) if cur < s.len() => {
1163                                    s.remove(cur);
1164                                    value.set(s.clone());
1165                                    changed = true;
1166                                }
1167                                Key::Named(NamedKey::ArrowLeft) if cur > 0 => {
1168                                    let prev = s[..cur]
1169                                        .char_indices()
1170                                        .next_back()
1171                                        .map(|(i, _)| i)
1172                                        .unwrap_or(0);
1173                                    cursor.set(prev);
1174                                }
1175                                Key::Named(NamedKey::ArrowRight) if cur < s.len() => {
1176                                    let next = s[cur..]
1177                                        .char_indices()
1178                                        .nth(1)
1179                                        .map(|(i, _)| cur + i)
1180                                        .unwrap_or(s.len());
1181                                    cursor.set(next);
1182                                }
1183                                Key::Named(NamedKey::ArrowUp) => {
1184                                    let line_height = font_size * 1.4;
1185                                    let (line_idx, col_off) = byte_to_line_col(&s, cur);
1186                                    if line_idx > 0 {
1187                                        cur = line_col_to_byte(&s, line_idx - 1, col_off);
1188                                        cursor.set(cur);
1189                                        let new_oy = (scroll_y.get() - line_height).max(0.0);
1190                                        scroll_y.set(new_oy);
1191                                    }
1192                                }
1193                                Key::Named(NamedKey::ArrowDown) => {
1194                                    let line_height = font_size * 1.4;
1195                                    let lines: Vec<&str> = s.split('\n').collect();
1196                                    let (line_idx, col_off) = byte_to_line_col(&s, cur);
1197                                    if line_idx + 1 < lines.len() {
1198                                        cur = line_col_to_byte(&s, line_idx + 1, col_off);
1199                                        cursor.set(cur);
1200                                        scroll_y.set(scroll_y.get() + line_height);
1201                                    }
1202                                }
1203                                Key::Named(NamedKey::Space) => {
1204                                    s.insert(cur, ' ');
1205                                    cur += 1;
1206                                    value.set(s.clone());
1207                                    cursor.set(cur);
1208                                    changed = true;
1209                                }
1210                                Key::Named(NamedKey::Home) => {
1211                                    cursor.set(0);
1212                                }
1213                                Key::Named(NamedKey::End) => {
1214                                    cursor.set(s.len());
1215                                }
1216                                Key::Named(NamedKey::Escape) => {
1217                                    focused.set(false);
1218                                }
1219                                Key::Named(NamedKey::Tab) => {
1220                                    focused.set(false);
1221                                    let next_idx = input_indices[(pos + 1) % input_indices.len()];
1222                                    set_focused_at(&flat, next_idx, true);
1223                                }
1224                                Key::Named(NamedKey::Enter) => {
1225                                    s.insert(cur, '\n');
1226                                    cur += 1;
1227                                    value.set(s.clone());
1228                                    cursor.set(cur);
1229                                    changed = true;
1230                                }
1231                                Key::Character(ch) => {
1232                                    s.insert_str(cur, ch.as_str());
1233                                    cur += ch.len();
1234                                    value.set(s.clone());
1235                                    cursor.set(cur);
1236                                    changed = true;
1237                                }
1238                                _ => {}
1239                            }
1240                            if changed {
1241                                if let Some(f) = on_change {
1242                                    f(s);
1243                                }
1244                            }
1245                            handled = true;
1246                            break;
1247                        }
1248                        _ => {}
1249                    }
1250                }
1251                let _ = handled;
1252                if needs_redraw() {
1253                    clear_redraw();
1254                }
1255                ws.window.request_redraw();
1256            }
1257
1258            WindowEvent::RedrawRequested => {
1259                let Some(ws) = self.windows.get_mut(&id) else {
1260                    return;
1261                };
1262                ws.frame = ws.frame.wrapping_add(1);
1263                let cursor_visible = (ws.frame / 30) % 2 == 0;
1264                let scale = ws.scale();
1265
1266                // Fast path: on scroll-only frames, skip ViewTree::build entirely.
1267                // The renderer reads scroll offsets from signals live, so scaled_cache
1268                // is still correct. We must still do a full rebuild if:
1269                //   - no cached layout yet
1270                //   - a VirtualList's visible row range has changed (new rows need layout)
1271                //   - any other signal changed (tweens, clicks, text input, etc.)
1272                let skip_rebuild = ws.scroll_dirty && !ws.scaled_cache.is_empty() && {
1273                    let new_ranges = vlist_ranges_from_flat(&ws.flat_cache);
1274                    new_ranges == ws.vlist_ranges
1275                };
1276                ws.scroll_dirty = false;
1277
1278                if skip_rebuild {
1279                    ws.renderer.render(&ws.scaled_cache, cursor_visible, ws.theme.background, scale);
1280                    return;
1281                }
1282
1283                let (w, h) = (ws.lw(), ws.lh());
1284                let view = ws.build(&opener);
1285                let mut flat = ViewTree::build(view, &ws.theme, w, h, &mut ws.renderer.measurer());
1286                decorate_text_input_state(&mut flat, &ws.text_edit);
1287                let any_focused = flat.iter().any(|fv| {
1288                    matches!(&fv.kind, FlatViewKind::TextInput { focused, .. } if focused.get())
1289                    || matches!(&fv.kind, FlatViewKind::TextArea { focused, .. } if focused.get())
1290                });
1291                if any_focused {
1292                    ws.window.request_redraw();
1293                }
1294                {
1295                    let mut scroll_stack: Vec<(f32, f32)> = Vec::new();
1296                    let mut pending: Option<(f32, f32)> = None;
1297                    let mut hit_items: Vec<HitItem> = Vec::new();
1298                    for fv in &flat {
1299                        match &fv.kind {
1300                            FlatViewKind::ScrollRegion {
1301                                offset_x, offset_y, ..
1302                            } => {
1303                                pending = Some((offset_x.get(), offset_y.get()));
1304                            }
1305                            FlatViewKind::ClipStart { .. } => {
1306                                scroll_stack.push(pending.take().unwrap_or((0.0, 0.0)));
1307                            }
1308                            FlatViewKind::ClipEnd => {
1309                                scroll_stack.pop();
1310                            }
1311                            _ => {
1312                                let sox: f32 = scroll_stack.iter().map(|(ox, _)| ox).sum();
1313                                let soy: f32 = scroll_stack.iter().map(|(_, oy)| oy).sum();
1314                                let x = fv.layout.location.x - sox;
1315                                let y = fv.layout.location.y - soy;
1316                                let w = fv.layout.size.width;
1317                                let h = fv.layout.size.height;
1318                                let item = match &fv.kind {
1319                                    FlatViewKind::Button { on_hover, .. } => Some(HitItem {
1320                                        x,
1321                                        y,
1322                                        w,
1323                                        h,
1324                                        kind: HitKind::Button(on_hover.is_some()),
1325                                    }),
1326                                    FlatViewKind::TextInput { .. }
1327                                    | FlatViewKind::TextArea { .. } => Some(HitItem {
1328                                        x,
1329                                        y,
1330                                        w,
1331                                        h,
1332                                        kind: HitKind::Text,
1333                                    }),
1334                                    FlatViewKind::Slider { .. } => Some(HitItem {
1335                                        x,
1336                                        y,
1337                                        w,
1338                                        h,
1339                                        kind: HitKind::Slider,
1340                                    }),
1341                                    _ => None,
1342                                };
1343                                if let Some(item) = item {
1344                                    hit_items.push(item);
1345                                }
1346                            }
1347                        }
1348                    }
1349                    ws.hit_items = hit_items;
1350                }
1351                // Build scroll_items. Store content-space positions and enclosing signal refs
1352                // so apply_scroll can compute live screen-space hit rects even after the page scrolls.
1353                ws.scroll_items = {
1354                    let mut items = Vec::new();
1355                    // Stack of (offset_x_signal, offset_y_signal) for enclosing scroll regions.
1356                    let mut enclosing_stack: Vec<(Signal<f32>, Signal<f32>)> = Vec::new();
1357                    #[allow(clippy::type_complexity)]
1358                    let mut pending: Option<(Signal<f32>, Signal<f32>, f32, f32, core_glyph::TaffyLayout)> = None;
1359                    for fv in &flat {
1360                        match &fv.kind {
1361                            FlatViewKind::ScrollRegion { offset_x, offset_y, max_x, max_y, .. } => {
1362                                pending = Some((offset_x.clone(), offset_y.clone(), *max_x, *max_y, fv.layout));
1363                            }
1364                            FlatViewKind::ClipStart { .. } => {
1365                                if let Some((offset_x, offset_y, max_x, max_y, l)) = pending.take() {
1366                                    items.push(ScrollItem {
1367                                        cx: l.location.x,
1368                                        cy: l.location.y,
1369                                        w: l.size.width,
1370                                        h: l.size.height,
1371                                        enclosing: enclosing_stack.clone(),
1372                                        offset_x: offset_x.clone(),
1373                                        offset_y: offset_y.clone(),
1374                                        max_x,
1375                                        max_y,
1376                                    });
1377                                    enclosing_stack.push((offset_x, offset_y));
1378                                } else {
1379                                    // Non-scroll clip (e.g. overflow:hidden container) — push dummy.
1380                                    enclosing_stack.push((Signal::new(0.0), Signal::new(0.0)));
1381                                }
1382                            }
1383                            FlatViewKind::ClipEnd => { enclosing_stack.pop(); }
1384                            _ => {}
1385                        }
1386                    }
1387                    items
1388                };
1389                ws.vlist_ranges = vlist_ranges_from_flat(&flat);
1390                ws.flat_cache = flat.clone();
1391                ws.scaled_cache = scale_flat(flat, scale);
1392                clear_redraw();
1393                ws.renderer
1394                    .render(&ws.scaled_cache, cursor_visible, ws.theme.background, scale);
1395            }
1396
1397            _ => {}
1398        }
1399    }
1400}
1401
1402// TextArea cursor helpers
1403
1404fn byte_to_line_col(s: &str, byte: usize) -> (usize, usize) {
1405    let before = &s[..byte.min(s.len())];
1406    let line_idx = before.chars().filter(|&c| c == '\n').count();
1407    let col = before.rfind('\n').map(|p| byte - p - 1).unwrap_or(byte);
1408    (line_idx, col)
1409}
1410
1411fn line_col_to_byte(s: &str, target_line: usize, col: usize) -> usize {
1412    let mut offset = 0;
1413    for (li, line) in s.split('\n').enumerate() {
1414        if li == target_line {
1415            return offset + col.min(line.len());
1416        }
1417        offset += line.len() + 1;
1418    }
1419    s.len()
1420}
1421
1422fn set_focused_at(flat: &[FlatView], idx: usize, focused: bool) {
1423    match &flat[idx].kind {
1424        FlatViewKind::TextInput { focused: f, .. } => f.set(focused),
1425        FlatViewKind::TextArea { focused: f, .. } => f.set(focused),
1426        _ => {}
1427    }
1428}
1429
1430fn normalized_selection(anchor: usize, cursor: usize) -> Option<(usize, usize)> {
1431    if anchor == cursor {
1432        None
1433    } else {
1434        Some((anchor.min(cursor), anchor.max(cursor)))
1435    }
1436}
1437
1438fn update_selection_for_move(
1439    edit: &mut TextEditState,
1440    old_cursor: usize,
1441    new_cursor: usize,
1442    extend: bool,
1443) {
1444    if extend {
1445        let anchor = edit.selection_anchor.unwrap_or(old_cursor);
1446        edit.selection_anchor = Some(anchor);
1447        edit.selection = normalized_selection(anchor, new_cursor);
1448    } else {
1449        edit.selection_anchor = None;
1450        edit.selection = None;
1451    }
1452    edit.composing = None;
1453}
1454
1455fn delete_selection(s: &mut String, cursor: &mut usize, selection: Option<(usize, usize)>) -> bool {
1456    let Some((start, end)) = selection else {
1457        return false;
1458    };
1459    let start = start.min(s.len());
1460    let end = end.min(s.len());
1461    if start >= end || !s.is_char_boundary(start) || !s.is_char_boundary(end) {
1462        return false;
1463    }
1464    s.replace_range(start..end, "");
1465    *cursor = start;
1466    true
1467}
1468
1469fn decorate_text_input_state(flat: &mut [FlatView], edit: &TextEditState) {
1470    let Some(idx) = edit.focused_flat_index else {
1471        return;
1472    };
1473    let Some(fv) = flat.get_mut(idx) else {
1474        return;
1475    };
1476    if let FlatViewKind::TextInput {
1477        selection,
1478        composing,
1479        ..
1480    } = &mut fv.kind
1481    {
1482        *selection = edit.selection;
1483        *composing = edit.composing.clone();
1484    }
1485}
1486
1487// DPI scaling: layout runs in logical pixels; renderer works in physical pixels.
1488// Scale every position, size, and font size in the flat list before rendering.
1489
1490fn scale_flat(flat: Vec<FlatView>, scale: f32) -> Vec<FlatView> {
1491    if (scale - 1.0).abs() < f32::EPSILON {
1492        return flat;
1493    }
1494    flat.into_iter()
1495        .map(|fv| {
1496            let l = &fv.layout;
1497            let mut layout = *l;
1498            layout.location.x *= scale;
1499            layout.location.y *= scale;
1500            layout.size.width *= scale;
1501            layout.size.height *= scale;
1502            let kind = match fv.kind {
1503                FlatViewKind::Text {
1504                    content,
1505                    font_size,
1506                    color,
1507                    weight,
1508                    align,
1509                    wrap,
1510                    family,
1511                } => FlatViewKind::Text {
1512                    content,
1513                    font_size: font_size * scale,
1514                    color,
1515                    weight,
1516                    align,
1517                    wrap,
1518                    family,
1519                },
1520                FlatViewKind::Button {
1521                    label,
1522                    on_click,
1523                    on_hover,
1524                    on_press,
1525                    bg_color,
1526                    hover_bg_color,
1527                    press_bg_color,
1528                    text_color,
1529                    corner_radius,
1530                    font_size,
1531                    wrap,
1532                    family,
1533                    disabled,
1534                } => FlatViewKind::Button {
1535                    label,
1536                    on_click,
1537                    on_hover,
1538                    on_press,
1539                    bg_color,
1540                    hover_bg_color,
1541                    press_bg_color,
1542                    text_color,
1543                    corner_radius: corner_radius * scale,
1544                    font_size: font_size * scale,
1545                    wrap,
1546                    family,
1547                    disabled,
1548                },
1549                FlatViewKind::TextInput {
1550                    value,
1551                    focused,
1552                    cursor,
1553                    scroll_x,
1554                    placeholder,
1555                    font_size,
1556                    bg_color,
1557                    text_color,
1558                    border_color,
1559                    corner_radius,
1560                    on_change,
1561                    on_submit,
1562                    selection,
1563                    composing,
1564                    disabled,
1565                } => FlatViewKind::TextInput {
1566                    value,
1567                    focused,
1568                    cursor,
1569                    scroll_x,
1570                    placeholder,
1571                    font_size: font_size * scale,
1572                    bg_color,
1573                    text_color,
1574                    border_color,
1575                    corner_radius: corner_radius * scale,
1576                    on_change,
1577                    on_submit,
1578                    selection,
1579                    composing,
1580                    disabled,
1581                },
1582                FlatViewKind::ContainerRect {
1583                    bg_color,
1584                    border_color,
1585                    border_width,
1586                    corner_radius,
1587                    shadow,
1588                } => {
1589                    let shadow = shadow.map(|s| core_glyph::Shadow {
1590                        offset_x: s.offset_x * scale,
1591                        offset_y: s.offset_y * scale,
1592                        blur: s.blur * scale,
1593                        color: s.color,
1594                    });
1595                    FlatViewKind::ContainerRect {
1596                        bg_color,
1597                        border_color,
1598                        border_width: border_width * scale,
1599                        corner_radius: corner_radius * scale,
1600                        shadow,
1601                    }
1602                }
1603                FlatViewKind::ClipStart {
1604                    x,
1605                    y,
1606                    width,
1607                    height,
1608                    is_virtual_list,
1609                } => FlatViewKind::ClipStart {
1610                    x: x * scale,
1611                    y: y * scale,
1612                    width: width * scale,
1613                    height: height * scale,
1614                    is_virtual_list,
1615                },
1616                FlatViewKind::Image {
1617                    path,
1618                    corner_radius,
1619                    tint,
1620                } => FlatViewKind::Image {
1621                    path,
1622                    corner_radius: corner_radius * scale,
1623                    tint,
1624                },
1625                FlatViewKind::Slider { value, on_drag } => FlatViewKind::Slider { value, on_drag },
1626                FlatViewKind::TextArea {
1627                    value,
1628                    focused,
1629                    cursor,
1630                    scroll_y,
1631                    placeholder,
1632                    font_size,
1633                    bg_color,
1634                    text_color,
1635                    border_color,
1636                    corner_radius,
1637                    on_change,
1638                } => FlatViewKind::TextArea {
1639                    value,
1640                    focused,
1641                    cursor,
1642                    scroll_y,
1643                    placeholder,
1644                    font_size: font_size * scale,
1645                    bg_color,
1646                    text_color,
1647                    border_color,
1648                    corner_radius: corner_radius * scale,
1649                    on_change,
1650                },
1651                FlatViewKind::Rect { color, corner_radius } => FlatViewKind::Rect {
1652                    color,
1653                    corner_radius: corner_radius * scale,
1654                },
1655                // ScrollRegion is logical-pixel metadata only; renderer ignores it.
1656                FlatViewKind::ScrollRegion {
1657                    offset_x,
1658                    offset_y,
1659                    max_x,
1660                    max_y,
1661                    is_virtual_list,
1662                } => FlatViewKind::ScrollRegion {
1663                    offset_x,
1664                    offset_y,
1665                    max_x,
1666                    max_y,
1667                    is_virtual_list,
1668                },
1669                other => other,
1670            };
1671            FlatView { kind, layout }
1672        })
1673        .collect()
1674}
1675
1676// VirtualList range tracking: compute (offset_y, first_row, last_row) for each
1677// ScrollRegion that precedes a VirtualList clip so we can detect when the visible
1678// set of rows changes and a full rebuild is needed.
1679
1680/// Snapshot the VirtualList offset values at the time of the last full rebuild.
1681/// Stored in `ws.vlist_ranges` after each rebuild. On the next frame, the live
1682/// signal values are read again and compared — if any changed, skip_rebuild is false.
1683fn vlist_ranges_from_flat(flat: &[FlatView]) -> Vec<f32> {
1684    flat.iter()
1685        .filter_map(|fv| {
1686            if let FlatViewKind::ScrollRegion { offset_y, is_virtual_list: true, .. } = &fv.kind {
1687                Some(offset_y.get())
1688            } else {
1689                None
1690            }
1691        })
1692        .collect()
1693}
1694
1695// Nested scroll dispatch: innermost container wins, bubbles to outer if at limit.
1696
1697fn apply_scroll(items: &[ScrollItem], cx: f32, cy: f32, dx: f32, dy: f32) {
1698    // Iterate in reverse so the last-emitted (innermost/deepest) container is
1699    // checked first. The innermost container whose bounds contain the cursor
1700    // absorbs the event unconditionally — no bubbling to outer containers.
1701    for item in items.iter().rev() {
1702        // Compute live screen-space position by subtracting current enclosing offsets.
1703        let sox: f32 = item.enclosing.iter().map(|(ox, _)| ox.get()).sum();
1704        let soy: f32 = item.enclosing.iter().map(|(_, oy)| oy.get()).sum();
1705        let sx = item.cx - sox;
1706        let sy = item.cy - soy;
1707        if cx >= sx && cx <= sx + item.w && cy >= sy && cy <= sy + item.h {
1708            let cur_x = item.offset_x.get();
1709            let cur_y = item.offset_y.get();
1710            let nx = (cur_x - dx).clamp(0.0, item.max_x);
1711            let ny = (cur_y - dy).clamp(0.0, item.max_y);
1712            item.offset_x.set(nx);
1713            item.offset_y.set(ny);
1714            return;
1715        }
1716    }
1717}
1718
1719// Scroll dispatch — only used by HotApp's MouseWheel handler.
1720
1721#[cfg(feature = "hot-reload")]
1722fn dispatch_scroll(
1723    view: &View,
1724    theme: &Theme,
1725    flat: &[FlatView],
1726    cx: f32,
1727    cy: f32,
1728    dx: f32,
1729    dy: f32,
1730    flat_idx: &mut usize,
1731) {
1732    match view {
1733        View::Scroll {
1734            child,
1735            offset_x,
1736            offset_y,
1737            ..
1738        } => {
1739            *flat_idx += 1; // skip ScrollRegion
1740            if let Some(fv) = flat.get(*flat_idx) {
1741                if let FlatViewKind::ClipStart {
1742                    x,
1743                    y,
1744                    width,
1745                    height,
1746                    ..
1747                } = &fv.kind
1748                {
1749                    if cx >= *x && cx <= x + width && cy >= *y && cy <= y + height {
1750                        offset_x.set((offset_x.get() - dx).max(0.0));
1751                        offset_y.set((offset_y.get() - dy).max(0.0));
1752                    }
1753                }
1754            }
1755            *flat_idx += 1;
1756            dispatch_scroll(child, theme, flat, cx, cy, dx, dy, flat_idx);
1757            *flat_idx += 1;
1758        }
1759        View::Column {
1760            children,
1761            bg_color,
1762            border_color,
1763            shadow,
1764            clip,
1765            ..
1766        }
1767        | View::Row {
1768            children,
1769            bg_color,
1770            border_color,
1771            shadow,
1772            clip,
1773            ..
1774        } => {
1775            if bg_color.is_some() || border_color.is_some() || shadow.is_some() {
1776                *flat_idx += 1;
1777            }
1778            if *clip {
1779                *flat_idx += 1;
1780            }
1781            for child in children {
1782                dispatch_scroll(child, theme, flat, cx, cy, dx, dy, flat_idx);
1783            }
1784            if *clip {
1785                *flat_idx += 1;
1786            }
1787        }
1788        View::ZStack { children, .. } => {
1789            for child in children {
1790                dispatch_scroll(child, theme, flat, cx, cy, dx, dy, flat_idx);
1791            }
1792        }
1793        View::Component(c) => {
1794            let rendered = c.render(theme);
1795            dispatch_scroll(&rendered, theme, flat, cx, cy, dx, dy, flat_idx);
1796        }
1797        View::Button { .. }
1798        | View::Rect { .. }
1799        | View::Text { .. }
1800        | View::TextInput { .. }
1801        | View::Image { .. }
1802        | View::TextArea { .. } => {
1803            *flat_idx += 1;
1804        }
1805        View::VirtualList {
1806            item_count,
1807            row_height,
1808            offset_y,
1809            viewport_height,
1810            ..
1811        } => {
1812            *flat_idx += 1; // skip ScrollRegion
1813            if let Some(fv) = flat.get(*flat_idx) {
1814                if let FlatViewKind::ClipStart {
1815                    x,
1816                    y,
1817                    width,
1818                    height,
1819                    ..
1820                } = &fv.kind
1821                {
1822                    if cx >= *x && cx <= x + width && cy >= *y && cy <= y + height {
1823                        let max_scroll =
1824                            ((*item_count as f32) * row_height - viewport_height).max(0.0);
1825                        offset_y.set((offset_y.get() - dy).clamp(0.0, max_scroll));
1826                    }
1827                }
1828            }
1829            // Skip forward past all flat entries belonging to this VirtualList
1830            // (ClipStart + rows + ClipEnd). We track nesting depth to handle
1831            // nested clips correctly.
1832            *flat_idx += 1; // consume ClipStart
1833            let mut depth = 1usize;
1834            while *flat_idx < flat.len() && depth > 0 {
1835                match &flat[*flat_idx].kind {
1836                    FlatViewKind::ClipStart { .. } => {
1837                        depth += 1;
1838                        *flat_idx += 1;
1839                    }
1840                    FlatViewKind::ClipEnd => {
1841                        depth -= 1;
1842                        *flat_idx += 1;
1843                    }
1844                    _ => {
1845                        *flat_idx += 1;
1846                    }
1847                }
1848            }
1849        }
1850        View::Flexible { child, .. } => {
1851            dispatch_scroll(child, theme, flat, cx, cy, dx, dy, flat_idx);
1852        }
1853        View::Opacity { child, .. } => {
1854            *flat_idx += 1; // OpacityStart
1855            dispatch_scroll(child, theme, flat, cx, cy, dx, dy, flat_idx);
1856            *flat_idx += 1; // OpacityEnd
1857        }
1858        View::Spacer => {}
1859    }
1860}
1861
1862#[cfg(test)]
1863mod tests {
1864    use super::*;
1865
1866    #[test]
1867    fn delete_selection_removes_valid_byte_range_and_moves_cursor() {
1868        let mut value = "hello".to_string();
1869        let mut cursor = 4;
1870
1871        assert!(delete_selection(&mut value, &mut cursor, Some((1, 4))));
1872        assert_eq!(value, "ho");
1873        assert_eq!(cursor, 1);
1874    }
1875
1876    #[test]
1877    fn delete_selection_rejects_non_char_boundaries() {
1878        let mut value = "éx".to_string();
1879        let mut cursor = value.len();
1880
1881        assert!(!delete_selection(&mut value, &mut cursor, Some((1, 2))));
1882        assert_eq!(value, "éx");
1883        assert_eq!(cursor, 3);
1884    }
1885
1886    #[test]
1887    fn delete_selection_noop_when_none() {
1888        let mut value = "hello".to_string();
1889        let mut cursor = 3;
1890        assert!(!delete_selection(&mut value, &mut cursor, None));
1891        assert_eq!(value, "hello");
1892        assert_eq!(cursor, 3);
1893    }
1894
1895    // --- normalized_selection ---
1896
1897    #[test]
1898    fn normalized_selection_returns_none_when_equal() {
1899        assert_eq!(normalized_selection(3, 3), None);
1900    }
1901
1902    #[test]
1903    fn normalized_selection_orders_low_high() {
1904        assert_eq!(normalized_selection(5, 2), Some((2, 5)));
1905        assert_eq!(normalized_selection(2, 5), Some((2, 5)));
1906    }
1907
1908    // --- update_selection_for_move ---
1909
1910    #[test]
1911    fn update_selection_extend_creates_selection() {
1912        let mut edit = TextEditState::default();
1913        update_selection_for_move(&mut edit, 0, 3, true);
1914        assert_eq!(edit.selection_anchor, Some(0));
1915        assert_eq!(edit.selection, Some((0, 3)));
1916    }
1917
1918    #[test]
1919    fn update_selection_no_extend_clears_selection() {
1920        let mut edit = TextEditState {
1921            selection: Some((1, 4)),
1922            selection_anchor: Some(1),
1923            ..Default::default()
1924        };
1925        update_selection_for_move(&mut edit, 4, 5, false);
1926        assert_eq!(edit.selection, None);
1927        assert_eq!(edit.selection_anchor, None);
1928    }
1929
1930    #[test]
1931    fn update_selection_extend_preserves_anchor() {
1932        let mut edit = TextEditState {
1933            selection_anchor: Some(2),
1934            ..Default::default()
1935        };
1936        update_selection_for_move(&mut edit, 4, 6, true);
1937        assert_eq!(edit.selection_anchor, Some(2));
1938        assert_eq!(edit.selection, Some((2, 6)));
1939    }
1940
1941    // --- scale_flat ---
1942
1943    fn rect_flat(x: f32, y: f32, w: f32, h: f32, corner_radius: f32) -> FlatView {
1944        use core_glyph::FlatView;
1945        let mut layout = core_glyph::TaffyLayout::default();
1946        layout.location.x = x;
1947        layout.location.y = y;
1948        layout.size.width = w;
1949        layout.size.height = h;
1950        FlatView {
1951            kind: FlatViewKind::Rect { color: core_glyph::Color::WHITE, corner_radius },
1952            layout,
1953        }
1954    }
1955
1956    #[test]
1957    fn scale_flat_scales_position_and_size() {
1958        let flat = vec![rect_flat(10.0, 20.0, 100.0, 50.0, 4.0)];
1959        let scaled = scale_flat(flat, 2.0);
1960        assert_eq!(scaled[0].layout.location.x, 20.0);
1961        assert_eq!(scaled[0].layout.location.y, 40.0);
1962        assert_eq!(scaled[0].layout.size.width, 200.0);
1963        assert_eq!(scaled[0].layout.size.height, 100.0);
1964    }
1965
1966    #[test]
1967    fn scale_flat_scales_corner_radius() {
1968        let flat = vec![rect_flat(0.0, 0.0, 50.0, 50.0, 8.0)];
1969        let scaled = scale_flat(flat, 2.0);
1970        assert!(matches!(scaled[0].kind, FlatViewKind::Rect { corner_radius, .. } if corner_radius == 16.0));
1971    }
1972
1973    #[test]
1974    fn scale_flat_identity_at_1x() {
1975        let flat = vec![rect_flat(5.0, 10.0, 80.0, 40.0, 2.0)];
1976        let scaled = scale_flat(flat, 1.0);
1977        assert_eq!(scaled[0].layout.location.x, 5.0);
1978        assert_eq!(scaled[0].layout.size.width, 80.0);
1979    }
1980
1981    // --- apply_scroll ---
1982
1983    fn scroll_item(x: f32, y: f32, w: f32, h: f32, max_y: f32) -> ScrollItem {
1984        ScrollItem {
1985            cx: x, cy: y, w, h,
1986            offset_x: core_glyph::Signal::new(0.0f32),
1987            offset_y: core_glyph::Signal::new(0.0f32),
1988            max_x: 0.0,
1989            max_y,
1990            enclosing: Vec::new(),
1991        }
1992    }
1993
1994    #[test]
1995    fn apply_scroll_updates_offset_when_cursor_inside() {
1996        let item = scroll_item(0.0, 0.0, 400.0, 300.0, 500.0);
1997        let items = vec![item];
1998        apply_scroll(&items, 200.0, 150.0, 0.0, -20.0);
1999        assert_eq!(items[0].offset_y.get(), 20.0);
2000    }
2001
2002    #[test]
2003    fn apply_scroll_clamps_to_max() {
2004        let item = scroll_item(0.0, 0.0, 400.0, 300.0, 100.0);
2005        let items = vec![item];
2006        apply_scroll(&items, 200.0, 150.0, 0.0, -9999.0);
2007        assert_eq!(items[0].offset_y.get(), 100.0);
2008    }
2009
2010    #[test]
2011    fn apply_scroll_clamps_to_zero() {
2012        let item = scroll_item(0.0, 0.0, 400.0, 300.0, 100.0);
2013        item.offset_y.set(50.0);
2014        let items = vec![item];
2015        apply_scroll(&items, 200.0, 150.0, 0.0, 9999.0);
2016        assert_eq!(items[0].offset_y.get(), 0.0);
2017    }
2018
2019    #[test]
2020    fn apply_scroll_ignores_cursor_outside_bounds() {
2021        let item = scroll_item(0.0, 0.0, 400.0, 300.0, 500.0);
2022        let items = vec![item];
2023        apply_scroll(&items, 500.0, 150.0, 0.0, -20.0); // cursor outside
2024        assert_eq!(items[0].offset_y.get(), 0.0);
2025    }
2026
2027    #[test]
2028    fn apply_scroll_innermost_container_wins() {
2029        let outer = scroll_item(0.0, 0.0, 400.0, 300.0, 500.0);
2030        let inner = scroll_item(50.0, 50.0, 200.0, 150.0, 500.0);
2031        // inner is pushed last so it's "innermost" in reverse iteration
2032        let items = vec![outer, inner];
2033        apply_scroll(&items, 100.0, 100.0, 0.0, -20.0);
2034        assert_eq!(items[1].offset_y.get(), 20.0); // inner absorbed
2035        assert_eq!(items[0].offset_y.get(), 0.0);  // outer untouched
2036    }
2037}
2038
2039// HotApp (hot-reload variant, unchanged from before)
2040
2041#[cfg(feature = "hot-reload")]
2042pub struct HotApp {
2043    loader: hot_glyph::HotLoader,
2044    theme: Theme,
2045    title: String,
2046    width: f64,
2047    height: f64,
2048    state: Option<HotAppState>,
2049}
2050
2051#[cfg(feature = "hot-reload")]
2052struct HotAppState {
2053    window: Arc<Window>,
2054    renderer: Renderer,
2055    cursor_pos: (f32, f32),
2056    frame: u32,
2057}
2058
2059#[cfg(feature = "hot-reload")]
2060impl HotApp {
2061    pub fn run(
2062        src_dir: impl AsRef<std::path::Path>,
2063        lib_path: impl AsRef<std::path::Path>,
2064        package_name: &str,
2065        theme: Theme,
2066        title: impl Into<String>,
2067        width: f64,
2068        height: f64,
2069    ) {
2070        let loader = hot_glyph::HotLoader::new(src_dir.as_ref(), lib_path.as_ref(), package_name);
2071        let event_loop = EventLoop::new().expect("event loop");
2072        event_loop.set_control_flow(winit::event_loop::ControlFlow::Wait);
2073        let mut app = HotApp {
2074            loader,
2075            theme,
2076            title: title.into(),
2077            width,
2078            height,
2079            state: None,
2080        };
2081        event_loop.run_app(&mut app).expect("event loop run");
2082    }
2083}
2084
2085#[cfg(feature = "hot-reload")]
2086impl ApplicationHandler for HotApp {
2087    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
2088        if self.state.is_some() {
2089            return;
2090        }
2091        let window = Arc::new(
2092            event_loop
2093                .create_window(
2094                    Window::default_attributes()
2095                        .with_title(&self.title)
2096                        .with_inner_size(winit::dpi::LogicalSize::new(self.width, self.height)),
2097                )
2098                .expect("window"),
2099        );
2100        let size = window.inner_size();
2101        let ctx = pollster::block_on(GpuContext::new_with_window(Arc::clone(&window)));
2102        let (surface, surface_cfg) =
2103            ctx.create_surface(Arc::clone(&window), size.width.max(1), size.height.max(1));
2104        let renderer = Renderer::new(ctx, surface, surface_cfg);
2105        self.state = Some(HotAppState {
2106            window,
2107            renderer,
2108            cursor_pos: (0.0, 0.0),
2109            frame: 0,
2110        });
2111    }
2112
2113    fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
2114        let Some(state) = &mut self.state else { return };
2115        if self.loader.poll_reload() {
2116            state.window.request_redraw();
2117        }
2118    }
2119
2120    fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
2121        let Some(state) = &mut self.state else { return };
2122        match event {
2123            WindowEvent::CloseRequested => event_loop.exit(),
2124
2125            WindowEvent::Resized(size) => {
2126                state.renderer.resize(size.width, size.height);
2127                state.window.request_redraw();
2128            }
2129
2130            WindowEvent::CursorMoved { position, .. } => {
2131                let scale = state.window.scale_factor() as f32;
2132                let (px, py) = (position.x as f32 / scale, position.y as f32 / scale);
2133                state.cursor_pos = (px, py);
2134                let w = state.renderer.surface_cfg.width as f32 / scale;
2135                let h = state.renderer.surface_cfg.height as f32 / scale;
2136                if let Some(view) = self.loader.build_view(&self.theme) {
2137                    let flat =
2138                        ViewTree::build(view, &self.theme, w, h, &mut state.renderer.measurer());
2139                    let mut changed = false;
2140                    for fv in &flat {
2141                        let l = fv.layout.location.x;
2142                        let t = fv.layout.location.y;
2143                        let hit = px >= l
2144                            && px <= l + fv.layout.size.width
2145                            && py >= t
2146                            && py <= t + fv.layout.size.height;
2147                        if let FlatViewKind::Button {
2148                            on_hover: Some(on_hover),
2149                            ..
2150                        } = &fv.kind
2151                        {
2152                            on_hover(hit);
2153                            changed = true;
2154                        }
2155                    }
2156                    if changed {
2157                        state.window.request_redraw();
2158                    }
2159                }
2160            }
2161
2162            WindowEvent::MouseWheel { delta, .. } => {
2163                let (dx, dy) = match delta {
2164                    MouseScrollDelta::LineDelta(x, y) => (x * 20.0, y * 20.0),
2165                    MouseScrollDelta::PixelDelta(pos) => (pos.x as f32, pos.y as f32),
2166                };
2167                let (cx, cy) = state.cursor_pos;
2168                let scale = state.window.scale_factor() as f32;
2169                let w = state.renderer.surface_cfg.width as f32 / scale;
2170                let h = state.renderer.surface_cfg.height as f32 / scale;
2171                if let Some(view) = self.loader.build_view(&self.theme) {
2172                    let flat =
2173                        ViewTree::build(view, &self.theme, w, h, &mut state.renderer.measurer());
2174                    if let Some(view2) = self.loader.build_view(&self.theme) {
2175                        let mut idx = 0;
2176                        dispatch_scroll(&view2, &self.theme, &flat, cx, cy, dx, dy, &mut idx);
2177                    }
2178                }
2179                if needs_redraw() {
2180                    clear_redraw();
2181                    state.window.request_redraw();
2182                }
2183            }
2184
2185            WindowEvent::MouseInput {
2186                state: ElementState::Pressed,
2187                button: MouseButton::Left,
2188                ..
2189            } => {
2190                let (cx, cy) = state.cursor_pos;
2191                let scale = state.window.scale_factor() as f32;
2192                let w = state.renderer.surface_cfg.width as f32 / scale;
2193                let h = state.renderer.surface_cfg.height as f32 / scale;
2194                if let Some(view) = self.loader.build_view(&self.theme) {
2195                    let flat =
2196                        ViewTree::build(view, &self.theme, w, h, &mut state.renderer.measurer());
2197                    for fv in &flat {
2198                        let l = fv.layout.location.x;
2199                        let t = fv.layout.location.y;
2200                        let hit = cx >= l
2201                            && cx <= l + fv.layout.size.width
2202                            && cy >= t
2203                            && cy <= t + fv.layout.size.height;
2204                        match &fv.kind {
2205                            FlatViewKind::Button { on_click, .. } => {
2206                                if hit {
2207                                    on_click();
2208                                }
2209                            }
2210                            FlatViewKind::TextInput { focused, .. } => {
2211                                focused.set(hit);
2212                                if hit {
2213                                    state.frame = 0;
2214                                }
2215                            }
2216                            _ => {}
2217                        }
2218                    }
2219                }
2220                if needs_redraw() {
2221                    clear_redraw();
2222                }
2223                state.window.request_redraw();
2224            }
2225
2226            WindowEvent::KeyboardInput {
2227                event:
2228                    KeyEvent {
2229                        logical_key,
2230                        state: ElementState::Pressed,
2231                        ..
2232                    },
2233                ..
2234            } => {
2235                let scale = state.window.scale_factor() as f32;
2236                let w = state.renderer.surface_cfg.width as f32 / scale;
2237                let h = state.renderer.surface_cfg.height as f32 / scale;
2238                if let Some(view) = self.loader.build_view(&self.theme) {
2239                    let flat =
2240                        ViewTree::build(view, &self.theme, w, h, &mut state.renderer.measurer());
2241                    for fv in &flat {
2242                        if let FlatViewKind::TextInput {
2243                            value,
2244                            focused,
2245                            on_submit,
2246                            ..
2247                        } = &fv.kind
2248                        {
2249                            if !focused.get() {
2250                                continue;
2251                            }
2252                            let mut s = value.get();
2253                            match &logical_key {
2254                                Key::Named(NamedKey::Backspace) => {
2255                                    if let Some((idx, _)) = s.char_indices().next_back() {
2256                                        s.truncate(idx);
2257                                        value.set(s);
2258                                    }
2259                                }
2260                                Key::Named(NamedKey::Delete) => {
2261                                    value.set(String::new());
2262                                }
2263                                Key::Named(NamedKey::Escape) => {
2264                                    focused.set(false);
2265                                }
2266                                Key::Named(NamedKey::Tab) => {
2267                                    focused.set(false);
2268                                }
2269                                Key::Named(NamedKey::Enter) => {
2270                                    if let Some(f) = on_submit {
2271                                        f(value.get());
2272                                    }
2273                                    focused.set(false);
2274                                }
2275                                Key::Character(ch) => {
2276                                    s.push_str(ch.as_str());
2277                                    value.set(s);
2278                                }
2279                                _ => {}
2280                            }
2281                        }
2282                    }
2283                }
2284                if needs_redraw() {
2285                    clear_redraw();
2286                }
2287                state.window.request_redraw();
2288            }
2289
2290            WindowEvent::RedrawRequested => {
2291                state.frame = state.frame.wrapping_add(1);
2292                let cursor_visible = (state.frame / 30) % 2 == 0;
2293                let scale = state.window.scale_factor() as f32;
2294                let w = state.renderer.surface_cfg.width as f32 / scale;
2295                let h = state.renderer.surface_cfg.height as f32 / scale;
2296                if let Some(view) = self.loader.build_view(&self.theme) {
2297                    let flat =
2298                        ViewTree::build(view, &self.theme, w, h, &mut state.renderer.measurer());
2299                    let any_focused = flat.iter().any(|fv| {
2300                        matches!(&fv.kind, FlatViewKind::TextInput { focused, .. } if focused.get())
2301                    });
2302                    if any_focused {
2303                        state.window.request_redraw();
2304                    }
2305                    let flat = scale_flat(flat, scale);
2306                    state
2307                        .renderer
2308                        .render(&flat, cursor_visible, self.theme.background, scale);
2309                }
2310            }
2311
2312            _ => {}
2313        }
2314    }
2315}