Skip to main content

ravenrs_widgets/
ui.rs

1use std::cell::{Cell, RefCell};
2use std::collections::{HashMap, HashSet};
3use std::fmt;
4use std::rc::Rc;
5
6use ravenrs_core::{
7    BuildCause, BuildCauses, Color, CursorIcon, Effect, Event, FrameWork, Key, Modifiers, PhaseTimes, Point,
8    PointerButton, Primitive, Quad, Rect, Scene, Size, ViewCost,
9};
10use ravenrs_layout::{Align, Axis, LayoutStyle, LayoutTree};
11use ravenrs_reactive::{Observer, State};
12use ravenrs_style::{Semantic, Theme};
13use ravenrs_text::TextSystem;
14
15use crate::View;
16use crate::context::{
17    BuildCx, Drag, DragPhase, Element, Interaction, LocalStore, NodeId, Overlay, Paint, PaintCx, Placement,
18    QuadPaint, ScrollState, Shortcut, TextLeaf, WidgetId,
19};
20use crate::motion::{GhostLayer, MotionEntry, MotionSystem, NodePaint, Sampled, flipped};
21
22mod access;
23
24type Waker = Rc<RefCell<Option<Rc<dyn Fn()>>>>;
25
26/// Lines scrolled per wheel notch are converted to pixels by the platform; this
27/// is the width of the overlay scrollbar.
28const SCROLLBAR_WIDTH: f32 = 4.0;
29/// How long a scrollbar stays after the last scroll, and how long it takes to go.
30const SCROLLBAR_HOLD: f64 = 0.9;
31const SCROLLBAR_FADE: f64 = 0.25;
32const SCROLLBAR_ALPHA: f32 = 0.45;
33
34/// Longest time between frames that animations treat as having really passed.
35const MAX_FRAME_GAP: f64 = 0.25;
36
37/// How much a longer gap counts for: little enough not to skip the animation,
38/// enough that a machine this slow still gets through it.
39const STALL_STEP: f64 = 0.05;
40
41/// Time between paints of animated drawing, in seconds.
42const LIVE_FRAME: f64 = 1.0 / 60.0;
43
44/// How far past a clip a node still gets painted: what it draws around itself, such as
45/// a shadow, can reach into the clip.
46const PAINT_MARGIN: f32 = 64.0;
47
48/// Whether anything `frame` draws, with its margin, can land inside `clip`.
49fn touches(frame: Rect, clip: Rect) -> bool {
50    frame.max_x() + PAINT_MARGIN > clip.x()
51        && frame.x() - PAINT_MARGIN < clip.max_x()
52        && frame.max_y() + PAINT_MARGIN > clip.y()
53        && frame.y() - PAINT_MARGIN < clip.max_y()
54}
55
56/// Work a [`Ui`] has done since it was created. These are exact counts, so a test
57/// can demand that moving the pointer shapes no text or that an animation builds nothing.
58#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
59#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
60pub struct UiStats {
61    /// Times the views ran.
62    pub builds: u64,
63    /// Times sizes and positions were solved.
64    pub layouts: u64,
65    /// Times every element was given its place.
66    pub placements: u64,
67    /// Times the scene was painted.
68    pub paints: u64,
69    /// Times text was shaped.
70    pub shapes: u64,
71    /// Why the views ran.
72    pub causes: BuildCauses,
73}
74
75/// How long each type of view takes, measured while it is switched on.
76pub(crate) struct ViewProfile {
77    clock: Rc<dyn Fn() -> f64>,
78    costs: HashMap<&'static str, ViewCost>,
79    open: Vec<OpenView>,
80}
81
82struct OpenView {
83    name: &'static str,
84    started: f64,
85    elements: usize,
86    shapes: u64,
87    /// What the views inside took, which is not this view's cost.
88    inner_time: f64,
89    inner_elements: usize,
90    inner_shapes: u64,
91}
92
93#[cfg_attr(feature = "live", ravenrs_live::expose)]
94impl ViewProfile {
95    pub(crate) fn enter(&mut self, name: &'static str, elements: usize, shapes: u64) {
96        let started = (self.clock)();
97        self.open.push(OpenView {
98            name,
99            started,
100            elements,
101            shapes,
102            inner_time: 0.0,
103            inner_elements: 0,
104            inner_shapes: 0,
105        });
106    }
107
108    pub(crate) fn leave(&mut self, elements: usize, shapes: u64) {
109        let Some(view) = self.open.pop() else { return };
110        let total = ((self.clock)() - view.started).max(0.0);
111        let made = elements.saturating_sub(view.elements);
112        let shaped = shapes.saturating_sub(view.shapes);
113        if let Some(outer) = self.open.last_mut() {
114            outer.inner_time += total;
115            outer.inner_elements += made;
116            outer.inner_shapes += shaped;
117        }
118        let cost = self
119            .costs
120            .entry(view.name)
121            .or_insert_with(|| ViewCost { name: view.name, ..ViewCost::default() });
122        cost.count += 1;
123        cost.time += std::time::Duration::from_secs_f64((total - view.inner_time).max(0.0));
124        cost.elements += made.saturating_sub(view.inner_elements) as u32;
125        cost.shapes += shaped.saturating_sub(view.inner_shapes) as u32;
126    }
127}
128
129/// The platform-independent UI runtime: owns the root view, reacts to input and
130/// state changes, and produces a [`Scene`] for the renderer.
131///
132/// Platform layers drive it with [`Ui::set_viewport`], [`Ui::handle_event`] and
133/// [`Ui::update`], and register a waker with [`Ui::set_waker`] to schedule a
134/// frame when state changes outside of event handling (timers, async tasks).
135#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
136pub struct Ui {
137    root: Box<dyn View>,
138    theme: State<Theme>,
139    text: TextSystem,
140    layout: LayoutTree<TextLeaf>,
141    elements: Vec<Element>,
142    root_node: Option<NodeId>,
143    overlays: Vec<Overlay>,
144    /// Where focus returns when the open modal layer closes (`Some(None)`: nowhere).
145    modal_return: Option<Option<WidgetId>>,
146    /// Absolute frames, indexed like `elements`.
147    frames: Vec<Rect>,
148    /// The clip in effect where each element is painted.
149    clips: Vec<Rect>,
150    /// Scrollable content size of each element.
151    content: Vec<Size>,
152    /// Elements in painting (back-to-front) order; hit testing walks it backwards.
153    paint_order: Vec<NodeId>,
154    scroll: ScrollState,
155    modifiers: Rc<Cell<Modifiers>>,
156    /// Parent of each element, for hover-within.
157    parents: Vec<Option<NodeId>>,
158    /// The hovered element and its ancestors.
159    hover_chain: Vec<WidgetId>,
160    local: LocalStore,
161    observer: Observer,
162    state_dirty: Rc<Cell<bool>>,
163    ui_waker: crate::UiWaker,
164    waker: Waker,
165    needs_build: bool,
166    /// A view that builds from its own size got a new one in the last layout.
167    viewport_moved: bool,
168    viewport: Size,
169    scale: f32,
170    interaction: Interaction,
171    /// The next placement scrolls the focused element into view (the keyboard
172    /// or code moved focus; a click never asks for this).
173    reveal_focus: bool,
174    dragging: Option<WidgetId>,
175    /// When each scroll container last moved: its scrollbar shows only around that.
176    scrolled_at: HashMap<WidgetId, f64>,
177    /// A scrollbar was on the last frame, so another one has to follow.
178    scrollbar_showing: bool,
179    /// Seconds since start, set by the platform.
180    time: f64,
181    shortcuts: Vec<Shortcut>,
182    activity: Option<crate::context::ActivityObserver>,
183    access_tree_asked: bool,
184    /// Earliest time a widget asked to be rebuilt at.
185    wake: Option<f64>,
186    pointer: Option<Point>,
187    cursor: CursorIcon,
188    scene: Scene,
189    access_map: access::AccessMap,
190    clipboard: crate::Clipboard,
191    sound: crate::Sound,
192    links: crate::Links,
193    full_screen: crate::FullScreen,
194    /// A scroll gliding to an anchor: which container, from and to, and when it began.
195    glide: Option<(WidgetId, f32, f32, f64)>,
196    /// Widgets whose `on_appear` already ran.
197    appeared: std::collections::HashSet<WidgetId>,
198    file_picker: crate::FilePicker,
199    files_over: State<bool>,
200    motion: MotionSystem,
201    motions: Vec<MotionEntry>,
202    /// What scrolling added to each element's frame.
203    shifts: Vec<Point>,
204    /// The visible area of the scroll container around each element (the window for
205    /// those outside any), for pinned elements and scroll-linked effects.
206    views: Vec<Rect>,
207    /// The picture's palette, to notice a theme change.
208    painted_palette: Option<ravenrs_style::Palette>,
209    /// The next build lands colors at once, because the picture crossfades.
210    crossfade: bool,
211    /// The next layout lands frames at once (the window was resized).
212    snap_layout: bool,
213    /// The system asked for reduced motion.
214    system_reduced_motion: bool,
215    /// When animated drawing wants its next paint.
216    next_live: Option<f64>,
217    /// The latest paint had animated drawing.
218    has_live: bool,
219    /// A value from `BuildCx::animated` was moving in the latest build.
220    value_moving: bool,
221    painting_overlays: bool,
222    stats: UiStats,
223    /// Tells the seconds, when someone wants to know how long the phases take.
224    phase_clock: Option<Rc<dyn Fn() -> f64>>,
225    phases: PhaseTimes,
226    /// Why the next build happens, when it was asked for directly.
227    build_cause: Option<BuildCause>,
228    frame: FrameWork,
229    /// Phase times and shapes when the current frame started.
230    frame_start: (PhaseTimes, u64),
231    view_profile: Option<Rc<RefCell<ViewProfile>>>,
232    hover_readers: Rc<RefCell<std::collections::HashSet<WidgetId>>>,
233}
234
235impl fmt::Debug for Ui {
236    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237        f.debug_struct("Ui")
238            .field("elements", &self.elements.len())
239            .field("viewport", &self.viewport)
240            .finish_non_exhaustive()
241    }
242}
243
244#[cfg_attr(feature = "live", ravenrs_live::expose(part = "part1"))]
245impl Ui {
246    /// A runtime for `root` under `theme`, with an 800×600 viewport at scale 1.
247    pub fn new(root: impl View + 'static, theme: Theme) -> Self {
248        let state_dirty = Rc::new(Cell::new(true));
249        let waker: Waker = Rc::new(RefCell::new(None));
250        let (dirty, wake) = (state_dirty.clone(), waker.clone());
251        let observer = Observer::new(move || {
252            dirty.set(true);
253            let callback = wake.borrow().clone();
254            if let Some(callback) = callback {
255                callback();
256            }
257        });
258        Self {
259            root: Box::new(root),
260            theme: State::new(theme),
261            text: TextSystem::new(),
262            layout: LayoutTree::new(),
263            elements: Vec::new(),
264            root_node: None,
265            overlays: Vec::new(),
266            modal_return: None,
267            frames: Vec::new(),
268            clips: Vec::new(),
269            content: Vec::new(),
270            paint_order: Vec::new(),
271            scroll: ScrollState::default(),
272            modifiers: Rc::new(Cell::new(Modifiers::default())),
273            parents: Vec::new(),
274            hover_chain: Vec::new(),
275            local: LocalStore::default(),
276            observer,
277            state_dirty,
278            ui_waker: crate::UiWaker::default(),
279            waker,
280            needs_build: true,
281            viewport_moved: false,
282            viewport: Size::new(800.0, 600.0),
283            scale: 1.0,
284            interaction: Interaction::default(),
285            reveal_focus: false,
286            dragging: None,
287            scrolled_at: HashMap::new(),
288            scrollbar_showing: false,
289            time: 0.0,
290            shortcuts: Vec::new(),
291            activity: None,
292            access_tree_asked: false,
293            wake: None,
294            pointer: None,
295            cursor: CursorIcon::Default,
296            scene: Scene::new(),
297            access_map: access::AccessMap::default(),
298            clipboard: crate::Clipboard::default(),
299            sound: crate::Sound::default(),
300            links: crate::Links::default(),
301            full_screen: crate::FullScreen::default(),
302            glide: None,
303            appeared: std::collections::HashSet::new(),
304            file_picker: crate::FilePicker::default(),
305            files_over: State::new(false),
306            motion: MotionSystem::default(),
307            motions: Vec::new(),
308            shifts: Vec::new(),
309            views: Vec::new(),
310            painted_palette: None,
311            crossfade: false,
312            snap_layout: false,
313            system_reduced_motion: false,
314            next_live: None,
315            has_live: false,
316            value_moving: false,
317            painting_overlays: false,
318            stats: UiStats::default(),
319            phase_clock: None,
320            phases: PhaseTimes::default(),
321            build_cause: Some(BuildCause::First),
322            frame: FrameWork::default(),
323            frame_start: (PhaseTimes::default(), 0),
324            view_profile: None,
325            hover_readers: Rc::default(),
326        }
327    }
328
329    fn ask_build(&mut self, cause: BuildCause) {
330        self.needs_build = true;
331        self.build_cause.get_or_insert(cause);
332    }
333
334    /// Why this rebuild is happening, and clears it.
335    fn take_build_cause(&mut self) -> BuildCause {
336        if let Some(cause) = self.build_cause.take() {
337            cause
338        } else if self.state_dirty.get() {
339            BuildCause::Data
340        } else if self.ui_waker.is_dirty() {
341            BuildCause::Remote
342        } else {
343            BuildCause::Clock
344        }
345    }
346
347    /// What was done since the last call: a platform asks once per frame.
348    pub fn take_frame_work(&mut self) -> FrameWork {
349        let shapes = self.text.stats().shapes;
350        let (phases, shapes_before) = self.frame_start;
351        let now = self.phases;
352        let mut work = std::mem::take(&mut self.frame);
353        work.phases = PhaseTimes {
354            build: now.build - phases.build,
355            layout: now.layout - phases.layout,
356            place: now.place - phases.place,
357            paint: now.paint - phases.paint,
358        };
359        work.shapes = (shapes - shapes_before) as u32;
360        work.elements = self.elements.len() as u32;
361        self.frame_start = (now, shapes);
362        work
363    }
364
365    /// Measures every type of view too, on top of the clock of [`Ui::set_phase_clock`].
366    /// It costs a little per view, so it stays off unless someone is looking.
367    pub fn set_view_profiling(&mut self, on: bool) {
368        self.view_profile = match (&self.phase_clock, on) {
369            (Some(clock), true) => Some(self.view_profile.take().unwrap_or_else(|| {
370                Rc::new(RefCell::new(ViewProfile {
371                    clock: clock.clone(),
372                    costs: HashMap::new(),
373                    open: Vec::new(),
374                }))
375            })),
376            _ => None,
377        };
378    }
379
380    /// What each type of view cost in the latest build, the most expensive first. Empty
381    /// without [`Ui::set_view_profiling`].
382    pub fn view_costs(&self) -> Vec<ViewCost> {
383        let Some(profile) = &self.view_profile else { return Vec::new() };
384        let mut costs: Vec<ViewCost> = profile.borrow().costs.values().cloned().collect();
385        costs.sort_by(|a, b| b.time.cmp(&a.time).then(a.name.cmp(b.name)));
386        costs
387    }
388
389    /// What this runtime has done so far.
390    pub fn stats(&self) -> UiStats {
391        UiStats { shapes: self.text.stats().shapes, ..self.stats }
392    }
393
394    /// Measures the phases with `clock`, which tells the seconds. Without a clock,
395    /// which is the default, measuring costs nothing. WebAssembly has no clock of its
396    /// own, so it is the platform that passes one in.
397    pub fn set_phase_clock(&mut self, clock: Option<Rc<dyn Fn() -> f64>>) {
398        self.phase_clock = clock;
399    }
400
401    /// Time spent in each phase since the clock was set; see [`Ui::set_phase_clock`].
402    pub fn phase_times(&self) -> PhaseTimes {
403        self.phases
404    }
405
406    fn phase_start(&self) -> Option<f64> {
407        self.phase_clock.as_ref().map(|clock| clock())
408    }
409
410    fn phase_elapsed(&self, started: Option<f64>) -> std::time::Duration {
411        match (started, &self.phase_clock) {
412            (Some(started), Some(clock)) => std::time::Duration::from_secs_f64((clock() - started).max(0.0)),
413            _ => std::time::Duration::ZERO,
414        }
415    }
416
417    /// Installs the system clipboard (platform layers do this); the default is
418    /// an in-memory clipboard.
419    pub fn set_clipboard(&mut self, provider: impl crate::ClipboardProvider + 'static) {
420        self.clipboard = crate::Clipboard::new(provider);
421        self.ask_build(BuildCause::System);
422    }
423
424    /// The clipboard widgets copy to and paste from.
425    pub fn clipboard(&self) -> crate::Clipboard {
426        self.clipboard.clone()
427    }
428
429    /// Installs how addresses open (the platform layers do). Until then they are only
430    /// recorded, which tests read through [`crate::Links::opened`].
431    pub fn set_links(&mut self, provider: impl crate::LinkOpener + 'static) {
432        self.links.set_opener(provider);
433    }
434
435    /// The links service views open addresses through.
436    pub fn links(&self) -> crate::Links {
437        self.links.clone()
438    }
439
440    /// Installs how the app enters and leaves full screen (the platform layers do).
441    /// Until then the request is only remembered.
442    pub fn set_full_screen(&mut self, provider: impl crate::FullScreenProvider + 'static) {
443        self.full_screen = crate::FullScreen::new(provider);
444        self.ask_build(BuildCause::System);
445    }
446
447    /// The full screen service views ask through.
448    pub fn full_screen(&self) -> crate::FullScreen {
449        self.full_screen.clone()
450    }
451
452    /// Installs the system's sound (platform layers do this); the default is silence.
453    pub fn set_sound(&mut self, provider: impl crate::SoundProvider + 'static) {
454        self.sound = crate::Sound::new(provider);
455        self.ask_build(BuildCause::System);
456    }
457
458    /// The sound system views play through.
459    pub fn sound(&self) -> crate::Sound {
460        self.sound.clone()
461    }
462
463    /// Installs the system's chooser of files (platform layers do this); the default
464    /// chooses nothing.
465    pub fn set_file_picker(&mut self, provider: impl crate::FilePickerProvider + 'static) {
466        self.file_picker = crate::FilePicker::new(provider);
467        self.ask_build(BuildCause::System);
468    }
469
470    /// Says whether files are being dragged over the window (platform layers do this).
471    pub fn set_files_over(&mut self, over: bool) {
472        self.files_over.set_if_changed(over);
473    }
474
475    /// Hands dropped files to the place under `position`. When no place is there, and the
476    /// screen has only one, it goes there: not every system says where a drop landed.
477    /// Whether anything took them.
478    pub fn drop_files(&mut self, position: Option<Point>, files: Vec<crate::PickedFile>) -> bool {
479        self.files_over.set_if_changed(false);
480        let mut node = position.and_then(|p| self.hit_test(p));
481        while let Some(n) = node {
482            if let Some(handler) = self.elements[n.0 as usize].on_file_drop.clone() {
483                handler(files);
484                return true;
485            }
486            node = self.parents.get(n.0 as usize).copied().flatten();
487        }
488        let mut places = self.elements.iter().filter_map(|e| e.on_file_drop.clone());
489        match (places.next(), places.next()) {
490            (Some(only), None) => {
491                only(files);
492                true
493            }
494            _ => false,
495        }
496    }
497
498    /// Called whenever reactive state the UI depends on changes.
499    pub fn set_waker(&mut self, waker: impl Fn() + 'static) {
500        *self.waker.borrow_mut() = Some(Rc::new(waker));
501    }
502
503    /// A handle that asks for a rebuild from another thread; see [`UiWaker`](crate::UiWaker).
504    pub fn ui_waker(&self) -> crate::UiWaker {
505        self.ui_waker.clone()
506    }
507
508    /// Tells a platform when a [`UiWaker`](crate::UiWaker) fires, from whichever thread that is,
509    /// so it can leave its sleep. Only the first call counts.
510    pub fn set_remote_waker(&mut self, notify: impl Fn() + Send + Sync + 'static) {
511        self.ui_waker.set_notify(Box::new(notify));
512    }
513
514    /// Sets the viewport in logical pixels and the device scale factor.
515    pub fn set_viewport(&mut self, size: Size, scale: f32) {
516        if size != self.viewport || scale != self.scale {
517            self.viewport = size;
518            self.scale = scale;
519            self.ask_build(BuildCause::Viewport);
520            self.snap_layout = true;
521        }
522    }
523
524    /// The viewport size in logical pixels.
525    pub fn viewport(&self) -> Size {
526        self.viewport
527    }
528
529    /// The device scale factor (physical pixels per logical pixel).
530    pub fn scale(&self) -> f32 {
531        self.scale
532    }
533
534    /// The current theme.
535    pub fn theme(&self) -> Theme {
536        *self.theme.peek()
537    }
538
539    /// Switches the theme; components can do the same through `BuildCx::theme_state`.
540    pub fn set_theme(&mut self, theme: Theme) {
541        self.theme.set(theme);
542    }
543
544    /// The theme as reactive state.
545    pub fn theme_state(&self) -> State<Theme> {
546        self.theme.clone()
547    }
548
549    /// Fonts registered here become available to every `Text`.
550    pub fn text_system(&mut self) -> &mut TextSystem {
551        &mut self.text
552    }
553
554    /// True when the next [`Ui::update`] would produce a different scene.
555    pub fn needs_redraw(&self) -> bool {
556        self.needs_rebuild() || self.motion.active || self.live_is_due()
557    }
558
559    fn needs_rebuild(&self) -> bool {
560        self.needs_build
561            || self.state_dirty.get()
562            || self.ui_waker.is_dirty()
563            || self.wake.is_some_and(|w| self.time >= w)
564    }
565
566    fn live_is_due(&self) -> bool {
567        self.next_live.is_some_and(|t| self.time >= t)
568    }
569
570    /// True while an animation is running, which means the next frame will
571    /// differ from this one even if nothing else happens.
572    pub fn is_animating(&self) -> bool {
573        self.motion.active || self.value_moving
574    }
575
576    /// Sets the clock (seconds since the app started). Platforms call it before
577    /// handling events and before each frame. Animations start only once a
578    /// platform drives this clock.
579    ///
580    /// Animations follow their own clock, which skips stalls: after a long pause
581    /// (a hidden window, a slow frame) they continue about where they were
582    /// instead of jumping to the end.
583    pub fn set_time(&mut self, seconds: f64) {
584        let elapsed = seconds - self.time;
585        self.motion.clock += if (0.0..=MAX_FRAME_GAP).contains(&elapsed) { elapsed } else { STALL_STEP };
586        self.motion.enabled = true;
587        self.time = seconds;
588    }
589
590    /// Tells the runtime that the system asks for reduced motion. Things stop
591    /// traveling and springing; colors and opacity may still fade. A theme can
592    /// ask for the same through its `reduced_motion` token.
593    pub fn set_reduced_motion(&mut self, reduced: bool) {
594        if self.system_reduced_motion != reduced {
595            self.system_reduced_motion = reduced;
596            self.ask_build(BuildCause::System);
597        }
598    }
599
600    /// When the UI wants its next frame even without input (seconds, same clock
601    /// as [`Ui::set_time`]). Platforms schedule a timer for it.
602    pub fn next_wake(&self) -> Option<f64> {
603        match (self.wake, self.next_live) {
604            (Some(a), Some(b)) => Some(a.min(b)),
605            (a, b) => a.or(b),
606        }
607    }
608
609    /// True while a pointer drag that started on a draggable element (a slider,
610    /// a splitter) is in progress. Touch platforms use it to tell drags from scrolls.
611    pub fn is_dragging(&self) -> bool {
612        self.dragging.is_some()
613    }
614
615    /// The cursor the platform should show.
616    pub fn cursor(&self) -> CursorIcon {
617        self.cursor
618    }
619
620    /// Rebuilds, lays out and paints as needed, then returns the current scene.
621    pub fn update(&mut self) -> &Scene {
622        let anchor = self.links.take_anchor();
623        if self.needs_rebuild() {
624            self.rebuild();
625            // The tree changed under a stationary pointer: refresh hover once.
626            if self.refresh_hover() {
627                self.rebuild();
628            }
629            // Those views were built for their old size. Their new build can move
630            // others, but a second round of that waits for the next frame.
631            if std::mem::take(&mut self.viewport_moved) {
632                self.rebuild();
633                self.viewport_moved = false;
634            }
635        } else if self.glide.is_some() && anchor.is_none() {
636            self.advance_glide();
637        } else if self.motion.active || self.live_is_due() {
638            // Only animations are due: the tree and its layout are still valid.
639            let _span = tracing::trace_span!("ravenrs::motion_frame").entered();
640            self.paint();
641        }
642        if let Some(name) = anchor {
643            self.start_glide(&name);
644        }
645        &self.scene
646    }
647
648    /// Starts scrolling the container around the anchor `name` so the anchor lands at
649    /// its offset below the top. Under reduced motion it jumps.
650    fn start_glide(&mut self, name: &str) {
651        let Some(i) = self.elements.iter().position(|e| e.anchor.as_ref().is_some_and(|(n, _)| &**n == name))
652        else {
653            tracing::debug!(name, "no anchor of that name");
654            return;
655        };
656        let offset = self.elements[i].anchor.as_ref().map_or(0.0, |a| a.1);
657        let mut node = self.parents.get(i).copied().flatten();
658        while let Some(n) = node {
659            let k = n.0 as usize;
660            if self.elements[k].scroll {
661                let current = self.clamped_offset(k);
662                let to = (current.y + self.frames[i].y() - self.frames[k].y() - offset)
663                    .clamp(0.0, self.max_offset(k).y);
664                let widget = self.elements[k].widget;
665                if self.reduced_motion() {
666                    self.scroll.offsets.insert(widget, Point::new(current.x, to));
667                    self.place_and_paint();
668                } else {
669                    self.glide = Some((widget, current.y, to, self.time));
670                    self.next_live = Some(self.time);
671                }
672                return;
673            }
674            node = self.parents.get(k).copied().flatten();
675        }
676    }
677
678    /// One frame of a glide: eases in and out over about half a second.
679    fn advance_glide(&mut self) {
680        let Some((widget, from, to, start)) = self.glide else { return };
681        const GLIDE: f64 = 0.55;
682        let t = ((self.time - start) / GLIDE).clamp(0.0, 1.0) as f32;
683        let eased = ravenrs_animation::Easing::EaseInOut.sample(t);
684        let x = self.scroll.offsets.get(&widget).map_or(0.0, |o| o.x);
685        self.scroll.offsets.insert(widget, Point::new(x, from + (to - from) * eased));
686        self.scrolled_at.insert(widget, self.time);
687        if t >= 1.0 {
688            self.glide = None;
689        } else {
690            self.next_live = Some(self.time + LIVE_FRAME);
691        }
692        self.place_and_paint();
693        self.refresh_hover();
694    }
695
696    /// Runs the `on_appear` handlers of elements that show for the first time.
697    fn tell_appeared(&mut self) {
698        let mut due = Vec::new();
699        for (i, e) in self.elements.iter().enumerate() {
700            let Some(handler) = &e.on_appear else { continue };
701            if self.appeared.contains(&e.widget) {
702                continue;
703            }
704            let (frame, view) = (self.frames[i], self.views[i]);
705            if frame.max_y() > view.y()
706                && frame.y() < view.max_y()
707                && frame.max_x() > view.x()
708                && frame.x() < view.max_x()
709            {
710                due.push((e.widget, handler.clone()));
711            }
712        }
713        for (widget, handler) in due {
714            self.appeared.insert(widget);
715            handler();
716        }
717    }
718
719    /// The last painted scene.
720    pub fn scene(&self) -> &Scene {
721        &self.scene
722    }
723
724    /// Number of elements produced by the last build.
725    pub fn element_count(&self) -> usize {
726        self.elements.len()
727    }
728
729    fn tell_activity(&self, activity: crate::context::UiActivity<'_>) {
730        if let Some(observer) = self.activity.clone() {
731            observer(activity, self.time);
732        }
733    }
734
735    /// Feeds one input event; returns true when a redraw is needed.
736    pub fn handle_event(&mut self, event: &Event) -> bool {
737        match event {
738            Event::PointerMove { position } => {
739                self.pointer = Some(*position);
740                self.tell_activity(crate::context::UiActivity::PointerMoved(*position));
741                if let Some(widget) = self.dragging {
742                    self.drag(widget, DragPhase::Move, *position);
743                }
744                self.refresh_hover();
745                if let Some(node) = self.hit_test(*position) {
746                    let i = node.0 as usize;
747                    if let Some(handler) = self.elements[i].on_pointer_move.clone() {
748                        handler(*position, self.frames[i]);
749                    }
750                }
751            }
752            Event::PointerDown { position, button: PointerButton::Primary } => {
753                self.pointer = Some(*position);
754                self.tell_activity(crate::context::UiActivity::Clicked(*position));
755                self.reveal_focus = false;
756                self.dismiss_overlays_outside(*position);
757                self.refresh_hover();
758                let target = self.hit_test(*position);
759                let element = target.map(|n| &self.elements[n.0 as usize]);
760                let widget = element.map(|e| e.widget);
761                let draggable = element.is_some_and(|e| e.on_drag.is_some());
762                if self.interaction.pressed != widget {
763                    self.interaction.pressed = widget;
764                    self.ask_build(BuildCause::Press);
765                }
766                // Like the DOM, a click focuses the nearest focusable element at or above the target.
767                let mut node = target;
768                let mut focused = None;
769                while let Some(n) = node {
770                    let e = &self.elements[n.0 as usize];
771                    if e.focus_radius.is_some() {
772                        focused = Some(e.widget);
773                        break;
774                    }
775                    node = self.parents.get(n.0 as usize).copied().flatten();
776                }
777                if self.interaction.focused != focused || self.interaction.focus_visible {
778                    self.interaction.focused = focused;
779                    self.interaction.focus_visible = false;
780                    self.ask_build(BuildCause::Focus);
781                }
782                if let (true, Some(w)) = (draggable, widget) {
783                    self.dragging = Some(w);
784                    self.drag(w, DragPhase::Start, *position);
785                }
786            }
787            Event::PointerUp { position, button: PointerButton::Primary } => {
788                if let Some(w) = self.dragging.take() {
789                    self.drag(w, DragPhase::End, *position);
790                }
791                let pressed = self.interaction.pressed.take();
792                if pressed.is_some() {
793                    self.ask_build(BuildCause::Press);
794                }
795                if let Some(node) = self.hit_test(*position) {
796                    let element = &self.elements[node.0 as usize];
797                    if pressed == Some(element.widget) {
798                        let handler = element.on_click.clone();
799                        if let Some(handler) = handler {
800                            handler();
801                        }
802                    }
803                }
804            }
805            Event::PointerLeave => {
806                self.pointer = None;
807                self.refresh_hover();
808                if self.dragging.is_none() && self.interaction.pressed.take().is_some() {
809                    self.ask_build(BuildCause::Press);
810                }
811            }
812            Event::Scroll { position, delta } => {
813                if self.scroll(*position, *delta) {
814                    if !self.needs_build {
815                        self.place_and_paint();
816                        self.refresh_hover();
817                    }
818                    return true;
819                }
820            }
821            Event::Pinch { position, delta } => {
822                self.pinch(*position, *delta);
823            }
824            Event::PointerDown { position, button: PointerButton::Secondary } => {
825                self.pointer = Some(*position);
826                self.dismiss_overlays_outside(*position);
827                let mut node = self.hit_test(*position);
828                while let Some(n) = node {
829                    if let Some(handler) = self.elements[n.0 as usize].on_context_menu.clone() {
830                        handler(*position);
831                        break;
832                    }
833                    if let Some(handler) = self.elements[n.0 as usize].on_context_menu_in.clone() {
834                        handler(*position, self.frames[n.0 as usize]);
835                        break;
836                    }
837                    node = self.parents.get(n.0 as usize).copied().flatten();
838                }
839            }
840            Event::DoubleClick { position } => {
841                if let Some(node) = self.hit_test(*position) {
842                    let handler = self.elements[node.0 as usize].on_double_click.clone();
843                    if let Some(handler) = handler {
844                        handler();
845                    }
846                }
847            }
848            Event::KeyUp { modifiers, .. } => self.modifiers.set(*modifiers),
849            Event::KeyDown { key, modifiers } => {
850                self.modifiers.set(*modifiers);
851                let handler = self.focused_element().and_then(|e| e.on_key.clone());
852                if let Some(handler) = handler
853                    && handler(key, *modifiers)
854                {
855                    return self.needs_redraw();
856                }
857                let command = modifiers.ctrl || modifiers.meta;
858                // Escape closes what opened last: a menu before anything that listens for it.
859                if *key == Key::Escape
860                    && let Some(dismiss) = self.overlays.iter().rev().find_map(|o| o.on_dismiss.clone())
861                {
862                    dismiss();
863                    return self.needs_redraw();
864                }
865                let shortcut = self.shortcuts.iter().find(|s| {
866                    s.command == command
867                        && (!s.shift || modifiers.shift)
868                        && match (&s.key, key) {
869                            (Key::Character(a), Key::Character(b)) => a.eq_ignore_ascii_case(b),
870                            (a, b) => a == b,
871                        }
872                });
873                if let Some(s) = shortcut.map(|s| s.handler.clone()) {
874                    s();
875                    return self.needs_redraw();
876                }
877                match key {
878                    Key::Tab => self.move_focus(!modifiers.shift),
879                    Key::Enter | Key::Space => {
880                        let handler = self.focused_element().and_then(|e| e.on_click.clone());
881                        if let Some(handler) = handler {
882                            handler();
883                        }
884                    }
885                    Key::Escape => {
886                        let dismiss = self.overlays.iter().rev().find_map(|o| o.on_dismiss.clone());
887                        if let Some(dismiss) = dismiss {
888                            dismiss();
889                        } else if self.interaction.focused.take().is_some() {
890                            self.ask_build(BuildCause::Focus);
891                        }
892                    }
893                    _ => {}
894                }
895            }
896            Event::FocusLost => {
897                self.dragging = None;
898                if self.interaction.pressed.take().is_some() {
899                    self.ask_build(BuildCause::Press);
900                }
901            }
902            _ => {}
903        }
904        self.needs_redraw()
905    }
906
907    fn focused_element(&self) -> Option<&Element> {
908        let w = self.interaction.focused?;
909        self.elements.iter().find(|e| e.widget == w && e.focus_radius.is_some())
910    }
911
912    /// The topmost full-window layer (dialog, sheet, command palette): while it
913    /// is open, focus stays inside it.
914    fn modal_root(&self) -> Option<NodeId> {
915        self.overlays.iter().rev().find(|o| o.placement == Placement::Fill).map(|o| o.root)
916    }
917
918    /// A modal layer and what is above it: a menu opened from it counts as inside it.
919    fn modal_layers(&self, root: NodeId) -> Vec<NodeId> {
920        let from = self.overlays.iter().position(|o| o.root == root).unwrap_or(0);
921        self.overlays[from..].iter().map(|o| o.root).collect()
922    }
923
924    fn is_inside(&self, mut node: NodeId, root: NodeId) -> bool {
925        loop {
926            if node == root {
927                return true;
928            }
929            match self.parents.get(node.0 as usize).copied().flatten() {
930                Some(parent) => node = parent,
931                None => return false,
932            }
933        }
934    }
935
936    /// The first focusable element under `root`, in tree order.
937    fn first_focusable(&self, root: NodeId) -> Option<WidgetId> {
938        let mut stack = vec![root];
939        while let Some(n) = stack.pop() {
940            let e = &self.elements[n.0 as usize];
941            if e.focus_radius.is_some() {
942                return Some(e.widget);
943            }
944            stack.extend(e.children.iter().rev());
945        }
946        None
947    }
948
949    /// Keeps focus inside an open modal layer, moving it in when the layer
950    /// opens and back to where it was when the layer closes.
951    fn trap_focus(&mut self) {
952        match self.modal_root() {
953            Some(root) => {
954                let focused = self.interaction.focused;
955                let layers = self.modal_layers(root);
956                let inside = (0..self.elements.len()).any(|i| {
957                    let e = &self.elements[i];
958                    Some(e.widget) == focused
959                        && e.focus_radius.is_some()
960                        && layers.iter().any(|layer| self.is_inside(NodeId(i as u32), *layer))
961                });
962                if !inside {
963                    self.modal_return.get_or_insert(focused);
964                    // With nothing to focus in the layer, focus still leaves the page behind it.
965                    let first = self.first_focusable(root);
966                    if self.interaction.focused != first {
967                        self.interaction.focused = first;
968                        self.ask_build(BuildCause::Focus);
969                    }
970                }
971            }
972            None => {
973                if let Some(back) = self.modal_return.take() {
974                    self.interaction.focused = back;
975                    self.ask_build(BuildCause::Focus);
976                }
977            }
978        }
979    }
980
981    fn drag(&mut self, widget: WidgetId, phase: DragPhase, position: Point) {
982        let found = self
983            .elements
984            .iter()
985            .enumerate()
986            .find(|(_, e)| e.widget == widget && e.on_drag.is_some())
987            .and_then(|(i, e)| {
988                let parent = self.parents.get(i).copied().flatten();
989                let container = parent.and_then(|p| self.frames.get(p.0 as usize).copied());
990                let frame = self.frames.get(i).copied()?;
991                Some((e.on_drag.clone()?, frame, container.unwrap_or(frame)))
992            });
993        if let Some((handler, frame, container)) = found {
994            handler(Drag { phase, position, frame, container });
995        }
996    }
997
998    fn dismiss_overlays_outside(&mut self, p: Point) {
999        let mut handlers = Vec::new();
1000        for (i, overlay) in self.overlays.iter().enumerate() {
1001            let Some(dismiss) = &overlay.on_dismiss else { continue };
1002            let inside = |n: NodeId| self.frames.get(n.0 as usize).is_some_and(|f| f.contains(p));
1003            let in_anchor = overlay.placement != Placement::Fill && inside(overlay.anchor);
1004            // A menu opened from this layer may stick out of it, and a click there is not outside.
1005            let in_popup_above =
1006                self.overlays[i + 1..].iter().any(|o| o.placement != Placement::Fill && inside(o.root));
1007            if !inside(overlay.root) && !in_anchor && !in_popup_above {
1008                handlers.push(dismiss.clone());
1009            }
1010        }
1011        for h in handlers {
1012            h();
1013        }
1014    }
1015
1016    /// Hands a zoom gesture to the topmost element under `p` that takes it. An
1017    /// open modal layer keeps it from what is behind.
1018    fn pinch(&mut self, p: Point, delta: f32) {
1019        let layers = self.modal_root().map(|root| self.modal_layers(root));
1020        let handler = self.paint_order.iter().rev().find_map(|&node| {
1021            let i = node.0 as usize;
1022            let e = &self.elements[i];
1023            let reachable = layers.as_ref().is_none_or(|l| l.iter().any(|root| self.is_inside(node, *root)));
1024            let inside = self.frames[i].intersect(&self.clips[i]).contains(p);
1025            if reachable && inside { e.on_pinch.clone() } else { None }
1026        });
1027        if let Some(handler) = handler {
1028            handler(delta);
1029        }
1030    }
1031
1032    /// Scrolls the innermost scroll container under `p` that can move in the
1033    /// direction of `delta`. Returns true if something scrolled. An open modal
1034    /// layer keeps it from what is behind, even when its own content is at the end.
1035    fn scroll(&mut self, p: Point, delta: Point) -> bool {
1036        let layers = self.modal_root().map(|root| self.modal_layers(root));
1037        for &node in self.paint_order.iter().rev() {
1038            let i = node.0 as usize;
1039            let e = &self.elements[i];
1040            if !(e.scroll || e.scrolls_with_widget) || !self.frames[i].intersect(&self.clips[i]).contains(p) {
1041                continue;
1042            }
1043            if !layers.as_ref().is_none_or(|l| l.iter().any(|root| self.is_inside(node, *root))) {
1044                continue;
1045            }
1046            // Part of a view that scrolls moves what scrolls, such as frozen row names.
1047            let i = if e.scroll {
1048                i
1049            } else {
1050                let widget = e.widget;
1051                match self.elements.iter().position(|other| other.scroll && other.widget == widget) {
1052                    Some(found) => found,
1053                    None => continue,
1054                }
1055            };
1056            let e = &self.elements[i];
1057            let max = self.max_offset(i);
1058            let current = self.scroll.offsets.get(&e.widget).copied().unwrap_or(Point::ZERO);
1059            let delta = if e.wheel_sideways && delta.x == 0.0 { Point::new(delta.y, 0.0) } else { delta };
1060            let next = Point::new(
1061                if e.scroll_x { (current.x - delta.x).clamp(0.0, max.x) } else { 0.0 },
1062                (current.y - delta.y).clamp(0.0, max.y),
1063            );
1064            if (next.x - current.x).abs() > f32::EPSILON || (next.y - current.y).abs() > f32::EPSILON {
1065                self.scroll.offsets.insert(e.widget, next);
1066                self.scrolled_at.insert(e.widget, self.time);
1067                self.scrollbar_showing = true;
1068                self.next_live = Some(self.time);
1069                let e = &self.elements[i];
1070                if e.rebuild_on_scroll {
1071                    self.ask_build(BuildCause::Scroll);
1072                }
1073                return true;
1074            }
1075        }
1076        false
1077    }
1078
1079    fn max_offset(&self, i: usize) -> Point {
1080        let (content, frame) = (self.content[i], self.frames[i]);
1081        Point::new((content.width - frame.width()).max(0.0), (content.height - frame.height()).max(0.0))
1082    }
1083
1084    fn clamped_offset(&self, i: usize) -> Point {
1085        let max = self.max_offset(i);
1086        let o = self.scroll.offsets.get(&self.elements[i].widget).copied().unwrap_or(Point::ZERO);
1087        Point::new(o.x.clamp(0.0, max.x), o.y.clamp(0.0, max.y))
1088    }
1089
1090    fn move_focus(&mut self, forward: bool) {
1091        let layers = self.modal_root().map(|root| self.modal_layers(root));
1092        let focusable: Vec<WidgetId> = self
1093            .paint_order
1094            .iter()
1095            .filter(|n| layers.as_ref().is_none_or(|l| l.iter().any(|root| self.is_inside(**n, *root))))
1096            .map(|n| &self.elements[n.0 as usize])
1097            .filter(|e| e.focus_radius.is_some())
1098            .map(|e| e.widget)
1099            .collect();
1100        if focusable.is_empty() {
1101            return;
1102        }
1103        let current = self.interaction.focused.and_then(|w| focusable.iter().position(|f| *f == w));
1104        let next = match (current, forward) {
1105            (None, true) => 0,
1106            (None, false) => focusable.len() - 1,
1107            (Some(i), true) => (i + 1) % focusable.len(),
1108            (Some(i), false) => (i + focusable.len() - 1) % focusable.len(),
1109        };
1110        self.interaction.focused = Some(focusable[next]);
1111        self.interaction.focus_visible = true;
1112        self.reveal_focus = true;
1113        self.ask_build(BuildCause::Focus);
1114    }
1115
1116    /// Recomputes the hovered widget and cursor; returns true if hover changed.
1117    fn refresh_hover(&mut self) -> bool {
1118        let target = self.pointer.and_then(|p| self.hit_test(p));
1119        let element = target.map(|n| &self.elements[n.0 as usize]);
1120        let hovered = element.map(|e| e.widget);
1121        self.cursor = element.and_then(|e| e.cursor).unwrap_or_default();
1122        let mut chain = Vec::new();
1123        let mut node = target;
1124        while let Some(n) = node {
1125            chain.push(self.elements[n.0 as usize].widget);
1126            node = self.parents.get(n.0 as usize).copied().flatten();
1127        }
1128        if hovered != self.interaction.hovered || chain != self.hover_chain {
1129            // Only a view that asked about the pointer can look different now. Those that stay
1130            // in the chain keep their answer, unless the pointer moved on to or off them.
1131            let readers = self.hover_readers.borrow();
1132            let matters = |widget: &WidgetId| readers.contains(widget);
1133            let affected = chain.iter().filter(|w| !self.hover_chain.contains(w)).any(matters)
1134                || self.hover_chain.iter().filter(|w| !chain.contains(w)).any(matters)
1135                || hovered.iter().chain(&self.interaction.hovered).any(matters);
1136            drop(readers);
1137            self.interaction.hovered = hovered;
1138            self.hover_chain = chain;
1139            if affected {
1140                self.ask_build(BuildCause::Hover);
1141            }
1142            return affected;
1143        }
1144        false
1145    }
1146
1147    fn hit_test(&self, p: Point) -> Option<NodeId> {
1148        self.paint_order.iter().rev().copied().find(|n| {
1149            let i = n.0 as usize;
1150            self.elements[i].is_interactive()
1151                && self.frames.get(i).zip(self.clips.get(i)).is_some_and(|(f, c)| f.intersect(c).contains(p))
1152        })
1153    }
1154
1155    fn rebuild(&mut self) {
1156        let _span = tracing::trace_span!("ravenrs::frame").entered();
1157        self.needs_build = false;
1158        let cause = self.take_build_cause();
1159        self.build_cause = Some(cause);
1160        self.state_dirty.set(false);
1161        // Before the build: a wake during it must cause another one.
1162        self.ui_waker.clear();
1163        if let Err(e) = self.build() {
1164            tracing::error!(error = %e, "build failed; keeping the previous frame");
1165            return;
1166        }
1167        if let Err(e) = self.compute_layout() {
1168            tracing::error!(error = %e, "layout failed; keeping the previous frame");
1169            return;
1170        }
1171        self.place_and_paint();
1172        self.forget_stale_frames();
1173    }
1174
1175    /// Drops the frames of elements the latest build did not ask about. Measured rows come
1176    /// and go as a list scrolls, and each one would otherwise stay in this map for good;
1177    /// what the list must remember of a row it keeps in its own track.
1178    fn forget_stale_frames(&mut self) {
1179        let asked = |e: &&Element| e.scroll || e.report_frame;
1180        // Counted first so that an ordinary frame allocates nothing here.
1181        if self.scroll.viewports.len() <= self.elements.iter().filter(asked).count() {
1182            return;
1183        }
1184        let live: HashSet<WidgetId> = self.elements.iter().filter(asked).map(|e| e.widget).collect();
1185        self.scroll.viewports.retain(|widget, _| live.contains(widget));
1186    }
1187
1188    fn build(&mut self) -> Result<(), ravenrs_layout::LayoutError> {
1189        let _span = tracing::trace_span!("ravenrs::build").entered();
1190        self.stats.builds += 1;
1191        self.frame.builds += 1;
1192        let cause = self.take_build_cause();
1193        self.stats.causes.add(cause);
1194        self.frame.cause.get_or_insert(cause);
1195        if let Some(profile) = &self.view_profile {
1196            profile.borrow_mut().costs.clear();
1197        }
1198        let started = self.phase_start();
1199        let result = self.build_views();
1200        self.phases.build += self.phase_elapsed(started);
1201        result
1202    }
1203
1204    fn build_views(&mut self) -> Result<(), ravenrs_layout::LayoutError> {
1205        self.layout.clear();
1206        self.elements.clear();
1207        self.root_node = None;
1208        self.overlays.clear();
1209        let placeholder = self.layout.node(LayoutStyle::leaf(), &[])?;
1210        let viewport = self.viewport;
1211        let theme = self.theme.clone();
1212        self.local.begin_build();
1213        let modifiers = self.modifiers.clone();
1214        let clipboard = self.clipboard.clone();
1215        let sound = self.sound.clone();
1216        let links = self.links.clone();
1217        let full_screen = self.full_screen.clone();
1218        let file_picker = self.file_picker.clone();
1219        let files_over = self.files_over.clone();
1220        let ui_waker = self.ui_waker.clone();
1221        let view_profile = self.view_profile.clone();
1222        self.hover_readers.borrow_mut().clear();
1223        let hover_readers = self.hover_readers.clone();
1224        let Self { root, text, layout, elements, interaction, observer, hover_chain, local, scroll, .. } =
1225            self;
1226        let now = self.time;
1227        let motion_clock = self.motion.clock;
1228        let motion_enabled = self.motion.enabled;
1229        let reduced_motion = self.system_reduced_motion || theme.peek().interaction.reduced_motion;
1230        self.motion.reduced = reduced_motion;
1231        let (node, overlays, requests, focus, wake, shortcuts, activity, asked, error, motions, moving) =
1232            observer.track(|| {
1233                let mut cx = BuildCx::new(
1234                    theme,
1235                    viewport,
1236                    text,
1237                    layout,
1238                    elements,
1239                    *interaction,
1240                    hover_chain,
1241                    scroll,
1242                    modifiers,
1243                    local,
1244                    placeholder,
1245                    clipboard,
1246                    ui_waker,
1247                );
1248                cx.now = now;
1249                cx.sound = sound;
1250                cx.links = links;
1251                cx.full_screen = full_screen;
1252                cx.file_picker = file_picker;
1253                cx.files_over = files_over;
1254                cx.motion_clock = motion_clock;
1255                cx.motion_enabled = motion_enabled;
1256                cx.reduced_motion = reduced_motion;
1257                cx.view_profile = view_profile;
1258                cx.hover_readers = hover_readers;
1259                let content = cx.child(&**root);
1260                let style = LayoutStyle {
1261                    width: Some(viewport.width),
1262                    height: Some(viewport.height),
1263                    align: Align::Stretch,
1264                    overflow_hidden: true,
1265                    ..LayoutStyle::stack(Axis::Vertical)
1266                };
1267                let node = cx.container(style, &[content], Paint::None);
1268                (
1269                    node,
1270                    std::mem::take(&mut cx.overlays),
1271                    std::mem::take(&mut cx.scroll_requests),
1272                    cx.focus_request.take(),
1273                    cx.wake.take(),
1274                    std::mem::take(&mut cx.shortcuts),
1275                    cx.activity.take(),
1276                    cx.access_tree_asked,
1277                    cx.error.take(),
1278                    std::mem::take(&mut cx.motions),
1279                    cx.value_moving,
1280                )
1281            });
1282        self.wake = wake;
1283        self.value_moving = moving;
1284        self.shortcuts = shortcuts;
1285        self.activity = activity;
1286        self.access_tree_asked = asked;
1287        if let Some(widget) = focus
1288            && self.interaction.focused != Some(widget)
1289        {
1290            // Focus moved by code keeps its ring only while the user is on the
1291            // keyboard (arrow keys moving through a radio group, say).
1292            self.interaction.focus_visible &= self.interaction.focused.is_some();
1293            self.interaction.focused = Some(widget);
1294            self.reveal_focus = true;
1295            // Rebuild once so the newly focused widget can draw its focus state.
1296            self.ask_build(BuildCause::Focus);
1297        }
1298        for (widget, offset) in requests {
1299            if self.scroll.offsets.insert(widget, offset) != Some(offset) {
1300                // Virtualized content depends on the offset: build again at the new position.
1301                self.ask_build(BuildCause::Scroll);
1302            }
1303        }
1304        self.local.end_build();
1305        if let Some(e) = error {
1306            return Err(e);
1307        }
1308        self.root_node = Some(node);
1309        let mut overlays = overlays;
1310        overlays.sort_by_key(|overlay| !overlay.floating);
1311        self.overlays = overlays;
1312        self.motions = motions;
1313        let palette = self.theme.peek().palette;
1314        self.crossfade = self.motion.enabled && self.painted_palette.is_some_and(|p| p != palette);
1315        let elements = &self.elements;
1316        let paints = |n: NodeId| {
1317            let e = &elements[n.0 as usize];
1318            NodePaint::of(&e.paint, e.text_color)
1319        };
1320        self.motion.sync(&self.motions, paints, self.crossfade);
1321        self.parents.clear();
1322        self.parents.resize(self.elements.len(), None);
1323        for (i, e) in self.elements.iter().enumerate() {
1324            for c in &e.children {
1325                self.parents[c.0 as usize] = Some(NodeId(i as u32));
1326            }
1327        }
1328        // Hovering a floating layer counts as hovering its anchor (hover cards stay open).
1329        for o in &self.overlays {
1330            if o.placement != Placement::Fill {
1331                self.parents[o.root.0 as usize] = Some(o.anchor);
1332            }
1333        }
1334        self.trap_focus();
1335        Ok(())
1336    }
1337
1338    fn compute_layout(&mut self) -> Result<(), ravenrs_layout::LayoutError> {
1339        let _span = tracing::trace_span!("ravenrs::layout").entered();
1340        self.stats.layouts += 1;
1341        let started = self.phase_start();
1342        let result = self.solve_layout();
1343        self.phases.layout += self.phase_elapsed(started);
1344        result
1345    }
1346
1347    fn solve_layout(&mut self) -> Result<(), ravenrs_layout::LayoutError> {
1348        let Some(root) = self.root_node else { return Ok(()) };
1349        let roots = std::iter::once(root).chain(self.overlays.iter().map(|o| o.root)).collect::<Vec<_>>();
1350        for node in roots {
1351            let layout_id = self.elements[node.0 as usize].layout;
1352            self.layout.compute(layout_id, self.viewport, |leaf, input| {
1353                let limit = input.known_width.or(input.available_width.limit());
1354                // Truncated text never wraps: it takes what it needs, up to the limit.
1355                let one_line = leaf.truncate.is_some();
1356                let size = leaf.layout.measure(if one_line { None } else { limit });
1357                let width = match limit {
1358                    Some(limit) if one_line => size.width.ceil().min(limit),
1359                    _ => size.width.ceil(),
1360                };
1361                Size::new(input.known_width.unwrap_or(width), input.known_height.unwrap_or(size.height))
1362            })?;
1363        }
1364        self.content.clear();
1365        self.content.resize(self.elements.len(), Size::ZERO);
1366        for (i, e) in self.elements.iter().enumerate() {
1367            if e.scroll {
1368                self.content[i] = self.layout.content_size(e.layout)?;
1369            }
1370            if e.measure {
1371                let content = self.layout.content_size(e.layout)?;
1372                let before = self.scroll.contents.insert(e.widget, content);
1373                // What is built from the room inside a scroll has to see that room change.
1374                self.viewport_moved |= e.rebuild_on_scroll && before != Some(content);
1375            }
1376        }
1377        Ok(())
1378    }
1379
1380    /// Positions every element (scroll offsets and overlays included) and paints.
1381    fn place_and_paint(&mut self) {
1382        if self.root_node.is_none() {
1383            return;
1384        }
1385        self.place_all();
1386        if self.scroll_focus_into_view() {
1387            self.place_all();
1388        }
1389        self.tell_appeared();
1390        let snap = std::mem::take(&mut self.snap_layout);
1391        for entry in &self.motions {
1392            if let Some(t) = entry.motion.layout {
1393                let i = entry.node.0 as usize;
1394                let unscrolled = self.frames[i].translate(Point::ZERO - self.shifts[i]);
1395                self.motion.sync_frame(entry.key, self.elements[i].widget, unscrolled, t, snap);
1396            }
1397        }
1398        self.paint();
1399    }
1400
1401    /// Gives every element its absolute frame, scroll offsets and overlays included.
1402    fn place_all(&mut self) {
1403        let Some(root) = self.root_node else { return };
1404        self.stats.placements += 1;
1405        let started = self.phase_start();
1406        self.frames.clear();
1407        self.frames.resize(self.elements.len(), Rect::ZERO);
1408        self.shifts.clear();
1409        self.shifts.resize(self.elements.len(), Point::ZERO);
1410        self.views.clear();
1411        self.views.resize(self.elements.len(), Rect::from_origin_size(Point::ZERO, self.viewport));
1412        self.place(root, Point::ZERO);
1413        let overlays = std::mem::take(&mut self.overlays);
1414        for overlay in &overlays {
1415            let size = self.layout.frame(self.elements[overlay.root.0 as usize].layout).map(|f| f.size);
1416            let size = size.unwrap_or(Size::ZERO);
1417            let anchor = self.frames.get(overlay.anchor.0 as usize).copied().unwrap_or(Rect::ZERO);
1418            let origin = overlay_origin(overlay.placement, anchor, size, overlay.gap, self.viewport);
1419            self.place(overlay.root, origin);
1420        }
1421        self.overlays = overlays;
1422        self.phases.place += self.phase_elapsed(started);
1423    }
1424
1425    /// If focus just moved, scrolls each container around the focused element,
1426    /// innermost first, just far enough to show it with its focus ring. Returns
1427    /// true if something scrolled, which makes the frames stale.
1428    fn scroll_focus_into_view(&mut self) -> bool {
1429        if !std::mem::take(&mut self.reveal_focus) {
1430            return false;
1431        }
1432        let focused = self.interaction.focused;
1433        let is_focused = |e: &Element| Some(e.widget) == focused && e.focus_radius.is_some();
1434        let Some(start) = self.elements.iter().position(is_focused) else { return false };
1435        let ix = self.theme().interaction;
1436        let mut target = self.frames[start].inflate(ix.focus_ring_offset + ix.focus_ring_width);
1437        let mut scrolled = false;
1438        let mut node = NodeId(start as u32);
1439        // A floating layer's parent is its anchor, but moving the page behind it shows nothing.
1440        while !self.overlays.iter().any(|o| o.root == node) {
1441            let Some(parent) = self.parents.get(node.0 as usize).copied().flatten() else { break };
1442            node = parent;
1443            let i = node.0 as usize;
1444            if !self.elements[i].scroll {
1445                continue;
1446            }
1447            let (frame, max, offset) = (self.frames[i], self.max_offset(i), self.clamped_offset(i));
1448            let next = Point::new(
1449                if self.elements[i].scroll_x {
1450                    reveal_offset(offset.x, (target.x(), target.max_x()), (frame.x(), frame.max_x()), max.x)
1451                } else {
1452                    offset.x
1453                },
1454                reveal_offset(offset.y, (target.y(), target.max_y()), (frame.y(), frame.max_y()), max.y),
1455            );
1456            if next != offset {
1457                self.scroll.offsets.insert(self.elements[i].widget, next);
1458                // Virtualized content depends on the offset: build again at the new position.
1459                if self.elements[i].rebuild_on_scroll {
1460                    self.ask_build(BuildCause::Scroll);
1461                }
1462                target = target.translate(offset - next);
1463                scrolled = true;
1464            }
1465        }
1466        scrolled
1467    }
1468
1469    /// Sets absolute frames for `node` and its subtree; `origin` is where the
1470    /// node's own frame starts (its layout location is ignored for roots).
1471    fn place(&mut self, node: NodeId, origin: Point) {
1472        let window = Rect::from_origin_size(Point::ZERO, self.viewport);
1473        let mut stack = vec![(node, origin, true, Point::ZERO, window, window)];
1474        while let Some((node, parent_origin, is_root, mut shift, parent_frame, view)) = stack.pop() {
1475            let i = node.0 as usize;
1476            let Ok(local) = self.layout.frame(self.elements[i].layout) else { continue };
1477            let mut frame = if is_root {
1478                Rect::from_origin_size(parent_origin, local.size)
1479            } else {
1480                local.translate(parent_origin)
1481            };
1482            // Sticky: held below the top of the view, and taken away by the parent's bottom.
1483            if let Some(top) = self.elements[i].pin.filter(|_| !is_root) {
1484                let lowest = (parent_frame.max_y() - frame.height()).max(frame.y());
1485                let y = frame.y().max(view.y() + top).min(lowest);
1486                let moved = Point::new(0.0, y - frame.y());
1487                frame = frame.translate(moved);
1488                shift = shift + moved;
1489            }
1490            self.frames[i] = frame;
1491            self.shifts[i] = shift;
1492            self.views[i] = view;
1493            let mut child_origin = frame.origin + self.elements[i].translate;
1494            let mut child_shift = shift;
1495            let mut child_view = view;
1496            if self.elements[i].scroll {
1497                child_view = frame;
1498                let offset = self.clamped_offset(i);
1499                child_origin = child_origin - offset;
1500                child_shift = child_shift - offset;
1501                let widget = self.elements[i].widget;
1502                let before = self.scroll.viewports.insert(widget, frame.size);
1503                self.viewport_moved |= self.elements[i].rebuild_on_scroll && before != Some(frame.size);
1504                // Store the clamped value so builds see where the content really is.
1505                self.scroll.offsets.insert(widget, offset);
1506            } else if self.elements[i].report_frame {
1507                let before = self.scroll.viewports.insert(self.elements[i].widget, frame.size);
1508                self.viewport_moved |= before != Some(frame.size);
1509            }
1510            let element = &self.elements[i];
1511            for child in &element.children {
1512                stack.push((*child, child_origin, false, child_shift, frame, child_view));
1513            }
1514        }
1515    }
1516
1517    fn paint(&mut self) {
1518        let _span = tracing::trace_span!("ravenrs::paint").entered();
1519        self.stats.paints += 1;
1520        self.frame.paints += 1;
1521        let started = self.phase_start();
1522        self.paint_scene();
1523        self.phases.paint += self.phase_elapsed(started);
1524    }
1525
1526    fn paint_scene(&mut self) {
1527        let theme = self.theme();
1528        self.scrollbar_showing = false;
1529        let viewport = Rect::from_origin_size(Point::ZERO, self.viewport);
1530        if self.crossfade {
1531            let mut old = Vec::with_capacity(self.scene.len() + 1);
1532            old.push(Primitive::Quad(Quad::new(viewport, self.scene.clear_color())));
1533            old.extend_from_slice(self.scene.primitives());
1534            self.motion.crossfade(old, viewport, theme.motion.base);
1535            self.crossfade = false;
1536        }
1537        self.painted_palette = Some(theme.palette);
1538        self.scene.clear();
1539        self.scene.set_clear_color(theme.color(Semantic::Canvas));
1540        self.paint_order.clear();
1541        self.clips.clear();
1542        self.clips.resize(self.elements.len(), Rect::ZERO);
1543        self.motion.active = false;
1544        self.has_live = false;
1545        self.painting_overlays = false;
1546        if let Some(root) = self.root_node {
1547            self.paint_node(root, viewport, &theme);
1548        }
1549        self.motion.paint_ghosts(GhostLayer::Main, &mut self.scene);
1550        self.painting_overlays = true;
1551        for i in 0..self.overlays.len() {
1552            let root = self.overlays[i].root;
1553            self.paint_node(root, viewport, &theme);
1554        }
1555        self.motion.paint_ghosts(GhostLayer::Overlay, &mut self.scene);
1556        self.motion.paint_ghosts(GhostLayer::Top, &mut self.scene);
1557        let live = self.has_live && !self.reduced_motion();
1558        self.next_live = (live || self.scrollbar_showing).then_some(self.time + LIVE_FRAME);
1559        self.tell_activity(crate::context::UiActivity::Painted(&self.scene));
1560        if std::mem::take(&mut self.access_tree_asked) {
1561            let tree = self.access_tree();
1562            self.tell_activity(crate::context::UiActivity::Described(&tree));
1563        }
1564    }
1565
1566    /// How far the scroll is along `range` for element `i`, from 0 to 1.
1567    fn progress_of(&self, i: usize, range: crate::ScrollRange) -> f32 {
1568        let mut scene = None;
1569        if range == crate::ScrollRange::Scene {
1570            let mut node = Some(NodeId(i as u32));
1571            while let Some(n) = node {
1572                let k = n.0 as usize;
1573                if let Some(track) = self.elements[k].scroll_track {
1574                    scene = Some((k, track));
1575                    break;
1576                }
1577                node = self.parents.get(k).copied().flatten();
1578            }
1579        }
1580        match scene {
1581            Some((k, track)) => range.progress(self.frames[i], self.views[k], Some((self.frames[k], track))),
1582            None => range.progress(self.frames[i], self.views[i], None),
1583        }
1584    }
1585
1586    /// The effect element `i`'s scroll link gives at the current scroll position.
1587    fn scroll_effect(&self, i: usize) -> Effect {
1588        let Some(link) = &self.elements[i].scroll_link else { return Effect::IDENTITY };
1589        let effect = link.effect_at(self.progress_of(i, link.scroll_range()));
1590        // Asked for less motion, the scroll still fades things, but nothing moves or grows.
1591        if self.reduced_motion() { Effect::IDENTITY.with_opacity(effect.opacity) } else { effect }
1592    }
1593
1594    fn reduced_motion(&self) -> bool {
1595        self.system_reduced_motion || self.theme.peek().interaction.reduced_motion
1596    }
1597
1598    /// The node's own shape, at `frame`.
1599    fn paint_shape(&mut self, i: usize, frame: Rect, quad: Option<QuadPaint>) {
1600        match &self.elements[i].paint {
1601            Paint::None => {}
1602            Paint::Quad(q) => {
1603                let q = quad.unwrap_or(*q);
1604                let mut quad = Quad::new(frame, q.fill).radius(q.corner_radius);
1605                quad.border = q.border;
1606                quad.shadow = q.shadow;
1607                self.scene.push_quad(quad);
1608            }
1609            Paint::Custom(draw) => {
1610                let draw = draw.clone();
1611                draw(frame, &mut self.scene);
1612            }
1613            Paint::Live(draw) => {
1614                let draw = draw.clone();
1615                let time = if self.reduced_motion() { 0.0 } else { self.time };
1616                self.has_live = true;
1617                draw(frame, time, &mut self.scene);
1618            }
1619            Paint::Rich(draw) => {
1620                let draw = draw.clone();
1621                let mut cx = PaintCx { scene: &mut self.scene, text: &mut self.text, scale: self.scale };
1622                draw(frame, &mut cx);
1623            }
1624        }
1625    }
1626
1627    /// The motion values of element `i` for this frame, if it has a motion.
1628    fn sample_motion(&mut self, i: usize) -> Option<(MotionEntry, Sampled)> {
1629        let index = self.elements[i].motion?;
1630        let entry = *self.motions.get(index as usize)?;
1631        let e = &self.elements[i];
1632        let paint = NodePaint::of(&e.paint, e.text_color);
1633        Some((entry, self.motion.sample(entry.key, &paint, self.shifts[i])))
1634    }
1635
1636    /// For an overlay root, its anchor's frame.
1637    fn anchor_of(&self, node: NodeId) -> Option<Rect> {
1638        let overlay = self.overlays.iter().find(|o| o.root == node)?;
1639        self.frames.get(overlay.anchor.0 as usize).copied()
1640    }
1641
1642    fn paint_node(&mut self, node: NodeId, clip: Rect, theme: &Theme) {
1643        let i = node.0 as usize;
1644        self.paint_order.push(node);
1645        self.clips[i] = clip;
1646        let frame = self.frames[i];
1647        let start = self.scene.len();
1648        let sampled = self.sample_motion(i);
1649        let motion = sampled.as_ref().map(|(entry, _)| *entry);
1650        let (quad, text_color, visual, mut effect) = match sampled {
1651            Some((_, s)) => (s.quad, s.text, s.frame.unwrap_or(frame), s.effect),
1652            None => (None, None, frame, Effect::IDENTITY),
1653        };
1654        if let Some((range, cell)) = self.elements[i].progress_out.clone() {
1655            cell.set(self.progress_of(i, range));
1656        }
1657        let mut linked = self.scroll_effect(i);
1658        if let Some(link) = self.elements[i].time_link.clone() {
1659            let time = if self.reduced_motion() { 0.0 } else { self.time };
1660            self.has_live = true;
1661            linked = linked.then(link.effect_at(time, frame));
1662        }
1663        effect = effect.then(linked);
1664        // Off screen, its pixels would be thrown away: what it draws goes nowhere, though its
1665        // children still get their turn, since they can stand outside it. A node under an
1666        // effect may land elsewhere, one leaving is captured for its way out, and one
1667        // without a size may draw around itself.
1668        let shown = effect != Effect::IDENTITY
1669            || motion.is_some_and(|m| m.motion.exit.is_some())
1670            || frame.is_empty()
1671            || touches(visual, clip);
1672        let underlaid = motion.is_some_and(|m| m.motion.underlay);
1673        if shown && !underlaid {
1674            self.paint_shape(i, visual, quad);
1675        }
1676        let exit = motion.filter(|m| m.motion.exit.is_some());
1677        if exit.is_some() {
1678            self.motion.begin_capture();
1679        }
1680        // Everything from here on travels with the node.
1681        let content_start = self.scene.len();
1682        if let Some(color) = self.elements[i].text_color.filter(|_| shown) {
1683            let color = text_color.unwrap_or(color);
1684            let layout_id = self.elements[i].layout;
1685            if let Some(leaf) = self.layout.context_mut(layout_id) {
1686                if let Some((text, attrs)) = &leaf.truncate {
1687                    let fitted = self.text.layout_ellipsized(text, attrs, frame.width() + 0.5);
1688                    fitted.paint(frame.origin, color, &mut self.scene);
1689                } else {
1690                    // Re-break at the final width; the half pixel absorbs rounding.
1691                    leaf.layout.measure(Some(frame.width() + 0.5));
1692                    leaf.layout.paint(frame.origin, color, &mut self.scene);
1693                }
1694            }
1695        }
1696        let clips_children = self.elements[i].clip;
1697        let child_clip = if clips_children { clip.intersect(&frame) } else { clip };
1698        let pushes_clip = clips_children && shown;
1699        if pushes_clip {
1700            self.scene.push_clip(frame);
1701        }
1702        let children = self.elements[i].children.clone();
1703        for &child in &children {
1704            if let Some(m) = self.elements[child.0 as usize].motion
1705                && self.motions.get(m as usize).is_some_and(|e| e.motion.underlay)
1706            {
1707                let c = child.0 as usize;
1708                if let Some((_, s)) = self.sample_motion(c) {
1709                    self.paint_shape(c, s.frame.unwrap_or(self.frames[c]), s.quad);
1710                }
1711            }
1712        }
1713        for child in children {
1714            self.paint_node(child, child_clip, theme);
1715        }
1716        let outside = self.elements[i].scrollbar_outside;
1717        if shown && self.elements[i].scroll && outside == 0.0 {
1718            self.paint_scrollbar(i, theme, 0.0);
1719        }
1720        if pushes_clip {
1721            self.scene.pop_clip();
1722        }
1723        if shown && self.elements[i].scroll && outside != 0.0 {
1724            self.paint_scrollbar(i, theme, outside);
1725        }
1726        let element = &self.elements[i];
1727        if let Some(radius) = element.focus_radius
1728            && shown
1729            && self.interaction.focus_visible
1730            && self.interaction.focused == Some(element.widget)
1731        {
1732            let ix = theme.interaction;
1733            let grow = ix.focus_ring_offset + ix.focus_ring_width;
1734            let ring = Quad::new(frame.inflate(grow), Color::TRANSPARENT)
1735                .radius(radius + grow)
1736                .border(ix.focus_ring_width, theme.color(Semantic::FocusRing));
1737            self.scene.push_quad(ring);
1738        }
1739        let Some(entry) = motion else {
1740            self.scene.apply_effect(start..self.scene.len(), linked, frame.center());
1741            return;
1742        };
1743        let end = self.scene.len();
1744        if !entry.motion.shape_only && visual.origin != frame.origin {
1745            self.scene.apply_effect(
1746                content_start..end,
1747                {
1748                    let d = visual.origin - frame.origin;
1749                    Effect::moved(d.x, d.y)
1750                },
1751                Point::ZERO,
1752            );
1753        }
1754        let mut pivot = visual.center();
1755        let mut flip = false;
1756        if entry.motion.anchored
1757            && let Some(anchor) = self.anchor_of(node)
1758        {
1759            let target = anchor.center();
1760            pivot = Point::new(
1761                target.x.clamp(visual.x(), visual.max_x()),
1762                target.y.clamp(visual.y(), visual.max_y()),
1763            );
1764            flip = visual.center().y < target.y;
1765            effect = flipped(effect, flip);
1766        }
1767        if exit.is_some() {
1768            let layer = if self.painting_overlays { GhostLayer::Overlay } else { GhostLayer::Main };
1769            let primitives = &self.scene.primitives()[start..end];
1770            self.motion.end_capture(entry.key, primitives, start..end, clip, pivot, flip, layer);
1771        }
1772        self.scene.apply_effect(start..end, effect, pivot);
1773    }
1774
1775    /// Draws the scrollbar around a scroll, and lets it fade. `outside` puts it that far
1776    /// past the container's edge, in the room around what it scrolls.
1777    fn paint_scrollbar(&mut self, i: usize, theme: &Theme, outside: f32) {
1778        let frame = self.frames[i];
1779        let (content, max) = (self.content[i], self.max_offset(i));
1780        let offset = self.clamped_offset(i);
1781        let Some(&moved) = self.scrolled_at.get(&self.elements[i].widget) else { return };
1782        let age = self.time - moved;
1783        if age >= SCROLLBAR_HOLD + SCROLLBAR_FADE {
1784            return;
1785        }
1786        self.scrollbar_showing = true;
1787        let fading = ((age - SCROLLBAR_HOLD) / SCROLLBAR_FADE).clamp(0.0, 1.0) as f32;
1788        let alpha = if self.reduced_motion() { SCROLLBAR_ALPHA } else { SCROLLBAR_ALPHA * (1.0 - fading) };
1789        let color = theme.color(Semantic::InkMuted).with_alpha(alpha);
1790        if max.y > 0.5 && frame.height() > 0.0 {
1791            let track = frame.height() - 8.0;
1792            let thumb = (track * frame.height() / content.height).max(24.0).min(track);
1793            let y = frame.y() + 4.0 + (track - thumb) * (offset.y / max.y);
1794            let rect = Rect::new(frame.max_x() - SCROLLBAR_WIDTH - 3.0 + outside, y, SCROLLBAR_WIDTH, thumb);
1795            self.scene.push_quad(Quad::new(rect, color).radius(SCROLLBAR_WIDTH / 2.0));
1796        }
1797        if self.elements[i].scroll_x && max.x > 0.5 && frame.width() > 0.0 {
1798            let track = frame.width() - 8.0;
1799            let thumb = (track * frame.width() / content.width).max(24.0).min(track);
1800            let x = frame.x() + 4.0 + (track - thumb) * (offset.x / max.x);
1801            let rect = Rect::new(x, frame.max_y() - SCROLLBAR_WIDTH - 3.0, thumb, SCROLLBAR_WIDTH);
1802            self.scene.push_quad(Quad::new(rect, color).radius(SCROLLBAR_WIDTH / 2.0));
1803        }
1804    }
1805}
1806
1807/// Top-left corner of an overlay of `size`, kept inside the viewport.
1808/// The scroll offset along one axis that shows `target` inside `view`, moving
1809/// as little as possible. A target larger than the view shows its start.
1810fn reveal_offset(offset: f32, target: (f32, f32), view: (f32, f32), max: f32) -> f32 {
1811    let next = if target.0 < view.0 || target.1 - target.0 > view.1 - view.0 {
1812        offset + target.0 - view.0
1813    } else if target.1 > view.1 {
1814        offset + target.1 - view.1
1815    } else {
1816        offset
1817    };
1818    next.clamp(0.0, max)
1819}
1820
1821/// How far a layer in a corner of the window keeps from its edges.
1822const VIEWPORT_MARGIN: f32 = 16.0;
1823
1824fn overlay_origin(placement: Placement, anchor: Rect, size: Size, gap: f32, viewport: Size) -> Point {
1825    let mut p = match placement {
1826        Placement::Below => Point::new(anchor.x(), anchor.max_y() + gap),
1827        Placement::Above => Point::new(anchor.x(), anchor.y() - gap - size.height),
1828        Placement::Right => Point::new(anchor.max_x() + gap, anchor.y()),
1829        Placement::AboveCenter => {
1830            Point::new(anchor.center().x - size.width / 2.0, anchor.y() - gap - size.height)
1831        }
1832        Placement::RightCenter => Point::new(anchor.max_x() + gap, anchor.center().y - size.height / 2.0),
1833        Placement::LeftCenter => {
1834            Point::new(anchor.x() - gap - size.width, anchor.center().y - size.height / 2.0)
1835        }
1836        Placement::RightEnd => Point::new(anchor.max_x() + gap, anchor.max_y() - size.height),
1837        Placement::LeftStart => Point::new(anchor.x() - gap - size.width, anchor.y()),
1838        Placement::LeftEnd => Point::new(anchor.x() - gap - size.width, anchor.max_y() - size.height),
1839        Placement::Fill => return Point::ZERO,
1840        Placement::At(p) => p,
1841        Placement::BottomRight => Point::new(
1842            viewport.width - size.width - VIEWPORT_MARGIN,
1843            viewport.height - size.height - VIEWPORT_MARGIN,
1844        ),
1845        Placement::BottomLeft => Point::new(VIEWPORT_MARGIN, viewport.height - size.height - VIEWPORT_MARGIN),
1846        Placement::BottomCenter => {
1847            Point::new((viewport.width - size.width) / 2.0, viewport.height - size.height - VIEWPORT_MARGIN)
1848        }
1849        Placement::TopLeft => Point::new(VIEWPORT_MARGIN, VIEWPORT_MARGIN),
1850        Placement::TopCenter => Point::new((viewport.width - size.width) / 2.0, VIEWPORT_MARGIN),
1851        Placement::TopRight => Point::new(viewport.width - size.width - VIEWPORT_MARGIN, VIEWPORT_MARGIN),
1852    };
1853    // A layer at the side changes sides when only the other one has room (a submenu at
1854    // the window's edge), the same way one below goes above.
1855    let left = anchor.x() - gap - size.width;
1856    if placement == Placement::Right && p.x + size.width > viewport.width && left >= 0.0 {
1857        p.x = left;
1858    }
1859    // Flip vertically when there is no room, then clamp.
1860    if placement == Placement::Below && p.y + size.height > viewport.height {
1861        p.y = anchor.y() - gap - size.height;
1862    }
1863    if matches!(placement, Placement::Above | Placement::AboveCenter) && p.y < 0.0 {
1864        p.y = anchor.max_y() + gap;
1865    }
1866    p.x = p.x.clamp(4.0, (viewport.width - size.width - 4.0).max(4.0));
1867    p.y = p.y.clamp(4.0, (viewport.height - size.height - 4.0).max(4.0));
1868    p
1869}
1870
1871#[cfg(test)]
1872mod frame_memory_tests {
1873    use ravenrs_core::Event;
1874
1875    use super::*;
1876    use crate::{Text, VirtualList};
1877
1878    #[test]
1879    fn a_measured_list_does_not_remember_every_row_it_ever_showed() {
1880        let list = VirtualList::new(5_000, |_| Text::new("row")).height(300.0).key(|i| i as u64);
1881        let mut ui = Ui::new(list, Theme::raven_dark());
1882        ui.set_viewport(Size::new(400.0, 400.0), 1.0);
1883        ui.update();
1884        ui.update();
1885        let at_rest = ui.scroll.viewports.len();
1886        assert!(at_rest > 1, "a measured row reports its frame, or this test proves nothing");
1887        for _ in 0..100 {
1888            ui.handle_event(&Event::Scroll {
1889                position: Point::new(100.0, 100.0),
1890                delta: Point::new(0.0, -600.0),
1891            });
1892            ui.update();
1893            ui.update();
1894        }
1895        assert!(ui.scroll.offsets.values().any(|o| o.y > 10_000.0), "the list did scroll");
1896        let scrolled = ui.scroll.viewports.len();
1897        assert!(scrolled <= at_rest + 4, "{scrolled} frames kept, {at_rest} at rest");
1898    }
1899}