frus_widgets/ui.rs
1//! The driver: walks the tree carrying a context (translation + clip), produces the
2//! [`Scene`] and the hit-test maps (click, focus, scroll), and makes it possible to find a
3//! widget by identity so that keyboard/edit events can be routed to it.
4
5use std::hash::{Hash, Hasher};
6
7use frus_core::{
8 Affine, ClipShape, Color, FontWeight, LayerFilter, LayerTransform, Path, Point, Primitive,
9 Rect, Scene, Size, TextStyle,
10};
11use frus_layout::{Layout, NodeId, Overflowing, Side};
12
13use crate::shortcuts::{Intent, KeyStroke};
14
15use crate::barrier::ModalBarrier;
16use crate::dismiss::{DismissPhase, Dismissable};
17use crate::dragdrop::{DragSource, DropZone};
18use crate::hero::{lerp_rect, HeroSpot};
19use crate::interaction::{Status, WidgetId};
20use crate::pageview::PageSnap;
21use crate::physics::{ScrollMetrics, ScrollPhysics};
22use crate::portal::Placement;
23use crate::positioned::Positioning;
24use crate::refresh::Refreshable;
25use crate::relayout::Constraints;
26use crate::runtime::Runtime;
27use crate::theme::Theme;
28use crate::widget::Widget;
29
30/// Thickness of a scrollbar, in pixels (`scrollbar.dart:12`).
31const BAR_SIZE: f32 = 8.0;
32/// Minimum length of a thumb (`scrollbar.dart:15`). It is the tap target: a thumb has to
33/// be catchable by a pointer, whatever fraction of the content the viewport shows.
34const MIN_THUMB: f32 = 48.0;
35/// The thumb's opacity at rest, over `on_surface` (`scrollbar.dart:248`, `:242`). The two
36/// numbers are the reference's, and which one applies follows the surface's own
37/// brightness — this crate's scheme carries no brightness flag, so it is read off the
38/// surface rather than declared.
39/// The gap between the thumb and the edge of the viewport (`scrollbar.dart:14`).
40const BAR_MARGIN: f32 = 2.0;
41const THUMB_ON_DARK: f32 = 0.30;
42const THUMB_ON_LIGHT: f32 = 0.10;
43/// And under a pointer (`scrollbar.dart:245`, `:239`) — a five-fold change on a light
44/// surface, which is what makes a bar you can barely see worth reaching for.
45const THUMB_HOVER_ON_DARK: f32 = 0.65;
46const THUMB_HOVER_ON_LIGHT: f32 = 0.50;
47/// And while it is being dragged (`scrollbar.dart:244`, `:238`), which arrives at once:
48/// the hand is already on it, and a fade would only lag behind the grab.
49const THUMB_DRAG_ON_DARK: f32 = 0.75;
50const THUMB_DRAG_ON_LIGHT: f32 = 0.60;
51/// An Android thumb at rest: the reference's highlight colours, made opaque
52/// (`theme_data.dart:501`, `scrollbar.dart:240`).
53const ANDROID_THUMB_ON_DARK: Color = Color::rgba8(0xCC, 0xCC, 0xCC, 0xFF);
54const ANDROID_THUMB_ON_LIGHT: Color = Color::rgba8(0xBC, 0xBC, 0xBC, 0xFF);
55
56/// One **axis** of a scroll area, as a scrollbar needs to see it: which way it runs, where
57/// it has got to, how far it may go, whether its numbers run backwards, and what the area
58/// itself said about whether it wants a bar at all.
59#[derive(Copy, Clone)]
60struct Bar {
61 vertical: bool,
62 offset: f32,
63 max: f32,
64 reverse: bool,
65 /// The area's own answer, over the application's. See [`crate::Scrollbars`].
66 asked: Option<crate::physics::Scrollbars>,
67 /// Whether the area asked for its bar to stay put instead of fading.
68 visible: Option<bool>,
69}
70
71/// A scrollbar thumb (for hit-testing a drag).
72#[derive(Copy, Clone, Debug)]
73pub struct Scrollbar {
74 /// The scrollable area the bar belongs to.
75 pub id: WidgetId,
76 /// Whether the bar runs vertically; otherwise it is horizontal.
77 pub vertical: bool,
78 /// The thumb's rectangle, in absolute coordinates.
79 pub thumb: Rect,
80 /// Start and length of the track, along the axis.
81 pub track_start: f32,
82 /// The track's length along the axis.
83 pub track_len: f32,
84 /// The thumb's length along the axis.
85 pub thumb_len: f32,
86 /// Offset maximal correspondant.
87 pub max: f32,
88 /// The offsets of a **reversed** axis run the other way along the track: offset 0
89 /// is the far end, so the thumb rests there and a drag towards the start raises the
90 /// number. See [`Scrollable::reverse_y`].
91 pub reverse: bool,
92 /// **How present the bar is**, `0.0..=1.0`. A bar at zero is painted by nothing and
93 /// cannot be grabbed — but it is still here, because a mouse that comes near it
94 /// brings it back.
95 pub opacity: f32,
96 /// The rectangle a **mouse** has to be inside for the bar to notice it: the track,
97 /// widened to take in a tap target's worth of room around the thumb. The reference
98 /// reaches out like this on purpose (`scrollbar.dart:762`) — a bar 8 pixels wide,
99 /// resting at a tenth of an opacity, is not something a hand aims at precisely.
100 pub reach: Rect,
101 /// Whether a pointer can wake and drag it; not under an Android theme, where the bar
102 /// only shows where the page is (milestone 617).
103 pub interactive: bool,
104}
105
106/// The physics an area scrolls with: its own laid over its scroll behaviour's, as the
107/// reference applies a widget's physics to the scroll configuration's (milestone 649).
108fn region_physics<Msg>(widget: &dyn Widget<Msg>, theme: &Theme) -> ScrollPhysics {
109 let platform = crate::ScrollConfiguration::of().scroll_physics(theme);
110 widget
111 .scroll_physics()
112 .map_or(platform, |own| own.apply_to(platform))
113}
114
115/// A scrollable area of the frame: where it is, how far it may scroll, and how it
116/// is meant to behave at its edges.
117///
118/// The registry the shell and the runtime both read. What the area does when it is
119/// dragged, flung or pulled past an edge is settled when it is registered: its own
120/// physics if it asked for some, else what the [`ScrollBehavior`](crate::ScrollBehavior)
121/// in force there says, under the theme it is laid out in (milestone 616).
122#[derive(Copy, Clone, Debug)]
123pub struct Scrollable {
124 /// The scrollable area's identity.
125 pub id: WidgetId,
126 /// The viewport, in absolute coordinates.
127 pub viewport: Rect,
128 /// Largest horizontal offset the content may rest at (0 = nothing to scroll).
129 pub max_x: f32,
130 /// Largest vertical offset the content may rest at.
131 pub max_y: f32,
132 /// The axis the area scrolls along (milestone 649): what an area that is always
133 /// scrollable takes a drag along when nothing overflows.
134 pub axis: crate::scroll::Axis,
135 /// How the area moves at its edges and after a fling: its own, when it asked for
136 /// some, else its scroll behaviour's.
137 pub physics: ScrollPhysics,
138 /// What the area shows when pulled past an edge it will not move beyond, from its
139 /// scroll behaviour; `None` for nothing.
140 pub overscroll: Option<crate::physics::OverscrollIndicator>,
141 /// How a fling on this area is read from the finger's last moves, from its scroll
142 /// behaviour.
143 pub fling: frus_core::VelocityStrategy,
144 /// The [`crate::RefreshIndicator`] area this scrollable sits inside, when there is one.
145 /// Movement refused at its **top** edge feeds that area's pull instead of the
146 /// overscroll glow.
147 pub refresh: Option<WidgetId>,
148 /// Set when this area rests **only on pages** ([`crate::PageView`]): the release
149 /// springs to a page boundary instead of flinging.
150 pub page: Option<PageSnap>,
151 /// Whether the horizontal axis is **reversed** — offset 0 is the right-hand end of
152 /// the content rather than the left.
153 pub reverse_x: bool,
154 /// Whether the vertical axis is **reversed** — offset 0 is the **bottom** of the
155 /// content rather than the top.
156 ///
157 /// Offsets are measured from the end an axis starts at, which is what makes a
158 /// reversed list stay where it is when content arrives: a conversation resting at
159 /// offset 0 is resting at the newest message, and the newest message is wherever the
160 /// end now happens to be. Measuring from the top instead would leave the view
161 /// drifting away from the end every time something was appended, which is the one
162 /// thing this exists to prevent.
163 ///
164 /// Everything the *user* does is unchanged by it. A finger pushes the content the
165 /// way it moves, in either direction; only the arithmetic between that push and the
166 /// number changes sign, in one place.
167 pub reverse_y: bool,
168 /// What the content asked to be kept in view, in the frame's coordinates.
169 pub keep_visible: Option<KeepVisible>,
170 /// The scroll region this one sits **inside**, when there is one.
171 ///
172 /// Scroll areas nest — a sideways strip inside a page that scrolls down — and
173 /// bringing something into view means moving every one of them, from the nearest
174 /// outwards. Identities are hashes and carry no ancestry, so the walk records it.
175 pub host: Option<WidgetId>,
176}
177
178impl Scrollable {
179 /// The tracker a drag on this area reads its fling with.
180 pub fn velocity_tracker(&self) -> frus_core::VelocityTracker {
181 frus_core::VelocityTracker::new(self.fling)
182 }
183
184 /// Whether a finger may move this area at all, resting at `offset`.
185 ///
186 /// The rule the reference states plainly: the user can change the offset **if, and
187 /// only if, there is content outside the viewport to reveal** — or the content is
188 /// already displaced, having shrunk under an offset it no longer reaches. An area that
189 /// fails this takes no drag at all: not a short one, not a refused one, and so no
190 /// end-of-content glow either. An edge effect where there is no edge to meet is a
191 /// statement about the content that is not true.
192 ///
193 /// A [`crate::RefreshIndicator`] listening above is the exception, and the same one the
194 /// reference makes: a list of two items must still pull down to reload.
195 ///
196 /// The physics have the last word (milestone 649): an area the user may never scroll
197 /// takes nothing, and one the user may always scroll takes everything.
198 pub fn accepts_user_offset(&self, offset: (f32, f32)) -> bool {
199 match self.physics.user() {
200 crate::physics::UserScroll::Never => false,
201 crate::physics::UserScroll::Always => true,
202 crate::physics::UserScroll::WhenOverflowing => {
203 self.max_x > 0.0
204 || self.max_y > 0.0
205 || offset.0 != 0.0
206 || offset.1 != 0.0
207 || self.refresh.is_some()
208 }
209 }
210 }
211
212 /// Whether a movement of the user's along the vertical (`down`) or across is this
213 /// area's to take: there is content that way to reveal, or a pull to refresh above
214 /// for a movement down — unless the physics say never, or always along its axis.
215 pub fn takes_user_scroll(&self, down: bool) -> bool {
216 let overflowing = if down {
217 self.max_y > 0.0 || self.refresh.is_some()
218 } else {
219 self.max_x > 0.0
220 };
221 let along = match self.axis {
222 crate::scroll::Axis::Vertical => down,
223 crate::scroll::Axis::Horizontal => !down,
224 crate::scroll::Axis::Both => true,
225 };
226 match self.physics.user() {
227 crate::physics::UserScroll::Never => false,
228 crate::physics::UserScroll::Always => overflowing || along,
229 crate::physics::UserScroll::WhenOverflowing => overflowing,
230 }
231 }
232
233 /// The horizontal axis as the physics sees it, at `offset`.
234 pub fn metrics_x(&self, offset: f32) -> ScrollMetrics {
235 ScrollMetrics::new(offset, self.max_x, self.viewport.width)
236 }
237
238 /// The vertical axis as the physics sees it, at `offset`.
239 pub fn metrics_y(&self, offset: f32) -> ScrollMetrics {
240 ScrollMetrics::new(offset, self.max_y, self.viewport.height)
241 }
242
243 /// Turns a **screen** delta — how far the content was pushed, in pixels — into the
244 /// change it makes to this area's offsets.
245 ///
246 /// Two sign changes, and both have a reason. The content moves *opposite* the
247 /// number: dragging down reveals what is above, which is a smaller offset. And a
248 /// reversed axis counts from the other end, so the same drag raises it instead.
249 ///
250 /// One function rather than a minus sign at each of the five places a delta becomes
251 /// an offset — the wheel, the drag, the release fling, and both axes of each — so
252 /// that a reversed scroll is right in all of them or in none.
253 pub fn offset_delta(&self, screen: (f32, f32)) -> (f32, f32) {
254 (
255 if self.reverse_x { screen.0 } else { -screen.0 },
256 if self.reverse_y { screen.1 } else { -screen.1 },
257 )
258 }
259
260 /// The edge a refusal happened at, given the refused movement **in offset space**.
261 ///
262 /// A negative refusal means the content was pulled towards the axis's start, and
263 /// where that is on screen is exactly what reversing changes: the start of a
264 /// reversed vertical axis is the **bottom**. Without this the glow would flash at
265 /// the far end of a conversation the moment it refused to go further back.
266 pub fn refused_edge(&self, vertical: bool, refused: f32) -> crate::overscroll::GlowEdge {
267 let reversed = if vertical {
268 self.reverse_y
269 } else {
270 self.reverse_x
271 };
272 crate::overscroll::edge_for(vertical, if reversed { -refused } else { refused })
273 }
274
275 /// The edge an axis **starts** at, which is where a pull-to-refresh listens.
276 pub fn start_edge(&self, vertical: bool) -> crate::overscroll::GlowEdge {
277 self.refused_edge(vertical, -1.0)
278 }
279
280 /// The offset that brings `target` fully inside this viewport, and how far the
281 /// content moves to get there.
282 ///
283 /// The **least** movement that does it: a box already in view does not budge, one
284 /// off the near edge comes to that edge and no further. Centring it instead would
285 /// make Tab through a form scroll on every stop, which is motion nobody asked for.
286 ///
287 /// A target **too big** for the viewport gets its leading edge aligned, which falls
288 /// out of asking about that edge first: a tall field half off the top comes down to
289 /// the top rather than up to the bottom, because its start is what you read.
290 ///
291 /// Both halves are returned because they are not the same number. The offset is
292 /// clamped to what this region can actually do, and an outer region has to be told
293 /// how far the target really moved — not how far it was asked to.
294 pub fn reveal(&self, target: Rect, current: (f32, f32)) -> ((f32, f32), (f32, f32)) {
295 let shift = |lo: f32, hi: f32, near: f32, far: f32| {
296 if near < lo {
297 lo - near
298 } else if far > hi {
299 hi - far
300 } else {
301 0.0
302 }
303 };
304 let wanted = (
305 shift(
306 self.viewport.x,
307 self.viewport.x + self.viewport.width,
308 target.x,
309 target.x + target.width,
310 ),
311 shift(
312 self.viewport.y,
313 self.viewport.y + self.viewport.height,
314 target.y,
315 target.y + target.height,
316 ),
317 );
318 // `offset_delta` turns a movement of the content into a movement of the offset,
319 // and it is its own inverse — it negates or it does not — so the same function
320 // turns the offset that was allowed back into the content movement it buys.
321 let delta = self.offset_delta(wanted);
322 let next = (
323 (current.0 + delta.0).clamp(0.0, self.max_x),
324 (current.1 + delta.1).clamp(0.0, self.max_y),
325 );
326 let applied = (next.0 - current.0, next.1 - current.1);
327 (next, self.offset_delta(applied))
328 }
329
330 /// The offset that puts `target` in the **middle** of this viewport, and how far the
331 /// content moves to get there.
332 ///
333 /// The same pair as [`Scrollable::reveal`], and the same clamp — which is what makes
334 /// the first item of a row rest at the start rather than half off it. A target
335 /// bigger than the viewport centres too, which leaves it hanging over both edges,
336 /// and that is the honest answer: there is no offset that shows all of it.
337 pub fn centre(&self, target: Rect, current: (f32, f32)) -> ((f32, f32), (f32, f32)) {
338 let wanted = (
339 (self.viewport.x + self.viewport.width / 2.0) - (target.x + target.width / 2.0),
340 (self.viewport.y + self.viewport.height / 2.0) - (target.y + target.height / 2.0),
341 );
342 let delta = self.offset_delta(wanted);
343 let next = (
344 (current.0 + delta.0).clamp(0.0, self.max_x),
345 (current.1 + delta.1).clamp(0.0, self.max_y),
346 );
347 let applied = (next.0 - current.0, next.1 - current.1);
348 (next, self.offset_delta(applied))
349 }
350
351 /// The offset that brings `keep` into view under its own policy, and how far the
352 /// content moves from **rest** to get there.
353 ///
354 /// Absolute, because [`KeepVisible::rect`] is recorded at rest: the base is offset
355 /// zero, not wherever the region currently is.
356 pub fn keep_offset(&self, keep: KeepVisible) -> ((f32, f32), (f32, f32)) {
357 match keep.centre {
358 true => self.centre(keep.rect, (0.0, 0.0)),
359 false => self.reveal(keep.rect, (0.0, 0.0)),
360 }
361 }
362
363 /// Where the content's leading edge sits, relative to the viewport's, at `offset`.
364 ///
365 /// Normally minus the offset. On a reversed axis the content is anchored to the far
366 /// end, so offset 0 puts the content's *end* against the viewport's — which is also
367 /// the answer for content too short to scroll, where it sits at the bottom of the
368 /// box rather than the top.
369 pub fn content_origin(&self, offset: (f32, f32), content: Size) -> (f32, f32) {
370 let axis = |reverse: bool, offset: f32, viewport: f32, content: f32| {
371 if reverse {
372 viewport - content + offset
373 } else {
374 -offset
375 }
376 };
377 (
378 axis(self.reverse_x, offset.0, self.viewport.width, content.width),
379 axis(
380 self.reverse_y,
381 offset.1,
382 self.viewport.height,
383 content.height,
384 ),
385 )
386 }
387}
388
389/// A box a scroll region has been asked to **keep in view**, and which box it is.
390///
391/// A tab bar wider than its window is what this is for. Declared by the content through
392/// [`Widget::keep_visible`], carried on the region so it survives a repaint boundary
393/// like everything else about the region, and acted on by
394/// [`crate::Runtime::sync_visible`].
395#[derive(Copy, Clone, Debug, PartialEq)]
396pub struct KeepVisible {
397 /// Which box this is. The region acts when this **changes** and not otherwise: the
398 /// box moves as the region scrolls, so a region chasing the box itself would pin
399 /// the content in place and no finger could move it.
400 pub key: u64,
401 /// The box: in the declaring widget's own coordinates when it comes back from
402 /// [`Widget::keep_visible`], and in the frame's — **as it would sit at offset zero**
403 /// — when it reaches the region.
404 ///
405 /// At rest rather than where it is actually drawn, and that is the whole reason the
406 /// answer can be trusted. A box recorded where it is drawn is a box whose position
407 /// already contains the current offset, so the offset that would centre it can only
408 /// be worked out **relative** to wherever the region happens to be — and on the
409 /// first frame, before anything has been retained, there is no wherever to be
410 /// relative to. Recorded at rest, the answer is absolute: the same number on the
411 /// first frame and the hundredth.
412 pub rect: Rect,
413 /// Put it in the **middle** of the window rather than merely inside it. What a tab
414 /// bar wants: a selected tab flush against the edge reads as the end of the row.
415 /// Clamped either way, so the first tab still rests at the start.
416 pub centre: bool,
417}
418
419/// Direction of arrow-key focus navigation.
420#[derive(Clone, Copy, Debug, PartialEq, Eq)]
421pub enum FocusDirection {
422 /// The up arrow: focus moves to the widget above.
423 Up,
424 /// The down arrow: focus moves to the widget below.
425 Down,
426 /// The left arrow: focus moves to the widget on the left.
427 Left,
428 /// The right arrow: focus moves to the widget on the right.
429 Right,
430}
431
432/// A region the mouse enters and leaves, as a frame registered it (milestone 583).
433#[derive(Clone, Copy, Debug)]
434struct HoverEntry {
435 id: WidgetId,
436 /// The part that shows, which the pointer can be over.
437 seen: Rect,
438 /// The whole box, which positions are measured from.
439 whole: Rect,
440 /// The region it is inside, if any.
441 parent: Option<WidgetId>,
442 spec: crate::HoverRegion,
443}
444
445#[derive(Clone)]
446struct Hit<Msg> {
447 id: WidgetId,
448 rect: Rect,
449 /// The message the target emits, or `None` for a target that **swallows** input
450 /// without doing anything — an [`crate::AbsorbPointer`]. Such a target still wins
451 /// the hit test, which is exactly what stops the click reaching what is behind it.
452 msg: Option<Msg>,
453 /// **Inverse** transform (screen → the flat frame) of a transformed subtree
454 /// (`Transform::scale`/`rotate`/a composition): the test point is passed through it to
455 /// get back to the untransformed frame where `rect` lives. `None` = an axis-aligned,
456 /// untransformed rect (the common case).
457 xform: Option<Affine>,
458}
459
460impl<Msg> Hit<Msg> {
461 /// `true` when `point` (in screen coordinates) falls inside the target, taking any
462 /// transform on the subtree into account.
463 fn contains(&self, point: Point) -> bool {
464 let p = self.xform.map_or(point, |inv| inv.apply(point));
465 self.rect.contains(p)
466 }
467}
468
469/// The painted output of a repaint-boundary subtree, cached (see `paintcache.rs`) and
470/// replayed as is on a *hit*. Erased behind a `Box<dyn Any>` in the cache; brought back down
471/// to `Msg` here (one `Msg` instance per app → the `downcast` always succeeds).
472#[derive(Clone)]
473struct BoundaryData<Msg> {
474 prims: Vec<frus_core::Primitive>,
475 hits: Vec<Hit<Msg>>,
476 long_presses: Vec<Hit<Msg>>,
477 dismisses: Vec<Msg>,
478 focusables: Vec<Focusable>,
479 scrollables: Vec<Scrollable>,
480 draggables: Vec<(WidgetId, Rect)>,
481 drag_sources: Vec<DragSource>,
482 drop_zones: Vec<DropZone>,
483 inks: Vec<(WidgetId, Rect)>,
484 /// The detectors that take a drag, in painted order, with the box a press can land in
485 /// (clipped) and the whole box (milestone 582).
486 pans: Vec<(WidgetId, Rect, Rect)>,
487 /// The widgets that hear the pointer's raw events, likewise (milestone 586).
488 pointer_listeners: Vec<(WidgetId, Rect, Rect)>,
489 /// The regions the mouse enters and leaves, in painted order (milestone 583).
490 hover_regions: Vec<HoverEntry>,
491 text_stops: Vec<crate::TextStop>,
492 semantics: Vec<(WidgetId, Rect, frus_core::SemanticsProperties)>,
493 system_ui: Vec<(Rect, crate::SystemUiOverlayStyle)>,
494}
495
496/// Lengths of the builder's collections on entering a boundary: the lower bounds of the
497/// slices to capture on the way out.
498struct Snapshot {
499 scene: usize,
500 hits: usize,
501 long_presses: usize,
502 dismisses: usize,
503 focusables: usize,
504 scrollables: usize,
505 draggables: usize,
506 drag_sources: usize,
507 drop_zones: usize,
508 inks: usize,
509 pans: usize,
510 pointer_listeners: usize,
511 hover_regions: usize,
512 text_stops: usize,
513 semantics: usize,
514 system_ui: usize,
515 overlays: usize,
516 focus_scope_start: Option<usize>,
517}
518
519/// Lower bounds of the **interaction** registries at the start of a transformed composition
520/// (a scale/rotation layer): only what the subtree has just added is re-mapped. Distinct from
521/// [`Snapshot`] (the boundary cache) because it **includes `reorderables`** — those are never
522/// cached but do have to be transformed. See [`transform_interaction_registries`].
523/// Lengths of the registries a [`ModalBarrier`] withholds from, on entering its subtree: the
524/// point each is truncated back to on the way out. Distinct from [`XformBase`] because a
525/// barrier also covers the **scene** and the **scrollbars**, and distinct from [`Snapshot`]
526/// because it covers the registries that are never cached.
527/// What is withdrawn from a layer of an [`crate::IndexedStack`] that is not the shown one:
528/// its paint, its input and its place in the accessibility tree. Not `absorb` — the layer
529/// is not a scrim over what is behind it, it is simply not there for the reader.
530const HIDDEN_LAYER: crate::barrier::ModalBarrier = crate::barrier::ModalBarrier {
531 pointer: true,
532 absorb: false,
533 paint: true,
534 semantics: true,
535};
536
537struct BarrierBase {
538 scene: usize,
539 hits: usize,
540 long_presses: usize,
541 focusables: usize,
542 scrollables: usize,
543 scrollbars: usize,
544 draggables: usize,
545 drag_sources: usize,
546 drop_zones: usize,
547 inks: usize,
548 pans: usize,
549 pointer_listeners: usize,
550 hover_regions: usize,
551 text_stops: usize,
552 reorderables: usize,
553 interactives: usize,
554 semantics: usize,
555}
556
557struct XformBase {
558 hits: usize,
559 long_presses: usize,
560 focusables: usize,
561 scrollables: usize,
562 draggables: usize,
563 drag_sources: usize,
564 drop_zones: usize,
565 inks: usize,
566 pans: usize,
567 pointer_listeners: usize,
568 hover_regions: usize,
569 text_stops: usize,
570 reorderables: usize,
571 semantics: usize,
572}
573
574/// Node count of the subtree **if** it is "plain" — that is, if the boundary and **all** its
575/// descendants take the default walk branch (children in prefix order): not scrollable, not a
576/// navigator, not a virtualised list, no `layout_builder`, not a stack, no overlay, no
577/// continuous animation. The count then follows the walk's rect order **exactly**, which is
578/// what makes the fingerprint and the bit-for-bit replay correct. `None` = a subtree that
579/// cannot be cached (it is repainted in full — the safe fallback).
580fn plain_subtree_len<Msg>(widget: &dyn Widget<Msg>) -> Option<usize> {
581 if widget.continuous()
582 || widget.scroll_content().is_some()
583 || widget.interactive().is_some()
584 || widget.fitted().is_some()
585 || widget.rotated_quarter_turns().is_some()
586 || widget.navigator().is_some()
587 // Only *whether*, never *what*: these two ask if the widget windows its
588 // children at all, and a size it will not read is the honest argument.
589 || widget.virtual_list(Size::ZERO).is_some()
590 || widget.page_view().is_some()
591 || widget.overflow_box().is_some()
592 || widget.constraints_transform().is_some()
593 || widget.layout_builder().is_some()
594 || widget.stack()
595 // Painted where the scroll has got to, not where it was laid out.
596 || widget.sticky_header()
597 || widget.custom_layout().is_some()
598 || widget.overlay().is_some()
599 // A reorderable (a Kanban card, a draggable header): its bounds feed the reorderables
600 // registry, which is not cached — so its subtree is not put in the paint cache.
601 || widget.reorder_index().is_some()
602 // Read by the shell every frame, from the walk (milestone 640).
603 || widget.wants_title_bar()
604 || widget.title_bar_role().is_some()
605 {
606 return None;
607 }
608 let mut n = 1;
609 for child in widget.children() {
610 n += plain_subtree_len(child.as_ref())?;
611 }
612 Some(n)
613}
614
615/// Moves `v[from..to]` after the rest of `v[from..]`: what a sticky header added, after what
616/// its section added (milestone 602).
617fn after<T>(v: &mut [T], from: usize, to: usize) {
618 if to > from && from <= v.len() {
619 v[from..].rotate_left(to - from);
620 }
621}
622
623/// The walk's constraints for a child a [`crate::CustomMultiChildLayout`] lays out. An
624/// exact extent is **forced** when nothing else is only allowed, as a pinned stack layer's
625/// is; beside an axis that is only allowed, it is handed, and a child with a size of its own
626/// there keeps it.
627fn custom_constraints(c: crate::ChildConstraints) -> Constraints {
628 use crate::ChildAxis::{AtMost, Exactly, Free};
629 let axis = |a| match a {
630 Free => (0.0, true, false, false),
631 AtMost(v) => (f32::max(v, 0.0), false, false, true),
632 Exactly(v) => (f32::max(v, 0.0), false, true, false),
633 };
634 let (w, free_x, exact_x, loose_x) = axis(c.width);
635 let (h, free_y, exact_y, loose_y) = axis(c.height);
636 if (exact_x || exact_y) && !loose_x && !loose_y {
637 Constraints::pinned(
638 (!free_x).then_some(w),
639 (!free_y).then_some(h),
640 Size::new(w, h),
641 )
642 } else {
643 Constraints::axes(w, h, [free_x, free_y], [exact_x, exact_y])
644 }
645}
646
647/// Quantises a `[0,1]` float for the fingerprint (independent of tiny binary differences: two
648/// visually identical progresses ⇒ the same fingerprint).
649fn quant(x: f32) -> i32 {
650 (x * 4096.0).round() as i32
651}
652
653/// Adds to `h` everything the paint reads from a `Status`, **except** the time (excluded: a
654/// cacheable boundary holds no `continuous` widget).
655fn hash_status<H: Hasher>(s: &Status, h: &mut H) {
656 (s.interaction as u8).hash(h);
657 s.focused.hash(h);
658 s.cursor.hash(h);
659 s.selection.hash(h);
660 s.composing.hash(h);
661 // Both change what a field floats over itself without touching its selection: a bar
662 // that opens on a selection already made, handles put away by a press.
663 s.handles.hash(h);
664 s.toolbar.hash(h);
665 // What a selection area has selected in this text: the highlight is part of its paint,
666 // and the bar floats from it.
667 s.region.hash(h);
668 s.region_bar.hash(h);
669 s.region_grips.hash(h);
670 quant(s.hover_progress).hash(h);
671 quant(s.focus_progress).hash(h);
672 quant(s.press_progress).hash(h);
673 quant(s.opacity).hash(h);
674 quant(s.value).hash(h);
675 // Sub-region hover (milestone 208): a change of position repaints the highlight.
676 s.hover_cursor.map(|p| (quant(p.x), quant(p.y))).hash(h);
677 // The thumb a drag took is the one that rises and is painted on top.
678 s.thumb.hash(h);
679}
680
681/// The result of building an interface for one frame.
682/// A **shortcut / action scope**: a subtree that binds keystrokes, answers intents, or
683/// both, together with the range of focus stops it contains.
684///
685/// The range is how "focus is inside this subtree" is answered without an ancestor test:
686/// the walk is depth-first, so a subtree's focus stops are contiguous, and the focused
687/// stop's index either falls inside the range or does not.
688struct Scope<Msg> {
689 strokes: Vec<(KeyStroke, Intent)>,
690 callbacks: Vec<(KeyStroke, Msg)>,
691 actions: Vec<(Intent, Msg)>,
692 listeners: Vec<(Intent, Msg)>,
693 /// The focus stops this subtree contains, as indices into `focusables`.
694 range: std::ops::Range<usize>,
695}
696
697/// A **focus stop**: somewhere the keyboard can land, and what the traversal should make
698/// of it.
699#[derive(Clone, Copy, Debug)]
700pub struct Focusable {
701 /// The widget's identity.
702 pub id: WidgetId,
703 /// Its **visible** box this frame: the box it was given, clipped by whatever
704 /// encloses it. Empty when it is scrolled out of sight, which is what a click test
705 /// wants — a tap on empty space must not land on something nobody can see.
706 pub rect: Rect,
707 /// Its box this frame, **unclipped**. What the geometry is really about: where the
708 /// widget is, whether or not any of it is on screen. Arrow navigation places
709 /// candidates by this, and [`Ui::reveal`] works out what to scroll from it.
710 pub bounds: Rect,
711 /// The [`crate::AutofillGroup`] this stop sits in — the form it is part of — when
712 /// there is one (milestone 512).
713 pub form: Option<WidgetId>,
714 /// The nearest enclosing scroll region, when there is one. What could bring this
715 /// stop into view, and the first link of the chain [`Ui::reveal`] walks.
716 pub scroll: Option<WidgetId>,
717 /// Focusable by a click, but **passed over by Tab** — the reference's
718 /// `ExcludeFocusTraversal`. The two are separate questions.
719 pub skip: bool,
720 /// An explicit traversal position, smallest first. `None` means tree order, which is
721 /// where everything sits until someone says otherwise.
722 pub order: Option<f32>,
723 /// The traversal group it belongs to, within which its order is resolved.
724 pub group: Option<WidgetId>,
725 /// Whether it takes the focus when it appears ([`Widget::autofocus`], milestone 605).
726 pub autofocus: bool,
727}
728
729/// The **interface** of one frame: what the walk over a widget tree produced.
730///
731/// Besides the [`Scene`] to paint it holds everything the shell asks of a frame afterwards —
732/// what a click, a long press or a dismissal answers, what can take the focus, which areas
733/// scroll, which widgets can be dragged or dropped on — so that events are routed against
734/// the frame the reader saw. It is made by [`build_ui`] and read until the next frame
735/// replaces it.
736pub struct Ui<Msg = crate::callback::Callback> {
737 scene: Scene,
738 hits: Vec<Hit<Msg>>,
739 /// Long-press targets (id, visible bounds, message).
740 long_presses: Vec<Hit<Msg>>,
741 /// Overlay dismissal messages, from the bottom to the **top**.
742 dismisses: Vec<Msg>,
743 /// The buttons of a selection bar that carry no message, and the editing action each
744 /// performs (milestone 568) — what the shell resolves a press on one to.
745 edit_actions: Vec<(WidgetId, crate::EditAction)>,
746 /// The boxes selection bars were placed in this frame: a press inside one is a press
747 /// **on the bar**, which must not put the selection it acts on away.
748 toolbars: Vec<Rect>,
749 /// Where a press lands **on an overlay** rather than on the pages under it: an overlay's
750 /// box, and the whole window under one that a press outside dismisses (milestone 642).
751 overlay_cover: Vec<Rect>,
752 focusables: Vec<Focusable>,
753 /// **Focus scope**: index of the topmost modal overlay's first focusable —
754 /// Tab/arrows/click-to-focus are trapped from there on (`None` = no modal, every
755 /// focusable takes part).
756 focus_scope_start: Option<usize>,
757 /// (id, viewport, offset max x, offset max y)
758 scrollables: Vec<Scrollable>,
759 scrollbars: Vec<Scrollbar>,
760 draggables: Vec<(WidgetId, Rect)>,
761 drag_sources: Vec<DragSource>,
762 drop_zones: Vec<DropZone>,
763 /// **Inked surfaces**: (id, the box the ink is clipped to). Tracked separately from
764 /// clicking because the splash needs the surface's **whole** box — where the finger
765 /// landed inside it, and how far the circle has to travel to cover it — which a
766 /// click target, recorded as its *visible* part, does not give.
767 inks: Vec<(WidgetId, Rect)>,
768 /// The detectors that take a drag, in painted order, with the box a press can land in
769 /// (clipped) and the whole box (milestone 582).
770 /// The row that wants the title bar's line, where it was laid out, and what the system
771 /// acts on there (milestones 640, 642).
772 title_bar_row: Option<Rect>,
773 title_bar_regions: Vec<(Rect, crate::TitleBarRole)>,
774 pans: Vec<(WidgetId, Rect, Rect)>,
775 /// The widgets that hear the pointer's raw events, likewise (milestone 586).
776 pointer_listeners: Vec<(WidgetId, Rect, Rect)>,
777 /// The regions the mouse enters and leaves, in painted order (milestone 583).
778 hover_regions: Vec<HoverEntry>,
779 /// The texts inside a [`SelectionArea`](crate::SelectionArea), in the order they were
780 /// painted — reading order — each with the area it is in.
781 text_stops: Vec<crate::TextStop>,
782 /// **Reorderables** (column headers, Kanban cards): (id, visible bounds). Tracked
783 /// independently of clicking — a card is not clickable but can still be picked up and
784 /// dropped onto.
785 reorderables: Vec<(WidgetId, Rect)>,
786 /// Interactive viewports (`InteractiveViewer`): (id, the screen viewport). The shell
787 /// routes panning (dragging) and zooming (wheel / pinch) to them.
788 interactives: Vec<(WidgetId, Rect)>,
789 /// Pull-to-refresh areas of the frame, with their configuration.
790 refreshes: Vec<Refreshable>,
791 /// Swipe-to-dismiss items of the frame, with their configuration.
792 dismissables: Vec<Dismissable>,
793 /// Draggable sheets of the frame, with their configuration and the scroll areas
794 /// walked inside each.
795 sheets: Vec<crate::sheet::SheetArea>,
796 wants_animation: bool,
797 /// The accessibility tree: semantic nodes (id, bounds, annotation), in paint order. The
798 /// shell maps it onto AccessKit.
799 semantics: Vec<(WidgetId, Rect, frus_core::SemanticsProperties)>,
800 /// The regions saying what the system bars over them should look like (paint order).
801 system_ui: Vec<(Rect, crate::SystemUiOverlayStyle)>,
802 /// Boxes whose children ran outside them, with the edge and the amount.
803 overflows: Vec<Overflowing>,
804 /// Shortcut and action scopes, in the order the walk closed them — innermost first
805 /// among any that overlap, which is the order the resolution wants.
806 scopes: Vec<Scope<Msg>>,
807 /// Keystroke listeners, with the focus stops they cover.
808 #[allow(clippy::type_complexity)]
809 listeners: Vec<(
810 std::ops::Range<usize>,
811 std::rc::Rc<dyn Fn(KeyStroke) -> Option<Msg>>,
812 )>,
813}
814
815impl<Msg: Clone> Ui<Msg> {
816 /// The scene to hand to the renderer.
817 /// Boxes in this frame whose children did not fit inside them.
818 ///
819 /// Empty is the normal answer. A non-empty one means content is being drawn outside
820 /// its parent — invisible where it is clipped, unhittable where it leaves the window,
821 /// and silent either way until something asks. That silence is what let a task row's
822 /// delete button sit off-screen through three milestones.
823 pub fn overflows(&self) -> &[Overflowing] {
824 &self.overflows
825 }
826
827 /// The scrollbars this frame drew, and therefore the ones a pointer may drag. Empty
828 /// where the platform draws none — see [`crate::Scrollbars`].
829 pub fn scrollbars(&self) -> &[Scrollbar] {
830 &self.scrollbars
831 }
832
833 /// The [`Scene`] this frame painted.
834 pub fn scene(&self) -> &Scene {
835 &self.scene
836 }
837
838 /// The frame's **accessibility tree**: every meaningful node with its bounds on screen and
839 /// its annotation ([`frus_core::SemanticsProperties`]). Paint order (= reading order). The shell
840 /// pushes it to AccessKit.
841 pub fn semantics(&self) -> &[(WidgetId, Rect, frus_core::SemanticsProperties)] {
842 &self.semantics
843 }
844
845 /// What the system bars should look like over this frame (#46): the regions under
846 /// `status_bar` answer for the status bar and those under `navigation_bar` for the
847 /// other, the one drawn last first and field by field. What none states is left
848 /// `None`, for [`crate::SystemUiOverlayStyle::resolve`] to take from the theme.
849 ///
850 /// The shell picks the two points: the first row of the screen's content under the
851 /// status bar, and the last one above the navigation bar.
852 pub fn system_ui_style(
853 &self,
854 status_bar: Point,
855 navigation_bar: Point,
856 ) -> crate::SystemUiOverlayStyle {
857 crate::system_ui::style_under(&self.system_ui, status_bar, navigation_bar)
858 }
859
860 /// `true` when a widget animates continuously (the framework must redraw).
861 /// Whether **this tree's widgets** want the next frame: a spinner turning, an ink
862 /// ripple spreading, a pull being held.
863 ///
864 /// It answers for the widgets and nothing else. A fetch still in flight also owes the
865 /// screen a frame — see [`frus_core::images_in_flight`] — and that is the *process's*
866 /// business, asked by whoever drives the loop. It used to be folded in here, which
867 /// made this method's answer depend on what an unrelated part of the program happened
868 /// to be doing: in the test suite, where everything shares one process, a refresh area
869 /// with nothing pulled asked for frames whenever another test was loading an image
870 /// (milestone 411).
871 pub fn wants_animation(&self) -> bool {
872 self.wants_animation
873 }
874
875 /// Whether something in this frame wants the title bar's line (milestone 640).
876 pub fn wants_title_bar(&self) -> bool {
877 self.title_bar_row.is_some()
878 }
879
880 /// Where the row that wants the title bar's line was laid out — a
881 /// [`WindowMenuBar`](crate::WindowMenuBar)'s, the window's and not a page's (milestone
882 /// 642). What the shell does to the pages, the back gesture at the window's edge, does
883 /// not start on it.
884 pub fn title_bar_row(&self) -> Option<Rect> {
885 self.title_bar_row
886 }
887
888 /// What the system acts on on the title bar's line: the window's icon and its buttons,
889 /// where they were painted (milestone 640).
890 pub fn title_bar_regions(&self) -> &[(Rect, crate::TitleBarRole)] {
891 &self.title_bar_regions
892 }
893
894 /// Every box a press can land on, in painted order: what on the title bar's line is the
895 /// application's, rather than the system's to move the window by (milestone 640).
896 pub fn hit_rects(&self) -> impl Iterator<Item = Rect> + '_ {
897 self.hits.iter().map(|hit| hit.rect)
898 }
899
900 /// Identity of the topmost clickable widget containing `point`.
901 pub fn hit(&self, point: Point) -> Option<WidgetId> {
902 self.hits
903 .iter()
904 .rev()
905 .find(|hit| hit.contains(point))
906 .map(|hit| hit.id)
907 }
908
909 /// The message tied to a given clickable widget. `None` for a target that swallows
910 /// input without emitting anything.
911 ///
912 /// It is always paired with [`hit`](Self::hit), so the two have to agree on which
913 /// target an identity means: every registered target therefore has one of its own,
914 /// the dismissal barrier below an overlay included.
915 pub fn msg_for(&self, id: WidgetId) -> Option<Msg> {
916 self.hits
917 .iter()
918 .find(|hit| hit.id == id)
919 .and_then(|hit| hit.msg.clone())
920 }
921
922 /// The editing action the button `id` of a selection bar performs, for a button that
923 /// carries no message (Cut, Copy, Paste, Select all — see [`crate::EditAction`]). The
924 /// shell holds the clipboard and the editing state, so it is the shell that performs it.
925 pub fn edit_action_for(&self, id: WidgetId) -> Option<crate::EditAction> {
926 self.edit_actions
927 .iter()
928 .find(|(button, _)| *button == id)
929 .map(|(_, action)| *action)
930 }
931
932 /// Whether the target `id` is one **on** a selection bar shown this frame — the click of
933 /// an assistive technology names its target by identity, not by where a pointer is, so
934 /// this is the identity-side counterpart of [`toolbar_contains`](Self::toolbar_contains).
935 pub fn hit_is_on_toolbar(&self, id: WidgetId) -> bool {
936 self.hits
937 .iter()
938 .rev()
939 .find(|hit| hit.id == id)
940 .is_some_and(|hit| {
941 self.toolbars.iter().any(|bar| {
942 bar.contains(Point::new(
943 hit.rect.x + hit.rect.width * 0.5,
944 hit.rect.y + hit.rect.height * 0.5,
945 ))
946 })
947 })
948 }
949
950 /// Whether `point` is inside a selection bar shown this frame — which a press must not
951 /// treat as a press elsewhere.
952 pub fn toolbar_contains(&self, point: Point) -> bool {
953 self.toolbars.iter().any(|bar| bar.contains(point))
954 }
955
956 /// Whether a press at `point` lands **on an overlay** — a menu, a dialog, a drawer, or
957 /// the window under one that a press outside dismisses — rather than on the pages under
958 /// it (milestone 642). What the shell does to the pages, the back gesture at the
959 /// window's edge, does not start there: in the reference that gesture is the page's, and
960 /// an overlay is above the page.
961 pub fn over_overlay(&self, point: Point) -> bool {
962 self.overlay_cover.iter().any(|r| r.contains(point))
963 }
964
965 /// Dismissal message of the **topmost** overlay (for Escape).
966 pub fn top_dismiss(&self) -> Option<Msg> {
967 self.dismisses.last().cloned()
968 }
969
970 /// **Long-press** message of the topmost target containing `point`.
971 pub fn long_press_at(&self, point: Point) -> Option<Msg> {
972 self.long_presses
973 .iter()
974 .rev()
975 .find(|hit| hit.contains(point))
976 .and_then(|hit| hit.msg.clone())
977 }
978
979 /// The frame's **pull-to-refresh areas**, with the configuration each was built
980 /// with. The shell steps them and reads their `refreshing` flag from here.
981 pub fn refresh_areas(&self) -> &[Refreshable] {
982 &self.refreshes
983 }
984
985 /// The frame's **dismissible items**, with the configuration each was built with.
986 pub fn dismissables(&self) -> &[Dismissable] {
987 &self.dismissables
988 }
989
990 /// The frame's **sheets**, with the configuration each was built with and the scroll
991 /// areas walked inside it.
992 pub fn sheets(&self) -> &[crate::sheet::SheetArea] {
993 &self.sheets
994 }
995
996 /// The sheet whose panel is under `point`, the last one walked winning — which is
997 /// the one painted on top.
998 pub fn sheet_at(&self, point: Point) -> Option<&crate::sheet::SheetArea> {
999 self.sheets.iter().rev().find(|sheet| sheet.contains(point))
1000 }
1001
1002 /// The sheet `id`, when it is in the frame.
1003 pub fn sheet(&self, id: WidgetId) -> Option<&crate::sheet::SheetArea> {
1004 self.sheets.iter().find(|sheet| sheet.id == id)
1005 }
1006
1007 /// The sheet the scroll area `area` was walked inside, when there is one — the
1008 /// innermost, should sheets nest, since an outer one counts the inner one's areas too.
1009 pub fn sheet_holding(&self, area: WidgetId) -> Option<&crate::sheet::SheetArea> {
1010 self.sheets
1011 .iter()
1012 .filter(|sheet| sheet.areas.contains(&area))
1013 .min_by_key(|sheet| sheet.areas.len())
1014 }
1015
1016 /// The topmost dismissible item containing `point` — the candidate a press has to
1017 /// weigh against the scrollable underneath it.
1018 pub fn dismissable_at(&self, point: Point) -> Option<Dismissable> {
1019 self.dismissables
1020 .iter()
1021 .rev()
1022 .find(|item| item.rect.contains(point))
1023 .copied()
1024 }
1025
1026 /// Every text that took part in a selection area this frame, in the order it was painted.
1027 pub fn text_stops(&self) -> &[crate::TextStop] {
1028 &self.text_stops
1029 }
1030
1031 /// The texts a [`SelectionArea`](crate::SelectionArea) holds, in reading order.
1032 pub fn text_stops_in(&self, area: WidgetId) -> impl Iterator<Item = &crate::TextStop> {
1033 self.text_stops.iter().filter(move |stop| stop.area == area)
1034 }
1035
1036 /// The topmost text in a selection area whose box contains `point`.
1037 pub fn text_stop_at(&self, point: Point) -> Option<crate::TextStop> {
1038 self.text_stops
1039 .iter()
1040 .rev()
1041 .find(|stop| stop.rect.contains(point))
1042 .copied()
1043 }
1044
1045 /// The text of `area` **nearest** `point`: the one under it, or the one closest to it —
1046 /// which is what a drag that has left the words still points at. Distance across lines
1047 /// counts four times what it does along one, so a point beside a line is nearest that
1048 /// line and not the one above it; ties go to the later text, so that a drag past the end
1049 /// of a paragraph ends at its last line.
1050 pub fn nearest_text_stop(&self, area: WidgetId, point: Point) -> Option<crate::TextStop> {
1051 let distance = |rect: Rect| {
1052 let dx = (rect.x - point.x)
1053 .max(point.x - (rect.x + rect.width))
1054 .max(0.0);
1055 let dy = (rect.y - point.y)
1056 .max(point.y - (rect.y + rect.height))
1057 .max(0.0);
1058 dy * 4.0 + dx
1059 };
1060 let mut best: Option<(f32, crate::TextStop)> = None;
1061 for stop in self.text_stops_in(area) {
1062 let d = distance(stop.rect);
1063 if best.is_none_or(|(so_far, _)| d <= so_far) {
1064 best = Some((d, *stop));
1065 }
1066 }
1067 best.map(|(_, stop)| stop)
1068 }
1069
1070 /// Topmost focusable widget containing `point`: (id, its bounds).
1071 pub fn focus_hit(&self, point: Point) -> Option<(WidgetId, Rect)> {
1072 self.focus_pool()
1073 .iter()
1074 .rev()
1075 .find(|f| f.rect.contains(point))
1076 .map(|f| (f.id, f.rect))
1077 }
1078
1079 /// The **participating** focusables: those of the topmost modal scope when there is one
1080 /// (a focus trap), otherwise all of them.
1081 fn focus_pool(&self) -> &[Focusable] {
1082 match self.focus_scope_start {
1083 Some(start) => &self.focusables[start.min(self.focusables.len())..],
1084 None => &self.focusables,
1085 }
1086 }
1087
1088 /// Nearest focusable in the given **direction** (arrow navigation, a geometric policy):
1089 /// among the focusables whose centre lies on the right side, it minimises the distance
1090 /// along the main axis with a penalty on the cross-axis offset. `None` when there is
1091 /// nothing in that direction.
1092 pub fn focus_directional(
1093 &self,
1094 current: WidgetId,
1095 direction: FocusDirection,
1096 ) -> Option<WidgetId> {
1097 // The starting point may be outside the scope (focus from before it opened); the
1098 // **candidates**, though, are trapped inside it.
1099 let from = self
1100 .focusables
1101 .iter()
1102 .find(|f| f.id == current)
1103 .map(|f| &f.rect)?;
1104 let center = |r: &Rect| (r.x + r.width * 0.5, r.y + r.height * 0.5);
1105 let (fx, fy) = center(from);
1106
1107 let mut best: Option<(WidgetId, f32)> = None;
1108 for candidate in self.focus_pool() {
1109 let (id, rect) = (candidate.id, &candidate.rect);
1110 if id == current {
1111 continue;
1112 }
1113 let (cx, cy) = center(rect);
1114 // (how far ahead in the direction, the cross-axis offset)
1115 let (ahead, cross) = match direction {
1116 FocusDirection::Right => (cx - fx, (cy - fy).abs()),
1117 FocusDirection::Left => (fx - cx, (cy - fy).abs()),
1118 FocusDirection::Down => (cy - fy, (cx - fx).abs()),
1119 FocusDirection::Up => (fy - cy, (cx - fx).abs()),
1120 };
1121 // Inside a **cone** around the direction (not a plain half-plane): a candidate
1122 // almost aligned on the cross axis but barely "ahead" (slightly different widths)
1123 // is not a directional target.
1124 if ahead <= 0.5 || cross > ahead * 3.0 {
1125 continue;
1126 }
1127 let score = ahead + cross * 3.0;
1128 if best.map(|(_, s)| score < s).unwrap_or(true) {
1129 best = Some((id, score));
1130 }
1131 }
1132 best.map(|(id, _)| id)
1133 }
1134
1135 /// Next/previous focus stop for Tab, wrapping. With no current focus, the first (or
1136 /// the last).
1137 ///
1138 /// **Tree order, unless something said otherwise.** Stops carrying an explicit order
1139 /// are sorted ahead of those that do not, smallest first, and the sort is applied
1140 /// **within a traversal group** rather than across the whole frame — so a reordered
1141 /// dialog does not reshuffle the page behind it. Stops marked `skip` are passed over
1142 /// here while remaining reachable by a click: the keyboard's order and the pointer's
1143 /// reach are two different questions.
1144 pub fn focus_next(&self, current: Option<WidgetId>, forward: bool) -> Option<WidgetId> {
1145 let order = self.traversal_order();
1146 if order.is_empty() {
1147 return None;
1148 }
1149 let n = order.len();
1150 // A current focus **outside the scope** (taken before the modal opened) is treated
1151 // as "none": Tab enters the trap.
1152 match current.and_then(|c| order.iter().position(|id| *id == c)) {
1153 Some(i) => {
1154 let j = if forward {
1155 (i + 1) % n
1156 } else {
1157 (i + n - 1) % n
1158 };
1159 Some(order[j])
1160 }
1161 None => Some(order[if forward { 0 } else { n - 1 }]),
1162 }
1163 }
1164
1165 /// The Tab order: the participating stops, minus those that asked to be skipped,
1166 /// with each traversal group's members sorted among themselves.
1167 ///
1168 /// A **stable** sort on the order key, so everything without one keeps tree order and
1169 /// ties do too — the property that makes an explicit order a local statement rather
1170 /// than a rearrangement of the whole frame.
1171 pub fn traversal_order(&self) -> Vec<WidgetId> {
1172 let pool: Vec<&Focusable> = self.focus_pool().iter().filter(|f| !f.skip).collect();
1173 if pool.iter().all(|f| f.order.is_none()) {
1174 return pool.iter().map(|f| f.id).collect();
1175 }
1176 let mut out: Vec<WidgetId> = Vec::with_capacity(pool.len());
1177 let mut i = 0;
1178 while i < pool.len() {
1179 // A group's members are contiguous: the walk is depth-first, so a subtree's
1180 // stops arrive together.
1181 let group = pool[i].group;
1182 let mut j = i;
1183 while j < pool.len() && pool[j].group == group {
1184 j += 1;
1185 }
1186 let mut run: Vec<&Focusable> = pool[i..j].to_vec();
1187 run.sort_by(|a, b| {
1188 let key = |f: &Focusable| f.order.unwrap_or(f32::INFINITY);
1189 key(a).total_cmp(&key(b))
1190 });
1191 out.extend(run.iter().map(|f| f.id));
1192 i = j;
1193 }
1194 out
1195 }
1196
1197 /// What a keystroke means here: the messages it sends, in the order they should be
1198 /// applied.
1199 ///
1200 /// The resolution, in one place because it is the whole feature:
1201 ///
1202 /// 1. Only scopes **containing the focused stop** are candidates; with nothing
1203 /// focused, all of them are. Innermost first — `scopes` is filled on the way *out*
1204 /// of the walk, so that order is already the one wanted.
1205 /// 2. A [`crate::KeyboardListener`] gets the first refusal, and a `None` from it lets
1206 /// the stroke carry on.
1207 /// 3. A direct binding ([`crate::CallbackShortcuts`]) sends its message.
1208 /// 4. Otherwise a [`crate::Shortcuts`] binding names an **intent**, and the innermost
1209 /// [`crate::Actions`] that answers it supplies the message. An intent nobody
1210 /// answers does nothing — deliberately: a key bound to a meaning the current screen
1211 /// has no answer for should be inert, not an error.
1212 /// 5. Every [`crate::ActionListener`] watching that intent adds its message too.
1213 ///
1214 /// Empty means *nobody wanted it*, and the caller should carry on to whatever it would
1215 /// have done with the key.
1216 pub fn keystroke(&self, stroke: KeyStroke, focused: Option<WidgetId>) -> Vec<Msg> {
1217 let at = focused.and_then(|id| self.focusables.iter().position(|f| f.id == id));
1218 let covers = |range: &std::ops::Range<usize>| match at {
1219 Some(i) => range.contains(&i),
1220 None => true,
1221 };
1222
1223 for (range, on_key) in self.listeners.iter().rev() {
1224 if covers(range) {
1225 if let Some(msg) = on_key(stroke) {
1226 return vec![msg];
1227 }
1228 }
1229 }
1230
1231 let mut out = Vec::new();
1232 for scope in self.scopes.iter().filter(|s| covers(&s.range)) {
1233 if let Some((_, msg)) = scope.callbacks.iter().find(|(s, _)| stroke.matches(s)) {
1234 out.push(msg.clone());
1235 return out;
1236 }
1237 let Some((_, intent)) = scope.strokes.iter().find(|(s, _)| stroke.matches(s)) else {
1238 continue;
1239 };
1240 let answer = self
1241 .scopes
1242 .iter()
1243 .filter(|s| covers(&s.range))
1244 .find_map(|s| {
1245 s.actions
1246 .iter()
1247 .find(|(i, _)| i == intent)
1248 .map(|(_, m)| m.clone())
1249 });
1250 if let Some(msg) = answer {
1251 out.push(msg);
1252 for watcher in self.scopes.iter().filter(|s| covers(&s.range)) {
1253 for (i, m) in &watcher.listeners {
1254 if i == intent {
1255 out.push(m.clone());
1256 }
1257 }
1258 }
1259 }
1260 return out;
1261 }
1262 out
1263 }
1264
1265 /// Topmost scrollable area containing `point`.
1266 pub fn scroll_hit(&self, point: Point) -> Option<Scrollable> {
1267 self.scrollables
1268 .iter()
1269 .rev()
1270 .find(|area| area.viewport.contains(point))
1271 .copied()
1272 }
1273
1274 /// **Every** scrollable area containing `point`, innermost first — the whole stack a
1275 /// finger has under it.
1276 ///
1277 /// [`Self::scroll_hit`] answers *which one is on top*, which is the right question for
1278 /// a wheel or a scrollbar. A finger asks a different one: a strip that only slides
1279 /// across, sitting in a page that only scrolls down, cannot take a drag downwards, and
1280 /// the page behind it can. Deciding that needs the ones behind as well.
1281 pub fn scroll_chain(&self, point: Point) -> impl Iterator<Item = Scrollable> + '_ {
1282 self.scrollables
1283 .iter()
1284 .rev()
1285 .filter(move |area| area.viewport.contains(point))
1286 .copied()
1287 }
1288
1289 /// Frame of a **focusable** widget `id`, when it is present this frame — so the shell can
1290 /// find the focused field's geometry (vertical caret movement).
1291 pub fn widget_rect(&self, id: WidgetId) -> Option<Rect> {
1292 self.focusables
1293 .iter()
1294 .find(|f| f.id == id)
1295 .map(|f| f.rect)
1296 // Falls back to the **reorderables** registry. Kanban cards (and headers that are
1297 // reorderable but **not** sortable) are not focusable: without this fallback the
1298 // shell cannot find their bounds, and the whole **vertical** drag preview (ghost,
1299 // insertion line, reflow) as well as the *insert-after* routing stay dead.
1300 .or_else(|| {
1301 self.reorderables
1302 .iter()
1303 .find(|(rid, _)| *rid == id)
1304 .map(|(_, rect)| *rect)
1305 })
1306 // And to the detectors that take a drag, which report where a drag is in their
1307 // own coordinates (milestone 582): the whole box, whatever a clip hides of it.
1308 .or_else(|| {
1309 self.pans
1310 .iter()
1311 .rev()
1312 .find(|(pid, _, _)| *pid == id)
1313 .map(|(_, _, whole)| *whole)
1314 })
1315 }
1316
1317 /// The box of the inked surface `id`, when the frame has one — what the shell needs
1318 /// to start a splash: where inside it the finger landed, and how big it is.
1319 ///
1320 /// The **topmost** match wins, as the hit-test does: two inked surfaces can overlap,
1321 /// and the one that took the tap is the one drawn last.
1322 pub fn ink_box(&self, id: WidgetId) -> Option<Rect> {
1323 self.inks
1324 .iter()
1325 .rev()
1326 .find(|(iid, _)| *iid == id)
1327 .map(|(_, rect)| *rect)
1328 }
1329
1330 /// The identities of **every** focusable widget of the frame (the scope included) — so
1331 /// the shell can detect the focus **disappearing** (an overlay closed) and restore it.
1332 pub fn focusable_ids(&self) -> impl Iterator<Item = WidgetId> + '_ {
1333 self.focusables.iter().map(|f| f.id)
1334 }
1335
1336 /// The focusable widgets of the frame that take the focus when they appear
1337 /// ([`Widget::autofocus`]), in tree order.
1338 pub fn autofocus_ids(&self) -> impl Iterator<Item = WidgetId> + '_ {
1339 self.focusables.iter().filter(|f| f.autofocus).map(|f| f.id)
1340 }
1341
1342 /// The focus stops of the **form** `id` is part of, in tree order, each with its box
1343 /// this frame (unclipped: a field scrolled out of sight is still a field of the form).
1344 /// A stop in no [`crate::AutofillGroup`] is a form of one. Empty when `id` is not a
1345 /// focus stop this frame.
1346 ///
1347 /// Stops, not fields: which of them take part in autofill is a question for the
1348 /// widgets ([`crate::Widget::autofill_hints`]), which the shell has and this does not.
1349 pub fn form_of(&self, id: WidgetId) -> Vec<(WidgetId, Rect)> {
1350 let Some(stop) = self.focusables.iter().find(|f| f.id == id) else {
1351 return Vec::new();
1352 };
1353 match stop.form {
1354 Some(form) => self.group_stops(form),
1355 None => vec![(stop.id, stop.bounds)],
1356 }
1357 }
1358
1359 /// The [`crate::AutofillGroup`] focus stop `id` sits in, if it is a stop this frame
1360 /// and sits in one.
1361 pub fn form_group(&self, id: WidgetId) -> Option<WidgetId> {
1362 self.focusables.iter().find(|f| f.id == id)?.form
1363 }
1364
1365 /// Every focus stop inside the [`crate::AutofillGroup`] `group`, in tree order, with
1366 /// its unclipped box. Empty once the group has left the screen — which is the moment
1367 /// a form is finished with, and the platform may offer to save what was typed in it.
1368 pub fn group_stops(&self, group: WidgetId) -> Vec<(WidgetId, Rect)> {
1369 self.focusables
1370 .iter()
1371 .filter(|f| f.form == Some(group))
1372 .map(|f| (f.id, f.bounds))
1373 .collect()
1374 }
1375
1376 /// Frame (viewport) of the scrollable area `id`, when there is one — so the shell can find
1377 /// a multi-line field's width/height (caret following).
1378 pub fn scrollable_viewport(&self, id: WidgetId) -> Option<Rect> {
1379 self.scrollables
1380 .iter()
1381 .find(|area| area.id == id)
1382 .map(|area| area.viewport)
1383 }
1384
1385 /// The scroll offsets that would bring `id` **into view**, nearest region first.
1386 ///
1387 /// Empty when it is already in view, when the frame does not contain it, or when
1388 /// nothing around it scrolls. Nobody has to check first: an empty answer is the
1389 /// answer that nothing needs to move.
1390 ///
1391 /// Nested regions are walked from the inside out, and each is told where the target
1392 /// **ended up** rather than where it started. A sideways strip inside a page that
1393 /// scrolls down moves the target across, and the page then decides how far down it
1394 /// still is; asking both about the original box would have them fight over the same
1395 /// pixels and land somewhere neither wanted.
1396 pub fn reveal(&self, id: WidgetId, runtime: &crate::Runtime) -> Vec<(WidgetId, (f32, f32))> {
1397 let Some(stop) = self.focusables.iter().find(|f| f.id == id) else {
1398 return Vec::new();
1399 };
1400 let mut target = stop.bounds;
1401 let mut host = stop.scroll;
1402 let mut moves = Vec::new();
1403 // Regions nest a handful deep at most. The bound is a backstop against a chain
1404 // that loops, which cannot happen and would hang the frame if it did.
1405 for _ in 0..16 {
1406 let Some(hid) = host else { break };
1407 let Some(area) = self.scrollables.iter().find(|area| area.id == hid) else {
1408 break;
1409 };
1410 let current = runtime.scroll.get(&hid).copied().unwrap_or((0.0, 0.0));
1411 let (offset, shift) = area.reveal(target, current);
1412 if shift.0 != 0.0 || shift.1 != 0.0 {
1413 target = target.translate(shift.0, shift.1);
1414 moves.push((hid, offset));
1415 }
1416 host = area.host;
1417 }
1418 moves
1419 }
1420
1421 /// Every scrollable area of the frame — geometry, bounds and physics — which is
1422 /// what the runtime needs to drive momentum and edges.
1423 pub fn scroll_regions(&self) -> &[Scrollable] {
1424 &self.scrollables
1425 }
1426
1427 /// The area `id`, when it is present this frame.
1428 pub fn scroll_region(&self, id: WidgetId) -> Option<Scrollable> {
1429 self.scrollables.iter().find(|area| area.id == id).copied()
1430 }
1431
1432 /// SingleChildScrollView bounds `(id, max_x, max_y)` of every scrollable area.
1433 pub fn scrollable_maxes(&self) -> Vec<(WidgetId, f32, f32)> {
1434 self.scrollables
1435 .iter()
1436 .map(|area| (area.id, area.max_x, area.max_y))
1437 .collect()
1438 }
1439
1440 /// Scrollbar thumb under `point` (to start a drag).
1441 pub fn scrollbar_at(&self, point: Point) -> Option<Scrollbar> {
1442 self.scrollbars
1443 .iter()
1444 .rev()
1445 .find(|bar| bar.interactive && bar.opacity > 0.0 && bar.thumb.contains(point))
1446 .copied()
1447 }
1448
1449 /// The scrollbar a **pointer is near enough to wake**: inside its track, or within a
1450 /// tap target's reach of its thumb.
1451 ///
1452 /// Wider than [`Ui::scrollbar_at`], and — unlike it — it answers for a bar that has
1453 /// faded out entirely, which is the whole point. A bar nobody can see is a bar nobody
1454 /// can grab, so the reference lets a mouse coming near one bring it back first
1455 /// (`scrollbar.dart:766`); only then is it something to take hold of.
1456 pub fn scrollbar_near(&self, point: Point) -> Option<Scrollbar> {
1457 self.scrollbars
1458 .iter()
1459 .rev()
1460 .find(|bar| bar.interactive && bar.reach.contains(point))
1461 .copied()
1462 }
1463
1464 /// Topmost draggable widget under `point`: (id, its bounds).
1465 pub fn draggable_at(&self, point: Point) -> Option<(WidgetId, Rect)> {
1466 self.draggables
1467 .iter()
1468 .rev()
1469 .find(|(_, rect)| rect.contains(point))
1470 .map(|(id, rect)| (*id, *rect))
1471 }
1472
1473 /// Topmost **drag source** under `point`, with what it carries.
1474 pub fn drag_source_at(&self, point: Point) -> Option<DragSource> {
1475 self.drag_sources
1476 .iter()
1477 .rev()
1478 .find(|source| source.rect.contains(point))
1479 .copied()
1480 }
1481
1482 /// Topmost **drop target** under `point`.
1483 pub fn drop_zone_at(&self, point: Point) -> Option<DropZone> {
1484 self.drop_zones
1485 .iter()
1486 .rev()
1487 .find(|zone| zone.rect.contains(point))
1488 .copied()
1489 }
1490
1491 /// Topmost **reorderable** widget under `point`: its id. The basis of reordering
1492 /// drag-and-drop (the source on press, the target on drop) — independent of clickability.
1493 pub fn reorderable_at(&self, point: Point) -> Option<WidgetId> {
1494 self.reorderables
1495 .iter()
1496 .rev()
1497 .find(|(_, rect)| rect.contains(point))
1498 .map(|(id, _)| *id)
1499 }
1500
1501 /// **Every** reorderable under `point`, topmost first.
1502 ///
1503 /// [`Self::reorderable_at`] answers *which one is on top*, which is the right question
1504 /// for a press: whatever is nearest the finger is what the finger grabbed. A **drop**
1505 /// asks a different one, because the thing on top may be a grip that nothing can be
1506 /// dropped on — see [`Widget::reorder_droppable`] — and the row it belongs to, which
1507 /// can, is the one behind it.
1508 pub fn reorderables_at(&self, point: Point) -> impl Iterator<Item = WidgetId> + '_ {
1509 self.reorderables
1510 .iter()
1511 .rev()
1512 .filter(move |(_, rect)| rect.contains(point))
1513 .map(|(id, _)| *id)
1514 }
1515
1516 /// Every **reorderable** of the frame, with its box, in paint order.
1517 pub fn reorderables(&self) -> &[(WidgetId, Rect)] {
1518 &self.reorderables
1519 }
1520
1521 /// Topmost **interactive** viewport (`InteractiveViewer`) under `point`: (id, its screen
1522 /// viewport). The shell routes panning and zooming to it.
1523 pub fn interactive_at(&self, point: Point) -> Option<(WidgetId, Rect)> {
1524 self.interactives
1525 .iter()
1526 .rev()
1527 .find(|(_, rect)| rect.contains(point))
1528 .map(|(id, rect)| (*id, *rect))
1529 }
1530
1531 /// The regions the mouse is in at `point`, the innermost first, each with its whole box
1532 /// (milestone 583).
1533 ///
1534 /// Every region under the pointer is in — a region inside another is in both — except
1535 /// those an **opaque** region hides: once one is found, only the regions it is inside
1536 /// are still in, and a region behind it that it is not inside is not.
1537 pub fn hover_regions_at(&self, point: Point) -> Vec<(WidgetId, Rect, crate::HoverRegion)> {
1538 let mut found: Vec<&HoverEntry> = Vec::new();
1539 // The regions an opaque one found so far is inside: its parent chain.
1540 let mut hidden_behind: Option<Vec<WidgetId>> = None;
1541 for region in self.hover_regions.iter().rev() {
1542 if !region.seen.contains(point) {
1543 continue;
1544 }
1545 if let Some(chain) = &hidden_behind {
1546 if !chain.contains(®ion.id) {
1547 continue;
1548 }
1549 }
1550 found.push(region);
1551 if region.spec.opaque && hidden_behind.is_none() {
1552 let mut chain = Vec::new();
1553 let mut parent = region.parent;
1554 while let Some(id) = parent {
1555 chain.push(id);
1556 parent = self
1557 .hover_regions
1558 .iter()
1559 .rev()
1560 .find(|r| r.id == id)
1561 .and_then(|r| r.parent);
1562 }
1563 hidden_behind = Some(chain);
1564 }
1565 }
1566 found
1567 .into_iter()
1568 .map(|region| (region.id, region.whole, region.spec))
1569 .collect()
1570 }
1571
1572 /// The whole box of the region `id` in this frame, if it is in it.
1573 pub fn hover_region_rect(&self, id: WidgetId) -> Option<Rect> {
1574 self.hover_regions
1575 .iter()
1576 .rev()
1577 .find(|region| region.id == id)
1578 .map(|region| region.whole)
1579 }
1580
1581 /// The widgets hearing the pointer's raw events under `point`, the innermost first, each
1582 /// with its whole box (milestone 586).
1583 pub fn pointer_listeners_at(&self, point: Point) -> Vec<(WidgetId, Rect)> {
1584 self.pointer_listeners
1585 .iter()
1586 .rev()
1587 .filter(|(_, seen, _)| seen.contains(point))
1588 .map(|(id, _, whole)| (*id, *whole))
1589 .collect()
1590 }
1591
1592 /// The innermost detector that takes a drag under `point`, and its box (milestone 582):
1593 /// the one drawn last, as the hit-test chooses.
1594 pub fn pan_at(&self, point: Point) -> Option<(WidgetId, Rect)> {
1595 self.pans
1596 .iter()
1597 .rev()
1598 .find(|(_, seen, _)| seen.contains(point))
1599 .map(|(id, seen, _)| (*id, *seen))
1600 }
1601
1602 /// Interactive viewports `(id, screen viewport)`, to drive the inertia (fling) and the
1603 /// clamping of the pan on the framework's side.
1604 pub fn interactive_bounds(&self) -> Vec<(WidgetId, Rect)> {
1605 self.interactives.clone()
1606 }
1607}
1608
1609/// Paints a **striped band** along the edge of every box whose children ran past it.
1610///
1611/// Milestone 335 taught the layout to notice an overflow and the shell to say so in the
1612/// console. That is the half a developer reads; this is the half a *screenshot* shows —
1613/// and a screenshot is what a bug report is made of. Three of this framework's own
1614/// milestones went looking for defects that a band would have pointed at: a delete button
1615/// laid out past the card (333, 334), a segmented control 221 px outside its parent (335).
1616///
1617/// The look is the reference's, because the point of it is to be recognised on sight:
1618/// black and yellow diagonal stripes, three quarters opaque, over a tenth of the box along
1619/// the offending edge, with the edge and the number written across it. Not the mechanism,
1620/// though — the reference paints one repeating gradient and this paints the stripes,
1621/// because a repeating diagonal gradient is a shader feature and a parallelogram is four
1622/// points.
1623///
1624/// **Debug builds only.** A band is a message to whoever is building the application, and
1625/// in a release build there is nobody to read it — the reference draws the same line.
1626fn paint_overflow_bands(scene: &mut Scene, overflows: &[Overflowing]) {
1627 if !cfg!(debug_assertions) || overflows.is_empty() {
1628 return;
1629 }
1630 // The reference's two colours and its 10 % band, exactly.
1631 const BLACK: Color = Color::rgba(0.0, 0.0, 0.0, 0.75);
1632 const YELLOW: Color = Color::rgba(1.0, 1.0, 0.0, 0.75);
1633 const FRACTION: f32 = 0.1;
1634 /// The stripes run at 45°; this is their period measured along the x axis.
1635 const PERIOD: f32 = 14.0;
1636
1637 let (outer, outer_bounds) = (scene.current_clip(), scene.current_bounds());
1638 scene.set_clip(Rect::UNBOUNDED);
1639 for over in overflows {
1640 let r = over.rect;
1641 let band = match over.side {
1642 Side::Left => Rect::new(r.x, r.y, r.width * FRACTION, r.height),
1643 Side::Right => Rect::new(
1644 r.x + r.width * (1.0 - FRACTION),
1645 r.y,
1646 r.width * FRACTION,
1647 r.height,
1648 ),
1649 Side::Top => Rect::new(r.x, r.y, r.width, r.height * FRACTION),
1650 Side::Bottom => Rect::new(
1651 r.x,
1652 r.y + r.height * (1.0 - FRACTION),
1653 r.width,
1654 r.height * FRACTION,
1655 ),
1656 };
1657 if band.width <= 0.0 || band.height <= 0.0 {
1658 continue;
1659 }
1660 scene.set_clip(band);
1661 scene.fill_rect(band, BLACK);
1662 // One parallelogram per stripe, sheared by the band's height so that every one
1663 // of them crosses it at 45° whatever shape the band is.
1664 let shear = band.height;
1665 let mut x = band.x - shear;
1666 while x < band.x + band.width {
1667 let stripe = Path::new()
1668 .move_to(Point::new(x, band.y))
1669 .line_to(Point::new(x + PERIOD / 2.0, band.y))
1670 .line_to(Point::new(x + PERIOD / 2.0 + shear, band.y + band.height))
1671 .line_to(Point::new(x + shear, band.y + band.height))
1672 .close();
1673 scene.fill_path(&stripe, YELLOW);
1674 x += PERIOD;
1675 }
1676 // The label is **not** clipped to the band: a vertical one is usually longer than
1677 // the box is tall, and half a sentence is worse than a sentence written over
1678 // whatever is beside it. The reference does not clip it either.
1679 scene.set_clip(Rect::UNBOUNDED);
1680 scene.set_bounds(Rect::UNBOUNDED);
1681 paint_overflow_label(scene, band, over);
1682 }
1683 scene.set_clip(outer);
1684 scene.set_bounds(outer_bounds);
1685}
1686
1687/// Writes the reference's sentence across a band: which edge overflowed, and by how much.
1688///
1689/// The console already says it — but the console is on the developer's machine and the
1690/// band is on the device, and a photograph of a phone is what half the bug reports in the
1691/// world are made of. A striped edge says *something* is too big; `RIGHT OVERFLOWED BY 86
1692/// PIXELS` says which edge, and by enough that you can tell a forgotten padding from a
1693/// missing wrap without running anything.
1694///
1695/// The words are the reference's, exactly, so that searching for them finds the same
1696/// answers. So are the numbers: 7.5 px, the heaviest weight there is, dark red on an
1697/// opaque white plate — a label that must stay readable over stripes cannot be subtle —
1698/// centred on the band's outer edge and turned a quarter turn on the vertical ones.
1699fn paint_overflow_label(scene: &mut Scene, band: Rect, over: &Overflowing) {
1700 /// The reference's label metrics.
1701 const SIZE: f32 = 7.5;
1702 /// The gap between the label and the outer edge of the band.
1703 const PAD: f32 = 1.0;
1704 /// `0xFF900000`.
1705 const INK: Color = Color::rgb(0.5647059, 0.0, 0.0);
1706
1707 let side = match over.side {
1708 Side::Left => "LEFT",
1709 Side::Right => "RIGHT",
1710 Side::Top => "TOP",
1711 Side::Bottom => "BOTTOM",
1712 };
1713 let text = format!("{side} OVERFLOWED BY {} PIXELS", format_pixels(over.amount));
1714 // The reference asks for an 800 weight; the heaviest this framework has is bold.
1715 let style = TextStyle::new(SIZE).weight(FontWeight::Bold);
1716 let size = frus_text::measure_styled(&text, SIZE, FontWeight::Bold, false);
1717
1718 // Where the middle of the label's leading edge goes, before any rotation, and by how
1719 // much the whole thing then turns about that point.
1720 let (anchor, angle) = match over.side {
1721 Side::Left => (
1722 Point::new(band.x + SIZE + PAD, band.y + band.height / 2.0),
1723 std::f32::consts::FRAC_PI_2,
1724 ),
1725 Side::Right => (
1726 Point::new(band.x + band.width - SIZE - PAD, band.y + band.height / 2.0),
1727 -std::f32::consts::FRAC_PI_2,
1728 ),
1729 Side::Top => (Point::new(band.x + band.width / 2.0, band.y + PAD), 0.0),
1730 Side::Bottom => (
1731 Point::new(band.x + band.width / 2.0, band.y + band.height - SIZE - PAD),
1732 0.0,
1733 ),
1734 };
1735 if angle == 0.0 {
1736 let at = Point::new(anchor.x - size.width / 2.0, anchor.y);
1737 scene.fill_rect(Rect::from_point_size(at, size), Color::WHITE);
1738 scene.text(at, text, &style.resolved(), INK);
1739 return;
1740 }
1741 // A rotation of a group, not of a glyph: the plate and the sentence turn together and
1742 // neither has to know it is turning.
1743 //
1744 // The group is painted **at the origin** rather than where it lands, because a layer
1745 // is rendered flat into a window-sized texture before it is composited: a vertical
1746 // label is written across a box near the right edge, so laid out flat it would run
1747 // off the texture and lose its last word before the rotation ever brought it back
1748 // inside. The transform is the one that carries the origin to where the label goes —
1749 // shift it half its width to the left, turn it, and land it on the anchor.
1750 let flat = Rect::from_point_size(Point::ZERO, size);
1751 let matrix = Affine::translation(-size.width / 2.0, 0.0)
1752 .then(Affine::rotation(angle))
1753 .then(Affine::translation(anchor.x, anchor.y));
1754 scene.transformed(LayerTransform::new(matrix), move |scene: &mut Scene| {
1755 scene.fill_rect(flat, Color::WHITE);
1756 scene.text(flat.origin(), text, &style.resolved(), INK);
1757 });
1758}
1759
1760/// How the reference writes an overflow: whole pixels past ten, one decimal past one, and
1761/// three significant figures below that — because the difference between 0.5 px and
1762/// 0.0001 px is the difference between a layout bug and a rounding error.
1763fn format_pixels(value: f32) -> String {
1764 if value > 10.0 {
1765 format!("{value:.0}")
1766 } else if value > 1.0 {
1767 format!("{value:.1}")
1768 } else {
1769 // Three significant figures: the first one sits at the first non-zero decimal.
1770 let leading = if value > 0.0 {
1771 (-value.abs().log10().floor()) as i32
1772 } else {
1773 0
1774 };
1775 format!("{value:.*}", (leading + 2).clamp(0, 12) as usize)
1776 }
1777}
1778
1779/// Identity of the `index`-th child: **by key** when the child declares one (stable whatever
1780/// its position), **positional** otherwise. It must be used everywhere a child identity is
1781/// derived (render, collection, lookup, animations) so they all stay consistent.
1782pub(crate) fn child_id<Msg>(parent: WidgetId, index: usize, child: &dyn Widget<Msg>) -> WidgetId {
1783 match child.key() {
1784 Some(key) => parent.keyed(key),
1785 None => parent.child(index),
1786 }
1787}
1788
1789/// A widget's **effective** style for layout: its `style()`, whose size is **replaced by the
1790/// runtime's interpolated size** when the widget is animated (`Widget::anim_size` +
1791/// `Container::animated_size`). This is the only place where an animated size enters layout —
1792/// used **identically** by [`build_layout`] and by the cache fingerprint (`hash_node`), which
1793/// is what keeps them consistent (the cache stays invalid for as long as the size moves).
1794pub(crate) fn effective_style<Msg>(
1795 widget: &dyn Widget<Msg>,
1796 id: WidgetId,
1797 runtime: &Runtime,
1798 theme: &Theme,
1799) -> frus_layout::Style {
1800 // The theme's say on size and spacing (milestone 309), before the runtime's
1801 // animated overrides below — an animation in flight wins over a resting default.
1802 let mut style = widget.style_themed(theme);
1803 if widget.anim_size().is_some() {
1804 if let Some(size) = runtime.anim_size(id) {
1805 style.width = frus_layout::Dimension::Length(size.width);
1806 style.height = frus_layout::Dimension::Length(size.height);
1807 }
1808 }
1809 if widget.anim_padding().is_some() {
1810 if let Some(padding) = runtime.anim_padding(id) {
1811 style.padding = padding;
1812 }
1813 }
1814 // A share of the parent, growing or shrinking. `Percent` and not `Length`, because
1815 // the whole point of the quantity is that it does not know what it will come to — and
1816 // an axis whose factor is unset goes back to following its content rather than to
1817 // nought, which is what "unset" has always meant here.
1818 if widget.anim_fractions().is_some() {
1819 if let Some((width, height)) = runtime.anim_fractions(id) {
1820 let dim = |factor: Option<f32>| match factor {
1821 Some(f) => frus_layout::Dimension::Percent(f),
1822 None => frus_layout::Dimension::Auto,
1823 };
1824 style.width = dim(width);
1825 style.height = dim(height);
1826 }
1827 }
1828 // A dismissed item closing its gap. It collapses along the axis it was **not**
1829 // swiped along — a row swiped sideways loses its height — which is what makes the
1830 // neighbours slide up rather than the row narrow to a sliver. Only an explicit
1831 // length can shrink: an `Auto` box has no number to scale, which is why
1832 // `Dismissible` asks for a size (see its docs).
1833 // A sheet's panel, as tall as the retained share of its box. The same `Percent` as a
1834 // fraction above, for the same reason: the share is the quantity, and the box it is a
1835 // share of is the layout's to know. Hashed through here as well, so the cache misses
1836 // on every frame the sheet moves.
1837 if let Some(spec) = widget.sheet() {
1838 style.height = frus_layout::Dimension::Percent(runtime.sheet_size(id, spec));
1839 }
1840 if let Some(spec) = widget.dismissible() {
1841 if let Some(factor) = runtime.dismiss_extent_factor(id) {
1842 let axis = if spec.axis.is_horizontal() {
1843 &mut style.height
1844 } else {
1845 &mut style.width
1846 };
1847 if let frus_layout::Dimension::Length(extent) = *axis {
1848 *axis = frus_layout::Dimension::Length(extent * factor);
1849 }
1850 }
1851 }
1852 style
1853}
1854
1855/// Builds the main layout tree (a scrollable is a **leaf**). `id`/`runtime` are what inject
1856/// the animated size through [`effective_style`], following the walk's identity scheme
1857/// (`child_id`) **exactly**.
1858pub(crate) fn build_layout<'a, Msg>(
1859 widget: &'a dyn Widget<Msg>,
1860 id: WidgetId,
1861 runtime: &'a Runtime,
1862 theme: &Theme,
1863 layout: &mut Layout<'a, BaselineData>,
1864) -> NodeId {
1865 let (node, fills) = build_layout_scoped(widget, id, runtime, theme, layout, true);
1866 // A root that wants to fill has no parent to fill: the flex machinery below needs one
1867 // on each side of the question. A percentage of the room the layout is being computed
1868 // in is the same answer without a parent, and it degrades to hugging the content when
1869 // that room is unbounded — a scrollable measuring its content, a natural size.
1870 if fills.horizontal {
1871 layout.fill_root(node, true);
1872 }
1873 if fills.vertical {
1874 layout.fill_root(node, false);
1875 }
1876 node
1877}
1878
1879/// The axes a subtree asks to **fill** rather than shrink-wrap.
1880///
1881/// It is not a property of one widget. A column whose row fills the width is itself as
1882/// wide as the room it was given, because the row inside it took that room — so the
1883/// request travels up as the layout is built, and each container answers it with the one
1884/// thing a container can do about it: grow along its own axis, or stretch across it.
1885#[derive(Clone, Copy, Default)]
1886pub(crate) struct Fills {
1887 horizontal: bool,
1888 vertical: bool,
1889 /// The axes on which **this** node's size is only a default, replaced by the room on
1890 /// offer when the fill is granted (milestone 590). Never passed up: it is about this
1891 /// node's own style, not the subtree's request.
1892 soft_horizontal: bool,
1893 soft_vertical: bool,
1894}
1895
1896impl Fills {
1897 /// What a widget asks for on its own account.
1898 ///
1899 /// A straight copy since milestone 405: the widget's answer already has a flag per
1900 /// axis, where it used to be one direction and a container that wanted both had no way
1901 /// to say so.
1902 fn own<Msg>(widget: &dyn Widget<Msg>, theme: &Theme) -> Self {
1903 let asked = widget.fill_axes(theme);
1904 let soft = widget.soft_extent();
1905 Fills {
1906 horizontal: asked.horizontal,
1907 vertical: asked.vertical,
1908 soft_horizontal: soft.horizontal && asked.horizontal,
1909 soft_vertical: soft.vertical && asked.vertical,
1910 }
1911 }
1912
1913 fn merge(self, other: Fills) -> Self {
1914 Fills {
1915 horizontal: self.horizontal || other.horizontal,
1916 vertical: self.vertical || other.vertical,
1917 // This node's own; a child's default is not this node's.
1918 ..self
1919 }
1920 }
1921
1922 /// Whether this node's size on one axis is only a default.
1923 fn soft(self, horizontal: bool) -> bool {
1924 if horizontal {
1925 self.soft_horizontal
1926 } else {
1927 self.soft_vertical
1928 }
1929 }
1930
1931 /// The half of a child's request a container passes on to **its** parent, given
1932 /// which way the container runs. Along its own axis a container divides the room up
1933 /// and the request stops there; across it, the container's size *is* the child's, so
1934 /// the request has to keep going.
1935 /// Whether one axis is asked for.
1936 fn wants(self, horizontal: bool) -> bool {
1937 if horizontal {
1938 self.horizontal
1939 } else {
1940 self.vertical
1941 }
1942 }
1943
1944 /// Drops any axis this box was **given a size on**: it has already been told how big
1945 /// to be, and a request to fill cannot reach past that answer.
1946 fn bounded_by(self, style: &frus_layout::Style) -> Self {
1947 let auto = |d| matches!(d, frus_layout::Dimension::Auto);
1948 // A box with an aspect ratio decides its children's size from its own, as the
1949 // reference's `AspectRatio` hands its child tight constraints: nothing inside it
1950 // reaches past it (milestone 633).
1951 if style.aspect_ratio.is_some() {
1952 return Fills {
1953 horizontal: false,
1954 vertical: false,
1955 ..self
1956 };
1957 }
1958 Fills {
1959 horizontal: self.horizontal && auto(style.width),
1960 vertical: self.vertical && auto(style.height),
1961 ..self
1962 }
1963 }
1964
1965 fn across(self, direction: frus_layout::FlexDirection) -> Self {
1966 if direction.is_horizontal() {
1967 Fills {
1968 horizontal: false,
1969 ..self
1970 }
1971 } else {
1972 Fills {
1973 vertical: false,
1974 ..self
1975 }
1976 }
1977 }
1978}
1979
1980/// A layout leaf's **text baseline**, when it has one: the distance from the top of its
1981/// box down to the line its letters sit on.
1982///
1983/// It rides on the layout tree rather than being asked of the widgets afterwards because
1984/// only the layout knows where a node ended up, and only the walk that built it knows
1985/// which branch it took — a scrollable, a stack and a page view are all *leaves* here,
1986/// with their contents laid out elsewhere, and a second walk guessing at that would be a
1987/// copy of this one waiting to drift out of step.
1988pub(crate) type BaselineData = Option<f32>;
1989
1990/// An owned copy of a theme, **made in a frame of its own**.
1991///
1992/// `#[inline(never)]` is the whole point: inlined, the eleven-kilobyte temporary is built
1993/// in the caller's frame, and the caller here is the recursive walk that pays for its
1994/// frame once per level of the tree. Behind the call it is built, copied into the
1995/// allocation, and gone before the walk recurses.
1996#[inline(never)]
1997fn owned_theme(theme: &Theme) -> std::rc::Rc<Theme> {
1998 std::rc::Rc::new(theme.clone())
1999}
2000
2001/// The body of [`build_layout`], carrying whether baselines are still being collected.
2002/// An [`crate::IgnoreBaseline`] turns them off for its subtree — the widgets inside still
2003/// have baselines, and the point is that nothing above may see them.
2004fn build_layout_scoped<'a, Msg>(
2005 widget: &'a dyn Widget<Msg>,
2006 id: WidgetId,
2007 runtime: &'a Runtime,
2008 theme: &Theme,
2009 layout: &mut Layout<'a, BaselineData>,
2010 baselines: bool,
2011) -> (NodeId, Fills) {
2012 // A component is built into what it stands for before anything is asked of the node,
2013 // the scope it introduces included — see `Widget::expand` — under the scope a wrapper
2014 // around it introduces (milestone 642).
2015 expand_scoped(widget, id, runtime, theme);
2016 // A themed subtree lays out under **its** theme, not the frame's: a theme reaches
2017 // sizes and spacing (milestone 309), so this has to happen here and not only at paint
2018 // time. `hash_node`, which fingerprints this same walk for the relayout cache, makes
2019 // the same swap — the two staying in step is what keeps the cache honest.
2020 let scoped = scoped_theme(widget, id, runtime, theme);
2021 let theme = scoped.as_deref().unwrap_or(theme);
2022 // And a **scoped surface**, the same idea one milestone later: a shell that hands a slot
2023 // a narrowed description narrows it for everything the slot builds, the deferred build
2024 // below included. Held for this subtree and put back on the way out.
2025 let _surface = widget
2026 .media_override(crate::MediaQuery::of())
2027 .map(crate::MediaQuery::install);
2028 // And the **shell**, the third inherited thing (milestone 422): a slot told what shell
2029 // it stands in composes differently — a bar grows a menu button — so it has to be in
2030 // force before `build_themed` below, exactly as the surface is.
2031 let _shell = widget.scaffold_override().map(crate::ScaffoldInfo::install);
2032 let _scrolling = widget
2033 .scroll_behavior_override()
2034 .map(crate::ScrollConfiguration::install);
2035 // A subtree that could not be composed until the theme was known (`ThemeBuilder`).
2036 // It has to happen **before** anything reads `children()`, and under the subtree's
2037 // own theme, which is why it sits after the swap above rather than at the call site.
2038 widget.build_in(id, runtime, theme);
2039 // Everything below an `IgnoreBaseline` keeps its baseline and loses the right to be
2040 // seen: the flag is narrowed once, here, so no branch below has to remember.
2041 let baselines = baselines && !widget.ignores_baseline();
2042 // A leaf's own baseline, or nothing when this subtree is being ignored.
2043 let own_baseline = if baselines {
2044 widget.text_baseline(theme)
2045 } else {
2046 None
2047 };
2048 // `Baseline`: the child is pushed down until **its** baseline lands `target` pixels
2049 // from the top of this box, and this box grows to contain it. If the child's own
2050 // baseline is already lower than that, there is nowhere to push it up to and the
2051 // child is top-aligned instead — the same answer the reference gives.
2052 if let Some(target) = widget.baseline_target() {
2053 let mut style = effective_style(widget, id, runtime, theme);
2054 if let Some(child) = widget.children().first() {
2055 let child = child.as_ref();
2056 let cid = child_id(id, 0, child);
2057 // A child with no baseline of its own is treated as if its baseline were its
2058 // bottom edge, which is what a box with no text amounts to.
2059 let child_baseline = natural_baseline(child, cid, runtime, theme)
2060 .unwrap_or_else(|| natural_size(child, cid, runtime, theme).height);
2061 style.padding.top += (target - child_baseline).max(0.0);
2062 let (node, fills) = build_layout_scoped(child, cid, runtime, theme, layout, baselines);
2063 return (layout.container(style, &[node]), fills);
2064 }
2065 return (layout.leaf(style, None), Fills::default());
2066 }
2067 // `RotatedBox`: a leaf whose box is the child's **natural** size, with its dimensions
2068 // **swapped** for an odd quarter turn (the rotation does affect layout). The child itself
2069 // is laid out separately (at render time).
2070 if let Some(q) = widget.rotated_quarter_turns() {
2071 let mut style = effective_style(widget, id, runtime, theme);
2072 if let Some(child) = widget.children().first() {
2073 let nat = natural_size(
2074 child.as_ref(),
2075 child_id(id, 0, child.as_ref()),
2076 runtime,
2077 theme,
2078 );
2079 let (w, h) = if q.rem_euclid(4) % 2 != 0 {
2080 (nat.height, nat.width)
2081 } else {
2082 (nat.width, nat.height)
2083 };
2084 style.width = frus_layout::Dimension::Length(w);
2085 style.height = frus_layout::Dimension::Length(h);
2086 }
2087 return (layout.leaf(style, own_baseline), Fills::default());
2088 }
2089 // `Intrinsic`: one axis is taken from what the content would **like** to be, not from
2090 // the space on offer. The content is measured once, unconstrained, and the answer is
2091 // written into this node's style as a length — the same trick `RotatedBox` uses, except
2092 // that here the child stays inside, laid out normally within the size it asked for.
2093 if let Some((axis, step)) = widget.intrinsic() {
2094 let mut style = effective_style(widget, id, runtime, theme);
2095 if let Some(child) = widget.children().first() {
2096 let child = child.as_ref();
2097 let cid = child_id(id, 0, child);
2098 let nat = natural_size(child, cid, runtime, theme);
2099 let quantise = |extent: f32| match step {
2100 Some(step) if step > 0.0 => (extent / step).ceil() * step,
2101 _ => extent,
2102 };
2103 match axis {
2104 crate::constraints::IntrinsicAxis::Width => {
2105 style.width = frus_layout::Dimension::Length(quantise(nat.width));
2106 }
2107 crate::constraints::IntrinsicAxis::Height => {
2108 style.height = frus_layout::Dimension::Length(quantise(nat.height));
2109 }
2110 }
2111 let (node, fills) = build_layout_scoped(child, cid, runtime, theme, layout, baselines);
2112 return (layout.container(style, &[node]), fills);
2113 }
2114 return (layout.leaf(style, own_baseline), Fills::default());
2115 }
2116 // `OverflowBox`: a leaf, because its child is laid out **separately**, to constraints of
2117 // its own. That separation is the whole feature — a child sharing this node's layout
2118 // could never be bigger than it.
2119 if widget.overflow_box().is_some() {
2120 return (
2121 layout.leaf(effective_style(widget, id, runtime, theme), own_baseline),
2122 Fills::default(),
2123 );
2124 }
2125 // A box that **transforms the space on offer**: its child is laid out in a box derived
2126 // from this one's, in a tree of its own, and this box is then as big as what came back
2127 // — held to what it was itself offered, which is the reference's
2128 // `constraints.constrain(child.size)`. `UnconstrainedBox` is this branch with the
2129 // constraint taken away.
2130 if let Some(transform) = widget.constraints_transform() {
2131 let style = effective_style(widget, id, runtime, theme);
2132 let child = widget.children().first().map(|c| c.as_ref());
2133 let owned = owned_theme(theme);
2134 let animated = widget.animated_size();
2135 let factor = widget.size_factor();
2136 let measure: frus_layout::MeasureFn<'a> = Box::new(move |w, h| {
2137 let Some(child) = child else {
2138 return Size::new(0.0, 0.0);
2139 };
2140 let (aw, free_x, fill_x) = transform.width.offered(w);
2141 let (ah, free_y, fill_y) = transform.height.offered(h);
2142 let mut inner: Layout<BaselineData> = Layout::new();
2143 let node = build_layout(child, child_id(id, 0, child), runtime, &owned, &mut inner);
2144 // **Handed** every axis that has a number, **asked** about every axis that does
2145 // not. Merely constraining them instead would leave a child with no size of its
2146 // own — a plain container — hugging at nothing, and a box asked how big its child
2147 // wanted to be would come back empty. A loose axis is the exception, and says so:
2148 // there the number is only the most the child may take (milestone 598).
2149 inner.compute_axes(node, aw, ah, [free_x, free_y], [fill_x, fill_y]);
2150 let mut size = inner.size_of(node);
2151 // A box following its child's size answers with the size it has got to, and is
2152 // told when the child's has changed (milestone 594). An intrinsic question is
2153 // answered and not recorded: on an axis the child is **given**, the layout
2154 // offering nothing (how big with no limit) or zero (how small could it be). Either
2155 // makes a paragraph a size it never ends up at — one line, or one word a line.
2156 // On an axis the child decides, the offer is not read, so it asks nothing.
2157 if let Some(spec) = &animated {
2158 let asks = |rule, offer: Option<f32>| {
2159 rule == crate::constraints::AxisConstraint::AsGiven
2160 && offer.is_none_or(|v| v <= 0.0)
2161 };
2162 let probe = asks(transform.width, w) || asks(transform.height, h);
2163 let shown = runtime
2164 .size_anims
2165 .borrow_mut()
2166 .observe(id, size, spec, !probe);
2167 // Whole pixels, rounded up and then held to the offer below: a size on its
2168 // way between two others is a fraction, and a box rounded down from one is
2169 // one its content no longer fits in (milestone 289).
2170 size = Size::new(frus_core::fits(shown.width), frus_core::fits(shown.height));
2171 }
2172 // A fraction of the child, on each axis, in whole pixels (milestone 596).
2173 if let Some((across, down)) = factor {
2174 size = Size::new(
2175 frus_core::fits(size.width * across.max(0.0)),
2176 frus_core::fits(size.height * down.max(0.0)),
2177 );
2178 }
2179 // Held to the offer: a child too big for the room spills, and the box stays the
2180 // size the room was.
2181 Size::new(
2182 size.width.min(w.unwrap_or(f32::INFINITY)),
2183 size.height.min(h.unwrap_or(f32::INFINITY)),
2184 )
2185 });
2186 return (
2187 layout.measured_leaf(style, own_baseline, measure),
2188 // An animated box asks for the room on the axes its child is given
2189 // (milestone 594); a plain transform box asks for nothing.
2190 Fills::own(widget, theme),
2191 );
2192 }
2193 // A `LayoutBuilder` **is measured**: it builds its content from the space offered and
2194 // is then as big as what it built, which is the reference's
2195 // `size = constraints.constrain(child.size)`. It cannot be a plain leaf, because a
2196 // leaf answers before it has been asked and there is nothing to answer with until the
2197 // box is known; and it cannot be a container, because its child does not exist yet.
2198 //
2199 // Taffy calls the closure during the computation with the space actually available —
2200 // the same moment the reference runs its layout callback — and the closure builds the
2201 // subtree, lays it out in a `Layout` of its own, and hands back the size. An axis the
2202 // style pins is still the style's: taffy does not ask about a dimension it already
2203 // knows, so `LayoutBuilder::height(200.0)` behaves exactly as it did.
2204 if let Some(build) = widget.layout_builder() {
2205 let style = effective_style(widget, id, runtime, theme);
2206 // The closure needs an **owned** theme: a themed subtree's theme is a local in
2207 // this function, so it cannot be borrowed for as long as the closure lives. The
2208 // runtime and the widget are borrowed, which is the whole reason `Layout` carries
2209 // a lifetime.
2210 //
2211 // This is a clone of eleven kilobytes, and it used to be a silent one — `Theme`
2212 // was `Copy` until milestone 448, so `*theme` read like a pointer copy and was
2213 // not. It happens once per `LayoutBuilder`, not once per node.
2214 //
2215 // **Behind an `Rc`, and made somewhere else**, because the cost that mattered was
2216 // not the copy. A `Theme` local here and a closure that captures one are two
2217 // slots of eleven kilobytes in *this* function's frame, and a frame is charged to
2218 // every level of the walk whether or not the branch is taken: 24 736 bytes a
2219 // level, which overflowed a Windows main thread's megabyte at about forty. The
2220 // `Rc` makes the capture a pointer, and `owned_theme` keeps the copy in a frame
2221 // that pops before the recursion goes any deeper.
2222 let owned = owned_theme(theme);
2223 let cid = id.child(0);
2224 let measure: frus_layout::MeasureFn<'a> = Box::new(move |w, h| {
2225 // What taffy offers. `None` is an *intrinsic* question — how big would you
2226 // be with no limit, or with none at all — and there is no honest answer to
2227 // it here: finding out means running the application's callback
2228 // speculatively. The reference refuses these outright (`computeDryLayout`
2229 // asserts); we build at the offered numbers, substituting zero for what is
2230 // not offered, and leave the unoffered axis free in the nested computation
2231 // so the content's own size comes back.
2232 let offered = Size::new(w.unwrap_or(0.0), h.unwrap_or(0.0));
2233 let child = build(offered);
2234 let mut inner: Layout<BaselineData> = Layout::new();
2235 let node = build_layout(child.as_ref(), cid, runtime, &owned, &mut inner);
2236 // The content is **handed** the offered box, not asked what it would like:
2237 // it was built from that box. An axis that was not offered is the one being
2238 // asked about, and stays free so the content's own size comes back.
2239 match (w, h) {
2240 (Some(w), Some(h)) => inner.compute_filled(node, w, h),
2241 _ => inner.compute_scroll(
2242 node,
2243 offered.width,
2244 offered.height,
2245 w.is_none(),
2246 h.is_none(),
2247 ),
2248 }
2249 inner.size_of(node)
2250 });
2251 return (
2252 layout.measured_leaf(style, own_baseline, measure),
2253 Fills::own(widget, theme),
2254 );
2255 }
2256 // **A viewport is sized from its content on the axis it does not scroll.**
2257 //
2258 // A scrollable area declares a size on the axis it scrolls — that is what a viewport
2259 // is, a window shorter than what is behind it. On the *other* axis there is nothing to
2260 // choose: the content is as tall as it is, and the viewport is that tall. Until
2261 // milestone 485 a horizontal strip took the 200 px that belongs to a vertical one, so a
2262 // scrollable tab bar 48 px tall claimed 200 and pushed its panel a hundred and fifty
2263 // pixels into empty space (#65).
2264 //
2265 // It cannot be answered by the widget alone, and that is the whole reason this lives
2266 // here. A scroll host lays its content out **against** the viewport on the axis that
2267 // does not scroll — the content of a horizontal strip is given the viewport's height —
2268 // so a viewport that said `Auto` there would hand the content nothing and come back
2269 // nothing tall, the circle closing at zero. Breaking it takes a measurement, at the one
2270 // moment the runtime and the theme are both to hand.
2271 if let Some(content) = widget.scroll_content() {
2272 let style = effective_style(widget, id, runtime, theme);
2273 let axis = widget.scroll_axis();
2274 // Only an axis that **neither scrolls nor was given a size**. A caller's number is
2275 // a caller's number, and an axis that scrolls has already answered.
2276 let hug_x = !axis.free_x() && matches!(style.width, frus_layout::Dimension::Auto);
2277 let hug_y = !axis.free_y() && matches!(style.height, frus_layout::Dimension::Auto);
2278 // And an axis that **scrolls with no size given and no growing asked**: as big as
2279 // the content, up to the room on offer — the reference's viewport,
2280 // `constraints.constrain(child.size)` (milestone 590). It used to be 200 px.
2281 let free_to_measure =
2282 |d| matches!(d, frus_layout::Dimension::Auto) && style.flex_grow == 0.0;
2283 let clamp_x = axis.free_x() && free_to_measure(style.width);
2284 let clamp_y = axis.free_y() && free_to_measure(style.height);
2285 if hug_x || hug_y || clamp_x || clamp_y {
2286 let owned = owned_theme(theme);
2287 let cid = child_id(id, 0, content);
2288 let measure: frus_layout::MeasureFn<'a> = Box::new(move |w, h| {
2289 let mut inner: Layout<BaselineData> = Layout::new();
2290 let node = build_layout(content, cid, runtime, &owned, &mut inner);
2291 // The content is unconstrained on every axis that scrolls — that is what
2292 // scrolling means — and on the one being measured. What is left is an axis
2293 // with a real number on it, and the content is held to it.
2294 inner.compute_scroll(
2295 node,
2296 w.unwrap_or(0.0),
2297 h.unwrap_or(0.0),
2298 hug_x || axis.free_x() || w.is_none(),
2299 hug_y || axis.free_y() || h.is_none(),
2300 );
2301 let content = inner.size_of(node);
2302 let pad = widget.scroll_padding();
2303 // **Only the hugged axis is answered.** On the axis that scrolls, a
2304 // viewport is precisely *not* as big as its content — answering with the
2305 // content's size there would make a flexible viewport ask for the whole
2306 // length of what is behind it as its basis, and a column holding one would
2307 // overflow by however much there was to scroll. Nought is what a leaf
2308 // answered before this branch existed, and it is still the right answer.
2309 let whole_x = content.width + pad.left + pad.right;
2310 let whole_y = content.height + pad.top + pad.bottom;
2311 Size::new(
2312 if hug_x {
2313 whole_x
2314 } else if clamp_x {
2315 whole_x.min(w.unwrap_or(f32::INFINITY))
2316 } else {
2317 0.0
2318 },
2319 if hug_y {
2320 whole_y
2321 } else if clamp_y {
2322 whole_y.min(h.unwrap_or(f32::INFINITY))
2323 } else {
2324 0.0
2325 },
2326 )
2327 });
2328 return (
2329 layout.measured_leaf(style, own_baseline, measure),
2330 Fills::own(widget, theme),
2331 );
2332 }
2333 }
2334
2335 // **A stack that is as big as its largest unpinned layer** (`stack.dart:625`): each such
2336 // layer is laid out in a tree of its own under the stack's fit, and the stack is the
2337 // largest of them, held to the room it is offered. With none, or under `Expand`, it is
2338 // the room itself — which its fill request asks for.
2339 if widget.stack() && widget.stack_measured() {
2340 let style = effective_style(widget, id, runtime, theme);
2341 let owned = owned_theme(theme);
2342 let fit = widget.stack_fit();
2343 let layers = widget.children();
2344 let measure: frus_layout::MeasureFn<'a> = Box::new(move |w, h| {
2345 let mut size = Size::new(0.0, 0.0);
2346 // Under `Expand` the stack is the room; otherwise its largest unpinned layer.
2347 // With none, it is nothing here, and its fill request asks for the room.
2348 let sized_by_layers = fit != crate::StackFit::Expand;
2349 for (i, layer) in layers.iter().enumerate() {
2350 if layer.positioned().is_some() {
2351 continue;
2352 }
2353 let mut inner: Layout<BaselineData> = Layout::new();
2354 let node = build_layout(
2355 layer.as_ref(),
2356 child_id(id, i, layer.as_ref()),
2357 runtime,
2358 &owned,
2359 &mut inner,
2360 );
2361 // Forced or handed the room, a layer fills it; allowed it, it is asked —
2362 // and one that asks for the room takes it on its own, as a root.
2363 let fill = fit != crate::StackFit::Loose;
2364 let (fill_x, fill_y) = (fill, fill);
2365 inner.compute_axes(
2366 node,
2367 w.unwrap_or(0.0),
2368 h.unwrap_or(0.0),
2369 [w.is_none(), h.is_none()],
2370 [fill_x && w.is_some(), fill_y && h.is_some()],
2371 );
2372 let own = inner.size_of(node);
2373 size = Size::new(size.width.max(own.width), size.height.max(own.height));
2374 }
2375 // The room, on each axis that was offered one, when the layers do not decide.
2376 if !sized_by_layers {
2377 size = Size::new(w.unwrap_or(size.width), h.unwrap_or(size.height));
2378 }
2379 Size::new(
2380 frus_core::fits(size.width).min(w.unwrap_or(f32::INFINITY)),
2381 frus_core::fits(size.height).min(h.unwrap_or(f32::INFINITY)),
2382 )
2383 });
2384 return (
2385 layout.measured_leaf(style, own_baseline, measure),
2386 Fills::own(widget, theme),
2387 );
2388 }
2389
2390 // Scrollables, interactive viewports, fitters (`FittedBox`), navigators, virtualised
2391 // lists and stacks: their content is laid out separately (independent layers / screens /
2392 // items, or a child laid out at its natural size).
2393 if widget.scroll_content().is_some()
2394 || widget.interactive().is_some()
2395 || widget.fitted().is_some()
2396 || widget.navigator().is_some()
2397 || widget.virtual_list(Size::ZERO).is_some()
2398 || widget.page_view().is_some()
2399 || widget.stack()
2400 || widget.custom_layout().is_some()
2401 {
2402 // Nothing bubbles out of these: what is inside is laid out somewhere else, to
2403 // constraints of its own, and cannot have an opinion about this box.
2404 return (
2405 layout.leaf(effective_style(widget, id, runtime, theme), own_baseline),
2406 Fills::own(widget, theme),
2407 );
2408 }
2409 // A portal only lays out its anchor (child 0); the overlay is deferred.
2410 if widget.overlay().is_some() {
2411 let anchor_w = widget.children()[0].as_ref();
2412 let (anchor, fills) = build_layout_scoped(
2413 anchor_w,
2414 child_id(id, 0, anchor_w),
2415 runtime,
2416 theme,
2417 layout,
2418 baselines,
2419 );
2420 // **The anchor takes the room the portal is given**, across the axis it fills. Laid
2421 // in a row, an anchor stretched to the portal's height but kept its own width, so a
2422 // menu row with a submenu open, which is a portal, shrank to its label while every
2423 // other row was as wide as the menu, and its chevron was drawn off the menu's start
2424 // edge (milestone 607). A column stretches it across instead; an anchor that fills
2425 // only the height keeps the row.
2426 let mut style = effective_style(widget, id, runtime, theme);
2427 style.flex_direction = if fills.vertical && !fills.horizontal {
2428 frus_layout::FlexDirection::Row
2429 } else {
2430 frus_layout::FlexDirection::Column
2431 };
2432 return (layout.container(style, &[anchor]), fills);
2433 }
2434 let children = widget.children();
2435 if children.is_empty() {
2436 // A leaf measured under constraints (a paragraph that wraps…): taffy calls the
2437 // closure during the computation.
2438 if let Some(measure) = widget.measure(theme) {
2439 return (
2440 layout.measured_leaf(
2441 effective_style(widget, id, runtime, theme),
2442 own_baseline,
2443 measure,
2444 ),
2445 Fills::own(widget, theme),
2446 );
2447 }
2448 (
2449 layout.leaf(effective_style(widget, id, runtime, theme), own_baseline),
2450 Fills::own(widget, theme),
2451 )
2452 } else {
2453 let style = effective_style(widget, id, runtime, theme);
2454 let built: Vec<(NodeId, Fills)> = children
2455 .iter()
2456 .enumerate()
2457 .map(|(i, child)| {
2458 build_layout_scoped(
2459 child.as_ref(),
2460 child_id(id, i, child.as_ref()),
2461 runtime,
2462 theme,
2463 layout,
2464 baselines,
2465 )
2466 })
2467 .collect();
2468 let child_ids: Vec<NodeId> = built.iter().map(|(node, _)| *node).collect();
2469 // `MainAxisSize::Max`: a run that fills the room its parent leaves it. Only the
2470 // parent knows what that means — grow along an axis it shares, stretch across
2471 // one it does not — which is why the child asks rather than saying.
2472 //
2473 // The request is the child's **and its subtree's**: a column whose row fills the
2474 // width is itself as wide as the room it was given.
2475 let alone = children.len() == 1;
2476 for (node, fills) in &built {
2477 for horizontal in [true, false] {
2478 if !fills.wants(horizontal) {
2479 continue;
2480 }
2481 let parallel = horizontal == style.flex_direction.is_horizontal();
2482 // Along an axis the parent runs too, the reference leaves the main axis
2483 // **unbounded** and `Max` quietly degrades to `Min`: there is no maximum
2484 // to take. The exception is a parent with a single child — a padding, an
2485 // alignment, a decorated box — which passes its own constraints straight
2486 // down, and there the run does fill.
2487 if parallel && !alone {
2488 continue;
2489 }
2490 // A size that was only a default gives way to the room (milestone 590).
2491 if fills.soft(horizontal) {
2492 layout.clear_size(*node, horizontal);
2493 }
2494 layout.fill_parent(*node, horizontal, parallel);
2495 }
2496 }
2497 // A **lone** child gives way; several do not.
2498 //
2499 // The reference hands an inflexible child of a row or a column an unbounded main
2500 // axis: it is never squeezed, and a line that does not fit overflows and says so.
2501 // That is the default here now, because a framework that quietly crushes a button
2502 // instead of reporting a layout that does not fit is one whose bugs are invisible
2503 // — milestone 333 spent three milestones on exactly that crushed button.
2504 //
2505 // But a box with one child is not dividing a line up: it is handing its own
2506 // constraints down, and the reference bounds a lone child by them. The same
2507 // exception the fill request and the floor below both make, for the same reason.
2508 if alone {
2509 for node in &child_ids {
2510 layout.allow_shrink(*node);
2511 }
2512 }
2513 // A grid's tiles are all the same shape. The ratio is the *container's*, since a
2514 // tile cannot know how wide its column came out, and the reference says the same:
2515 // a grid's delegate hands every child a tight box derived from the track, square
2516 // unless the application asked for another ratio.
2517 if let Some(ratio) = widget.tile_shape() {
2518 for node in &child_ids {
2519 layout.set_tile_shape(*node, ratio);
2520 }
2521 }
2522 // A child that refuses to be squeezed along the row. Only a row of **several**
2523 // children may say so. Across a column the same floor would refuse a width the
2524 // column was handing it, which is how a paragraph is told how wide to be; and a
2525 // box with a single child — a padding, an alignment, a decorated box — is handing
2526 // one down rather than dividing a line up, whichever way it nominally runs.
2527 if style.flex_direction.is_horizontal() && !alone {
2528 for (child, node) in children.iter().zip(&child_ids) {
2529 if let Some(floor) = child.main_axis_floor(theme) {
2530 layout.set_min_width(*node, floor);
2531 }
2532 }
2533 }
2534 // **Baseline** cross-alignment: the children are pushed down until their
2535 // baselines meet, which is the only alignment that makes two runs of different
2536 // sizes read as one line rather than as one row.
2537 //
2538 // It is resolved here rather than by taffy, which cannot: taffy asks a leaf for
2539 // a size and a leaf can only answer with one. The measurement a baseline needs
2540 // is the font's, so the answer has to come from up here, where the widgets are.
2541 //
2542 // The **natural** baseline is the right one to measure even though the children
2543 // will be laid out at some other width: the first line of a piece of text sits
2544 // in the same place however narrow the box gets, since narrowing it adds lines
2545 // below rather than moving the first one.
2546 if style.align == frus_layout::Align::Baseline && style.flex_direction.is_horizontal() {
2547 let own: Vec<Option<f32>> = children
2548 .iter()
2549 .enumerate()
2550 .map(|(i, child)| {
2551 natural_baseline(
2552 child.as_ref(),
2553 child_id(id, i, child.as_ref()),
2554 runtime,
2555 theme,
2556 )
2557 })
2558 .collect();
2559 if let Some(deepest) = own.iter().flatten().copied().reduce(f32::max) {
2560 for (node, b) in child_ids.iter().zip(&own) {
2561 // A child with no baseline keeps its place: it is a box, and a box
2562 // has nothing to line up.
2563 if let Some(b) = b {
2564 layout.add_margin_top(*node, deepest - b);
2565 }
2566 }
2567 }
2568 }
2569 // What this container passes on to **its** parent. Across its own axis the
2570 // container's size is its child's, so the request keeps going; along it the
2571 // container divides the room up and the request stops. A container with a single
2572 // child divides nothing, and passes the whole request on.
2573 let mut fills = Fills::own(widget, theme);
2574 // A grid divides its room on both axes and sizes its cells itself — the
2575 // reference's grid hands its tiles tight constraints — so a cell's request stops at
2576 // it (milestone 633).
2577 let divides_both = style.grid_columns.is_some();
2578 for (_, child) in &built {
2579 if divides_both {
2580 continue;
2581 }
2582 fills = fills.merge(if alone {
2583 *child
2584 } else {
2585 child.across(style.flex_direction)
2586 });
2587 }
2588 (
2589 layout.container(style, &child_ids),
2590 fills.bounded_by(&style),
2591 )
2592 }
2593}
2594
2595/// The distance from the top of `widget`'s **natural** box to the first baseline inside
2596/// it, or `None` when there is no text in there to have one.
2597///
2598/// It lays the subtree out on its own, exactly as [`natural_size`] does, and takes the
2599/// first baseline the layout collected. First in *layout* order, which is the order a
2600/// reader would find it in — and it is the leaves that carry them, so an intervening
2601/// padding or alignment is already counted in the rectangle it comes with.
2602pub(crate) fn natural_baseline<Msg>(
2603 widget: &dyn Widget<Msg>,
2604 id: WidgetId,
2605 runtime: &Runtime,
2606 theme: &Theme,
2607) -> Option<f32> {
2608 let mut layout = Layout::new();
2609 let node = build_layout(widget, id, runtime, theme, &mut layout);
2610 layout.compute_scroll(node, 0.0, 0.0, true, true);
2611 let rects = layout.absolute_rects(node);
2612 let top = rects.first()?.0.y;
2613 rects
2614 .iter()
2615 .find_map(|(rect, data)| data.copied().flatten().map(|b| rect.y + b - top))
2616}
2617
2618/// **Natural** (intrinsic) size of a subtree: laid out under free axes (`MaxContent`), with
2619/// no imposed constraint. Used by `RotatedBox` (the dimensions to swap) and — at render time
2620/// — by `FittedBox` (the fit factor).
2621pub(crate) fn natural_size<Msg>(
2622 widget: &dyn Widget<Msg>,
2623 id: WidgetId,
2624 runtime: &Runtime,
2625 theme: &Theme,
2626) -> Size {
2627 let mut layout = Layout::new();
2628 let node = build_layout(widget, id, runtime, theme, &mut layout);
2629 layout.compute_scroll(node, 0.0, 0.0, true, true);
2630 layout
2631 .absolute_rects(node)
2632 .first()
2633 .map(|(r, _)| Size::new(r.width, r.height))
2634 .unwrap_or(Size::new(0.0, 0.0))
2635}
2636
2637/// A deferred overlay: `(content, id, the anchor's bounds, placement, dismissal,
2638/// progress 0..=1, whether it takes the pointer, the scrim's colour, the theme it was
2639/// declared under)`. The progress animates the appearance — a drawer sliding in, a
2640/// scrim fading — and is `1.0` for overlays that are not animated.
2641///
2642/// The **theme travels with it** because an overlay is painted long after the walk has
2643/// left the node that declared it: a dialog opened from inside a [`crate::Themed`]
2644/// subtree would otherwise come out in the root's theme, which is the surprise the
2645/// reference had to grow a whole mechanism to avoid. The scrim's colour travels for the
2646/// same reason — by the time it is painted, the widget that asked for it is long
2647/// behind.
2648type Overlay<'a, Msg> = (
2649 &'a dyn Widget<Msg>,
2650 WidgetId,
2651 Rect,
2652 Placement,
2653 Option<Msg>,
2654 f32,
2655 bool,
2656 Option<frus_core::Color>,
2657 std::rc::Rc<Theme>,
2658);
2659
2660struct Builder<'a, Msg> {
2661 scene: Scene,
2662 hits: Vec<Hit<Msg>>,
2663 long_presses: Vec<Hit<Msg>>,
2664 dismisses: Vec<Msg>,
2665 edit_actions: Vec<(WidgetId, crate::EditAction)>,
2666 toolbars: Vec<Rect>,
2667 overlay_cover: Vec<Rect>,
2668 focusables: Vec<Focusable>,
2669 /// Start of the topmost modal overlay's focus scope.
2670 focus_scope_start: Option<usize>,
2671 /// Set while inside an `ExcludeFocus`: nothing in here registers a focus stop.
2672 focus_excluded: bool,
2673 /// Where a press or a drag can land, when that is less than what is drawn: inside a
2674 /// stack that lets its layers hang over its edges, its own box (milestone 639).
2675 touch_clip: Option<Rect>,
2676 /// The identity of the nearest enclosing backdrop group — the key a backdrop
2677 /// asking to be shared takes. Pushed and popped by the walk, like the focus flags.
2678 backdrop_group: Option<u64>,
2679 /// Set while inside an `ExcludeFocusTraversal`: stops register, Tab passes them by.
2680 focus_skipped: bool,
2681 /// The traversal order in force, from the nearest enclosing `FocusTraversalOrder`.
2682 focus_order: Option<f32>,
2683 /// The nearest enclosing `FocusTraversalGroup`, within which an order is resolved.
2684 focus_group: Option<WidgetId>,
2685 /// The nearest enclosing [`crate::AutofillGroup`]: the form a field is part of.
2686 autofill_group: Option<WidgetId>,
2687 /// Shortcut and action scopes closed so far this frame.
2688 scopes: Vec<Scope<Msg>>,
2689 /// Keystroke listeners, with the focus stops they cover.
2690 #[allow(clippy::type_complexity)]
2691 listeners: Vec<(
2692 std::ops::Range<usize>,
2693 std::rc::Rc<dyn Fn(KeyStroke) -> Option<Msg>>,
2694 )>,
2695 scrollables: Vec<Scrollable>,
2696 scrollbars: Vec<Scrollbar>,
2697 draggables: Vec<(WidgetId, Rect)>,
2698 drag_sources: Vec<DragSource>,
2699 drop_zones: Vec<DropZone>,
2700 inks: Vec<(WidgetId, Rect)>,
2701 /// The detectors that take a drag, in painted order, with the box a press can land in
2702 /// (clipped) and the whole box (milestone 582).
2703 /// The row that wants the title bar's line, where it was laid out, and what the system
2704 /// acts on there (milestones 640, 642).
2705 title_bar_row: Option<Rect>,
2706 title_bar_regions: Vec<(Rect, crate::TitleBarRole)>,
2707 pans: Vec<(WidgetId, Rect, Rect)>,
2708 /// The widgets that hear the pointer's raw events, likewise (milestone 586).
2709 pointer_listeners: Vec<(WidgetId, Rect, Rect)>,
2710 /// The regions the mouse enters and leaves, in painted order (milestone 583).
2711 hover_regions: Vec<HoverEntry>,
2712 text_stops: Vec<crate::TextStop>,
2713 /// The region the mouse enters and leaves that the walk is inside, if any: the parent of
2714 /// the next one registered (milestone 583).
2715 hover_parent: Option<WidgetId>,
2716 /// The [`SelectionArea`](crate::SelectionArea) the walk is inside, if it is inside one, and
2717 /// its widget — which may have a say over the bar.
2718 area: Option<WidgetId>,
2719 area_widget: Option<&'a dyn Widget<Msg>>,
2720 /// Whether the bar over a selection area's selection has been placed this frame: it goes
2721 /// against the first text the selection covers, and only that one.
2722 region_bar_placed: bool,
2723 reorderables: Vec<(WidgetId, Rect)>,
2724 interactives: Vec<(WidgetId, Rect)>,
2725 /// Deferred overlays: (content, id, the anchor's bounds, placement, dismissal, progress
2726 /// `0..=1`). The progress animates the appearance (a drawer sliding in, a scrim fading);
2727 /// it is `1.0` for overlays that are not animated.
2728 overlays: Vec<Overlay<'a, Msg>>,
2729 /// A widget is asking for a continuous, time-driven animation.
2730 wants_animation: bool,
2731 available: Size,
2732 runtime: &'a Runtime,
2733 /// The theme **in force at this point of the walk** — the root's, unless a
2734 /// [`crate::Themed`] ancestor replaced it. Owned rather than borrowed because a
2735 /// subtree's theme is derived from the one above it and outlives nothing.
2736 ///
2737 /// Owned **behind an `Rc`**: a `Theme` is eleven kilobytes, and this field is swapped
2738 /// on the way into a themed subtree and swapped back on the way out. By value that is
2739 /// two copies of eleven kilobytes standing in the walk's frame — one being moved in,
2740 /// one held across the recursion waiting to be put back — on every level of the tree,
2741 /// taken or not. Behind a pointer the swap is a pointer's width, and the walk's frame
2742 /// went from 28 144 bytes a level to 5 264.
2743 theme: std::rc::Rc<Theme>,
2744 /// The inspector's collection (`Some` only while it is on): one node per painted widget,
2745 /// in paint order.
2746 inspector: Option<Vec<crate::inspector::InspectorNode>>,
2747 /// **The widgets this frame builds as it goes** — a list's rows, a paged view's pages,
2748 /// what a builder makes from its box — kept for the rest of the walk, so that they go
2749 /// through the same walk as the tree they sit in (milestone 592). They used to go through
2750 /// a reduced copy of it that knew no overlay, no transform and no nested scroll, and an
2751 /// open menu in a list's row crashed the frame.
2752 built: &'a Built<Msg>,
2753 /// Current depth of the walk (for the dump's indentation and the palette of the
2754 /// inspector's outlines).
2755 depth: usize,
2756 /// The [`crate::RefreshIndicator`] area currently being walked, if any: every scrollable
2757 /// registered under it records it, so the shell knows where to send the movement
2758 /// its physics refuses. Saved and restored around the subtree, so sibling areas do
2759 /// not inherit one another's.
2760 refresh_host: Option<WidgetId>,
2761 /// The scroll region currently being walked, if any: the nearest one enclosing
2762 /// whatever the walk is looking at. Focus stops record it so the shell can reveal
2763 /// them, and nested regions record one another. Saved and restored around the
2764 /// subtree, like [`Builder::refresh_host`].
2765 scroll_host: Option<WidgetId>,
2766 /// Which screen of a route transition the walk is inside (`0` = leaving, `1` =
2767 /// entering); `None` outside one. Recorded on each hero so the two sides of a
2768 /// flight are told apart rather than guessed from paint order.
2769 hero_screen: Option<u8>,
2770 /// The shared elements seen so far, each with the widget that declared it (needed
2771 /// to lift its subtree's painting) and the transition screen it belongs to.
2772 heroes: Vec<(HeroSpot, &'a dyn Widget<Msg>)>,
2773 /// The frame's refresh areas, in paint order.
2774 refreshes: Vec<Refreshable>,
2775 /// The frame's dismissible items, in paint order.
2776 dismissables: Vec<Dismissable>,
2777 /// The frame's sheets, in the order their subtrees finished walking.
2778 sheets: Vec<crate::sheet::SheetArea>,
2779 /// Accessibility nodes collected during the walk (paint order).
2780 semantics: Vec<(WidgetId, Rect, frus_core::SemanticsProperties)>,
2781 /// The parts of the screen saying what the system bars over them should look like,
2782 /// with their boxes, in paint order.
2783 system_ui: Vec<(Rect, crate::SystemUiOverlayStyle)>,
2784 /// Boxes whose children did not fit, screen-positioned, from every sub-root walked
2785 /// this frame.
2786 overflows: std::cell::RefCell<Vec<Overflowing>>,
2787 /// The same, still in their sub-root's own coordinates, waiting for the walk to reach
2788 /// that sub-root and say where on screen it ended up.
2789 pending_overflows: std::cell::RefCell<std::collections::HashMap<WidgetId, Vec<Overflowing>>>,
2790}
2791
2792impl<'a, Msg: Clone + 'static> Builder<'a, Msg> {
2793 /// A layout root's rects, through the relayout cache retained in the runtime (it only
2794 /// recomputes through taffy when the style/structure/constraints have changed). A brief
2795 /// mutable borrow: the `Vec` returned is owned.
2796 ///
2797 /// In **RTL**, taffy computes in LTR (canonical, and cached), then each rect is
2798 /// **mirrored** about the root's width (the 1st rect): the rows reverse, the alignment
2799 /// and the margins flip — without touching the widgets. The text itself is drawn
2800 /// normally inside its moved box (the *internal* bidi is handled by cosmic-text).
2801 fn cached_rects(&self, key: WidgetId, root: &dyn Widget<Msg>, c: Constraints) -> Vec<Rect> {
2802 let (mut rects, overflows) =
2803 self.runtime
2804 .layout_cache
2805 .borrow_mut()
2806 .rects(key, root, self.runtime, &self.theme, c);
2807 self.mirror(&mut rects);
2808 self.record_overflows(key, &rects, overflows);
2809 rects
2810 }
2811
2812 /// Files this sub-root's overflowing boxes under its identity, mirrored with
2813 /// everything else in RTL — where an edge changes name as well as position, since the
2814 /// layout is the mirror image and "ran past the right" becomes "ran past the left".
2815 ///
2816 /// They wait here rather than going straight out because a sub-root's rectangles are
2817 /// in its **own** coordinates: a scrollable's content, a page, a list item and a
2818 /// stack's layer each get their own taffy pass, and where that pass lands on screen is
2819 /// only known when the walk reaches it. [`Self::claim_overflows`] is the other half.
2820 /// Records a child that came out **bigger than the box holding it**, one entry per
2821 /// edge it ran past — the same yellow and black band a row too full of children gets,
2822 /// from a box that laid its child out itself and so was never measured against it.
2823 ///
2824 /// The rectangle is already on the screen, this being the walk rather than a sub-root's
2825 /// own pass, so it goes straight out rather than through [`Self::record_overflows`].
2826 fn report_spill(&self, own: Rect, child: Rect) {
2827 // Per edge, from where the child actually landed: a centred child that is too wide
2828 // runs past **both** sides, and a band on one of them would be a half-truth.
2829 let over = [
2830 (Side::Left, own.x - child.x),
2831 (Side::Top, own.y - child.y),
2832 (Side::Right, (child.x + child.width) - (own.x + own.width)),
2833 (
2834 Side::Bottom,
2835 (child.y + child.height) - (own.y + own.height),
2836 ),
2837 ];
2838 let spills: Vec<Overflowing> = over
2839 .into_iter()
2840 .filter(|(_, amount)| *amount > 0.5)
2841 .map(|(side, amount)| Overflowing {
2842 rect: own,
2843 side,
2844 amount,
2845 })
2846 .collect();
2847 self.overflows.borrow_mut().extend(spills);
2848 }
2849
2850 fn record_overflows(&self, key: WidgetId, mirrored: &[Rect], overflows: Vec<Overflowing>) {
2851 if overflows.is_empty() {
2852 return;
2853 }
2854 let rtl = self.rtl();
2855 let root = mirrored.first().copied();
2856 let list: Vec<Overflowing> = overflows
2857 .into_iter()
2858 .map(|mut o| {
2859 if let (true, Some(root)) = (rtl, root) {
2860 o.rect.x = root.x + (root.width - (o.rect.x - root.x) - o.rect.width);
2861 o.side = match o.side {
2862 Side::Left => Side::Right,
2863 Side::Right => Side::Left,
2864 other => other,
2865 };
2866 }
2867 o
2868 })
2869 .collect();
2870 self.pending_overflows.borrow_mut().insert(key, list);
2871 }
2872
2873 /// The walk has reached sub-root `id` and knows where it sits: its overflowing boxes
2874 /// become screen rectangles.
2875 fn claim_overflows(&self, id: WidgetId, translation: (f32, f32)) {
2876 let taken = self.pending_overflows.borrow_mut().remove(&id);
2877 if let Some(list) = taken {
2878 self.overflows
2879 .borrow_mut()
2880 .extend(list.into_iter().map(|o| Overflowing {
2881 rect: o.rect.translate(translation.0, translation.1),
2882 ..o
2883 }));
2884 }
2885 }
2886
2887 /// The layout direction **in force here** — read from the ambient theme rather than
2888 /// from a field set once at the root, so a [`crate::Themed`] subtree carries its own.
2889 ///
2890 /// What that reaches is everything decided during the walk: which edge a drawer
2891 /// slides from, which way an anchored overlay opens, the sign of a rotation, the
2892 /// mirroring of any **layout root** inside the subtree (a scrollable, an overlay).
2893 /// What it does not reach is ordinary flow inside the subtree: the frame's rects are
2894 /// computed once at the root and mirrored there as a whole, so a direction set part
2895 /// way down does not reverse the rows around it. Setting the direction on the theme
2896 /// handed to `build_ui` is still the way to flip an application.
2897 fn rtl(&self) -> bool {
2898 self.theme.direction.is_rtl()
2899 }
2900
2901 /// Mirrors a root's rects horizontally (RTL). The first rect is the root itself: its
2902 /// width is the axis of symmetry.
2903 fn mirror(&self, rects: &mut [Rect]) {
2904 if !self.rtl() {
2905 return;
2906 }
2907 let Some(root) = rects.first().copied() else {
2908 return;
2909 };
2910 // The axis: the rects are relative to the root (origin at root.x).
2911 for r in rects.iter_mut() {
2912 r.x = root.x + (root.width - (r.x - root.x) - r.width);
2913 }
2914 }
2915
2916 /// 64-bit fingerprint of everything painting a boundary subtree reads **without**
2917 /// rebuilding the `view`: each descendant's `Status` (in walk order) and the subtree's
2918 /// absolute rects. An unchanged fingerprint + generation ⇒ a bit-for-bit identical paint.
2919 /// The time is excluded (a cacheable boundary holds no `continuous` widget).
2920 fn boundary_fingerprint(
2921 &self,
2922 widget: &dyn Widget<Msg>,
2923 id: WidgetId,
2924 translation: (f32, f32),
2925 sub: &[Rect],
2926 ) -> u64 {
2927 let mut h = std::collections::hash_map::DefaultHasher::new();
2928 self.rtl().hash(&mut h);
2929 translation.0.to_bits().hash(&mut h);
2930 translation.1.to_bits().hash(&mut h);
2931 for r in sub {
2932 r.x.to_bits().hash(&mut h);
2933 r.y.to_bits().hash(&mut h);
2934 r.width.to_bits().hash(&mut h);
2935 r.height.to_bits().hash(&mut h);
2936 }
2937 self.hash_statuses(widget, id, &mut h);
2938 h.finish()
2939 }
2940
2941 /// Adds `widget`'s `Status` to `h`, then its children's, recursively — following the
2942 /// walk's identity scheme (`child_id`) **exactly**, so it lines up with the order in which
2943 /// `walk_node` paints them.
2944 fn hash_statuses<H: Hasher>(&self, widget: &dyn Widget<Msg>, id: WidgetId, h: &mut H) {
2945 hash_status(&self.full_status(widget, id), h);
2946 // Live ink is part of what the boundary paints, and it moves every frame while a
2947 // splash is alive. Without it here the cached primitives would be replayed and
2948 // the ripple would stand still — and once the ink dries the hash goes back to
2949 // what it was, so the boundary starts hitting again.
2950 if let Some(ripples) = self.runtime.ink.get(&id) {
2951 ripples.hash_state(h);
2952 }
2953 for (i, child) in widget.children().iter().enumerate() {
2954 self.hash_statuses(child.as_ref(), child_id(id, i, child.as_ref()), h);
2955 }
2956 }
2957
2958 /// Current lengths of the builder's collections: the lower bound of the slices to capture
2959 /// for a boundary (see [`capture_since`](Self::capture_since)).
2960 fn snapshot(&self) -> Snapshot {
2961 Snapshot {
2962 scene: self.scene.primitives().len(),
2963 hits: self.hits.len(),
2964 long_presses: self.long_presses.len(),
2965 dismisses: self.dismisses.len(),
2966 focusables: self.focusables.len(),
2967 scrollables: self.scrollables.len(),
2968 draggables: self.draggables.len(),
2969 drag_sources: self.drag_sources.len(),
2970 drop_zones: self.drop_zones.len(),
2971 inks: self.inks.len(),
2972 pans: self.pans.len(),
2973 pointer_listeners: self.pointer_listeners.len(),
2974 hover_regions: self.hover_regions.len(),
2975 text_stops: self.text_stops.len(),
2976 semantics: self.semantics.len(),
2977 system_ui: self.system_ui.len(),
2978 overlays: self.overlays.len(),
2979 focus_scope_start: self.focus_scope_start,
2980 }
2981 }
2982
2983 /// Captures the output produced since `snap` (the collections' *tail slices*) into a
2984 /// [`BoundaryData`]. Returns `None` — hence **not cacheable** — when the subtree pushed an
2985 /// overlay or touched the modal focus scope (global state that cannot be captured here).
2986 fn capture_since(&self, snap: &Snapshot) -> Option<BoundaryData<Msg>> {
2987 if self.overlays.len() != snap.overlays || self.focus_scope_start != snap.focus_scope_start
2988 {
2989 return None;
2990 }
2991 Some(BoundaryData {
2992 prims: self.scene.primitives()[snap.scene..].to_vec(),
2993 hits: self.hits[snap.hits..].to_vec(),
2994 long_presses: self.long_presses[snap.long_presses..].to_vec(),
2995 dismisses: self.dismisses[snap.dismisses..].to_vec(),
2996 focusables: self.focusables[snap.focusables..].to_vec(),
2997 scrollables: self.scrollables[snap.scrollables..].to_vec(),
2998 draggables: self.draggables[snap.draggables..].to_vec(),
2999 drag_sources: self.drag_sources[snap.drag_sources..].to_vec(),
3000 drop_zones: self.drop_zones[snap.drop_zones..].to_vec(),
3001 inks: self.inks[snap.inks..].to_vec(),
3002 pans: self.pans[snap.pans..].to_vec(),
3003 pointer_listeners: self.pointer_listeners[snap.pointer_listeners..].to_vec(),
3004 hover_regions: self.hover_regions[snap.hover_regions..].to_vec(),
3005 text_stops: self.text_stops[snap.text_stops..].to_vec(),
3006 semantics: self.semantics[snap.semantics..].to_vec(),
3007 system_ui: self.system_ui[snap.system_ui..].to_vec(),
3008 })
3009 }
3010
3011 /// Replays a boundary from the cache: primitives already formed (clip/owner baked in) and
3012 /// interaction maps, appended as is.
3013 fn splice_boundary(&mut self, data: BoundaryData<Msg>) {
3014 for p in data.prims {
3015 self.scene.push_primitive(p);
3016 }
3017 self.hits.extend(data.hits);
3018 self.long_presses.extend(data.long_presses);
3019 self.dismisses.extend(data.dismisses);
3020 self.focusables.extend(data.focusables);
3021 self.scrollables.extend(data.scrollables);
3022 self.draggables.extend(data.draggables);
3023 self.drag_sources.extend(data.drag_sources);
3024 self.drop_zones.extend(data.drop_zones);
3025 self.inks.extend(data.inks);
3026 self.pans.extend(data.pans);
3027 self.pointer_listeners.extend(data.pointer_listeners);
3028 self.hover_regions.extend(data.hover_regions);
3029 self.text_stops.extend(data.text_stops);
3030 self.semantics.extend(data.semantics);
3031 self.system_ui.extend(data.system_ui);
3032 }
3033
3034 /// Lengths of every registry a [`ModalBarrier`] can withhold from, taken before its subtree
3035 /// is walked.
3036 fn barrier_base(&self) -> BarrierBase {
3037 BarrierBase {
3038 scene: self.scene.primitives().len(),
3039 hits: self.hits.len(),
3040 long_presses: self.long_presses.len(),
3041 focusables: self.focusables.len(),
3042 scrollables: self.scrollables.len(),
3043 scrollbars: self.scrollbars.len(),
3044 draggables: self.draggables.len(),
3045 drag_sources: self.drag_sources.len(),
3046 drop_zones: self.drop_zones.len(),
3047 inks: self.inks.len(),
3048 pans: self.pans.len(),
3049 pointer_listeners: self.pointer_listeners.len(),
3050 hover_regions: self.hover_regions.len(),
3051 text_stops: self.text_stops.len(),
3052 reorderables: self.reorderables.len(),
3053 interactives: self.interactives.len(),
3054 semantics: self.semantics.len(),
3055 }
3056 }
3057
3058 /// Drops what the subtree added since `base`, according to `barrier`, and — for an
3059 /// absorbing barrier — puts a message-less hit target in its place so that input stops
3060 /// at the barrier instead of reaching whatever is painted behind it.
3061 /// Resolves the **shared elements** of a route transition: every tag that appears
3062 /// on both screens flies from where it was to where it is going.
3063 ///
3064 /// Called once, after both screens have been drawn, because that is the first
3065 /// moment both boxes are known. `hero_base` and `scene_base` mark where those two
3066 /// screens started.
3067 ///
3068 /// What flies is the **destination**'s own painting, mapped onto the box it is
3069 /// travelling through. Both originals are taken out of the frame: a thing that is
3070 /// flying is not also sitting at either end, and leaving them in is the difference
3071 /// between a shared element and three copies of one.
3072 fn fly_heroes(&mut self, hero_base: usize, scene_base: usize, progress: f32) {
3073 let spots: Vec<(HeroSpot, &'a dyn Widget<Msg>)> = self.heroes.split_off(hero_base);
3074 if spots.len() < 2 {
3075 return;
3076 }
3077 // The pairs: one tag, one hero on each side. A tag on one side only has nothing
3078 // to fly to, and a tag used more than once per side is ambiguous — both are
3079 // left alone rather than guessed at.
3080 let mut flights: Vec<(Rect, Rect, &'a dyn Widget<Msg>, WidgetId, WidgetId)> = Vec::new();
3081 for (from, _) in spots.iter().filter(|(s, _)| s.screen == 0) {
3082 let mut matching = spots
3083 .iter()
3084 .filter(|(s, _)| s.screen == 1 && s.tag == from.tag);
3085 let Some((to, widget)) = matching.next() else {
3086 continue;
3087 };
3088 if matching.next().is_some() {
3089 continue;
3090 }
3091 flights.push((from.rect, to.rect, *widget, from.id, to.id));
3092 }
3093 if flights.is_empty() {
3094 return;
3095 }
3096
3097 // Everything the two screens painted, so the originals can be taken out and the
3098 // travelling copies laid on top.
3099 let painted = self.scene.split_off(scene_base);
3100 let mut hidden: std::collections::HashSet<u64> = std::collections::HashSet::new();
3101 for (_, _, widget, from_id, to_id) in &flights {
3102 for id in subtree_ids(*widget, *to_id) {
3103 hidden.insert(id.as_u64());
3104 }
3105 // The source's subtree is the *other* screen's widget, whose identity is
3106 // rooted at `from_id`; the same relative shape, so the ids follow.
3107 for id in subtree_ids(*widget, *from_id) {
3108 hidden.insert(id.as_u64());
3109 }
3110 }
3111 // The destinations' own painting, wherever it was drawn: a page in a transition may
3112 // be inside a layer that zooms or fades it (milestone 620), and a hero in flight is
3113 // above both pages, untouched by either — as the reference's flies in the overlay.
3114 let destinations: std::collections::HashSet<u64> = flights
3115 .iter()
3116 .flat_map(|(_, _, widget, _, to_id)| subtree_ids(*widget, *to_id))
3117 .map(|id| id.as_u64())
3118 .collect();
3119 let mut taken = Vec::new();
3120 for primitive in take_owned(painted, &hidden, &destinations, &mut taken) {
3121 self.scene.push_primitive(primitive);
3122 }
3123 let painted = taken;
3124
3125 self.scene.set_clip(Rect::UNBOUNDED);
3126 for (from, to, _, _, to_id) in &flights {
3127 let travelling = lerp_rect(*from, *to, progress);
3128 if to.width <= 0.0 || to.height <= 0.0 {
3129 continue;
3130 }
3131 let (sx, sy) = (travelling.width / to.width, travelling.height / to.height);
3132 let pivot = Point::new(to.x, to.y);
3133 let owners: std::collections::HashSet<u64> = subtree_ids(
3134 flights
3135 .iter()
3136 .find(|(_, _, _, _, id)| id == to_id)
3137 .map(|(_, _, w, _, _)| *w)
3138 .expect("the flight just matched"),
3139 *to_id,
3140 )
3141 .iter()
3142 .map(|id| id.as_u64())
3143 .collect();
3144 for primitive in painted.iter().filter(|p| owners.contains(&p.owner())) {
3145 let flown = primitive
3146 .scaled_about_xy(pivot, sx, sy)
3147 .translated(travelling.x - to.x, travelling.y - to.y)
3148 .with_clip(Rect::UNBOUNDED);
3149 self.scene.push_primitive(flown);
3150 }
3151 }
3152 }
3153
3154 /// Reconciles what a [`crate::Semantics`] wrapper's subtree announced with what the
3155 /// wrapper says about it.
3156 ///
3157 /// The wrapper's own answers win, and the subtree's show through wherever it said
3158 /// nothing — a container that knows a child is a *heading* rarely knows what the words
3159 /// are, and dropping them would replace a spoken title with silence.
3160 ///
3161 /// The merged node goes in **where the subtree's first one was**, not at the end. The
3162 /// accessibility tree is flat and its order is reading order, so a wrapper appended
3163 /// after its own contents would be read after them.
3164 fn apply_description(
3165 &mut self,
3166 described: crate::semantics::Description,
3167 base: usize,
3168 id: WidgetId,
3169 own: Rect,
3170 ) {
3171 let mut props = described.props;
3172 // Merging swallows every child's own entry, so **something** has to answer for
3173 // this node when it is activated — the wrapper itself never does (see
3174 // `Semantics::on_click`'s own reasoning: an annotation that swallowed a click
3175 // would put a hit target over the widget it describes). Found testing #18 in a
3176 // real browser and real screen reader: a `Checkbox::new(..).on_toggle(..)`
3177 // merged with its row's caption read correctly and did **nothing** on
3178 // activation, on every platform, because the merged entry carried the row's own
3179 // id, which nothing routes a click through. When exactly one child in the
3180 // swallowed subtree was clickable, its id is who a click or a keyboard
3181 // activation has to reach, so the merged entry keys on that id instead — the
3182 // wrapper's id is kept only when there is no such child, or more than one and
3183 // "the one that gets clicked" has no single answer.
3184 let mut operable = None;
3185 if described.merging {
3186 let mut clickable_count = 0;
3187 for (child_id, _, child) in self.semantics.drain(base..).collect::<Vec<_>>() {
3188 if child.clickable {
3189 clickable_count += 1;
3190 operable = Some(child_id);
3191 }
3192 props = props.over(&child);
3193 }
3194 if clickable_count != 1 {
3195 operable = None;
3196 }
3197 }
3198 let id = operable.unwrap_or(id);
3199 if props.is_meaningful() && own.width > 0.0 && own.height > 0.0 {
3200 self.semantics.insert(base, (id, own, props));
3201 }
3202 }
3203
3204 fn apply_barrier(
3205 &mut self,
3206 barrier: ModalBarrier,
3207 base: &BarrierBase,
3208 id: WidgetId,
3209 own: Rect,
3210 ) {
3211 if barrier.pointer {
3212 self.hits.truncate(base.hits);
3213 self.long_presses.truncate(base.long_presses);
3214 self.focusables.truncate(base.focusables);
3215 self.scrollables.truncate(base.scrollables);
3216 self.scrollbars.truncate(base.scrollbars);
3217 self.draggables.truncate(base.draggables);
3218 self.drag_sources.truncate(base.drag_sources);
3219 self.drop_zones.truncate(base.drop_zones);
3220 self.inks.truncate(base.inks);
3221 self.pans.truncate(base.pans);
3222 self.pointer_listeners.truncate(base.pointer_listeners);
3223 self.hover_regions.truncate(base.hover_regions);
3224 self.text_stops.truncate(base.text_stops);
3225 self.reorderables.truncate(base.reorderables);
3226 self.interactives.truncate(base.interactives);
3227 // The modal focus scope is an index **into** `focusables`. A barrier that cut
3228 // away the focusable it pointed at would leave it dangling past the end, and
3229 // every later slice of the pool would then be empty — no focusable at all,
3230 // rather than the ones the barrier meant to spare.
3231 if let Some(start) = self.focus_scope_start {
3232 self.focus_scope_start = Some(start.min(self.focusables.len()));
3233 }
3234 }
3235 if barrier.absorb && own.width > 0.0 && own.height > 0.0 {
3236 self.hits.push(Hit {
3237 id,
3238 rect: own,
3239 msg: None,
3240 xform: None,
3241 });
3242 }
3243 if barrier.paint {
3244 // The clip is baked into each primitive when it is pushed, so dropping the tail
3245 // leaves nothing for the next sibling to repair.
3246 let _hidden = self.scene.split_off(base.scene);
3247 }
3248 if barrier.semantics {
3249 self.semantics.truncate(base.semantics);
3250 }
3251 }
3252
3253 /// Lower bounds of the interaction registries **before** a transformed composited subtree.
3254 fn xform_base(&self) -> XformBase {
3255 XformBase {
3256 hits: self.hits.len(),
3257 long_presses: self.long_presses.len(),
3258 focusables: self.focusables.len(),
3259 scrollables: self.scrollables.len(),
3260 draggables: self.draggables.len(),
3261 drag_sources: self.drag_sources.len(),
3262 drop_zones: self.drop_zones.len(),
3263 inks: self.inks.len(),
3264 pans: self.pans.len(),
3265 pointer_listeners: self.pointer_listeners.len(),
3266 hover_regions: self.hover_regions.len(),
3267 text_stops: self.text_stops.len(),
3268 reorderables: self.reorderables.len(),
3269 semantics: self.semantics.len(),
3270 }
3271 }
3272
3273 /// Applies the layer transform `matrix` to the registry entries **added since** `base`:
3274 /// the test point of click / long-press targets goes through `M⁻¹` (without overwriting an
3275 /// inner transform already set); and when `matrix` preserves axis alignment
3276 /// (scale/translation, no rotation), the **rects** of focus / scrolling / dragging /
3277 /// **reordering** / accessibility are mapped exactly by `M` (otherwise they are left as
3278 /// they are — the click stays correct through `M⁻¹`). The **shared factor** of the two
3279 /// transformed-composition sites (`walk`'s boundary and `emit_transformed_child`): one
3280 /// place to keep up to date, which forgets no registry (milestone 250 had forgotten
3281 /// `reorderables` in exactly one of the two).
3282 fn transform_interaction_registries(&mut self, base: &XformBase, matrix: Affine) {
3283 let inverse = matrix.inverse();
3284 for h in &mut self.hits[base.hits..] {
3285 h.xform.get_or_insert(inverse);
3286 }
3287 for h in &mut self.long_presses[base.long_presses..] {
3288 h.xform.get_or_insert(inverse);
3289 }
3290 if matrix.is_axis_aligned() {
3291 for f in &mut self.focusables[base.focusables..] {
3292 f.rect = matrix.apply_rect(f.rect);
3293 f.bounds = matrix.apply_rect(f.bounds);
3294 }
3295 for area in &mut self.scrollables[base.scrollables..] {
3296 area.viewport = matrix.apply_rect(area.viewport);
3297 }
3298 for source in &mut self.drag_sources[base.drag_sources..] {
3299 source.rect = matrix.apply_rect(source.rect);
3300 }
3301 for zone in &mut self.drop_zones[base.drop_zones..] {
3302 zone.rect = matrix.apply_rect(zone.rect);
3303 }
3304 for (_, r) in &mut self.inks[base.inks..] {
3305 *r = matrix.apply_rect(*r);
3306 }
3307 for (_, seen, whole) in &mut self.pans[base.pans..] {
3308 *seen = matrix.apply_rect(*seen);
3309 *whole = matrix.apply_rect(*whole);
3310 }
3311 for (_, seen, whole) in &mut self.pointer_listeners[base.pointer_listeners..] {
3312 *seen = matrix.apply_rect(*seen);
3313 *whole = matrix.apply_rect(*whole);
3314 }
3315 for region in &mut self.hover_regions[base.hover_regions..] {
3316 region.seen = matrix.apply_rect(region.seen);
3317 region.whole = matrix.apply_rect(region.whole);
3318 }
3319 for stop in &mut self.text_stops[base.text_stops..] {
3320 stop.rect = matrix.apply_rect(stop.rect);
3321 }
3322 for (_, r) in &mut self.draggables[base.draggables..] {
3323 *r = matrix.apply_rect(*r);
3324 }
3325 for (_, r) in &mut self.reorderables[base.reorderables..] {
3326 *r = matrix.apply_rect(*r);
3327 }
3328 for (_, r, _) in &mut self.semantics[base.semantics..] {
3329 *r = matrix.apply_rect(*r);
3330 }
3331 }
3332 }
3333
3334 /// Entry point for walking a node. When the node is a cacheable **repaint boundary**, it
3335 /// tries to replay its subtree from the paint cache; otherwise (or on a *miss*) it
3336 /// delegates to the full walk [`walk_node`](Self::walk_node), capturing its output for the
3337 /// next frame. Every recursion goes through here → nested boundaries get cached too.
3338 /// The focus flags a subtree carries — exclusion, Tab-skipping, an order, a group —
3339 /// are **subtree-scoped**, so they are pushed here and popped on the way out rather
3340 /// than asked of each widget at the moment it registers. The body of the walk has
3341 /// early returns in a dozen branches; wrapping it is the only way to be sure the
3342 /// scope is closed on every one of them.
3343 fn walk(
3344 &mut self,
3345 widget: &'a dyn Widget<Msg>,
3346 id: WidgetId,
3347 translation: (f32, f32),
3348 clip: Rect,
3349 rects: &[Rect],
3350 index: &mut usize,
3351 ) {
3352 let outer = (
3353 self.focus_excluded,
3354 self.focus_skipped,
3355 self.focus_order,
3356 self.focus_group,
3357 );
3358 if !widget.descendants_focusable() {
3359 self.focus_excluded = true;
3360 }
3361 if widget.focus_skip_traversal() {
3362 self.focus_skipped = true;
3363 }
3364 if let Some(order) = widget.focus_order() {
3365 self.focus_order = Some(order);
3366 }
3367 if widget.focus_group() {
3368 self.focus_group = Some(id);
3369 }
3370 // The backdrop group a subtree sits in, scoped the same way and for the same
3371 // reason. Its own identity is the key: stable across frames, and unique
3372 // without anything having to hand one out.
3373 let outer_group = self.backdrop_group;
3374 if widget.backdrop_group() {
3375 self.backdrop_group = Some(id.as_u64());
3376 }
3377 // The form a field belongs to, scoped the same way (milestone 512).
3378 let outer_form = self.autofill_group;
3379 if widget.autofill_group() {
3380 self.autofill_group = Some(id);
3381 }
3382 // The focus stops this subtree contains start here; the walk is depth-first, so
3383 // they are contiguous and the range closes below.
3384 let stops_before = self.focusables.len();
3385 self.walk_scoped(widget, id, translation, clip, rects, index);
3386 (
3387 self.focus_excluded,
3388 self.focus_skipped,
3389 self.focus_order,
3390 self.focus_group,
3391 ) = outer;
3392 self.backdrop_group = outer_group;
3393 self.autofill_group = outer_form;
3394 self.close_scope(widget, stops_before);
3395 }
3396
3397 /// Files whatever tables this subtree carried, with the focus stops it turned out to
3398 /// contain. Recorded **on the way out**, because the range is not known on the way in;
3399 /// so `scopes` ends up innermost-first among any that overlap, which is the order the
3400 /// resolution wants and the reason it is not sorted afterwards.
3401 fn close_scope(&mut self, widget: &'a dyn Widget<Msg>, stops_before: usize) {
3402 let range = stops_before..self.focusables.len();
3403 if let Some(on_key) = widget.on_keystroke() {
3404 self.listeners.push((range.clone(), on_key));
3405 }
3406 let (strokes, callbacks) = (widget.shortcut_bindings(), widget.shortcut_callbacks());
3407 let (actions, listeners) = (widget.action_bindings(), widget.action_listeners());
3408 if strokes.is_empty() && callbacks.is_empty() && actions.is_empty() && listeners.is_empty()
3409 {
3410 return;
3411 }
3412 self.scopes.push(Scope {
3413 strokes: strokes.to_vec(),
3414 callbacks: callbacks.to_vec(),
3415 actions: actions.to_vec(),
3416 listeners: listeners.to_vec(),
3417 range,
3418 });
3419 }
3420
3421 fn walk_scoped(
3422 &mut self,
3423 widget: &'a dyn Widget<Msg>,
3424 id: WidgetId,
3425 translation: (f32, f32),
3426 clip: Rect,
3427 rects: &[Rect],
3428 index: &mut usize,
3429 ) {
3430 self.claim_overflows(id, translation);
3431 // A **sheet's panel**: the subtree is walked as usual, and every scroll area it
3432 // registered is recorded against the sheet, so the shell knows which lists share a
3433 // finger with it. By range, since a subtree's registrations are contiguous —
3434 // a replayed boundary splices its own in the same place.
3435 if let Some(spec) = widget.sheet() {
3436 let panel = rects[*index].translate(translation.0, translation.1);
3437 let first = self.scrollables.len();
3438 self.walk_node(widget, id, translation, clip, rects, index);
3439 let areas = self.scrollables[first..]
3440 .iter()
3441 .map(|area| area.id)
3442 .collect();
3443 // What the share is a share of. At nought there is nothing to divide by, so the
3444 // height last measured stands in: a finger that lowered the panel to nothing
3445 // may still be holding it.
3446 let size = self.runtime.sheet_size(id, spec);
3447 let available = if size > 0.0 {
3448 panel.height / size
3449 } else {
3450 self.runtime
3451 .sheets
3452 .get(&id)
3453 .map_or(0.0, |sheet| sheet.available())
3454 };
3455 self.sheets.push(crate::sheet::SheetArea {
3456 id,
3457 panel,
3458 available,
3459 spec: spec.clone(),
3460 areas,
3461 });
3462 return;
3463 }
3464
3465 // A **refresh area**: the subtree is walked with this widget named as the host, so
3466 // every scrollable inside records where to send the movement its physics refuses at
3467 // the top edge. The indicator is then painted **over** the subtree — painting it in
3468 // the widget's own `paint` would put it under the list it belongs to.
3469 if let Some(spec) = widget.refresh() {
3470 let viewport = rects[*index]
3471 .translate(translation.0, translation.1)
3472 .intersect(clip);
3473 let outer = self.refresh_host.replace(id);
3474 self.walk_node(widget, id, translation, clip, rects, index);
3475 self.refresh_host = outer;
3476
3477 let area = Refreshable { id, viewport, spec };
3478 if let Some(pull) = self.runtime.refresh.get(&id) {
3479 crate::refresh::paint_refresh(&mut self.scene, &area, pull, &self.theme, clip);
3480 // A pull that is settling, spinning or fading away drives itself, so the
3481 // frame after this one has to happen.
3482 self.wants_animation = true;
3483 }
3484 self.refreshes.push(area);
3485 return;
3486 }
3487
3488 // A **barrier** (`IgnorePointer`, `AbsorbPointer`, a hidden `Visibility`,
3489 // `ExcludeSemantics`): the subtree is walked exactly as usual, and then whatever it
3490 // added to the withheld registries is dropped again.
3491 //
3492 // Removing afterwards rather than skipping the walk is what makes it exact. A widget
3493 // deep inside registers its click target, focus stop, scrollable area or accessibility
3494 // node without knowing that something above is holding the subtree out of the frame;
3495 // truncating at the barrier catches every one of them, including those added by
3496 // widgets written after this code. It also keeps the walk's rect indexing untouched,
3497 // which a skipped subtree would break.
3498 // A **description** (`Semantics`): something above states what the subtree *is*,
3499 // which the subtree cannot state for itself. Same shape as the barrier below and for
3500 // the same reason — walk first, reconcile after, so a widget deep inside annotates
3501 // itself without knowing anything is speaking for it.
3502 if let Some(described) = widget.describes() {
3503 let base = self.semantics.len();
3504 let own = rects[*index]
3505 .translate(translation.0, translation.1)
3506 .intersect(clip);
3507 self.walk_node(widget, id, translation, clip, rects, index);
3508 self.apply_description(described, base, id, own);
3509 return;
3510 }
3511
3512 if let Some(barrier) = widget.barrier().filter(|b| !b.is_none()) {
3513 let base = self.barrier_base();
3514 // The barrier's own box, before `walk_node` advances the index past it.
3515 let own = rects[*index]
3516 .translate(translation.0, translation.1)
3517 .intersect(clip);
3518 self.walk_node(widget, id, translation, clip, rects, index);
3519 self.apply_barrier(barrier, &base, id, own);
3520 return;
3521 }
3522
3523 // An opacity group: the subtree is painted normally, then its range of primitives is
3524 // **drained** into a composited layer at the group's opacity — so overlaps do not
3525 // double-blend. The animated opacity is the value the runtime tweened; otherwise the
3526 // fixed target. Opaque (≈1): no layer at all (zero cost).
3527 if let Some(target) = widget.opacity_group() {
3528 let opacity = self.runtime.value_or(id, target).clamp(0.0, 1.0);
3529 // Transparent (≈0): walked, so that what it registers is still there, and not
3530 // painted at all — the reference's faded-out child costs nothing to draw either
3531 // (milestone 595). A layer of nothing would still be a primitive in the frame.
3532 if opacity < 0.001 {
3533 let start = self.scene.primitives().len();
3534 self.walk_node(widget, id, translation, clip, rects, index);
3535 let _unseen = self.scene.split_off(start);
3536 return;
3537 }
3538 if opacity < 0.999 {
3539 let start = self.scene.primitives().len();
3540 self.walk_node(widget, id, translation, clip, rects, index);
3541 let group = self.scene.split_off(start);
3542 self.scene.push_primitive(Primitive::Layer {
3543 primitives: group,
3544 opacity,
3545 clip,
3546 clip_shape: ClipShape::Rect,
3547 transform: None,
3548 filter: LayerFilter::NONE,
3549 owner: id.as_u64(),
3550 });
3551 return;
3552 }
3553 // Fully opaque: an ordinary render (no pointless layer).
3554 }
3555
3556 // A **pixel effect** over the subtree (`ColorFiltered`, `ImageFiltered`,
3557 // `ShaderMask`): painted normally, then drained into a composited layer that
3558 // carries the effect. The renderer applies it when it composites — the image
3559 // filter as a pre-pass over the layer texture, the colour filter and the mask
3560 // in the compositing fragment.
3561 //
3562 // The box is passed in because a mask is written in fractions of the box it
3563 // covers, and this is the first point at which that box is a place on screen.
3564 if let Some(filter) = widget.layer_filter(crate::widget::FilterContext {
3565 box_rect: rects[*index].translate(translation.0, translation.1),
3566 backdrop_group: self.backdrop_group,
3567 }) {
3568 // The layer keeps the **ambient** clip rather than its own box: an image
3569 // filter reaches past the pixels it came from, and cutting it at the box
3570 // would shave the edge off every blur.
3571 let start = self.scene.primitives().len();
3572 self.walk_node(widget, id, translation, clip, rects, index);
3573 let group = self.scene.split_off(start);
3574 let (group, filter, clip_box) = fold_filter(group, filter, clip);
3575 self.scene.push_primitive(Primitive::Layer {
3576 primitives: group,
3577 opacity: 1.0,
3578 clip: clip_box,
3579 clip_shape: ClipShape::Rect,
3580 transform: None,
3581 filter,
3582 owner: id.as_u64(),
3583 });
3584 return;
3585 }
3586
3587 // **Shape** clipping: the subtree is painted normally, bounded to the widget's box,
3588 // then its primitives are **drained** into a composited layer whose shape (rounded
3589 // corners / ellipse) modulates the alpha — the overflowing corners are erased at
3590 // compositing time. The shape's box is the layer's clip rect. Two sources: `ClipPath`
3591 // (an arbitrary path, taking priority) or `ClipRRect`/`ClipOval` (an analytic shape).
3592 // The local path is **offset to the box's screen position**.
3593 if widget.clip_path().is_some() || widget.clip_shape().is_some() {
3594 let box_rect = rects[*index].translate(translation.0, translation.1);
3595 let shape = widget
3596 .clip_path()
3597 .map(|p| ClipShape::Path(p.translated(box_rect.x, box_rect.y)))
3598 .or_else(|| widget.clip_shape())
3599 .expect("clip_path or clip_shape is Some");
3600 let clip_box = clip.intersect(box_rect);
3601 let start = self.scene.primitives().len();
3602 self.walk_node(widget, id, translation, clip_box, rects, index);
3603 let group = self.scene.split_off(start);
3604 // A clip around a filter is **one** layer, not two. It matters most for a
3605 // backdrop: the reference's own advice is to wrap one in a clip to give it
3606 // its shape, and a backdrop pushed a level down would be filtering the
3607 // clip's contents rather than the frame.
3608 let (group, filter, clip_box) = fold_filter(group, LayerFilter::NONE, clip_box);
3609 self.scene.push_primitive(Primitive::Layer {
3610 primitives: group,
3611 opacity: 1.0,
3612 clip: clip_box,
3613 clip_shape: shape,
3614 transform: None,
3615 filter,
3616 owner: id.as_u64(),
3617 });
3618 return;
3619 }
3620
3621 // Composable paint transforms (`Transform`: scale and/or rotation; translation goes
3622 // through the child offset instead). Scale and rotation are melted into **a single
3623 // affine matrix** `M`, the subtree is painted **flat**, then wrapped in a composited
3624 // layer transformed by `M`. The hit-test applies `M⁻¹` to the point. The translation,
3625 // applied upstream through `child_offset`, is the innermost one.
3626 //
3627 // A transform that was told to **animate** hands over its numbers, and only its
3628 // numbers: the pivots stay the widget's, because a pivot is a choice of origin
3629 // rather than a quantity and interpolating it would slide the subtree across the
3630 // screen with nothing describing it having changed. Where the runtime has never
3631 // heard of this widget the static value stands, which is the same rule every
3632 // other animated property follows on mount.
3633 let moving = self.runtime.anim_transform(id);
3634 let scale = widget
3635 .transform_scale()
3636 .map(|(sx, sy, pivot)| {
3637 moving.map_or((sx, sy, pivot), |m| (m.scale_x, m.scale_y, pivot))
3638 })
3639 .filter(|(sx, sy, _)| (sx - 1.0).abs() > 1e-4 || (sy - 1.0).abs() > 1e-4);
3640 let rotate = widget
3641 .transform_rotate()
3642 .map(|(angle, pivot)| moving.map_or((angle, pivot), |m| (m.rotation, pivot)))
3643 .filter(|(a, _)| a.abs() > 1e-4);
3644 if scale.is_some() || rotate.is_some() {
3645 // The pivots are taken on the **child's** box (the next node in prefix order): it
3646 // is the child that gets transformed, and its box hugs the content even when the
3647 // Transform's own box is stretched by the parent.
3648 let basis = rects.get(*index + 1).copied().unwrap_or(rects[*index]);
3649 let box_rect = basis.translate(translation.0, translation.1);
3650 let pivot_of = |align: frus_core::Alignment| {
3651 Point::new(
3652 box_rect.x + box_rect.width * align.fraction_x(),
3653 box_rect.y + box_rect.height * align.fraction_y(),
3654 )
3655 };
3656 // The composition: scale (about its own pivot) **then** rotation (about its own).
3657 // In RTL the world is mirrored → the rotation direction is inverted.
3658 let mut matrix = Affine::IDENTITY;
3659 if let Some((sx, sy, pivot_align)) = scale {
3660 matrix = matrix.then(Affine::scale(sx, sy).about(pivot_of(pivot_align)));
3661 }
3662 if let Some((angle, pivot_align)) = rotate {
3663 let angle = if self.rtl() { -angle } else { angle };
3664 matrix = matrix.then(Affine::rotation(angle).about(pivot_of(pivot_align)));
3665 }
3666
3667 let p0 = self.scene.primitives().len();
3668 let base = self.xform_base();
3669 self.walk_node(widget, id, translation, clip, rects, index);
3670 // Wraps the range of primitives — painted flat — in a layer transformed by `M`
3671 // (scale/rotation applied at compositing time).
3672 let group = self.scene.split_off(p0);
3673 self.scene.push_primitive(Primitive::Layer {
3674 primitives: group,
3675 opacity: 1.0,
3676 clip,
3677 clip_shape: ClipShape::Rect,
3678 transform: Some(LayerTransform::new(matrix)),
3679 filter: LayerFilter::NONE,
3680 owner: id.as_u64(),
3681 });
3682 // Counter-transforms the hit-test and maps the subtree's interaction rects.
3683 self.transform_interaction_registries(&base, matrix);
3684 return;
3685 }
3686
3687 // The inspector wants a full walk (it collects every node); caching only happens
3688 // outside inspection.
3689 if self.inspector.is_none() && widget.repaint_boundary() {
3690 if let Some(count) = plain_subtree_len(widget) {
3691 let start = *index;
3692 let sub = &rects[start..start + count];
3693 let fp = self.boundary_fingerprint(widget, id, translation, sub);
3694
3695 // Hit: the same generation (config) + the same fingerprint (state+geometry)
3696 // ⇒ the paint would be identical. The output is cloned and replayed.
3697 let hit = {
3698 let mut pc = self.runtime.paint_cache.borrow_mut();
3699 pc.get(id, fp).and_then(|(rc, any)| {
3700 any.downcast_ref::<BoundaryData<Msg>>()
3701 .map(|d| (rc, d.clone()))
3702 })
3703 };
3704 if let Some((rc, data)) = hit {
3705 self.runtime.paint_cache.borrow_mut().note_hit();
3706 self.splice_boundary(data);
3707 *index += rc;
3708 return;
3709 }
3710
3711 // Miss: paints normally, capturing the subtree's output (primitives +
3712 // interaction maps) for the next frame.
3713 let snap = self.snapshot();
3714 self.walk_node(widget, id, translation, clip, rects, index);
3715 self.runtime.paint_cache.borrow_mut().note_miss();
3716 if let Some(data) = self.capture_since(&snap) {
3717 self.runtime
3718 .paint_cache
3719 .borrow_mut()
3720 .put(id, fp, count, Box::new(data));
3721 }
3722 return;
3723 }
3724 }
3725 self.walk_node(widget, id, translation, clip, rects, index);
3726 }
3727
3728 /// Deforms the region's content by its overscroll **stretch**, if it has one
3729 /// (milestone 591): everything painted since `mark` goes into one layer, scaled along
3730 /// the axis from the edge pulled, and what can be touched in it moves with it.
3731 fn add_overscroll_stretch(
3732 &mut self,
3733 id: WidgetId,
3734 viewport: Rect,
3735 clip: Rect,
3736 mark: (usize, XformBase),
3737 ) {
3738 let Some(matrix) = self
3739 .runtime
3740 .scroll_stretch
3741 .get(&id)
3742 .and_then(|stretch| stretch.transform(viewport))
3743 else {
3744 return;
3745 };
3746 let (p0, base) = mark;
3747 let group = self.scene.split_off(p0);
3748 self.scene.push_primitive(Primitive::Layer {
3749 primitives: group,
3750 opacity: 1.0,
3751 clip,
3752 clip_shape: ClipShape::Rect,
3753 transform: Some(LayerTransform::new(matrix)),
3754 filter: LayerFilter::NONE,
3755 owner: id.as_u64(),
3756 });
3757 self.transform_interaction_registries(&base, matrix);
3758 }
3759
3760 /// Paints a child **flat** (laid out separately, at `translation`, with its own rects)
3761 /// then wraps it in a composited layer transformed by `matrix` and clipped to `clip`. The
3762 /// hit-test counter-transforms the point (`M⁻¹`); when `matrix` stays axis-aligned
3763 /// (scale/translation), the focus / scroll / drag / accessibility bounds are transformed
3764 /// too. The shared factor of `InteractiveViewer`, `RotatedBox` and `FittedBox`.
3765 // The child, its identity, and the four things that place it. Splitting them would
3766 // only mean passing the same values in two hops.
3767 #[allow(clippy::too_many_arguments)]
3768 fn emit_transformed_child(
3769 &mut self,
3770 child: &'a dyn Widget<Msg>,
3771 cid: WidgetId,
3772 translation: (f32, f32),
3773 clip: Rect,
3774 child_rects: &[Rect],
3775 matrix: Affine,
3776 owner: WidgetId,
3777 ) {
3778 let p0 = self.scene.primitives().len();
3779 let base = self.xform_base();
3780 let mut child_index = 0;
3781 self.walk(child, cid, translation, clip, child_rects, &mut child_index);
3782 let group = self.scene.split_off(p0);
3783 self.scene.push_primitive(Primitive::Layer {
3784 primitives: group,
3785 opacity: 1.0,
3786 clip,
3787 clip_shape: ClipShape::Rect,
3788 transform: Some(LayerTransform::new(matrix)),
3789 filter: LayerFilter::NONE,
3790 owner: owner.as_u64(),
3791 });
3792 self.transform_interaction_registries(&base, matrix);
3793 }
3794
3795 /// Paints one node, **under the theme it asks for**. A [`crate::Themed`] replaces the
3796 /// ambient theme for itself and everything below it; it is put back on the way out, so
3797 /// the sibling that follows is untouched. Everything the subtree reads — its paint,
3798 /// its ink, its focus ring, its layout through `cached_rects`, and any overlay it
3799 /// defers — goes through this field, which is what makes one swap enough.
3800 fn walk_node(
3801 &mut self,
3802 widget: &'a dyn Widget<Msg>,
3803 id: WidgetId,
3804 translation: (f32, f32),
3805 clip: Rect,
3806 rects: &[Rect],
3807 index: &mut usize,
3808 ) {
3809 // The box this node is about to be given, read **before** the walk consumes the
3810 // index: a decoration painted over the children needs it once they are done, and
3811 // by then `*index` has moved past the whole subtree.
3812 let over = rects
3813 .get(*index)
3814 .copied()
3815 .map(|r| r.translate(translation.0, translation.1));
3816 let outer = scoped_theme(widget, id, self.runtime, &self.theme)
3817 .map(|theme| std::mem::replace(&mut self.theme, std::rc::Rc::from(theme)));
3818 // The node that turns the texts under it selectable together is a selection area:
3819 // the texts it registers below belong to it, until the walk leaves it.
3820 let outer_area = outer
3821 .as_ref()
3822 .filter(|before| !before.widgets.text.selectable && self.theme.widgets.text.selectable)
3823 .map(|_| (self.area.replace(id), self.area_widget.replace(widget)));
3824 // The scoped surface, held for this subtree exactly as the layout walk holds it —
3825 // a widget that paints from `MediaQuery::of()` must see the same description it
3826 // was measured against, or the two disagree by whatever the scope removed.
3827 let _surface = widget
3828 .media_override(crate::MediaQuery::of())
3829 .map(crate::MediaQuery::install);
3830 let _shell = widget.scaffold_override().map(crate::ScaffoldInfo::install);
3831 let _scrolling = widget
3832 .scroll_behavior_override()
3833 .map(crate::ScrollConfiguration::install);
3834 // A region the mouse enters and leaves: registered before its subtree, so that the
3835 // regions inside it name it as their parent (milestone 583).
3836 let outer_hover = match (widget.hover_region(), over) {
3837 (Some(spec), Some(whole)) => {
3838 self.register_hover_region(id, spec, whole, clip);
3839 Some(self.hover_parent.replace(id))
3840 }
3841 _ => None,
3842 };
3843 self.walk_node_themed(widget, id, translation, clip, rects, index);
3844 if let Some(parent) = outer_hover {
3845 self.hover_parent = parent;
3846 }
3847 if let Some((area, area_widget)) = outer_area {
3848 self.area = area;
3849 self.area_widget = area_widget;
3850 }
3851 // Over its own children: the reference's `foregroundDecoration`, and the only
3852 // point in the walk where a widget paints after its subtree. Still under this
3853 // node's own theme, since it is this node's decoration.
3854 if let Some(bounds) = over {
3855 if let Some(decoration) = widget.foreground(&self.theme) {
3856 let status = self.full_status(widget, id);
3857 self.scene.set_clip(clip);
3858 self.scene.set_owner(id.as_u64());
3859 self.scene.set_bounds(bounds);
3860 decoration.paint_into_in(
3861 &mut self.scene,
3862 bounds,
3863 status.opacity,
3864 self.theme.direction,
3865 );
3866 self.scene.set_clip(clip);
3867 }
3868 // And what the widget paints over its children itself (milestone 652).
3869 if widget.paints_over() {
3870 let status = self.full_status(widget, id);
3871 self.scene.set_clip(clip);
3872 self.scene.set_owner(id.as_u64());
3873 self.scene.set_bounds(bounds);
3874 widget.paint_over(bounds, status, &self.theme, &mut self.scene);
3875 self.scene.set_clip(clip);
3876 }
3877 }
3878 if let Some(outer) = outer {
3879 self.theme = outer;
3880 }
3881 }
3882
3883 /// The walk proper, with the theme already in place — split out so that no early
3884 /// return inside it can skip putting the outer theme back.
3885 fn walk_node_themed(
3886 &mut self,
3887 widget: &'a dyn Widget<Msg>,
3888 id: WidgetId,
3889 translation: (f32, f32),
3890 clip: Rect,
3891 rects: &[Rect],
3892 index: &mut usize,
3893 ) {
3894 let rect = rects[*index];
3895 *index += 1;
3896 let draw_rect = rect.translate(translation.0, translation.1);
3897 self.inspect_enter(widget, id, draw_rect);
3898
3899 let status = self.full_status(widget, id);
3900 if widget.continuous() {
3901 self.wants_animation = true;
3902 }
3903
3904 self.scene.set_clip(clip);
3905 self.scene.set_owner(id.as_u64());
3906 // The box this widget was given. Text primitives record it: a line of text
3907 // says only where it starts, and the renderer has to know what it covers to
3908 // order it against anything else (milestone 295).
3909 self.scene.set_bounds(draw_rect);
3910 widget.paint(draw_rect, status, &self.theme, &mut self.scene);
3911 // A widget may have tightened the clip (TextField, for one): it is restored here.
3912 self.scene.set_clip(clip);
3913 // A part of the screen saying what the system bars over it should look like, kept
3914 // with its box: the shell asks which ones lie under each bar (#46).
3915 if let Some(style) = widget.system_ui_style() {
3916 self.system_ui.push((draw_rect, style));
3917 }
3918 // The ink a tap left on this surface: over the surface's own paint, under its
3919 // children — where a material surface puts it. The box is recorded too, so the
3920 // shell can start the next splash at the right place, in the right size.
3921 if let Some(style) = widget.ink(&self.theme) {
3922 self.inks.push((id, draw_rect));
3923 if let Some(ripples) = self.runtime.ink.get(&id) {
3924 ripples.paint(
3925 id.as_u64(),
3926 draw_rect,
3927 style.radius,
3928 style.color,
3929 &mut self.scene,
3930 );
3931 self.scene.set_clip(clip);
3932 }
3933 }
3934 self.draw_focus_ring(draw_rect, &status, widget);
3935 if let Some(can_paste) = status.toolbar {
3936 self.push_selection_toolbar(widget, id, draw_rect, clip, &status, can_paste);
3937 }
3938 if status.region_bar && !self.region_bar_placed {
3939 if let Some(range) = status.region {
3940 self.push_region_toolbar(widget, id, draw_rect, clip, range);
3941 }
3942 }
3943
3944 // A shared element is recorded **whether or not it is on screen**: half way
3945 // through a transition the one being left behind has usually slid off the edge,
3946 // and its box is exactly what the flight starts from. Every other registry here
3947 // is about what a pointer can reach, which is why they are inside the guard.
3948 if let Some(screen) = self.hero_screen {
3949 if let Some(tag) = widget.hero_tag() {
3950 self.heroes.push((
3951 HeroSpot {
3952 tag,
3953 screen,
3954 id,
3955 rect: draw_rect,
3956 },
3957 widget,
3958 ));
3959 }
3960 }
3961
3962 let visible = draw_rect.intersect(clip);
3963 // A text in a selection area: a place a selection can begin, end or pass through,
3964 // registered as far as it is visible, in the order it is painted.
3965 if self.theme.widgets.text.selectable && visible.width > 0.0 && visible.height > 0.0 {
3966 if let (Some(area), Some(_)) = (self.area, widget.selection_text()) {
3967 self.text_stops.push(crate::TextStop {
3968 id,
3969 rect: draw_rect,
3970 area,
3971 });
3972 }
3973 }
3974 // A focus stop clipped entirely away by a **scroll** is still a focus stop.
3975 // Tab has to reach the field below the fold — the shell brings it into view
3976 // when it lands there — and registering only what the eye can see is how a long
3977 // form came to have half its fields unreachable from the keyboard. Clipped away
3978 // by anything else it is gone: nothing could reveal it, so focus would land
3979 // where the eye cannot follow.
3980 //
3981 // Only the focus registry does this. A click still needs the visible box, or a
3982 // tap on empty space would land on something scrolled out of sight.
3983 if widget.focusable()
3984 && !self.focus_excluded
3985 && (self.scroll_host.is_some() || (visible.width > 0.0 && visible.height > 0.0))
3986 {
3987 self.focusables.push(Focusable {
3988 id,
3989 rect: visible,
3990 bounds: draw_rect,
3991 form: self.autofill_group,
3992 scroll: self.scroll_host,
3993 skip: self.focus_skipped || widget.focus_skip_traversal(),
3994 order: widget.focus_order().or(self.focus_order),
3995 group: self.focus_group,
3996 autofocus: widget.autofocus(),
3997 });
3998 }
3999 if visible.width > 0.0 && visible.height > 0.0 {
4000 // What a press or a drag can reach: the visible box, cut at a hanging stack's.
4001 let touch = self.touch_clip.map_or(visible, |t| visible.intersect(t));
4002 // A press that lands on a **surface** and on nothing inside it stops there.
4003 // Registered before the children, so anything inside still wins on the way
4004 // back out — see [`Widget::opaque`].
4005 if widget.opaque() {
4006 self.hits.push(Hit {
4007 id,
4008 rect: touch,
4009 msg: None,
4010 xform: None,
4011 });
4012 }
4013 // A target for a press: something that answers a tap, or a double or a secondary
4014 // tap without one — the shell asks it for those once the press lands on it.
4015 let tap = widget.on_click();
4016 if tap.is_some()
4017 || widget.on_double_tap().is_some()
4018 || widget.on_secondary_tap().is_some()
4019 {
4020 self.hits.push(Hit {
4021 id,
4022 rect: touch,
4023 msg: tap,
4024 xform: None,
4025 });
4026 }
4027 // A detector that takes a drag (milestone 582).
4028 if widget.pan_axis().is_some() {
4029 self.pans.push((id, touch, draw_rect));
4030 }
4031 // The title bar's line (milestone 640).
4032 if widget.wants_title_bar() {
4033 self.title_bar_row = Some(draw_rect);
4034 }
4035 if let Some(role) = widget.title_bar_role() {
4036 self.title_bar_regions.push((draw_rect, role));
4037 }
4038 // A widget that hears the pointer's raw events (milestone 586).
4039 if widget.pointer_listener() {
4040 self.pointer_listeners.push((id, touch, draw_rect));
4041 }
4042 if let Some(msg) = widget.on_long_press() {
4043 self.long_presses.push(Hit {
4044 id,
4045 rect: touch,
4046 msg: Some(msg),
4047 xform: None,
4048 });
4049 }
4050 if let Some(action) = widget.edit_action() {
4051 self.edit_actions.push((id, action));
4052 }
4053 if widget.draggable() {
4054 self.draggables.push((id, touch));
4055 }
4056 if let Some(payload) = widget.drag_payload() {
4057 self.drag_sources.push(DragSource {
4058 id,
4059 rect: touch,
4060 payload,
4061 });
4062 }
4063 if widget.drop_zone() {
4064 self.drop_zones.push(DropZone { id, rect: touch });
4065 }
4066 if widget.reorder_index().is_some() {
4067 self.reorderables.push((id, touch));
4068 }
4069 // The accessibility tree: nodes that carry meaning (a role or a label).
4070 if let Some(sem) = widget.semantics().filter(|s| s.is_meaningful()) {
4071 self.semantics.push((id, visible, sem));
4072 }
4073 // A **multi-line** field whose content overflows: a scrollable region (wheel and
4074 // inertia through the generic machinery; the shell follows the caret) plus a
4075 // draggable scrollbar (mouse and touch).
4076 if let Some(vp) = widget.text_viewport(draw_rect) {
4077 if let Some((content_h, visible_h, _, _)) = widget.text_metrics(draw_rect.width, 0)
4078 {
4079 let max_y = (content_h - visible_h).max(0.0);
4080 if max_y > 0.0 {
4081 self.scrollables.push(Scrollable {
4082 id,
4083 viewport: vp,
4084 max_x: 0.0,
4085 max_y,
4086 axis: crate::scroll::Axis::Vertical,
4087 physics: region_physics(widget, &self.theme),
4088 overscroll: crate::ScrollConfiguration::of()
4089 .overscroll_indicator(&self.theme),
4090 fling: crate::ScrollConfiguration::of()
4091 .velocity_tracker(&self.theme)
4092 .strategy(),
4093 refresh: self.refresh_host,
4094 page: None,
4095 reverse_x: false,
4096 reverse_y: false,
4097 host: self.scroll_host,
4098 keep_visible: None,
4099 });
4100 let offset_y = self
4101 .runtime
4102 .scroll
4103 .get(&id)
4104 .map(|s| s.1)
4105 .unwrap_or(0.0)
4106 .clamp(0.0, max_y);
4107 self.add_scrollbar(
4108 id,
4109 vp,
4110 Bar {
4111 vertical: true,
4112 offset: offset_y,
4113 max: max_y,
4114 reverse: false,
4115 asked: widget.scrollbars(),
4116 visible: widget.thumb_visibility(),
4117 },
4118 );
4119 }
4120 }
4121 }
4122 }
4123
4124 if let Some((progress, forward)) = widget.navigator() {
4125 let bounds = draw_rect;
4126 let children = widget.children();
4127 // **The pages stop at the navigator's edge.** A screen sliding in comes from
4128 // outside the box, and one sliding out goes outside it; before milestone 398
4129 // the only thing stopping them was the window, so a navigator that was not the
4130 // whole window painted its pages over its siblings — and even a full-window
4131 // one spent the frame drawing a screen nobody could see. The reference clips
4132 // by default (`Clip.hardEdge`) for the same reason.
4133 let clip = if widget.navigator_clips() {
4134 clip.intersect(bounds)
4135 } else {
4136 clip
4137 };
4138 // The pages under the one on show are kept in the tree and stay out of the picture.
4139 let hidden = widget.navigator_retained().min(children.len());
4140 let shown = &children[hidden..];
4141 if shown.len() >= 2 {
4142 // **A transition, as the theme's platform draws it** (milestone 620): the
4143 // builder the theme maps the platform to says where each page is, how big,
4144 // how opaque, and what lies between them, `progress` of the way through.
4145 let builder = self.theme.page_transitions.builder_for(self.theme.platform);
4146 let frame = builder.frame(progress, forward, widget.navigator_gesture());
4147 // `Navigator::from` inserts the screen being left at index 0, so
4148 // `shown[1]` is always the destination: the page on top on a push, the one
4149 // underneath on a pop.
4150 let (top, below) = if forward { (1, 0) } else { (0, 1) };
4151 let mirror = if frame.mirrored && self.rtl() {
4152 -1.0
4153 } else {
4154 1.0
4155 };
4156 // Where the shared elements of this transition start, in both the
4157 // registry and the scene: everything after this point belongs to the
4158 // two screens, and the flight is resolved from it once both are drawn.
4159 let hero_base = self.heroes.len();
4160 let scene_base = self.scene.primitives().len();
4161 let surface = self.theme.scheme.surface;
4162 let fill = |builder: &mut Self, color: Color| {
4163 builder.scene.set_owner(0);
4164 builder.scene.set_clip(clip);
4165 builder.scene.fill_rect(bounds, color);
4166 };
4167 if frame.backdrop > 0.0 {
4168 fill(self, surface.fade(frame.backdrop));
4169 }
4170 let outer_screen = self.hero_screen.replace(below as u8);
4171 // A screen being left keeps its floating layers to itself.
4172 // `process_overlays` draws every overlay above the **whole window**, after
4173 // both screens, so a menu left open on the departing screen would be
4174 // painted on top of the screen that replaced it. Whatever the departing one
4175 // defers is dropped.
4176 let overlay_base = self.overlays.len();
4177 self.render_posed(
4178 shown[below].as_ref(),
4179 child_id(id, hidden + below, shown[below].as_ref()),
4180 bounds,
4181 frame.below,
4182 mirror,
4183 clip,
4184 );
4185 if below != 1 {
4186 self.overlays.truncate(overlay_base);
4187 }
4188 if frame.barrier > 0.0 {
4189 fill(self, Color::BLACK.fade(frame.barrier));
4190 }
4191 if frame.scrim > 0.0 {
4192 fill(self, surface.fade(frame.scrim));
4193 }
4194 if frame.edge_shadow > 0.0 {
4195 self.paint_edge_shadow(bounds, frame, mirror, clip);
4196 }
4197 self.hero_screen = Some(top as u8);
4198 let overlay_base = self.overlays.len();
4199 self.render_posed(
4200 shown[top].as_ref(),
4201 child_id(id, hidden + top, shown[top].as_ref()),
4202 bounds,
4203 frame.top,
4204 mirror,
4205 clip,
4206 );
4207 if top != 1 {
4208 self.overlays.truncate(overlay_base);
4209 }
4210 self.hero_screen = outer_screen;
4211 self.fly_heroes(hero_base, scene_base, progress);
4212 } else if let Some(screen) = shown.first() {
4213 self.render_screen(
4214 screen.as_ref(),
4215 child_id(id, hidden, screen.as_ref()),
4216 bounds,
4217 0.0,
4218 clip,
4219 );
4220 }
4221 } else if widget.interactive().is_some() {
4222 // An **interactive** viewport (`InteractiveViewer`): the child fills the viewport
4223 // at scale 1, then the retained transform (scale + translation, from the shell's
4224 // gestures) is applied to it through **a single layer** carrying both the matrix
4225 // `M` and the clip to the viewport.
4226 let viewport = draw_rect;
4227 let content_clip = clip.intersect(viewport);
4228 self.interactives.push((id, viewport));
4229 if let Some(content) = widget.children().first() {
4230 let content = content.as_ref();
4231 let cid = child_id(id, 0, content);
4232 let content_rects = self.cached_rects(
4233 cid,
4234 content,
4235 Constraints::definite(Size::new(viewport.width, viewport.height)),
4236 );
4237 let matrix = self
4238 .runtime
4239 .interactive
4240 .get(&id)
4241 .copied()
4242 .unwrap_or_default()
4243 .matrix();
4244 self.emit_transformed_child(
4245 content,
4246 cid,
4247 (viewport.x, viewport.y),
4248 content_clip,
4249 &content_rects,
4250 matrix,
4251 id,
4252 );
4253 }
4254 } else if let Some(q) = widget.rotated_quarter_turns() {
4255 // `RotatedBox`: the child, measured at its **natural** size, is centred in the box
4256 // (with the dimensions swapped for an odd quarter, see `build_layout`) then rotated
4257 // about the centre. The rotation, applied at compositing time, does not stay
4258 // axis-aligned for an odd quarter (the focus bounds are left as they are — the
4259 // click itself stays correct through `M⁻¹`).
4260 let box_rect = draw_rect;
4261 if let Some(child) = widget.children().first() {
4262 let child = child.as_ref();
4263 let cid = child_id(id, 0, child);
4264 let child_rects =
4265 self.cached_rects(cid, child, Constraints::scroll(0.0, 0.0, true, true));
4266 let nat = child_rects
4267 .first()
4268 .copied()
4269 .unwrap_or(Rect::new(0.0, 0.0, 0.0, 0.0));
4270 let center = Point::new(
4271 box_rect.x + box_rect.width / 2.0,
4272 box_rect.y + box_rect.height / 2.0,
4273 );
4274 let translation = (center.x - nat.width / 2.0, center.y - nat.height / 2.0);
4275 let angle = q.rem_euclid(4) as f32 * std::f32::consts::FRAC_PI_2;
4276 let angle = if self.rtl() { -angle } else { angle };
4277 let matrix = Affine::rotation(angle).about(center);
4278 self.emit_transformed_child(
4279 child,
4280 cid,
4281 translation,
4282 clip,
4283 &child_rects,
4284 matrix,
4285 id,
4286 );
4287 }
4288 } else if let Some(fit) = widget.fitted() {
4289 // `FittedBox`: the child, measured at its **natural** size, is scaled according to
4290 // the `BoxFit` (uniformly, except for `Fill`), centred and clipped to the box. The
4291 // scale stays axis-aligned → the focus bounds follow.
4292 let box_rect = draw_rect;
4293 let content_clip = clip.intersect(box_rect);
4294 if let Some(child) = widget.children().first() {
4295 let child = child.as_ref();
4296 let cid = child_id(id, 0, child);
4297 let child_rects =
4298 self.cached_rects(cid, child, Constraints::scroll(0.0, 0.0, true, true));
4299 let nat = child_rects
4300 .first()
4301 .copied()
4302 .unwrap_or(Rect::new(0.0, 0.0, 0.0, 0.0));
4303 let (sx, sy) = fit.scale(
4304 Size::new(nat.width, nat.height),
4305 Size::new(box_rect.width, box_rect.height),
4306 );
4307 let center = Point::new(
4308 box_rect.x + box_rect.width / 2.0,
4309 box_rect.y + box_rect.height / 2.0,
4310 );
4311 let translation = (center.x - nat.width / 2.0, center.y - nat.height / 2.0);
4312 let matrix = Affine::scale(sx, sy).about(center);
4313 self.emit_transformed_child(
4314 child,
4315 cid,
4316 translation,
4317 content_clip,
4318 &child_rects,
4319 matrix,
4320 id,
4321 );
4322 }
4323 } else if let Some(content) = widget.scroll_content() {
4324 let axis = widget.scroll_axis();
4325 let viewport = draw_rect;
4326 let content_clip = clip.intersect(viewport);
4327 // `reverse` applies to the axis this scrolls along; a two-dimensional scroll
4328 // takes it on the vertical, which is the one a reversed view is ever about.
4329 let reverse = if widget.scroll_reverse() {
4330 (axis.free_x() && !axis.free_y(), axis.free_y())
4331 } else {
4332 (false, false)
4333 };
4334
4335 // The padding is **inside** the viewport and scrolls with the content:
4336 // room at the end of a feed is reachable by scrolling to it, room taken out
4337 // of the window would sit there for ever. The reference's `SliverPadding`.
4338 let pad = widget.scroll_padding();
4339 let content_rects = self.cached_rects(
4340 child_id(id, 0, content),
4341 content,
4342 Constraints::scroll(
4343 (viewport.width - pad.left - pad.right).max(0.0),
4344 (viewport.height - pad.top - pad.bottom).max(0.0),
4345 axis.free_x(),
4346 axis.free_y(),
4347 ),
4348 );
4349
4350 let content_size = content_rects
4351 .first()
4352 .copied()
4353 .unwrap_or(Rect::new(0.0, 0.0, 0.0, 0.0));
4354 // What scrolls is the content **and** its room, which is what makes the far
4355 // inset reachable rather than decorative.
4356 let scrolled = Size::new(
4357 content_size.width + pad.left + pad.right,
4358 content_size.height + pad.top + pad.bottom,
4359 );
4360 let max_x = (scrolled.width - viewport.width).max(0.0);
4361 let max_y = (scrolled.height - viewport.height).max(0.0);
4362
4363 // The geometry without the identity: enough to ask where the content sits at
4364 // an offset, and what offset would put a box where it is wanted.
4365 let frame = Scrollable {
4366 id,
4367 viewport,
4368 max_x,
4369 max_y,
4370 axis,
4371 physics: crate::ScrollPhysics::CLAMPING,
4372 overscroll: None,
4373 fling: frus_core::VelocityStrategy::Regression,
4374 refresh: None,
4375 page: None,
4376 reverse_x: reverse.0,
4377 reverse_y: reverse.1,
4378 host: None,
4379 keep_visible: None,
4380 };
4381 // What the content asks to be kept in view — a tab bar's selected tab — in
4382 // the content's own coordinates, which is the only frame it can speak in.
4383 let asked = content.keep_visible(
4384 Size::new(content_size.width, content_size.height),
4385 &self.theme,
4386 );
4387 // Where that box would sit if nobody had scrolled: the frame's coordinates,
4388 // with the offset taken out. See [`KeepVisible::rect`] for why it is stored
4389 // this way rather than where it is drawn.
4390 let keep = asked.map(|keep| {
4391 let rest = frame.content_origin((0.0, 0.0), scrolled);
4392 KeepVisible {
4393 rect: keep.rect.translate(
4394 viewport.x + rest.0 + pad.left,
4395 viewport.y + rest.1 + pad.top,
4396 ),
4397 ..keep
4398 }
4399 });
4400 // Inside the extent this frame has just measured, unless something owns the
4401 // offset: content that shrank is drawn where it rests (milestone 533).
4402 let (offset_x, offset_y) = match self.runtime.scroll_offset_within(id, (max_x, max_y)) {
4403 Some(offset) => offset,
4404 // Never scrolled, and something inside wants to be seen: **open** there
4405 // rather than sliding across a frame later. A tab bar restored on its
4406 // eighth tab should show the eighth tab, not show the first and then
4407 // travel. The same answer a paged view gives its initial page, and the
4408 // runtime retains the offset on that first frame so it stays.
4409 None => match keep {
4410 Some(keep) => frame.keep_offset(keep).0,
4411 None => (0.0, 0.0),
4412 },
4413 };
4414
4415 let origin = frame.content_origin((offset_x, offset_y), scrolled);
4416 let content_translation = (
4417 viewport.x + origin.0 + pad.left,
4418 viewport.y + origin.1 + pad.top,
4419 );
4420 self.scrollables.push(Scrollable {
4421 id,
4422 viewport,
4423 max_x,
4424 max_y,
4425 axis,
4426 physics: region_physics(widget, &self.theme),
4427 overscroll: crate::ScrollConfiguration::of().overscroll_indicator(&self.theme),
4428 fling: crate::ScrollConfiguration::of()
4429 .velocity_tracker(&self.theme)
4430 .strategy(),
4431 refresh: self.refresh_host,
4432 page: None,
4433 reverse_x: reverse.0,
4434 reverse_y: reverse.1,
4435 host: self.scroll_host,
4436 keep_visible: keep,
4437 });
4438 let mut content_index = 0;
4439 // Where the content starts, so that a stretch can take all of it (milestone 591).
4440 let stretch_mark = (self.scene.primitives().len(), self.xform_base());
4441 let outer = self.scroll_host.replace(id);
4442 self.walk(
4443 content,
4444 child_id(id, 0, content),
4445 content_translation,
4446 content_clip,
4447 &content_rects,
4448 &mut content_index,
4449 );
4450 self.scroll_host = outer;
4451 self.add_overscroll_stretch(id, viewport, content_clip, stretch_mark);
4452
4453 // The overscroll glow, then the scrollbars, over the content (not
4454 // clipped by it).
4455 self.scene.set_clip(clip);
4456 self.add_overscroll_glow(id, viewport);
4457 if max_y > 0.0 {
4458 self.add_scrollbar(
4459 id,
4460 viewport,
4461 Bar {
4462 vertical: true,
4463 offset: offset_y,
4464 max: max_y,
4465 reverse: reverse.1,
4466 asked: widget.scrollbars(),
4467 visible: widget.thumb_visibility(),
4468 },
4469 );
4470 }
4471 if max_x > 0.0 {
4472 self.add_scrollbar(
4473 id,
4474 viewport,
4475 Bar {
4476 vertical: false,
4477 offset: offset_x,
4478 max: max_x,
4479 reverse: reverse.0,
4480 asked: widget.scrollbars(),
4481 visible: widget.thumb_visibility(),
4482 },
4483 );
4484 }
4485 } else if let Some(vlist) =
4486 widget.virtual_list(Size::new(draw_rect.width, draw_rect.height))
4487 {
4488 // A virtualised list: only build/lay out/paint the visible window.
4489 let viewport = draw_rect;
4490 let content_clip = clip.intersect(viewport);
4491 let reverse = widget.scroll_reverse();
4492 // Down the screen or across it. A list virtualises along **one** axis, which
4493 // is what lets it place item `n` without building the ones before it, so
4494 // `Axis::Both` has no meaning here and reads as vertical.
4495 let across = matches!(vlist.axis, crate::scroll::Axis::Horizontal);
4496 // Room around the items, inside the viewport and scrolling with them. The
4497 // **leading** inset is the one at the end the items start from, so a
4498 // reversed list clears the far end first and both keep their sides.
4499 let pad = widget.scroll_padding();
4500 let (lead, trail, cross_near, cross_far) = match (across, reverse) {
4501 (true, false) => (pad.left, pad.right, pad.top, pad.bottom),
4502 (true, true) => (pad.right, pad.left, pad.top, pad.bottom),
4503 (false, false) => (pad.top, pad.bottom, pad.left, pad.right),
4504 (false, true) => (pad.bottom, pad.top, pad.left, pad.right),
4505 };
4506 // From here down it is a main axis and a cross axis, and only the two
4507 // branches at the end have to know which is which. The alternative was a
4508 // second copy of the window arithmetic below, which is where a reversed
4509 // list gets its signs right -- and a fix applied to one copy and not the
4510 // other is exactly how the two would drift apart.
4511 let (extent, cross_extent) = match across {
4512 true => (viewport.width, viewport.height),
4513 false => (viewport.height, viewport.width),
4514 };
4515 let content = lead + trail + vlist.count as f32 * vlist.item_extent;
4516 let max = (content - extent).max(0.0);
4517 let bounds = if across { (max, 0.0) } else { (0.0, max) };
4518 let offset = self
4519 .runtime
4520 .scroll_offset_within(id, bounds)
4521 .map_or(0.0, |(x, y)| if across { x } else { y });
4522 self.scrollables.push(Scrollable {
4523 id,
4524 viewport,
4525 max_x: if across { max } else { 0.0 },
4526 max_y: if across { 0.0 } else { max },
4527 axis: if across {
4528 crate::scroll::Axis::Horizontal
4529 } else {
4530 crate::scroll::Axis::Vertical
4531 },
4532 physics: region_physics(widget, &self.theme),
4533 overscroll: crate::ScrollConfiguration::of().overscroll_indicator(&self.theme),
4534 fling: crate::ScrollConfiguration::of()
4535 .velocity_tracker(&self.theme)
4536 .strategy(),
4537 refresh: self.refresh_host,
4538 page: None,
4539 reverse_x: across && reverse,
4540 reverse_y: !across && reverse,
4541 host: self.scroll_host,
4542 keep_visible: None,
4543 });
4544
4545 // Where the content starts, so that a stretch can take all of it (milestone 591).
4546 let stretch_mark = (self.scene.primitives().len(), self.xform_base());
4547 let outer = self.scroll_host.replace(id);
4548 if vlist.item_extent > 0.0 && vlist.count > 0 {
4549 // The **window** is the same arithmetic either way, and that is not a
4550 // coincidence: a reversed list counts its indices from the end, and a
4551 // reversed offset counts its pixels from the end, so index and offset
4552 // agree about which way is forward. Only where an item lands differs.
4553 let first = ((offset - lead) / vlist.item_extent).floor().max(0.0) as usize;
4554 let last = ((((offset + extent - lead) / vlist.item_extent).ceil()).max(0.0)
4555 as usize)
4556 .min(vlist.count);
4557 let origin = if across { viewport.x } else { viewport.y };
4558 for i in first..last {
4559 let item = (vlist.build)(i);
4560 let along = if reverse {
4561 origin + extent - lead - (i + 1) as f32 * vlist.item_extent + offset
4562 } else {
4563 origin + lead + i as f32 * vlist.item_extent - offset
4564 };
4565 let (x, y) = match across {
4566 true => (along, viewport.y + cross_near),
4567 false => (viewport.x + cross_near, along),
4568 };
4569
4570 // **Filled**, not merely constrained: a list hands its children a
4571 // box rather than asking them how big they would like to be. The
4572 // reference gives a list's children a *tight* cross-axis extent, and
4573 // for a fixed-extent list a tight main-axis one too — which is
4574 // exactly the pair of numbers here.
4575 //
4576 // Constrained-but-not-filled, a row whose width nobody set hugged
4577 // its own text: a list of coloured rows painted a column of chips
4578 // down the left instead of rows across the list. Found on a device
4579 // in milestone 349.
4580 let free_cross = (cross_extent - cross_near - cross_far).max(0.0);
4581 let item_rects = self.cached_rects(
4582 id.child(i),
4583 item.as_ref(),
4584 Constraints::filled(match across {
4585 true => Size::new(vlist.item_extent, free_cross),
4586 false => Size::new(free_cross, vlist.item_extent),
4587 }),
4588 );
4589
4590 let item = self.keep(item);
4591 let mut item_index = 0;
4592 self.walk(
4593 item,
4594 id.child(i),
4595 (x, y),
4596 content_clip,
4597 &item_rects,
4598 &mut item_index,
4599 );
4600 }
4601 }
4602
4603 self.scroll_host = outer;
4604 self.add_overscroll_stretch(id, viewport, content_clip, stretch_mark);
4605 self.scene.set_clip(clip);
4606 self.add_overscroll_glow(id, viewport);
4607 if max > 0.0 {
4608 self.add_scrollbar(
4609 id,
4610 viewport,
4611 Bar {
4612 vertical: !across,
4613 offset,
4614 max,
4615 reverse,
4616 asked: widget.scrollbars(),
4617 visible: widget.thumb_visibility(),
4618 },
4619 );
4620 }
4621 } else if let Some(pages) = widget.page_view() {
4622 // A paged view: like a virtualised list turned on its side, and with the
4623 // page extent — the whole geometry — derived from the viewport rather than
4624 // given. Only the pages the viewport touches are built.
4625 let viewport = draw_rect;
4626 let content_clip = clip.intersect(viewport);
4627 let snap = pages.snap(viewport);
4628 let (viewport_along, page_across) = if snap.horizontal {
4629 (viewport.width, viewport.height)
4630 } else {
4631 (viewport.height, viewport.width)
4632 };
4633 let reverse = widget.scroll_reverse();
4634 // Room at both ends, so a page narrower than the viewport comes to rest
4635 // **centred** in it rather than jammed against the leading edge — and so
4636 // the last one can be reached at all: unpadded, the travel stops with the
4637 // last page's trailing edge on the viewport's, which is short of where that
4638 // page rests. Padded, the total works out to `extent × (count - 1)`,
4639 // the last page's own offset, exactly.
4640 let pad = pages.end_padding(viewport);
4641 let total = pages.count as f32 * snap.extent + 2.0 * pad;
4642 let max = (total - viewport_along).max(0.0);
4643 // With no retained offset this is the view's **first** frame, and it opens
4644 // on the page it was asked for. Reading the initial page here rather than
4645 // correcting it a frame later is what keeps page 0 from flashing past on
4646 // the way to page 3.
4647 let bounds = if snap.horizontal {
4648 (max, 0.0)
4649 } else {
4650 (0.0, max)
4651 };
4652 let along = match self.runtime.scroll_offset_within(id, bounds) {
4653 Some((x, y)) => {
4654 if snap.horizontal {
4655 x
4656 } else {
4657 y
4658 }
4659 }
4660 None => snap
4661 .offset_of(snap.requested.min(pages.count.saturating_sub(1)))
4662 .clamp(0.0, max),
4663 };
4664 self.scrollables.push(Scrollable {
4665 id,
4666 viewport,
4667 max_x: if snap.horizontal { max } else { 0.0 },
4668 max_y: if snap.horizontal { 0.0 } else { max },
4669 axis: if snap.horizontal {
4670 crate::scroll::Axis::Horizontal
4671 } else {
4672 crate::scroll::Axis::Vertical
4673 },
4674 physics: region_physics(widget, &self.theme),
4675 overscroll: crate::ScrollConfiguration::of().overscroll_indicator(&self.theme),
4676 fling: crate::ScrollConfiguration::of()
4677 .velocity_tracker(&self.theme)
4678 .strategy(),
4679 refresh: self.refresh_host,
4680 page: Some(snap),
4681 reverse_x: snap.horizontal && reverse,
4682 reverse_y: !snap.horizontal && reverse,
4683 host: self.scroll_host,
4684 keep_visible: None,
4685 });
4686
4687 // Where the content starts, so that a stretch can take all of it (milestone 591).
4688 let stretch_mark = (self.scene.primitives().len(), self.xform_base());
4689 let outer = self.scroll_host.replace(id);
4690 if pages.count > 0 {
4691 // The window is the same arithmetic whichever end index 0 is at: a
4692 // reversed view counts its indices from the far end and a reversed
4693 // offset counts its pixels from the same one, so the two agree about
4694 // which way forward is. Only where a page **lands** differs.
4695 let first = ((along - pad) / snap.extent).floor().max(0.0) as usize;
4696 let last = ((((along + viewport_along - pad) / snap.extent).ceil()).max(0.0)
4697 as usize)
4698 .min(pages.count)
4699 .max(first + 1);
4700 // A wheel shows **more** rows than a flat strip would: the ones a quarter
4701 // turn away are compressed into the last few pixels at either end, and
4702 // their flat positions are a whole quarter-circumference outside the
4703 // viewport. Widening the window in both directions keeps the reversed
4704 // arithmetic above untouched, and the rows that turn out to have gone
4705 // over the horizon are dropped below — before they are built.
4706 let (first, last) = match pages.wheel {
4707 Some(wheel) => {
4708 let span = wheel.radius(viewport_along) * std::f32::consts::FRAC_PI_2;
4709 let extra = (span / snap.extent).ceil().max(0.0) as usize + 1;
4710 (first.saturating_sub(extra), (last + extra).min(pages.count))
4711 }
4712 None => (first, last),
4713 };
4714 for index in first..last.min(pages.count) {
4715 let start = match reverse {
4716 true => viewport_along - pad - (index + 1) as f32 * snap.extent + along,
4717 false => pad + index as f32 * snap.extent - along,
4718 };
4719 // Where this row's centre would sit on the unrolled strip, measured
4720 // from the middle of the viewport — which is the whole of what the
4721 // cylinder needs to know about it.
4722 let on_wheel = match pages.wheel {
4723 Some(wheel) => {
4724 let flat = start + snap.extent / 2.0 - viewport_along / 2.0;
4725 match wheel.row(viewport_along, flat) {
4726 // Past the horizon: not drawn, and not built either.
4727 None => continue,
4728 Some(row) => Some((row, flat)),
4729 }
4730 }
4731 None => None,
4732 };
4733 let page = (pages.build)(index);
4734 let (size, origin) = if snap.horizontal {
4735 (
4736 Size::new(snap.extent, page_across),
4737 (viewport.x + start, viewport.y),
4738 )
4739 } else {
4740 (
4741 Size::new(page_across, snap.extent),
4742 (viewport.x, viewport.y + start),
4743 )
4744 };
4745
4746 // A page is **given** its box, not asked for one: it fills the
4747 // panel even when its content is a single centred line.
4748 let page_rects = self.cached_rects(
4749 id.child(index),
4750 page.as_ref(),
4751 Constraints::filled(size),
4752 );
4753
4754 let mut page_index = 0;
4755 // The row is drawn **flat**, then the whole of it is composited on
4756 // the cylinder — one layer per row, the way `Transform` does it, so
4757 // the text, the rules and the backgrounds all turn together instead
4758 // of each primitive having to know it is on a wheel.
4759 let before = self.scene.primitives().len();
4760 let base = self.xform_base();
4761 let page = self.keep(page);
4762 self.walk(
4763 page,
4764 id.child(index),
4765 origin,
4766 content_clip,
4767 &page_rects,
4768 &mut page_index,
4769 );
4770 if let Some((row, flat)) = on_wheel {
4771 // The row's own centre on screen: the scale is about it, so a row
4772 // shrinks in place rather than creeping towards a corner.
4773 let centre = Point::new(
4774 viewport.x + viewport.width / 2.0,
4775 viewport.y + viewport_along / 2.0 + flat,
4776 );
4777 let matrix = Affine::scale(row.scale_x, row.scale_y)
4778 .about(centre)
4779 // And then up or down to where the cylinder actually put it.
4780 .then(Affine::translation(0.0, row.offset - flat));
4781 let group = self.scene.split_off(before);
4782 self.scene.push_primitive(Primitive::Layer {
4783 primitives: group,
4784 opacity: row.opacity.clamp(0.0, 1.0),
4785 clip: content_clip,
4786 clip_shape: ClipShape::Rect,
4787 transform: Some(LayerTransform::new(matrix)),
4788 filter: LayerFilter::NONE,
4789 owner: id.child(index).as_u64(),
4790 });
4791 self.transform_interaction_registries(&base, matrix);
4792 }
4793 }
4794 }
4795
4796 self.scroll_host = outer;
4797 self.add_overscroll_stretch(id, viewport, content_clip, stretch_mark);
4798 self.scene.set_clip(clip);
4799 self.add_overscroll_glow(id, viewport);
4800 } else if let Some(overflow) = widget.overflow_box() {
4801 // The child is laid out to constraints of its own — which is why it can
4802 // come out bigger than the box holding it — then anchored inside it and
4803 // rendered **without** the box's clip. A spill that got clipped here would
4804 // be no spill at all; a `ClipRect` above is how a caller asks for one.
4805 let own = draw_rect;
4806 if let Some(child) = widget.children().first() {
4807 let child = child.as_ref();
4808 let cid = child_id(id, 0, child);
4809 let asked = overflow.child_size(Size::new(own.width, own.height));
4810 let child_rects = if overflow.unconstrained {
4811 // Unconstrained: the child is asked how big it wants to be, and
4812 // gets exactly that.
4813 self.cached_rects(cid, child, Constraints::scroll(0.0, 0.0, true, true))
4814 } else if overflow.natural_height {
4815 // The box's width, and the height the child asks for at it.
4816 self.cached_rects(
4817 cid,
4818 child,
4819 Constraints::scroll(asked.width, 0.0, false, true),
4820 )
4821 } else {
4822 self.cached_rects(cid, child, Constraints::filled(asked))
4823 };
4824 let size = child_rects
4825 .first()
4826 .map(|r| Size::new(r.width, r.height))
4827 .unwrap_or(asked);
4828 let origin = overflow.origin(own, size);
4829
4830 let mut child_index = 0;
4831 self.walk(child, cid, origin, clip, &child_rects, &mut child_index);
4832 }
4833 } else if let Some(transform) = widget.constraints_transform() {
4834 // The child is laid out at **the same box the measurement gave it**, worked out
4835 // again from what is left of that measurement — the box it produced. Each of
4836 // the three things an axis can say survives the round trip; see
4837 // `AxisConstraint::at`.
4838 let own = draw_rect;
4839 if let Some(child) = widget.children().first() {
4840 let child = child.as_ref();
4841 let cid = child_id(id, 0, child);
4842 // A box a multiple of its child below one is smaller than the child: on a
4843 // loose axis, the child is allowed what it had, the box divided back by the
4844 // factor, and at zero there is nothing to divide, so it is asked. At one or
4845 // more the box already holds it (milestone 598).
4846 let factor = widget.size_factor().unwrap_or((1.0, 1.0));
4847 let back = |rule, own: f32, factor: f32| match rule {
4848 crate::constraints::AxisConstraint::Loose if factor <= 0.0 => {
4849 crate::constraints::AxisConstraint::Unbounded.at(own)
4850 }
4851 crate::constraints::AxisConstraint::Loose if factor < 1.0 => {
4852 rule.at(own / factor)
4853 }
4854 _ => rule.at(own),
4855 };
4856 let (aw, free_x, fill_x) = back(transform.width, own.width, factor.0);
4857 let (ah, free_y, fill_y) = back(transform.height, own.height, factor.1);
4858 // A box on its way to its child's size is not that size yet, and the child is
4859 // laid out as ever (milestone 594). On the axes the child decides, it comes
4860 // back at its own size, which is where the box is going; an axis it is given
4861 // as it came is not animated at all, the box filling the room there.
4862 let animated = widget.animated_size();
4863 let constraints = Constraints::axes(aw, ah, [free_x, free_y], [fill_x, fill_y]);
4864 let child_rects = self.cached_rects(cid, child, constraints);
4865 let size = child_rects
4866 .first()
4867 .map(|r| Size::new(r.width, r.height))
4868 .unwrap_or(Size::new(0.0, 0.0));
4869 // An anchor that may be directional is resolved here, where the reading
4870 // direction is known (milestone 597).
4871 // An anchor on its way to another is where the runtime has it (milestone 600).
4872 let geometry = widget.alignment_geometry().map(|geometry| {
4873 match self.runtime.anim_offset(id) {
4874 Some((x, y)) => geometry.with_fractions(x, y),
4875 None => geometry,
4876 }
4877 });
4878 let align = match geometry {
4879 Some(geometry) => geometry.resolve(if self.rtl() {
4880 frus_core::TextDirection::Rtl
4881 } else {
4882 frus_core::TextDirection::Ltr
4883 }),
4884 None => transform.alignment,
4885 };
4886 let origin = (
4887 own.x + (own.width - size.width) * align.fraction_x(),
4888 own.y + (own.height - size.height) * align.fraction_y(),
4889 );
4890 if transform.report {
4891 self.report_spill(own, Rect::new(origin.0, origin.1, size.width, size.height));
4892 }
4893 let clip = match &animated {
4894 Some(spec) if spec.clip => clip.intersect(own),
4895 // A reveal is the child cut to it (milestone 596).
4896 _ if widget.clips_to_box() => clip.intersect(own),
4897 _ => clip,
4898 };
4899 let mut child_index = 0;
4900 self.walk(child, cid, origin, clip, &child_rects, &mut child_index);
4901 }
4902 } else if let Some(build) = widget.layout_builder() {
4903 // Builds the content from the actual box, then lays it out and renders it inside
4904 // (like a list item: with no retained state).
4905 let bounds = draw_rect;
4906 let content_clip = clip.intersect(bounds);
4907 let child = build(Size::new(bounds.width, bounds.height));
4908
4909 // **Filled**, not merely constrained: the content is handed this box, and it
4910 // was built from this box in the first place. Constraining it instead let a
4911 // root with no width of its own hug its content — a grid built here laid its
4912 // columns out at nothing, which is what `GridView::extent`'s first tests found
4913 // (milestone 356). The same distinction a list's items and a paged view's
4914 // pages already draw.
4915 let child_rects = self.cached_rects(
4916 id.child(0),
4917 child.as_ref(),
4918 Constraints::filled(Size::new(bounds.width, bounds.height)),
4919 );
4920
4921 let child = self.keep(child);
4922 let mut child_index = 0;
4923 self.walk(
4924 child,
4925 id.child(0),
4926 (bounds.x, bounds.y),
4927 content_clip,
4928 &child_rects,
4929 &mut child_index,
4930 );
4931 } else if let Some(build) = widget.scroll_overlay() {
4932 // The **body first**, exactly as an ordinary single child: it keeps its own
4933 // identity, registers itself as a scroll region in its own right, and consumes
4934 // its rects from this array the way the generic walk would. Nothing about it
4935 // changes for being watched.
4936 let children = widget.children();
4937 for (child_index, child) in children.iter().enumerate() {
4938 self.walk(
4939 child.as_ref(),
4940 child_id(id, child_index, child.as_ref()),
4941 translation,
4942 clip,
4943 rects,
4944 index,
4945 );
4946 }
4947 // Then the overlay, built from **where the body has got to this frame**. Read
4948 // from the retained offset rather than from the registry the walk is still
4949 // filling: the offset is written before the frame is drawn, so it is current,
4950 // where the extents beside it are not yet known — which is why the builder is
4951 // handed the one and not the other.
4952 let offset = children
4953 .first()
4954 .map(|body| child_id(id, 0, body.as_ref()))
4955 .and_then(|body| self.runtime.scroll.get(&body).copied())
4956 .unwrap_or((0.0, 0.0));
4957 let bounds = draw_rect;
4958 let over = build(offset, Size::new(bounds.width, bounds.height));
4959 // `child(1)`, beside the body's `child(0)` — or beside its key, which cannot
4960 // collide with a positional identity. The overlay is laid out in a tree of its
4961 // own, so it needs an identity of its own for hover, ink and the paint cache.
4962 let over_id = id.child(1);
4963 let over_rects = self.cached_rects(
4964 over_id,
4965 over.as_ref(),
4966 // **Filled**, not constrained: an overlay covers the region it is over, and
4967 // where inside that box its content sits is the caller's business — a
4968 // column pins it to the top, an `Align` puts it anywhere else. The same
4969 // rule a `layout_builder`'s content is given.
4970 Constraints::filled(Size::new(bounds.width, bounds.height)),
4971 );
4972 let over = self.keep(over);
4973 let mut over_index = 0;
4974 self.walk(
4975 over,
4976 over_id,
4977 (bounds.x, bounds.y),
4978 clip.intersect(bounds),
4979 &over_rects,
4980 &mut over_index,
4981 );
4982 } else if let Some(spec) = widget.dismissible() {
4983 // A dismissible item is a stack whose **last** layer — the item itself — is
4984 // offset by however far it has been swiped, and whose earlier layers, the
4985 // backgrounds, show only through the strip the item has uncovered. Clipping
4986 // them is what makes a background read as something the item is sliding
4987 // *off*, rather than a block that was always there.
4988 let bounds = draw_rect;
4989 let state = self.runtime.dismiss.get(&id).copied();
4990 let progress = state.map(|s| s.progress()).unwrap_or(0.0);
4991 let layers = widget.children();
4992 let last = layers.len().saturating_sub(1);
4993 // Which background: the first covers a swipe towards the end, the second —
4994 // when there is one — the other way. With only one, it serves both.
4995 let shown_background = if progress < 0.0 && last >= 2 { 1 } else { 0 };
4996 let strip = crate::dismiss::revealed_strip(bounds, progress);
4997
4998 for (i, layer) in layers.iter().enumerate() {
4999 let is_item = i == last;
5000 if !is_item && (i != shown_background || progress == 0.0) {
5001 // Not the background this direction shows: skip it, and skip the
5002 // rects it would have consumed — it is laid out separately, so
5003 // there is no index to keep in step.
5004 continue;
5005 }
5006 let offset = if is_item {
5007 if spec.axis.is_horizontal() {
5008 (bounds.width * progress, 0.0)
5009 } else {
5010 (0.0, bounds.height * progress)
5011 }
5012 } else {
5013 (0.0, 0.0)
5014 };
5015 let layer_clip = if is_item {
5016 clip.intersect(bounds)
5017 } else {
5018 clip.intersect(strip)
5019 };
5020 if layer_clip.width <= 0.0 || layer_clip.height <= 0.0 {
5021 continue;
5022 }
5023 let cid = child_id(id, i, layer.as_ref());
5024 let layer_rects = self.cached_rects(
5025 cid,
5026 layer.as_ref(),
5027 Constraints::filled(Size::new(bounds.width, bounds.height)),
5028 );
5029 let mut layer_index = 0;
5030 self.walk(
5031 layer.as_ref(),
5032 cid,
5033 (bounds.x + offset.0, bounds.y + offset.1),
5034 layer_clip,
5035 &layer_rects,
5036 &mut layer_index,
5037 );
5038 }
5039
5040 self.dismissables.push(Dismissable {
5041 id,
5042 rect: bounds,
5043 spec,
5044 });
5045 // A swipe that is settling, flying or collapsing drives itself.
5046 if state.is_some_and(|s| s.phase() != DismissPhase::Drag) {
5047 self.wants_animation = true;
5048 }
5049 } else if let Some(custom) = widget.custom_layout() {
5050 // Children placed by a function (milestone 601): it is handed this box, measures
5051 // each child in a layout of its own under the constraints it chooses, and says
5052 // where it goes. The function runs here, where the box is known, and nowhere
5053 // else, which is why the box's size cannot depend on its children.
5054 let own = draw_rect;
5055 let children = widget.children();
5056 let measure = |i: usize, c: crate::ChildConstraints| -> Vec<Rect> {
5057 let child = children[i].as_ref();
5058 self.cached_rects(child_id(id, i, child), child, custom_constraints(c))
5059 };
5060 let mut layout = crate::ChildLayout::new(custom.keys, &measure);
5061 (custom.delegate)(Size::new(own.width, own.height), &mut layout);
5062 let (laid, at) = (layout.laid, layout.at);
5063 for (i, child) in children.iter().enumerate() {
5064 let Some(child_rects) = &laid[i] else {
5065 continue;
5066 };
5067 let mut child_index = 0;
5068 self.walk(
5069 child.as_ref(),
5070 child_id(id, i, child.as_ref()),
5071 (own.x + at[i].x, own.y + at[i].y),
5072 clip,
5073 child_rects,
5074 &mut child_index,
5075 );
5076 }
5077 } else if widget.stack() {
5078 // A stack: the layers are rendered in order, bottom first.
5079 let bounds = draw_rect;
5080 // Cut at the stack's edges unless it says otherwise (`stack.dart:718`).
5081 let layer_clip = if widget.stack_clips() {
5082 clip.intersect(bounds)
5083 } else {
5084 clip
5085 };
5086 // Drawn past its edges or not, a stack is only touched inside them: the
5087 // reference asks a box whether a point is in it before its children.
5088 let outer_touch = self.touch_clip;
5089 if !widget.stack_clips() {
5090 self.touch_clip = Some(outer_touch.map_or(bounds, |t| t.intersect(bounds)));
5091 }
5092 let fit = widget.stack_fit();
5093 // The stack's own reading direction, or the theme's.
5094 let rtl = widget
5095 .stack_direction()
5096 .map_or(self.rtl(), |d| d == frus_core::TextDirection::Rtl);
5097 // An **indexed** stack shows one layer and lays out the rest. Which is a
5098 // question for here rather than for the widget: a layer that wrapped itself in
5099 // something to keep quiet would no longer be laid out the way a bare layer is,
5100 // and the layout is the whole promise.
5101 let shown = widget.stack_visible();
5102 // Where a layer smaller than the stack sits. Resolved once, against the
5103 // reading direction, so a start-anchored badge follows the script.
5104 let align = widget
5105 .alignment_geometry()
5106 .unwrap_or(frus_core::AlignmentGeometry::Physical(
5107 frus_core::Alignment::TOP_LEFT,
5108 ))
5109 .resolve(if rtl {
5110 frus_core::TextDirection::Rtl
5111 } else {
5112 frus_core::TextDirection::Ltr
5113 });
5114 for (i, layer) in widget.children().iter().enumerate() {
5115 let cid = child_id(id, i, layer.as_ref());
5116 // What this layer asks for on each axis: a pinned pair gives a number, a
5117 // lone pin plus an explicit extent gives one, and neither leaves the axis
5118 // to the layer itself — or, under `Expand`, to the stack.
5119 // A layer moving between two sets of pins is read from the runtime rather
5120 // than from the widget: the widget carries where it is **going**, and a
5121 // layer that jumped there would be the whole bug this animates away.
5122 // Both the size and the place come from the one value, so a box cannot be
5123 // measured against one frame's pins and placed against another's.
5124 let pins = match layer.positioned() {
5125 Some(spec) if layer.anim_pins().is_some() => {
5126 Some(self.runtime.anim_pins(cid).unwrap_or(spec))
5127 }
5128 other => other,
5129 };
5130 // A start or an end pin is a left or a right once the direction is known
5131 // (milestone 589). The stack's layers are placed on the screen as they are,
5132 // not mirrored, so this is the one place it is decided.
5133 let pins = pins.map(|p| p.for_direction(rtl));
5134 let (want_w, want_h) = match pins {
5135 Some(p) => (
5136 p.resolved_width(bounds.width),
5137 p.resolved_height(bounds.height),
5138 ),
5139 None => (Some(bounds.width), Some(bounds.height)),
5140 };
5141 // A pinned layer is **forced** into what its edges say: two opposite
5142 // pins are not a suggestion, and a width of its own is a contradiction
5143 // rather than a second opinion. An unpinned layer under `Expand` is
5144 // merely handed the box, which leaves a layer that chose a size alone —
5145 // that is what a badge sitting in a filled stack has always relied on.
5146 // An axis nobody pinned is left free, so the layer's own size comes back.
5147 let layer_rects = if pins.is_some() {
5148 self.cached_rects(
5149 cid,
5150 layer.as_ref(),
5151 Constraints::pinned(want_w, want_h, Size::new(bounds.width, bounds.height)),
5152 )
5153 } else {
5154 let room = Size::new(
5155 want_w.unwrap_or(bounds.width),
5156 want_h.unwrap_or(bounds.height),
5157 );
5158 // The stack's fit (`stack.dart:634`): allowed up to the room, forced
5159 // into it, or handed it.
5160 let c = match fit {
5161 // A layer that asks for the room takes it here unasked: laid out on
5162 // its own, its fill request is the root's.
5163 crate::StackFit::Loose => Constraints::axes(
5164 room.width,
5165 room.height,
5166 [false, false],
5167 [false, false],
5168 ),
5169 crate::StackFit::Expand => {
5170 Constraints::pinned(Some(room.width), Some(room.height), room)
5171 }
5172 crate::StackFit::Passthrough => Constraints::filled(room),
5173 };
5174 self.cached_rects(cid, layer.as_ref(), c)
5175 };
5176 let own = layer_rects
5177 .first()
5178 .copied()
5179 .unwrap_or(Rect::new(0.0, 0.0, 0.0, 0.0));
5180 let (dx, dy) = match pins {
5181 Some(p) => (
5182 Positioning::place(
5183 p.left,
5184 p.right,
5185 bounds.width,
5186 own.width,
5187 align.fraction_x(),
5188 ),
5189 Positioning::place(
5190 p.top,
5191 p.bottom,
5192 bounds.height,
5193 own.height,
5194 align.fraction_y(),
5195 ),
5196 ),
5197 None => (
5198 (bounds.width - own.width).max(0.0) * align.fraction_x(),
5199 (bounds.height - own.height).max(0.0) * align.fraction_y(),
5200 ),
5201 };
5202 let mut layer_index = 0;
5203 // A layer that is not the shown one is walked in full — so it is measured,
5204 // and so the runtime keeps its scroll offset, its caret and its focus — and
5205 // then everything it added is dropped again. The same withdrawal a
5206 // `Visibility` that maintains its size performs, applied from outside.
5207 let hidden = shown.is_some_and(|which| which != i);
5208 let base = hidden.then(|| self.barrier_base());
5209 self.walk(
5210 layer.as_ref(),
5211 cid,
5212 (bounds.x + dx, bounds.y + dy),
5213 layer_clip,
5214 &layer_rects,
5215 &mut layer_index,
5216 );
5217 if let Some(base) = base {
5218 self.apply_barrier(HIDDEN_LAYER, &base, cid, Rect::new(0.0, 0.0, 0.0, 0.0));
5219 }
5220 }
5221 self.touch_clip = outer_touch;
5222 } else if let Some((content, placement)) = widget.overlay() {
5223 // The anchor (child 0) is rendered inline; the overlay (child 1) is deferred.
5224 self.walk(
5225 widget.children()[0].as_ref(),
5226 child_id(id, 0, widget.children()[0].as_ref()),
5227 translation,
5228 clip,
5229 rects,
5230 index,
5231 );
5232 // The appearance progress: for an animated overlay (a drawer), the value the
5233 // runtime interpolated; otherwise `1.0` (shown at once). With no value recorded
5234 // (an isolated render), the target is adopted immediately.
5235 let target = widget.anim_target().unwrap_or(1.0);
5236 let progress = self.runtime.value_or(id, target);
5237 // A tooltip only shows while the anchor is hovered; an animated overlay disappears
5238 // once its progress has fallen back to zero.
5239 let visible = match placement {
5240 Placement::Tooltip => self.runtime.input.hovered == Some(id.child(0)),
5241 _ => true,
5242 };
5243 if visible && progress > 0.001 {
5244 self.overlays.push((
5245 content,
5246 child_id(id, 1, content),
5247 draw_rect,
5248 placement,
5249 widget.overlay_dismiss(),
5250 progress,
5251 widget.overlay_traps_focus(),
5252 widget.overlay_scrim(&self.theme),
5253 self.theme.clone(),
5254 ));
5255 }
5256 } else {
5257 let children = widget.children();
5258 // Fractional alignment (`Container::alignment`) + paint offset
5259 // (`Transform::translate`): both offset the subtree. `(0, 0)` otherwise → the
5260 // ordinary flex walk.
5261 let extra = self.child_offset(widget, id, rect, rects, *index, children);
5262 if widget.sticky_header() && children.len() == 2 {
5263 self.walk_sticky(
5264 widget,
5265 id,
5266 draw_rect,
5267 (translation.0 + extra.0, translation.1 + extra.1),
5268 clip,
5269 rects,
5270 index,
5271 );
5272 self.depth -= 1;
5273 return;
5274 }
5275 for (child_index, child) in children.iter().enumerate() {
5276 self.walk(
5277 child.as_ref(),
5278 child_id(id, child_index, child.as_ref()),
5279 (translation.0 + extra.0, translation.1 + extra.1),
5280 clip,
5281 rects,
5282 index,
5283 );
5284 }
5285 }
5286 self.depth -= 1;
5287 }
5288
5289 /// A section with a sticky header (milestone 602). The header is moved down to the top of
5290 /// what is visible — the clip — but never above the section's top nor so far that it
5291 /// leaves the section's bottom, which is how the next section's header pushes it off.
5292 ///
5293 /// It is drawn **over** the section, which goes under it, so what the header adds is
5294 /// moved after what the section adds: its primitives, and the registries a pointer is
5295 /// matched against last-first. Focus and semantics keep the reading order, header first.
5296 #[allow(clippy::too_many_arguments)]
5297 fn walk_sticky(
5298 &mut self,
5299 widget: &'a dyn Widget<Msg>,
5300 id: WidgetId,
5301 section: Rect,
5302 translation: (f32, f32),
5303 clip: Rect,
5304 rects: &[Rect],
5305 index: &mut usize,
5306 ) {
5307 let children = widget.children();
5308 let header = rects
5309 .get(*index)
5310 .copied()
5311 .unwrap_or(Rect::new(0.0, 0.0, 0.0, 0.0))
5312 .translate(translation.0, translation.1);
5313 let room = (section.y + section.height - (header.y + header.height)).max(0.0);
5314 let down = (clip.y - header.y).clamp(0.0, room);
5315 let start = self.barrier_base();
5316 self.walk(
5317 children[0].as_ref(),
5318 child_id(id, 0, children[0].as_ref()),
5319 (translation.0, translation.1 + down),
5320 clip,
5321 rects,
5322 index,
5323 );
5324 let mid = self.barrier_base();
5325 self.walk(
5326 children[1].as_ref(),
5327 child_id(id, 1, children[1].as_ref()),
5328 translation,
5329 clip,
5330 rects,
5331 index,
5332 );
5333 let mut primitives = self.scene.split_off(start.scene);
5334 after(&mut primitives, 0, mid.scene - start.scene);
5335 for primitive in primitives {
5336 self.scene.push_primitive(primitive);
5337 }
5338 after(&mut self.hits, start.hits, mid.hits);
5339 after(&mut self.long_presses, start.long_presses, mid.long_presses);
5340 after(&mut self.scrollables, start.scrollables, mid.scrollables);
5341 after(&mut self.draggables, start.draggables, mid.draggables);
5342 after(&mut self.drag_sources, start.drag_sources, mid.drag_sources);
5343 after(&mut self.drop_zones, start.drop_zones, mid.drop_zones);
5344 after(&mut self.inks, start.inks, mid.inks);
5345 after(&mut self.pans, start.pans, mid.pans);
5346 after(
5347 &mut self.pointer_listeners,
5348 start.pointer_listeners,
5349 mid.pointer_listeners,
5350 );
5351 after(
5352 &mut self.hover_regions,
5353 start.hover_regions,
5354 mid.hover_regions,
5355 );
5356 after(&mut self.interactives, start.interactives, mid.interactives);
5357 }
5358
5359 /// Offset to apply to a child's subtree: the sum of the **fractional alignment**
5360 /// (`Widget::alignment`, a single child slid through the free space) and the **paint
5361 /// offset** (`Transform::translate`, the whole subtree offset without touching layout).
5362 /// `(0, 0)` by default. Since it is added to the walk's translation, this offset follows
5363 /// both the primitives **and** the hit-test.
5364 fn child_offset(
5365 &self,
5366 widget: &dyn Widget<Msg>,
5367 id: WidgetId,
5368 container: Rect,
5369 rects: &[Rect],
5370 child_index: usize,
5371 children: &[Box<dyn Widget<Msg>>],
5372 ) -> (f32, f32) {
5373 let mut off = (0.0, 0.0);
5374
5375 // Fractional alignment: it targets a single child. taffy laid the child out at the
5376 // top left (Start/Start) at its natural size; it is then slid by `free × fraction`.
5377 // A **stack** answers this question too, and means something else by it: where a
5378 // layer smaller than the box sits, which the stack branch applies per layer. It
5379 // must not also be applied to the stack itself, or a stack with exactly one layer
5380 // would shift twice.
5381 if let (Some(geo), 1, false) = (widget.alignment_geometry(), children.len(), widget.stack())
5382 {
5383 // An **animated** anchor moves through the runtime's pair rather than the
5384 // widget's own numbers, in the anchor's own coordinates — so the reading
5385 // direction is applied to the interpolated anchor below, and not to the two
5386 // it came from.
5387 let geo = match self.runtime.anim_offset(id) {
5388 Some((x, y)) => geo.with_fractions(x, y),
5389 None => geo,
5390 };
5391 // Resolves the alignment (physical or directional) against the reading direction;
5392 // `resolve` produces a physical `Alignment` that the rest (with its RTL correction)
5393 // handles uniformly.
5394 let direction = if self.rtl() {
5395 frus_core::TextDirection::Rtl
5396 } else {
5397 frus_core::TextDirection::Ltr
5398 };
5399 let align = geo.resolve(direction);
5400 let child = rects[child_index];
5401 let pad = effective_style(widget, id, self.runtime, &self.theme).padding;
5402 let free_w = (container.width - pad.left - pad.right - child.width).max(0.0);
5403 let free_h = (container.height - pad.top - pad.bottom - child.height).max(0.0);
5404 // In RTL, taffy lays the child out on the left and `mirror` has sent it back to
5405 // the right: the baseline is therefore already right-aligned. 1 is subtracted so
5406 // the fraction stays **physical** (x = +1 ⇒ the right in both directions).
5407 let fx = align.fraction_x() - if self.rtl() { 1.0 } else { 0.0 };
5408 off.0 += free_w * fx;
5409 off.1 += free_h * align.fraction_y();
5410 }
5411
5412 // Paint offset (`Transform::translate`): in RTL the world's x axis is mirrored, so a
5413 // logical +x offset ("towards the end") points left — the sign is inverted to stay
5414 // consistent with the reading direction.
5415 if let Some((tx, ty)) = widget.transform_translate() {
5416 off.0 += if self.rtl() { -tx } else { tx };
5417 off.1 += ty;
5418 }
5419
5420 // The same offset, given as a **fraction of the child's own box**
5421 // (`FractionalTranslation`). The child's rectangle is the one thing this function
5422 // has that the widget did not: a widget cannot multiply by a width it was never
5423 // told.
5424 if let Some((fx, fy)) = widget.translate_fraction() {
5425 // The same tween, on the other offset rule: a slide that animates reads the
5426 // runtime's pair, and one that does not reads its own.
5427 let (fx, fy) = self.runtime.anim_offset(id).unwrap_or((fx, fy));
5428 let child = rects.get(child_index).copied().unwrap_or(container);
5429 let tx = child.width * fx;
5430 off.0 += if self.rtl() { -tx } else { tx };
5431 off.1 += child.height * fy;
5432 }
5433
5434 off
5435 }
5436
5437 /// Records the widget in the inspector's collection (when it is on) and opens a depth
5438 /// level — every render path closes it again at the end of the function
5439 /// (`self.depth -= 1`).
5440 fn inspect_enter(&mut self, widget: &dyn Widget<Msg>, id: WidgetId, draw_rect: Rect) {
5441 if let Some(nodes) = &mut self.inspector {
5442 nodes.push(crate::inspector::InspectorNode {
5443 id,
5444 rect: draw_rect,
5445 name: widget.debug_name(),
5446 depth: self.depth,
5447 });
5448 }
5449 self.depth += 1;
5450 }
5451
5452 /// A widget's full status: pointer interaction + focus + animation progresses + the
5453 /// cursor/selection when there is one.
5454 fn full_status(&self, widget: &dyn Widget<Msg>, id: WidgetId) -> crate::interaction::Status {
5455 let mut status = self.runtime.input.status_for(id);
5456 status.hover_progress = self.runtime.hover_progress(id);
5457 status.focus_progress = self.runtime.focus_progress(id);
5458 // A press is a progression now, and where the runtime has never heard of this
5459 // widget the flag is adopted whole — the rule `value` follows just below. Without
5460 // it a frame built outside the loop would draw a held widget as untouched.
5461 status.press_progress = self.runtime.press_progress_or(
5462 id,
5463 if status.interaction == crate::interaction::Interaction::Pressed {
5464 1.0
5465 } else {
5466 0.0
5467 },
5468 );
5469 status.opacity = self.runtime.opacity(id);
5470 // A widget's own animated value, or **its target** where the runtime has never
5471 // heard of it: the same rule the runtime applies on mount (adopt, do not animate
5472 // in from zero). Without the fallback an isolated render — a test, a frame built
5473 // before the loop has advanced anything — draws every such widget at zero: a
5474 // switch that is on drawn off, an indicator under the wrong tab.
5475 status.value = match widget.anim_target() {
5476 Some(target) => self.runtime.value_or(id, target),
5477 None => self.runtime.value(id),
5478 };
5479 status.value_placed = self.runtime.value_placed(id);
5480 status.anim_color = self.runtime.anim_color(id);
5481 status.anim_radius = self.runtime.anim_radius(id);
5482 status.time = self.runtime.time;
5483 status.drag_over = self.runtime.drag_over == Some(id);
5484 status.scroll_y = self.runtime.scroll.get(&id).map(|s| s.1).unwrap_or(0.0);
5485 // What a selection area has selected in this text, focused or not.
5486 status.region = self
5487 .runtime
5488 .region
5489 .as_ref()
5490 .and_then(|selection| selection.ranges.get(&id).copied());
5491 status.region_bar = status.region.is_some()
5492 && self
5493 .runtime
5494 .region
5495 .as_ref()
5496 .is_some_and(|selection| selection.bar);
5497 if let Some(selection) = self.runtime.region.as_ref().filter(|s| s.handles) {
5498 let here =
5499 |point: Option<crate::RegionPoint>| point.filter(|p| p.text == id).map(|p| p.index);
5500 status.region_grips = (here(selection.start), here(selection.end));
5501 }
5502 if status.focused {
5503 if let Some(edit) = self.runtime.edits.get(&id) {
5504 status.cursor = Some(edit.cursor);
5505 status.selection = edit.selection_range();
5506 status.composing = edit.composing;
5507 }
5508 status.handles = self.runtime.selection_handles == Some(id);
5509 status.toolbar = self
5510 .runtime
5511 .selection_toolbar
5512 .filter(|mark| mark.id == id)
5513 .map(|mark| mark.can_paste);
5514 status.caret_hidden = self.runtime.caret_hidden;
5515 }
5516 status
5517 }
5518
5519 /// The generic focus ring (for widgets that do not draw their own).
5520 fn draw_focus_ring(
5521 &mut self,
5522 draw_rect: Rect,
5523 status: &crate::interaction::Status,
5524 widget: &dyn Widget<Msg>,
5525 ) {
5526 // The generic ring only appears when the last interaction was a **keyboard** one
5527 // (`focus_visible`) — a click must not flash a ring.
5528 if status.focused
5529 && self.runtime.focus_visible
5530 && widget.focusable()
5531 && !widget.draws_own_focus()
5532 {
5533 let ring = Rect::new(
5534 draw_rect.x - 2.0,
5535 draw_rect.y - 2.0,
5536 draw_rect.width + 4.0,
5537 draw_rect.height + 4.0,
5538 );
5539 let alpha = 0.4 + 0.6 * status.focus_progress.clamp(0.0, 1.0);
5540 self.scene.draw_rect(
5541 ring,
5542 Color::TRANSPARENT,
5543 self.theme.radius + 2.0,
5544 2.0,
5545 self.theme.focus.fade(alpha),
5546 );
5547 }
5548 }
5549
5550 /// Keeps a widget this frame has just built for the rest of the walk, and hands back
5551 /// the reference the walk takes (milestone 592).
5552 fn keep(&self, widget: Box<dyn Widget<Msg>>) -> &'a dyn Widget<Msg> {
5553 let built: &'a Built<Msg> = self.built;
5554 let kept: &'a mut Box<dyn Widget<Msg>> = built.alloc(widget);
5555 &**kept
5556 }
5557
5558 /// Lays out a full-window screen and renders it offset by `off_x`.
5559 /// Renders a page in a transition, as `pose` puts it: moved along the reading direction
5560 /// (`mirror` turns that round), scaled about its centre and faded. A page at its
5561 /// natural size and opacity is drawn as it always was; otherwise it goes into one
5562 /// layer, and what can be touched in it moves with it.
5563 fn render_posed(
5564 &mut self,
5565 screen: &'a dyn Widget<Msg>,
5566 id: WidgetId,
5567 bounds: Rect,
5568 pose: crate::PagePose,
5569 mirror: f32,
5570 clip: Rect,
5571 ) {
5572 let off_x = pose.dx * bounds.width * mirror;
5573 let plain = (pose.scale - 1.0).abs() < 1e-4 && pose.opacity > 0.999;
5574 if plain {
5575 self.render_screen(screen, id, bounds, off_x, clip);
5576 return;
5577 }
5578 let mark = (self.scene.primitives().len(), self.xform_base());
5579 self.render_screen(screen, id, bounds, off_x, clip);
5580 let (p0, base) = mark;
5581 let group = self.scene.split_off(p0);
5582 // A page not yet faded in is still a layer, at nothing: a hero on it is about to
5583 // fly from it, and the flight takes it out of here.
5584 let centre = Point::new(
5585 bounds.x + off_x + bounds.width * 0.5,
5586 bounds.y + bounds.height * 0.5,
5587 );
5588 let matrix = Affine::scale(pose.scale, pose.scale).about(centre);
5589 self.scene.push_primitive(Primitive::Layer {
5590 primitives: group,
5591 opacity: pose.opacity.clamp(0.0, 1.0),
5592 clip,
5593 clip_shape: ClipShape::Rect,
5594 transform: Some(LayerTransform::new(matrix)),
5595 filter: LayerFilter::NONE,
5596 owner: id.as_u64(),
5597 });
5598 self.transform_interaction_registries(&base, matrix);
5599 }
5600
5601 /// The iOS page's shadow along its start edge (`route.dart:1065`): black at `0x04`
5602 /// fading to nothing over 5 % of the page's width, outside the page, at `frame`'s
5603 /// strength.
5604 fn paint_edge_shadow(
5605 &mut self,
5606 bounds: Rect,
5607 frame: crate::TransitionFrame,
5608 mirror: f32,
5609 clip: Rect,
5610 ) {
5611 let width = bounds.width * 0.05;
5612 let edge = if mirror < 0.0 {
5613 bounds.x + bounds.width + frame.top.dx * bounds.width * mirror
5614 } else {
5615 bounds.x + frame.top.dx * bounds.width
5616 };
5617 let shade = Color::rgba8(0, 0, 0, 0x04).fade(frame.edge_shadow);
5618 // Darkest at the page's edge, nothing at the far side: left to right, the colours
5619 // are ordered by which side the page is on.
5620 let (rect, from, to) = if mirror < 0.0 {
5621 (
5622 Rect::new(edge, bounds.y, width, bounds.height),
5623 shade,
5624 Color::TRANSPARENT,
5625 )
5626 } else {
5627 (
5628 Rect::new(edge - width, bounds.y, width, bounds.height),
5629 Color::TRANSPARENT,
5630 shade,
5631 )
5632 };
5633 self.scene.set_owner(0);
5634 self.scene.set_clip(clip);
5635 self.scene
5636 .gradient_rect(rect, from, to, [1.0, 0.0], 0.0, 0.0, Color::TRANSPARENT);
5637 }
5638
5639 fn render_screen(
5640 &mut self,
5641 screen: &'a dyn Widget<Msg>,
5642 id: WidgetId,
5643 bounds: Rect,
5644 off_x: f32,
5645 clip: Rect,
5646 ) {
5647 let rects = self.cached_rects(
5648 id,
5649 screen,
5650 Constraints::definite(Size::new(bounds.width, bounds.height)),
5651 );
5652 let screen_clip = clip.intersect(bounds);
5653 let mut index = 0;
5654 self.walk(
5655 screen,
5656 id,
5657 (bounds.x + off_x, bounds.y),
5658 screen_clip,
5659 &rects,
5660 &mut index,
5661 );
5662 }
5663
5664 /// Floats field `id`'s **selection bar** over everything, when the shell has marked it as
5665 /// showing (milestone 568): asks the field for the bar its situation calls for, and for
5666 /// the box to place it against, and defers it like any other overlay.
5667 ///
5668 /// The three conditions are read where they are true — the selection from the retained
5669 /// edit, the text from the widget, the clipboard from the mark the shell set when the
5670 /// bar opened — so they are the same ones the field's default list is decided by.
5671 fn push_selection_toolbar(
5672 &mut self,
5673 widget: &'a dyn Widget<Msg>,
5674 id: WidgetId,
5675 draw_rect: Rect,
5676 clip: Rect,
5677 status: &Status,
5678 can_paste: bool,
5679 ) {
5680 let Some(edit) = self.runtime.edits.get(&id).copied() else {
5681 return;
5682 };
5683 let Some(local) = widget.selection_anchor(draw_rect.width, &edit, status.scroll_y) else {
5684 return;
5685 };
5686 let anchor = local.translate(draw_rect.x, draw_rect.y);
5687 // Opened by a right-click, the bar goes where the pointer was (milestone 618), as
5688 // the reference's context menu does (`editable_text.dart:3211`); the selection
5689 // still decides whether it is shown at all.
5690 let pointer = self
5691 .runtime
5692 .selection_toolbar
5693 .filter(|mark| mark.id == id)
5694 .and_then(|mark| mark.at)
5695 .map(|at| Rect::new(at.x, at.y, 0.0, 0.0));
5696 // The bar goes with what it points at: a selection scrolled out of the region it is
5697 // drawn in takes its bar with it, rather than leaving it floating over nothing.
5698 let seen = anchor.x + anchor.width >= clip.x
5699 && anchor.x <= clip.x + clip.width
5700 && anchor.y + anchor.height > clip.y
5701 && anchor.y < clip.y + clip.height;
5702 if !seen {
5703 return;
5704 }
5705 let len = widget.text_value().map_or(0, |text| text.chars().count());
5706 let selection = edit
5707 .selection_range()
5708 .map(|(start, end)| (start.min(len), end.min(len)))
5709 .filter(|(start, end)| start < end);
5710 let context = crate::ToolbarContext {
5711 has_selection: selection.is_some(),
5712 can_paste,
5713 all_selected: len == 0 || selection == Some((0, len)),
5714 };
5715 let Some(content) = widget.selection_toolbar(context) else {
5716 return;
5717 };
5718 self.overlays.push((
5719 content,
5720 id.toolbar(context.variant()),
5721 pointer.unwrap_or(anchor),
5722 Placement::Selection,
5723 None,
5724 1.0,
5725 false,
5726 None,
5727 self.theme.clone(),
5728 ));
5729 }
5730
5731 /// Registers a region the mouse enters and leaves, as far as it shows (milestone 583).
5732 fn register_hover_region(
5733 &mut self,
5734 id: WidgetId,
5735 spec: crate::HoverRegion,
5736 whole: Rect,
5737 clip: Rect,
5738 ) {
5739 let seen = whole.intersect(clip);
5740 if seen.width > 0.0 && seen.height > 0.0 {
5741 self.hover_regions.push(HoverEntry {
5742 id,
5743 seen,
5744 whole,
5745 parent: self.hover_parent,
5746 spec,
5747 });
5748 }
5749 }
5750
5751 /// Floats the bar of a selection area's selection over `range` of this text — the first
5752 /// the selection covers, in reading order, since the walk meets them in that order.
5753 fn push_region_toolbar(
5754 &mut self,
5755 widget: &'a dyn Widget<Msg>,
5756 id: WidgetId,
5757 draw_rect: Rect,
5758 clip: Rect,
5759 range: (usize, usize),
5760 ) {
5761 let all_selected = self
5762 .runtime
5763 .region
5764 .as_ref()
5765 .is_some_and(|selection| selection.all);
5766 let context = crate::ToolbarContext {
5767 has_selection: true,
5768 can_paste: false,
5769 all_selected,
5770 };
5771 let Some((local, own)) = widget.region_toolbar(context, draw_rect.width, range) else {
5772 return;
5773 };
5774 // The area's bar, if the application said what it holds; the text's otherwise.
5775 let content = match self.area_widget.and_then(|area| area.area_toolbar(context)) {
5776 Some(Some(bar)) => bar,
5777 Some(None) => return,
5778 None => own,
5779 };
5780 let anchor = local.translate(draw_rect.x, draw_rect.y);
5781 let seen = anchor.x + anchor.width >= clip.x
5782 && anchor.x <= clip.x + clip.width
5783 && anchor.y + anchor.height > clip.y
5784 && anchor.y < clip.y + clip.height;
5785 if !seen {
5786 return;
5787 }
5788 self.region_bar_placed = true;
5789 self.overlays.push((
5790 content,
5791 id.toolbar(context.variant()),
5792 anchor,
5793 Placement::Selection,
5794 None,
5795 1.0,
5796 false,
5797 None,
5798 self.theme.clone(),
5799 ));
5800 }
5801
5802 /// Processes the deferred overlays: sub-layout, positioning and rendering **above**
5803 /// everything (their clickable areas win). May spawn further overlays (nested portals).
5804 fn process_overlays(&mut self) {
5805 let window = Rect::new(0.0, 0.0, self.available.width, self.available.height);
5806 while let Some((content, oid, anchor, placement, dismiss, progress, traps, scrim, theme)) =
5807 self.overlays.pop()
5808 {
5809 // The theme this overlay was declared under, for the whole of its layout and
5810 // painting; whatever was in force when the walk ended is put back after it,
5811 // so two overlays declared under two themes do not bleed into each other.
5812 let outer = std::mem::replace(&mut self.theme, theme);
5813 // Drawers slide along a **spring curve** (a gentle arrival), not linearly; the
5814 // other overlays keep their raw progress.
5815 let progress = if matches!(
5816 placement,
5817 Placement::Left | Placement::Right | Placement::Bottom
5818 ) {
5819 crate::runtime::spring_ease(progress)
5820 } else {
5821 progress
5822 };
5823 // The content's natural size. A drawer (`Left`) is constrained in height to the
5824 // window (so its `Percent(1.0)` panel unfolds) with a free width; the other
5825 // overlays take their natural size.
5826 let (free_x, free_y) = match placement {
5827 Placement::Left | Placement::Right => (true, false),
5828 // The sheet is full-width (constrained to the window) with its natural height:
5829 // its `Percent(1.0)` panel unfolds across the width.
5830 Placement::Bottom => (false, true),
5831 _ => (true, true),
5832 };
5833 let mut rects = self.cached_rects(
5834 oid,
5835 content,
5836 Constraints::scroll(self.available.width, self.available.height, free_x, free_y),
5837 );
5838 let mut size = rects
5839 .first()
5840 .copied()
5841 .unwrap_or(Rect::new(0.0, 0.0, 0.0, 0.0));
5842 // **Nothing wider than the window.** A free axis asks the content how big it
5843 // would like to be, and a dialog answers with the width of its widest line —
5844 // which, at a reader's font size, is wider than the screen. Centred, that
5845 // spills off *both* edges, and a button drawn at a negative x is a button
5846 // nobody can press. So the answer is taken as a wish rather than a size: past
5847 // the room there is, the content is laid out again at that room, and whatever
5848 // can fold — a paragraph, a bar of actions — folds.
5849 //
5850 // The room is the window less the content's **own margin**, which is how an
5851 // overlay says how far off the edges it wants to be held: a dialog's inset
5852 // padding is exactly that, and a clamp that ignored it would push the surface
5853 // flat against both sides of the screen.
5854 //
5855 // The height is deliberately left alone. A dialog too tall for the screen
5856 // wants its content to scroll, which is a question of its own; squashing it
5857 // here would only move the spill inside the surface.
5858 let inset = content.style_themed(&self.theme).margin;
5859 let room = (self.available.width - inset.left - inset.right).max(0.0);
5860 if free_x && size.width > room {
5861 rects = self.cached_rects(
5862 oid,
5863 content,
5864 Constraints::scroll(room, self.available.height, false, free_y),
5865 );
5866 size = rects
5867 .first()
5868 .copied()
5869 .unwrap_or(Rect::new(0.0, 0.0, 0.0, 0.0));
5870 }
5871
5872 // A drawer's slide-in from the left / right edge.
5873 let from_left = -(1.0 - progress) * size.width;
5874 let from_right = self.available.width - progress * size.width;
5875 // An anchored menu/tooltip: aligned to the anchor's **start** edge — in RTL that
5876 // edge is the right one (the menu opens leftwards).
5877 let anchor_x = if self.rtl() {
5878 anchor.x + anchor.width - size.width
5879 } else {
5880 anchor.x
5881 };
5882 let mut pos = match placement {
5883 Placement::Below => (anchor_x, anchor.y + anchor.height + 4.0),
5884 // A submenu: past the row's end edge, its first row level with the row —
5885 // the panel's own room above its rows taken back (milestone 603).
5886 Placement::Beside if self.rtl() => (anchor.x - size.width, anchor.y - 8.0),
5887 Placement::Beside => (anchor.x + anchor.width, anchor.y - 8.0),
5888 Placement::Center => (
5889 (self.available.width - size.width) * 0.5,
5890 (self.available.height - size.height) * 0.5,
5891 ),
5892 Placement::Tooltip => (anchor_x, anchor.y - size.height - 6.0),
5893 // The bar over a selection: on the desktops, a menu hanging from the pointer
5894 // or the selection's top (milestone 618); elsewhere centred on it, above or
5895 // below, inside the window.
5896 Placement::Selection if crate::selectiontoolbar::uses_menu(self.theme.platform) => {
5897 crate::selectiontoolbar::place_menu(
5898 crate::selectiontoolbar::primary_anchor(anchor),
5899 Size::new(size.width, size.height),
5900 window,
5901 )
5902 }
5903 Placement::Selection => crate::selectiontoolbar::place(
5904 anchor,
5905 Size::new(size.width, size.height),
5906 window,
5907 crate::textinput::HANDLE_SIZE,
5908 ),
5909 // `Left` = a drawer on the **start** side; in RTL, start = the right.
5910 Placement::Left if self.rtl() => (from_right, 0.0),
5911 Placement::Left => (from_left, 0.0),
5912 // `Right` = the **end** side; in RTL, end = the left.
5913 Placement::Right if self.rtl() => (from_left, 0.0),
5914 Placement::Right => (from_right, 0.0),
5915 // The sheet slides up from the bottom: its bottom edge stays flush with the
5916 // window, offset downwards by `(1-progress)·height`.
5917 Placement::Bottom => (0.0, self.available.height - progress * size.height),
5918 };
5919
5920 // An **anchored** overlay follows a widget; the others (a modal, a drawer, a
5921 // sheet) are positioned against the window and have no anchor worth the name.
5922 // Only the first kind can find its anchor gone from the window: a screen
5923 // sliding out under a `Navigator`, a row scrolled sideways past the edge.
5924 let anchored = matches!(
5925 placement,
5926 Placement::Below | Placement::Tooltip | Placement::Beside
5927 );
5928 let anchor_on_screen = !anchored
5929 || (anchor.x < self.available.width
5930 && anchor.x + anchor.width > 0.0
5931 && anchor.y < self.available.height
5932 && anchor.y + anchor.height > 0.0);
5933
5934 // Auto-flip: when an anchored overlay overflows an edge, it is flipped / nudged
5935 // back inside the window.
5936 //
5937 // This is for a menu opened near the right margin, and it assumes the anchor is
5938 // something the window is showing. When the anchor has **left** the window the
5939 // nudge does the opposite of its job: it drags a departing screen's menu back
5940 // into view, fully opaque, over the screen that replaced it. An overlay whose
5941 // anchor is off screen goes off screen with it.
5942 if anchored && anchor_on_screen {
5943 // A vertical overflow → flip to the other side of the anchor.
5944 if placement == Placement::Below
5945 && pos.1 + size.height > self.available.height
5946 && anchor.y - size.height - 4.0 >= 0.0
5947 {
5948 pos.1 = anchor.y - size.height - 4.0;
5949 } else if placement == Placement::Tooltip
5950 && pos.1 < 0.0
5951 && anchor.y + anchor.height + size.height + 6.0 <= self.available.height
5952 {
5953 pos.1 = anchor.y + anchor.height + 6.0;
5954 }
5955 // A submenu with no room on its end side opens on the other side of its row,
5956 // rather than being pushed back over the row it came from.
5957 if placement == Placement::Beside {
5958 let (end, start) = if self.rtl() {
5959 (anchor.x - size.width, anchor.x + anchor.width)
5960 } else {
5961 (anchor.x + anchor.width, anchor.x - size.width)
5962 };
5963 let fits = |x: f32| x >= 0.0 && x + size.width <= self.available.width;
5964 if !fits(end) && fits(start) {
5965 pos.0 = start;
5966 }
5967 if pos.1 + size.height > self.available.height {
5968 pos.1 = (self.available.height - size.height).max(0.0);
5969 }
5970 }
5971 // A horizontal overflow → nudge back inside the window.
5972 if pos.0 + size.width > self.available.width {
5973 pos.0 = (self.available.width - size.width).max(0.0);
5974 }
5975 if pos.0 < 0.0 {
5976 pos.0 = 0.0;
5977 }
5978 }
5979
5980 if matches!(
5981 placement,
5982 Placement::Center | Placement::Left | Placement::Right | Placement::Bottom
5983 ) {
5984 // The scrim behind the modal / the drawer, modulated by the progress (a fade
5985 // synchronised with the slide). The colour is the overlay's own when it asked
5986 // for one — alpha included, since a scrim's transparency *is* the colour —
5987 // and the `scrim` role at half opacity otherwise.
5988 let base = scrim.unwrap_or_else(|| self.theme.scheme.scrim.with_alpha(0.5));
5989 self.scene.set_owner(0);
5990 self.scene.set_clip(window);
5991 self.scene
5992 .fill_rect(window, base.with_alpha(base.a * progress));
5993 }
5994
5995 // Dismissal on a click **outside** the content (a modal, a menu…): a full-screen
5996 // hit added **before** the content, so the content beats it where they overlap.
5997 // The dismissal is also remembered for Escape (the last one rendered = the
5998 // topmost).
5999 //
6000 // Gated on the same condition as the nudge, and for the same reason: a
6001 // window-wide barrier belonging to an overlay nobody can see would swallow the
6002 // next press anywhere on the screen that replaced it.
6003 if let Some(msg) = dismiss.filter(|_| anchor_on_screen) {
6004 self.overlay_cover.push(window);
6005 self.dismisses.push(msg.clone());
6006 self.hits.push(Hit {
6007 id: oid.barrier(),
6008 rect: window,
6009 msg: Some(msg),
6010 xform: None,
6011 });
6012 }
6013
6014 // A **modal** (scrimmed) or **trapping anchored** (menu) overlay: its focusables
6015 // form a **scope** that traps Tab/arrows/click-to-focus. The last one rendered (the
6016 // topmost) wins; anchored overlays that do not trap (a tooltip, an autocomplete
6017 // list) leave the focus alone.
6018 let modal = matches!(
6019 placement,
6020 Placement::Center | Placement::Left | Placement::Right | Placement::Bottom
6021 );
6022 if modal || traps {
6023 self.focus_scope_start = Some(self.focusables.len());
6024 }
6025
6026 // Where a press is the overlay's and not the pages' — a tooltip takes none.
6027 if placement != Placement::Tooltip && anchor_on_screen {
6028 self.overlay_cover
6029 .push(Rect::new(pos.0, pos.1, size.width, size.height));
6030 }
6031
6032 // A bar over a selection: where it ended up, so that a press on it is known for
6033 // one — see [`Ui::toolbar_contains`].
6034 if placement == Placement::Selection {
6035 self.toolbars
6036 .push(Rect::new(pos.0, pos.1, size.width, size.height));
6037 }
6038
6039 let mut index = 0;
6040 self.walk(content, oid, pos, window, &rects, &mut index);
6041 self.theme = outer;
6042 }
6043 }
6044}
6045
6046impl<Msg: Clone> Builder<'_, Msg> {
6047 /// Draws a scrollbar (track + thumb) and registers it for hit-testing a drag.
6048 /// Paints the region's overscroll glows over its viewport.
6049 ///
6050 /// The colour is the scheme's **secondary**: the edge feedback is an
6051 /// acknowledgement, not a call to action, so it must read as part of the surface
6052 /// rather than compete with whatever primary-coloured control sits nearby.
6053 fn add_overscroll_glow(&mut self, id: WidgetId, viewport: Rect) {
6054 if let Some(glows) = self.runtime.scroll_glow.get(&id) {
6055 glows.paint(viewport, self.theme.scheme.secondary, &mut self.scene);
6056 }
6057 }
6058
6059 fn add_scrollbar(&mut self, id: WidgetId, viewport: Rect, axis: Bar) {
6060 let Bar {
6061 vertical,
6062 offset,
6063 max,
6064 reverse,
6065 asked,
6066 visible,
6067 } = axis;
6068 // **Along the horizontal axis, never** (`app.dart:861`) — on any platform, under
6069 // any setting. A sideways strip is scrolled by the thing that made it sideways,
6070 // and a bar under it takes room from content that is short of it already.
6071 if !vertical {
6072 return;
6073 }
6074 // And down the vertical one, only where the platform's own scroll views have a
6075 // bar (`app.dart:865` against `:870`). A finger already knows where it is on the
6076 // page; a permanent bar over the content is an affordance for a pointer that
6077 // cannot feel the edges. This drew one everywhere, on both axes, always.
6078 let behavior = crate::ScrollConfiguration::of().scrollbars(&self.theme);
6079 if asked.unwrap_or(behavior) == crate::physics::Scrollbars::Never {
6080 return;
6081 }
6082 let (track_start, track_len, content_len) = if vertical {
6083 (viewport.y, viewport.height, viewport.height + max)
6084 } else {
6085 (viewport.x, viewport.width, viewport.width + max)
6086 };
6087 // Every metric below is `theme ?? framework`. There is no per-call override for a
6088 // scrollbar — it is drawn by the walk, not by a widget a caller configured — so
6089 // the chain is two terms here rather than three.
6090 let bar = &self.theme.widgets.scrollbar;
6091 // **Under an Android theme, the platform's own bar** (`scrollbar.dart:331`): half
6092 // as thick, flush with the edge, square-ended, an opaque grey at rest, and not
6093 // something a pointer takes hold of. The theme's platform decides it, as the
6094 // reference's does, so a theme set to Android draws it on any device.
6095 let android = self.theme.platform == frus_core::TargetPlatform::Android;
6096 let bar_size = bar
6097 .thickness
6098 .unwrap_or(if android { BAR_SIZE / 2.0 } else { BAR_SIZE });
6099 let bar_margin = bar.margin.unwrap_or(if android { 0.0 } else { BAR_MARGIN });
6100 let thumb_len = (track_len * track_len / content_len)
6101 .max(bar.min_thumb_length.unwrap_or(MIN_THUMB))
6102 .min(track_len);
6103 let travel = track_len - thumb_len;
6104 // Along a reversed axis the numbers run the other way, so the thumb does too:
6105 // offset 0 is the far end, and that is where it rests.
6106 let fraction = if max > 0.0 { offset / max } else { 0.0 };
6107 let fraction = if reverse { 1.0 - fraction } else { fraction };
6108 let thumb_pos = track_start + fraction * travel;
6109
6110 // The track's rectangle is still worked out — it is the room a pointer has to be
6111 // in, and the geometry the thumb sits in — but nothing is painted for it.
6112 //
6113 // `BAR_SIZE` is the **thumb's** thickness, not a slot's: the reference keeps the
6114 // thumb clear of the edge with `crossAxisMargin` (`scrollbar.dart:357`) rather
6115 // than shrinking it to fit. This drew a 6-pixel thumb inside an 8-pixel slot,
6116 // which is the same arithmetic with the wrong number left over.
6117 let (track, thumb) = if vertical {
6118 let x = viewport.x + viewport.width - bar_margin - bar_size;
6119 (
6120 Rect::new(
6121 x - bar_margin,
6122 viewport.y,
6123 bar_size + bar_margin * 2.0,
6124 viewport.height,
6125 ),
6126 Rect::new(x, thumb_pos, bar_size, thumb_len),
6127 )
6128 } else {
6129 let y = viewport.y + viewport.height - bar_margin - bar_size;
6130 (
6131 Rect::new(
6132 viewport.x,
6133 y - bar_margin,
6134 viewport.width,
6135 bar_size + bar_margin * 2.0,
6136 ),
6137 Rect::new(thumb_pos, y, thumb_len, bar_size),
6138 )
6139 };
6140 // What a mouse has to be inside for the bar to notice it: the track, widened to
6141 // take in a tap target's worth of room around the thumb (`scrollbar.dart:762`).
6142 let reach = {
6143 let (cx, cy) = (thumb.x + thumb.width * 0.5, thumb.y + thumb.height * 0.5);
6144 let r = crate::theme::MIN_TAP_TARGET * 0.5;
6145 let x0 = track.x.min(cx - r);
6146 let y0 = track.y.min(cy - r);
6147 let x1 = (track.x + track.width).max(cx + r);
6148 let y1 = (track.y + track.height).max(cy + r);
6149 Rect::new(x0, y0, x1 - x0, y1 - y0)
6150 };
6151
6152 // **How present the bar is**, this frame. It arrives when the area moves and goes
6153 // again once the area has been still long enough; an area nobody has scrolled has
6154 // none at all. Unless it was asked to stay (`scrollbar.dart:214`).
6155 let fade = self.runtime.scrollbar_fade_of(id);
6156 let opacity = if visible.unwrap_or(false) {
6157 1.0
6158 } else {
6159 fade.opacity.clamp(0.0, 1.0)
6160 };
6161
6162 // **No track.** The reference's is transparent unless a caller asks for one
6163 // (`scrollbar.dart:281`), and even then it is 3 to 5 %.
6164 //
6165 // The thumb takes the reference's opacity for the surface's brightness: at rest
6166 // (`scrollbar.dart:242`, `:248`), warmed towards its hovered value while a
6167 // pointer is near it (`:239`, `:245`), and at its dragged value the moment the
6168 // thumb is held (`:238`, `:244`) — that one does not fade in, because the hand is
6169 // already on it and a fade would only lag behind the grab.
6170 // The scheme's own answer, which is what the reference reads here
6171 // (`scrollbar.dart:232`). This used to be `surface.compute_luminance() < 0.5`:
6172 // right on both built-in schemes and a guess on any other.
6173 let dark = self.theme.brightness().is_dark();
6174 let (rest, warm, grabbed) = if dark {
6175 (THUMB_ON_DARK, THUMB_HOVER_ON_DARK, THUMB_DRAG_ON_DARK)
6176 } else {
6177 (THUMB_ON_LIGHT, THUMB_HOVER_ON_LIGHT, THUMB_DRAG_ON_LIGHT)
6178 };
6179 // A theme's levels replace the scheme's, one at a time: naming the resting
6180 // opacity says nothing about the held one.
6181 let rest = bar.opacity.unwrap_or(rest);
6182 let warm = bar.hover_opacity.unwrap_or(warm);
6183 let grabbed = bar.drag_opacity.unwrap_or(grabbed);
6184 let base = bar.thumb_color.unwrap_or(self.theme.scheme.on_surface);
6185 // At rest under an Android theme, the reference's thumb is the theme's highlight
6186 // made opaque (`scrollbar.dart:240`, `:246`), not a fade of the content colour —
6187 // unless the theme chose a colour or a resting level of its own.
6188 let idle = if android && bar.thumb_color.is_none() && bar.opacity.is_none() {
6189 if dark {
6190 ANDROID_THUMB_ON_DARK
6191 } else {
6192 ANDROID_THUMB_ON_LIGHT
6193 }
6194 } else {
6195 base.fade(rest)
6196 };
6197 let color = if self.runtime.scrollbar_dragged == Some(id) {
6198 base.fade(grabbed)
6199 } else {
6200 idle.lerp(base.fade(warm), fade.hover.clamp(0.0, 1.0))
6201 };
6202 if opacity > 0.0 {
6203 self.scene.draw_rect(
6204 thumb,
6205 color.fade(opacity),
6206 bar.radius
6207 .unwrap_or(if android { 0.0 } else { bar_size * 0.5 }),
6208 0.0,
6209 Color::TRANSPARENT,
6210 );
6211 }
6212 // Registered even when nothing was painted for it — unlike a bar the platform
6213 // does not draw at all, which is not registered because it is not there. A mouse
6214 // coming near a faded bar is what brings it back (`scrollbar.dart:2132`), so
6215 // something has to be here for the mouse to find. What a bar at zero cannot be is
6216 // **grabbed**; see `Ui::scrollbar_at`.
6217 self.scrollbars.push(Scrollbar {
6218 id,
6219 vertical,
6220 thumb,
6221 track_start,
6222 track_len,
6223 thumb_len,
6224 reverse,
6225 max,
6226 opacity,
6227 reach,
6228 interactive: bar.interactive.unwrap_or(!android),
6229 });
6230 }
6231}
6232
6233/// `primitives` without those owned by `hidden`, looking inside layers too; the ones owned
6234/// by `keep` among them are moved into `taken`, as painted, outside any layer. A layer left
6235/// with nothing in it is dropped.
6236fn take_owned(
6237 primitives: Vec<Primitive>,
6238 hidden: &std::collections::HashSet<u64>,
6239 keep: &std::collections::HashSet<u64>,
6240 taken: &mut Vec<Primitive>,
6241) -> Vec<Primitive> {
6242 let mut out = Vec::with_capacity(primitives.len());
6243 for primitive in primitives {
6244 match primitive {
6245 Primitive::Layer {
6246 primitives: inner,
6247 opacity,
6248 clip,
6249 clip_shape,
6250 transform,
6251 filter,
6252 owner,
6253 } => {
6254 let inner = take_owned(inner, hidden, keep, taken);
6255 if !inner.is_empty() {
6256 out.push(Primitive::Layer {
6257 primitives: inner,
6258 opacity,
6259 clip,
6260 clip_shape,
6261 transform,
6262 filter,
6263 owner,
6264 });
6265 }
6266 }
6267 other if hidden.contains(&other.owner()) => {
6268 if keep.contains(&other.owner()) {
6269 taken.push(other);
6270 }
6271 }
6272 other => out.push(other),
6273 }
6274 }
6275 out
6276}
6277
6278/// Turns a widget tree into a [`Ui`] for a given size, runtime state and theme.
6279pub fn build_ui<'a, Msg: Clone + 'static>(
6280 root: &'a dyn Widget<Msg>,
6281 available: Size,
6282 runtime: &'a Runtime,
6283 theme: &'a Theme,
6284) -> Ui<Msg> {
6285 build_ui_impl(root, available, runtime, theme, false).0
6286}
6287
6288/// Like [`build_ui`], but it also collects the **inspection nodes** (one per painted widget:
6289/// name, box, depth) — the raw material of the runtime inspector and of the diagnostic dump.
6290/// Reserve it for the frames where the inspector is on.
6291pub fn build_ui_inspected<'a, Msg: Clone + 'static>(
6292 root: &'a dyn Widget<Msg>,
6293 available: Size,
6294 runtime: &'a Runtime,
6295 theme: &'a Theme,
6296) -> (Ui<Msg>, Vec<crate::inspector::InspectorNode>) {
6297 let (ui, nodes) = build_ui_impl(root, available, runtime, theme, true);
6298 (ui, nodes.unwrap_or_default())
6299}
6300
6301/// Panics, **in debug builds only**, when the reader's font size is installed and the
6302/// surface it belongs to is not.
6303///
6304/// The two travel together — [`MediaQuery::install`] sets both — so a scale away from 1
6305/// with no description means somebody installed half a surface, and the half they left out
6306/// is about to be missing for the layout that follows. That is not a theory: for four
6307/// milestones the shell scoped only the build, the scale was 1 while text was measured and
6308/// painted, and the reader's setting reached a device without moving a pixel. Nothing said
6309/// a word — not the 91 goldens, not clippy, not the strict rustdoc.
6310///
6311/// Debug only, because it is a wiring mistake rather than a state a running application can
6312/// reach: it should stop a test, not a user's frame. A scale of exactly 1 is the neutral
6313/// value and says nothing either way, so it is never a failure.
6314#[cfg(debug_assertions)]
6315fn assert_surface_is_whole() {
6316 let scale = frus_core::text_scale();
6317 if (scale - 1.0).abs() > f32::EPSILON && !crate::MediaQuery::of().is_described() {
6318 panic!(
6319 "a text scale of {scale} is installed with no surface described: something \
6320 installed half a surface. Use `MediaQuery::install` (or `scope`), which sets \
6321 the description and the reader's font size together \u{2014} see milestone 408."
6322 );
6323 }
6324}
6325
6326/// Where a frame keeps the widgets it builds as it walks (see `Builder::built`).
6327type Built<Msg> = typed_arena::Arena<Box<dyn Widget<Msg>>>;
6328
6329fn build_ui_impl<Msg: Clone + 'static>(
6330 root: &dyn Widget<Msg>,
6331 available: Size,
6332 runtime: &Runtime,
6333 theme: &Theme,
6334 inspect: bool,
6335) -> (Ui<Msg>, Option<Vec<crate::inspector::InspectorNode>>) {
6336 // Made first, so that it outlives the walk that borrows from it.
6337 let built = Built::new();
6338 build_ui_walk(root, available, runtime, theme, inspect, &built)
6339}
6340
6341fn build_ui_walk<'a, Msg: Clone + 'static>(
6342 root: &'a dyn Widget<Msg>,
6343 available: Size,
6344 runtime: &'a Runtime,
6345 theme: &'a Theme,
6346 inspect: bool,
6347 built: &'a Built<Msg>,
6348) -> (Ui<Msg>, Option<Vec<crate::inspector::InspectorNode>>) {
6349 #[cfg(debug_assertions)]
6350 assert_surface_is_whole();
6351
6352 let (mut rects, overflows) = runtime.layout_cache.borrow_mut().rects(
6353 WidgetId::ROOT,
6354 root,
6355 runtime,
6356 theme,
6357 Constraints::definite(available),
6358 );
6359
6360 let mut builder = Builder {
6361 scene: Scene::new(),
6362 hits: Vec::new(),
6363 long_presses: Vec::new(),
6364 dismisses: Vec::new(),
6365 edit_actions: Vec::new(),
6366 toolbars: Vec::new(),
6367 overlay_cover: Vec::new(),
6368 focusables: Vec::new(),
6369 focus_scope_start: None,
6370 scrollables: Vec::new(),
6371 scrollbars: Vec::new(),
6372 draggables: Vec::new(),
6373 drag_sources: Vec::new(),
6374 drop_zones: Vec::new(),
6375 inks: Vec::new(),
6376 pans: Vec::new(),
6377 title_bar_row: None,
6378 title_bar_regions: Vec::new(),
6379 pointer_listeners: Vec::new(),
6380 hover_regions: Vec::new(),
6381 hover_parent: None,
6382 text_stops: Vec::new(),
6383 area: None,
6384 area_widget: None,
6385 region_bar_placed: false,
6386 reorderables: Vec::new(),
6387 interactives: Vec::new(),
6388 overlays: Vec::new(),
6389 wants_animation: false,
6390 available,
6391 runtime,
6392 theme: std::rc::Rc::new(theme.clone()),
6393 inspector: inspect.then(Vec::new),
6394 depth: 0,
6395 refresh_host: None,
6396 scroll_host: None,
6397 hero_screen: None,
6398 heroes: Vec::new(),
6399 refreshes: Vec::new(),
6400 dismissables: Vec::new(),
6401 sheets: Vec::new(),
6402 semantics: Vec::new(),
6403 system_ui: Vec::new(),
6404 focus_excluded: false,
6405 touch_clip: None,
6406 backdrop_group: None,
6407 focus_skipped: false,
6408 focus_order: None,
6409 focus_group: None,
6410 autofill_group: None,
6411 scopes: Vec::new(),
6412 listeners: Vec::new(),
6413 overflows: std::cell::RefCell::new(Vec::new()),
6414 pending_overflows: std::cell::RefCell::new(std::collections::HashMap::new()),
6415 built,
6416 };
6417 // The root is mirrored in RTL (like every layout root).
6418 builder.mirror(&mut rects);
6419 builder.record_overflows(WidgetId::ROOT, &rects, overflows);
6420 let mut index = 0;
6421 builder.walk(
6422 root,
6423 WidgetId::ROOT,
6424 (0.0, 0.0),
6425 Rect::UNBOUNDED,
6426 &rects,
6427 &mut index,
6428 );
6429
6430 // Overlays (floating menus, modals, tooltips) above everything else. (Their walk restarts
6431 // from depth 0: roots as far as the inspector is concerned.)
6432 builder.process_overlays();
6433
6434 // End of frame: forget the layout roots and repaint boundaries of widgets that have gone,
6435 // and freeze the caches' diagnostic counters.
6436 runtime.layout_cache.borrow_mut().end_frame();
6437 runtime.paint_cache.borrow_mut().end_frame();
6438
6439 // Replays the outgoing subtrees, fading out, over the current scene.
6440 builder.scene.set_clip(Rect::UNBOUNDED);
6441 for (primitives, opacity) in runtime.leaving.values() {
6442 for primitive in primitives {
6443 builder.scene.push_faded(primitive, *opacity);
6444 }
6445 }
6446
6447 // A box whose children ran past it now **says so on the screen**, not only in the
6448 // console — see `paint_overflow_bands`. Last, so nothing paints over it.
6449 paint_overflow_bands(&mut builder.scene, &builder.overflows.borrow());
6450
6451 let ui = Ui {
6452 scene: builder.scene,
6453 hits: builder.hits,
6454 long_presses: builder.long_presses,
6455 dismisses: builder.dismisses,
6456 edit_actions: builder.edit_actions,
6457 toolbars: builder.toolbars,
6458 overlay_cover: builder.overlay_cover,
6459 focusables: builder.focusables,
6460 focus_scope_start: builder.focus_scope_start,
6461 scrollables: builder.scrollables,
6462 scrollbars: builder.scrollbars,
6463 draggables: builder.draggables,
6464 drag_sources: builder.drag_sources,
6465 drop_zones: builder.drop_zones,
6466 inks: builder.inks,
6467 pans: builder.pans,
6468 title_bar_row: builder.title_bar_row,
6469 title_bar_regions: builder.title_bar_regions,
6470 pointer_listeners: builder.pointer_listeners,
6471 hover_regions: builder.hover_regions,
6472 text_stops: builder.text_stops,
6473 reorderables: builder.reorderables,
6474 interactives: builder.interactives,
6475 refreshes: builder.refreshes,
6476 dismissables: builder.dismissables,
6477 sheets: builder.sheets,
6478 wants_animation: builder.wants_animation,
6479 semantics: builder.semantics,
6480 system_ui: builder.system_ui,
6481 overflows: builder.overflows.into_inner(),
6482 scopes: builder.scopes,
6483 listeners: builder.listeners,
6484 };
6485 (ui, builder.inspector)
6486}
6487
6488/// Collects the identities of every widget in the tree (prefix order), following the same
6489/// positional scheme as [`build_ui`]. Used to detect mounts/unmounts between two frames.
6490pub fn collect_ids<Msg>(root: &dyn Widget<Msg>) -> Vec<WidgetId> {
6491 fn walk<Msg>(widget: &dyn Widget<Msg>, id: WidgetId, out: &mut Vec<WidgetId>) {
6492 out.push(id);
6493 for (index, child) in widget.children().iter().enumerate() {
6494 walk(child.as_ref(), child_id(id, index, child.as_ref()), out);
6495 }
6496 }
6497 let mut out = Vec::new();
6498 walk(root, WidgetId::ROOT, &mut out);
6499 out
6500}
6501
6502/// Identities of the **subtree** rooted at `root_id` — `widget` being the widget with that
6503/// identity — that is `[root_id, …descendants]`, derived by the same positional scheme as
6504/// [`collect_ids`].
6505///
6506/// Used by the **drag ghost**: a **rich** card's primitives are painted by its children (owners
6507/// other than the card itself). To capture all of its visuals, the shell gathers the primitives
6508/// of the **whole** subtree, not only the card's own.
6509pub fn subtree_ids<Msg>(widget: &dyn Widget<Msg>, root_id: WidgetId) -> Vec<WidgetId> {
6510 fn walk<Msg>(widget: &dyn Widget<Msg>, id: WidgetId, out: &mut Vec<WidgetId>) {
6511 out.push(id);
6512 for (index, child) in widget.children().iter().enumerate() {
6513 walk(child.as_ref(), child_id(id, index, child.as_ref()), out);
6514 }
6515 }
6516 let mut out = Vec::new();
6517 walk(widget, root_id, &mut out);
6518 out
6519}
6520
6521/// The owners of everything a carried row, card or item may be **moved past** this frame:
6522/// every reorderable that can be dropped on, and all it paints.
6523///
6524/// What a reorder preview slides out of the way is decided by geometry — the band the
6525/// source sits in — and geometry alone moves whatever shares that band: a button floating
6526/// over a list, the navigation bar under it. This is the other half of the question, *is
6527/// it part of something that can be reordered*, asked of the tree. A grip is inside its
6528/// row, so it moves with it without being a target itself.
6529pub fn reorderable_owners<Msg>(
6530 ui: &Ui<Msg>,
6531 root: &dyn Widget<Msg>,
6532) -> std::collections::HashSet<u64> {
6533 ui.reorderables
6534 .iter()
6535 .filter_map(|(id, _)| {
6536 find_widget(root, *id)
6537 .filter(|widget| widget.reorder_droppable())
6538 .map(|widget| subtree_ids(widget, *id))
6539 })
6540 .flatten()
6541 .map(|id| id.as_u64())
6542 .collect()
6543}
6544
6545/// The slots a reorderable `id` can be dropped among **in its own list**: its siblings — the
6546/// children of its parent — that can be dropped on, with their boxes this frame, in order.
6547///
6548/// A list's rows are one widget's children, and so are a board column's cards and the drop
6549/// zone that ends it. Nothing else on the page is, which is what lets a drop that landed on
6550/// no row at all still mean *somewhere in this list* rather than anywhere reorderable.
6551/// Siblings the frame registered no box for are left out.
6552pub fn reorder_siblings<Msg>(
6553 ui: &Ui<Msg>,
6554 root: &dyn Widget<Msg>,
6555 id: WidgetId,
6556) -> Vec<(WidgetId, Rect)> {
6557 fn parent_of<Msg>(
6558 widget: &dyn Widget<Msg>,
6559 own: WidgetId,
6560 target: WidgetId,
6561 ) -> Option<(&dyn Widget<Msg>, WidgetId)> {
6562 for (index, child) in widget.children().iter().enumerate() {
6563 let id = child_id(own, index, child.as_ref());
6564 if id == target {
6565 return Some((widget, own));
6566 }
6567 if let Some(found) = parent_of(child.as_ref(), id, target) {
6568 return Some(found);
6569 }
6570 }
6571 None
6572 }
6573 let Some((parent, parent_id)) = parent_of(root, WidgetId::ROOT, id) else {
6574 return Vec::new();
6575 };
6576 parent
6577 .children()
6578 .iter()
6579 .enumerate()
6580 .filter(|(_, child)| child.reorder_index().is_some() && child.reorder_droppable())
6581 .filter_map(|(index, child)| {
6582 let id = child_id(parent_id, index, child.as_ref());
6583 ui.reorderables
6584 .iter()
6585 .find(|(registered, _)| *registered == id)
6586 .map(|(_, rect)| (id, *rect))
6587 })
6588 .collect()
6589}
6590
6591/// Identity of the **first** widget declaring the key `key` (a hash), or `None`. It is how the
6592/// shell resolves a focus-by-key request (`Command::focus`): the application wraps a field in
6593/// `keyed(k, …)`, then focuses by `k`.
6594pub fn find_by_key<Msg>(root: &dyn Widget<Msg>, key: u64) -> Option<WidgetId> {
6595 fn walk<Msg>(widget: &dyn Widget<Msg>, id: WidgetId, key: u64) -> Option<WidgetId> {
6596 if widget.key() == Some(key) {
6597 return Some(id);
6598 }
6599 for (index, child) in widget.children().iter().enumerate() {
6600 if let Some(found) = walk(child.as_ref(), child_id(id, index, child.as_ref()), key) {
6601 return Some(found);
6602 }
6603 }
6604 None
6605 }
6606 walk(root, WidgetId::ROOT, key)
6607}
6608
6609/// Identity of the **sheet** the key `key` names: the first sheet's panel at or under the
6610/// widget declaring it, which is the identity the sheet's state is kept under and the one the
6611/// frame's sheets are reported by. `None` when nothing declares the key, or nothing under it
6612/// is a sheet.
6613///
6614/// A sheet's state belongs to its panel, which the application never builds itself, so a
6615/// request by key (`Command::sheet`) needs this one step more than a focus request does.
6616pub fn find_sheet_by_key<Msg>(root: &dyn Widget<Msg>, key: u64) -> Option<WidgetId> {
6617 fn first_sheet<Msg>(widget: &dyn Widget<Msg>, id: WidgetId) -> Option<WidgetId> {
6618 if widget.sheet().is_some() {
6619 return Some(id);
6620 }
6621 widget
6622 .children()
6623 .iter()
6624 .enumerate()
6625 .find_map(|(index, child)| {
6626 first_sheet(child.as_ref(), child_id(id, index, child.as_ref()))
6627 })
6628 }
6629 fn walk<Msg>(widget: &dyn Widget<Msg>, id: WidgetId, key: u64) -> Option<WidgetId> {
6630 if widget.key() == Some(key) {
6631 return first_sheet(widget, id);
6632 }
6633 widget
6634 .children()
6635 .iter()
6636 .enumerate()
6637 .find_map(|(index, child)| {
6638 walk(child.as_ref(), child_id(id, index, child.as_ref()), key)
6639 })
6640 }
6641 walk(root, WidgetId::ROOT, key)
6642}
6643
6644/// The messages a **move of the focus** from `before` to `after` sends (milestone 660): each
6645/// widget whose subtree the focus left hears `false`, deepest first, then each whose subtree
6646/// it entered hears `true`, deepest first. The widgets around both — the paths' shared start
6647/// — hear nothing; a widget no longer in the tree is not heard from.
6648pub fn focus_changes<Msg>(
6649 root: &dyn Widget<Msg>,
6650 before: Option<WidgetId>,
6651 after: Option<WidgetId>,
6652) -> Vec<Msg> {
6653 let path = |id: Option<WidgetId>| id.map_or_else(Vec::new, |id| find_path(root, id));
6654 let (left, entered) = (path(before), path(after));
6655 let shared = left
6656 .iter()
6657 .zip(&entered)
6658 .take_while(|(a, b)| std::ptr::addr_eq(**a, **b))
6659 .count();
6660 let lost = left[shared..]
6661 .iter()
6662 .rev()
6663 .filter_map(|widget| widget.on_focus_change(false));
6664 let gained = entered[shared..]
6665 .iter()
6666 .rev()
6667 .filter_map(|widget| widget.on_focus_change(true));
6668 lost.chain(gained).collect()
6669}
6670
6671/// Path from the **root down to the widget** with identity `target` (`[root, …, target]`), for
6672/// the leaf→root bubbling of keys. Empty when it cannot be found.
6673pub fn find_path<Msg>(root: &dyn Widget<Msg>, target: WidgetId) -> Vec<&dyn Widget<Msg>> {
6674 fn walk<'a, Msg>(
6675 widget: &'a dyn Widget<Msg>,
6676 id: WidgetId,
6677 target: WidgetId,
6678 path: &mut Vec<&'a dyn Widget<Msg>>,
6679 ) -> bool {
6680 path.push(widget);
6681 if id == target {
6682 return true;
6683 }
6684 for (index, child) in widget.children().iter().enumerate() {
6685 if walk(
6686 child.as_ref(),
6687 child_id(id, index, child.as_ref()),
6688 target,
6689 path,
6690 ) {
6691 return true;
6692 }
6693 }
6694 path.pop();
6695 false
6696 }
6697 let mut path = Vec::new();
6698 if !walk(root, WidgetId::ROOT, target, &mut path) {
6699 path.clear();
6700 }
6701 path
6702}
6703
6704/// The theme a subtree inherits at this node — the widget's own override, with any
6705/// **animated text style** the runtime is holding for it laid on top.
6706///
6707/// The one place the theme swap happens, and it is one place on purpose. The swap is made
6708/// four times over a frame — by the layout pass, by the relayout fingerprint that has to
6709/// agree with it or the cache lies, by the paint walk, and by [`build_deferred`] before
6710/// any of them — and every animated value in this framework is read from the runtime by
6711/// whoever consumes it. This one's consumer is the swap itself, so a copy of the rule at
6712/// each of the four sites is four chances to drift. `Responsive` cost three silent bugs
6713/// that way (milestone 495); this is the same lesson written down as a function.
6714///
6715/// Cheap where nothing is animated: no `anim_text_style`, no clone, and the widget's own
6716/// `theme_override` is returned untouched — which is every node but the rare one.
6717pub(crate) fn scoped_theme<Msg>(
6718 widget: &dyn Widget<Msg>,
6719 id: WidgetId,
6720 runtime: &Runtime,
6721 inherited: &Theme,
6722) -> Option<Box<Theme>> {
6723 let own = widget.theme_override(inherited);
6724 let Some(target) = widget.anim_text_style() else {
6725 return own;
6726 };
6727 // Mid-flight if the runtime has a value for this node, and the target otherwise —
6728 // which is the first frame, before the family has been advanced over this tree.
6729 let style = runtime.anim_text_style(id).unwrap_or(target);
6730 let mut theme = own.map_or_else(|| inherited.clone(), |boxed| *boxed);
6731 // **Merged over**, not assigned: the node's own `theme_override` has already put the
6732 // target style and the four unanimated answers (alignment, wrapping, overflow, line
6733 // count) in place, and this replaces field by field exactly what is moving.
6734 theme.widgets.text.style = theme.widgets.text.style.merge(style);
6735 Some(Box::new(theme))
6736}
6737
6738/// Builds what `widget` stands for ([`Widget::expand`]) **under the scopes the node says it
6739/// introduces before it is built** — the surface, the theme, the shell and the scrolling a
6740/// [`MediaScope`](crate::MediaScope) or a [`Themed`](crate::Themed) around a component
6741/// imposes (milestone 642).
6742///
6743/// A wrapper and the component inside it are one node, and the walks ask a node for its
6744/// scopes after building it, since what a component builds may introduce one. That left
6745/// the wrapper's own scope out of the build: `MediaScope::tweak(.., page)` built `page` for
6746/// the whole window, and a scaffold in it read a size it was not laid out in. An unbuilt
6747/// component answers for no scope, so what is asked here is exactly the wrapper's; the walk
6748/// asks again once the component is built, and gets the whole chain.
6749///
6750/// Out of line, so that the walks' recursive frames do not carry what is held here.
6751#[inline(never)]
6752fn expand_scoped<Msg>(widget: &dyn Widget<Msg>, id: WidgetId, runtime: &Runtime, theme: &Theme) {
6753 let _surface = widget
6754 .media_override(crate::MediaQuery::of())
6755 .map(crate::MediaQuery::install);
6756 let _shell = widget.scaffold_override().map(crate::ScaffoldInfo::install);
6757 let _scrolling = widget
6758 .scroll_behavior_override()
6759 .map(crate::ScrollConfiguration::install);
6760 let scoped = scoped_theme(widget, id, runtime, theme);
6761 widget.expand(id, runtime, scoped.as_deref().unwrap_or(theme));
6762}
6763
6764/// Runs the **deferred builds** over a tree, the way the layout pass does on its way down.
6765///
6766/// A [`ThemeBuilder`](crate::ThemeBuilder) — and everything built on one, an
6767/// [`AppBar`](crate::AppBar) included — has no children at all until something calls
6768/// [`Widget::build_themed`] on it. The layout pass does that first thing, so within a frame
6769/// nothing ever meets an unbuilt subtree.
6770///
6771/// **A tree built outside a frame does.** The shell rebuilds from `view` alone on a burst of
6772/// keystrokes, faster than a frame, so the next key sees the current value rather than the
6773/// retained tree's — and then reads that tree straight away. Every traversal that arrives
6774/// there ([`find_widget`], focus resolution, a caret being revealed) walks a tree whose
6775/// deferred subtrees are empty, and silently finds nothing.
6776///
6777/// This is that half of the walk and nothing else: the theme swap
6778/// ([`Widget::theme_override`]) then the build, top-down. The swap is not optional — a
6779/// builder inside a [`Themed`](crate::Themed) has to see its own subtree's theme, and a
6780/// preparation that is *nearly* the walk is worse than none, because what it builds is wrong
6781/// rather than absent.
6782///
6783/// **Which is why it takes a runtime.** A subtree under an animated text style
6784/// (`AnimatedDefaultTextStyle`) inherits a theme that is moving, and a builder inside one
6785/// composes against whatever theme reaches it *first* — this walk. Preparing it against
6786/// the target instead of the value in flight is the same failure as skipping the swap: what
6787/// it builds is wrong rather than absent, and only for as long as the movement lasts, which
6788/// is the hardest kind to see. The runtime is one frame behind here, because this runs
6789/// before the frame advances its animations, and one frame behind is the whole tree agreeing
6790/// with itself rather than a builder disagreeing with the layout around it.
6791pub fn build_deferred<Msg>(root: &dyn Widget<Msg>, theme: &Theme, runtime: &Runtime) {
6792 fn walk<Msg>(widget: &dyn Widget<Msg>, id: WidgetId, runtime: &Runtime, theme: &Theme) {
6793 expand_scoped(widget, id, runtime, theme);
6794 let scoped = scoped_theme(widget, id, runtime, theme);
6795 let theme = scoped.as_deref().unwrap_or(theme);
6796 let _surface = widget
6797 .media_override(crate::MediaQuery::of())
6798 .map(crate::MediaQuery::install);
6799 let _shell = widget.scaffold_override().map(crate::ScaffoldInfo::install);
6800 let _scrolling = widget
6801 .scroll_behavior_override()
6802 .map(crate::ScrollConfiguration::install);
6803 widget.build_in(id, runtime, theme);
6804 for (index, child) in widget.children().iter().enumerate() {
6805 walk(
6806 child.as_ref(),
6807 child_id(id, index, child.as_ref()),
6808 runtime,
6809 theme,
6810 );
6811 }
6812 }
6813 walk(root, WidgetId::ROOT, runtime, theme);
6814}
6815
6816/// The widget of the tree under `root` that has the identity `target`, or `None` if none
6817/// does. Identities are the ones the walk that built the frame gave, so an id read from a
6818/// [`Ui`] finds its widget here.
6819pub fn find_widget<Msg>(root: &dyn Widget<Msg>, target: WidgetId) -> Option<&dyn Widget<Msg>> {
6820 fn walk<Msg>(
6821 widget: &dyn Widget<Msg>,
6822 id: WidgetId,
6823 target: WidgetId,
6824 ) -> Option<&dyn Widget<Msg>> {
6825 if id == target {
6826 return Some(widget);
6827 }
6828 for (index, child) in widget.children().iter().enumerate() {
6829 if let Some(found) = walk(child.as_ref(), child_id(id, index, child.as_ref()), target) {
6830 return Some(found);
6831 }
6832 }
6833 None
6834 }
6835 walk(root, WidgetId::ROOT, target)
6836}
6837
6838/// Folds a filter into the single filtered layer its subtree turned out to be, so two
6839/// filter widgets wrapped one inside the other cost one layer rather than two.
6840///
6841/// It is not only a saving. Compositing renders a layer into a texture and composites
6842/// that texture; a layer *inside* that texture is not composited again, so a nested
6843/// filter would simply not be applied. Folding is what makes `ColorFiltered` around
6844/// `ImageFiltered` mean what it reads as.
6845///
6846/// Everything about the inner layer must be neutral — full opacity, a plain
6847/// rectangular clip, no transform — because those are applied at compositing time and
6848/// there is only one composite to apply them at. And the two filters must not want
6849/// the same slot: greyscale of an inverted picture is not the inversion of a
6850/// greyscale one, so there is no single layer that means both, and the two nest.
6851fn fold_filter(
6852 group: Vec<Primitive>,
6853 filter: LayerFilter,
6854 clip: Rect,
6855) -> (Vec<Primitive>, LayerFilter, Rect) {
6856 if group.len() != 1 {
6857 return (group, filter, clip);
6858 }
6859 let Primitive::Layer {
6860 opacity,
6861 clip: inner_clip,
6862 clip_shape,
6863 transform,
6864 filter: inner,
6865 ..
6866 } = &group[0]
6867 else {
6868 return (group, filter, clip);
6869 };
6870 if *opacity < 0.999 || !matches!(clip_shape, ClipShape::Rect) || transform.is_some() {
6871 return (group, filter, clip);
6872 }
6873 let (inner, inner_clip) = (*inner, *inner_clip);
6874 let Some(merged) = filter.merge(inner) else {
6875 return (group, filter, clip);
6876 };
6877 let Some(Primitive::Layer { primitives, .. }) = group.into_iter().next() else {
6878 unreachable!("checked just above")
6879 };
6880 (primitives, merged, clip.intersect(inner_clip))
6881}
6882
6883#[cfg(test)]
6884mod tests {
6885 use super::*;
6886 use crate::physics::Scrollbars;
6887 use crate::runtime::Edit;
6888 use crate::{
6889 Button, Container, Flex, Key, Keyed, OverlayPortal, Placement, SingleChildScrollView,
6890 TextField,
6891 };
6892 use frus_core::{Color, Point, Primitive, Rect, Size};
6893
6894 /// **A deep tree on the smallest stack any platform gives a frame.**
6895 ///
6896 /// Building a tree recurses twice over it — the layout walk and the scene walk — and
6897 /// what each level costs is a property of those two functions, not of the widgets.
6898 /// Two `Theme` copies in each frame made that cost 53 kilobytes a level, so a tree
6899 /// forty deep exhausted the megabyte a Windows main thread gets and the process died
6900 /// with no panic and no message. A real screen nests further than forty.
6901 ///
6902 /// One megabyte and 64 levels is what this holds to: the platform's smallest stack,
6903 /// against a tree deeper than a screen builds and half again the depth that used to
6904 /// die. It is deliberately not the deepest that fits — around 128 now, at about seven
6905 /// kilobytes a level — because a tripwire wants margin over the number it guards. An
6906 /// overflow aborts rather than fails, so this one kills the run outright, which is the
6907 /// right noise for what it watches.
6908 #[test]
6909 fn a_deep_tree_builds_on_a_platform_s_smallest_stack() {
6910 let built = std::thread::Builder::new()
6911 .stack_size(1024 * 1024)
6912 .spawn(|| {
6913 let mut node: Box<dyn Widget<Msg>> = Box::new(crate::Text::new("the bottom"));
6914 for _ in 0..64 {
6915 node = Box::new(Container::new().padding(1.0).child(node));
6916 }
6917 let ui = build_ui(
6918 node.as_ref(),
6919 Size::new(800.0, 600.0),
6920 &Runtime::default(),
6921 &Theme::default(),
6922 );
6923 ui.scene().primitives().len()
6924 })
6925 .expect("spawning the deep-tree thread")
6926 .join()
6927 .expect("the deep tree was built");
6928 assert!(built > 0, "and it painted something");
6929 }
6930
6931 #[derive(Clone, Debug, PartialEq)]
6932 enum Msg {
6933 A,
6934 B,
6935 C,
6936 D,
6937 Edited(String),
6938 }
6939
6940 #[test]
6941 fn semantics_tree_carries_roles_and_labels() {
6942 use crate::{Button, Checkbox, Role, Text};
6943 let rt = Runtime::default();
6944 let tree = crate::Flex::column()
6945 .child(Text::new("Titre"))
6946 .child(Button::new("Submit").on_press(Msg::A))
6947 .child(Checkbox::new(true).on_toggle(|_| Msg::B));
6948 let ui = build_ui(&tree, Size::new(300.0, 200.0), &rt, &Theme::default());
6949 let sem = ui.semantics();
6950 // One node per meaningful widget (the containing Flex is skipped).
6951 let roles: Vec<Role> = sem.iter().map(|(_, _, s)| s.role).collect();
6952 assert!(roles.contains(&Role::Label));
6953 assert!(roles.contains(&Role::Button));
6954 assert!(roles.contains(&Role::CheckBox));
6955 // The button carries its label and is actionable.
6956 let button = sem.iter().find(|(_, _, s)| s.role == Role::Button).unwrap();
6957 assert_eq!(button.2.label.as_deref(), Some("Submit"));
6958 assert!(button.2.clickable);
6959 // The checked box mirrors its state.
6960 let check = sem
6961 .iter()
6962 .find(|(_, _, s)| s.role == Role::CheckBox)
6963 .unwrap();
6964 assert_eq!(check.2.toggled, crate::Toggled::True);
6965 }
6966
6967 fn clickable_sample() -> Flex<Msg> {
6968 Flex::row()
6969 .width(400.0)
6970 .height(100.0)
6971 .padding(10.0)
6972 .gap(8.0)
6973 .child(
6974 Container::new()
6975 .width(120.0)
6976 .color(Color::rgb(1.0, 0.0, 0.0))
6977 .hover_color(Color::rgb(0.0, 1.0, 0.0))
6978 .on_click(Msg::A),
6979 )
6980 .child(
6981 Container::new()
6982 .flex(1.0)
6983 .color(Color::rgb(0.0, 0.0, 1.0))
6984 .on_click(Msg::B),
6985 )
6986 }
6987
6988 /// A box whose children ran past it is **marked on the screen**, over a tenth of it,
6989 /// on the edge they ran past — and a box whose children fit is left alone.
6990 #[test]
6991 fn an_overflow_paints_a_band_on_the_edge_it_ran_past() {
6992 use crate::{Container, Flex};
6993 // Two children, because a row with one is not dividing a line up — it hands its
6994 // constraints down, and a lone child is bounded by them rather than overflowing.
6995 let row = |child_width: f32| {
6996 let root: Flex<()> = Flex::row()
6997 .width(100.0)
6998 .height(40.0)
6999 .child(
7000 Container::new()
7001 .width(child_width)
7002 .height(20.0)
7003 .color(Color::WHITE),
7004 )
7005 .child(Container::new().width(20.0).height(20.0));
7006 let rt = crate::runtime::Runtime::default();
7007 build_ui(
7008 &root,
7009 Size::new(200.0, 100.0),
7010 &rt,
7011 &crate::theme::Theme::dark(),
7012 )
7013 };
7014 // The stripes are paths; nothing else in this tree draws one.
7015 let stripes = |ui: &Ui<()>| {
7016 ui.scene()
7017 .primitives()
7018 .iter()
7019 .filter(|p| matches!(p, Primitive::Path { .. }))
7020 .count()
7021 };
7022 let over = row(300.0);
7023 assert!(!over.overflows().is_empty(), "the fixture has to overflow");
7024 assert!(stripes(&over) > 2, "striped: {}", stripes(&over));
7025 // The band is on the right-hand tenth: 90..100 of a 100-wide row.
7026 let band = over
7027 .scene()
7028 .primitives()
7029 .iter()
7030 .rev()
7031 .find_map(|p| match p {
7032 Primitive::Rect { rect, color, .. } if color.a > 0.7 && color.r == 0.0 => {
7033 Some(*rect)
7034 }
7035 _ => None,
7036 })
7037 .expect("the band under the stripes");
7038 assert!((band.x - 90.0).abs() < 0.5, "at the right edge: {band:?}");
7039 assert!((band.width - 10.0).abs() < 0.5, "a tenth of it: {band:?}");
7040
7041 let fits = row(40.0);
7042 assert!(fits.overflows().is_empty(), "the fixture has to fit");
7043 assert!(fits.overflows().is_empty());
7044 assert_eq!(stripes(&fits), 0, "nothing to mark");
7045 }
7046
7047 /// The band says **which edge and by how much**, in the reference's words. A striped
7048 /// edge tells you something is too big; the sentence tells you what to look for, and
7049 /// it is the only half of the report that survives being photographed off a device.
7050 #[test]
7051 fn a_band_writes_which_edge_overflowed_and_by_how_much() {
7052 use crate::{Container, Flex};
7053 let root: Flex<()> = Flex::row()
7054 .width(100.0)
7055 .height(40.0)
7056 .child(
7057 Container::new()
7058 .width(300.0)
7059 .height(20.0)
7060 .color(Color::WHITE),
7061 )
7062 .child(Container::new().width(20.0).height(20.0));
7063 let ui = build_ui(
7064 &root,
7065 Size::new(200.0, 100.0),
7066 &Runtime::default(),
7067 &Theme::dark(),
7068 );
7069 // The label is inside the rotated layer, which is why the search is recursive.
7070 fn labels(primitives: &[Primitive], out: &mut Vec<String>) {
7071 for p in primitives {
7072 match p {
7073 Primitive::Text { text, .. } => out.push(text.clone()),
7074 Primitive::Layer { primitives, .. } => labels(primitives, out),
7075 _ => {}
7076 }
7077 }
7078 }
7079 let mut found = Vec::new();
7080 labels(ui.scene().primitives(), &mut found);
7081 assert!(
7082 found.iter().any(|t| t == "RIGHT OVERFLOWED BY 220 PIXELS"),
7083 "the sentence, in the reference's words: {found:?}"
7084 );
7085 }
7086
7087 /// The reference's precision rule: whole pixels when there are many, a decimal when
7088 /// there are few, three significant figures when there is almost nothing — which is
7089 /// the difference between a layout bug and a rounding error.
7090 #[test]
7091 fn an_overflow_is_written_to_the_precision_it_deserves() {
7092 assert_eq!(format_pixels(86.0), "86");
7093 // Past ten pixels the decimal is noise; at ten exactly it is not yet.
7094 assert_eq!(format_pixels(10.4), "10");
7095 assert_eq!(format_pixels(10.0), "10.0");
7096 assert_eq!(format_pixels(4.5), "4.5");
7097 assert_eq!(format_pixels(1.0), "1.00");
7098 assert_eq!(format_pixels(0.5), "0.500");
7099 assert_eq!(format_pixels(0.0123), "0.0123");
7100 }
7101
7102 #[test]
7103 fn rtl_mirrors_row_horizontally() {
7104 let size = Size::new(400.0, 100.0);
7105 // A = a fixed 120 px button (on the left in LTR), B = the flexible remainder.
7106 let ltr = build_ui(
7107 &clickable_sample(),
7108 size,
7109 &Runtime::default(),
7110 &Theme::default(),
7111 );
7112 let rtl = build_ui(
7113 &clickable_sample(),
7114 size,
7115 &Runtime::default(),
7116 &Theme::default().rtl(),
7117 );
7118 let hit = |ui: &Ui<Msg>, x: f32| ui.hit(Point::new(x, 50.0)).and_then(|id| ui.msg_for(id));
7119
7120 // LTR: button A takes the left edge.
7121 assert_eq!(hit(<r, 40.0), Some(Msg::A));
7122 assert_eq!(hit(<r, 360.0), Some(Msg::B));
7123 // RTL: everything is mirrored — A moves to the right, B takes the left.
7124 assert_eq!(hit(&rtl, 360.0), Some(Msg::A), "A on the right in RTL");
7125 assert_eq!(hit(&rtl, 40.0), Some(Msg::B), "B on the left in RTL");
7126 }
7127
7128 #[test]
7129 fn wrapped_text_wraps_in_layout_and_invalidates_the_cache() {
7130 let tree = |text: &str| {
7131 crate::Flex::column()
7132 .width(120.0)
7133 .child(crate::Text::new(text).wrap())
7134 .child(Container::new().height(10.0).on_click(Msg::A))
7135 };
7136 // Position of the clickable follower: the first y hit while sweeping.
7137 let follower_y = |ui: &Ui<Msg>| {
7138 (0..600)
7139 .map(|y| y as f32)
7140 .find(|&y| ui.hit(Point::new(60.0, y)).is_some())
7141 .expect("suiveur cliquable")
7142 };
7143
7144 let rt = Runtime::default();
7145 let long = "a rather long paragraph that will wrap onto several lines";
7146 let ui = build_ui(&tree(long), Size::new(120.0, 600.0), &rt, &Theme::default());
7147
7148 // The paragraph's render carries its wrap width (≤ the column).
7149 let max_w = ui
7150 .scene()
7151 .primitives()
7152 .iter()
7153 .find_map(|p| match p {
7154 Primitive::Text { max_width, .. } => *max_width,
7155 _ => None,
7156 })
7157 .expect("a wrapped paragraph");
7158 assert!(max_w <= 120.5, "wrapped to the column width: {max_w}");
7159
7160 // The wrapped text takes several lines: the follower is pushed down.
7161 let y_long = follower_y(&ui);
7162 assert!(
7163 y_long > 30.0,
7164 "the follower is pushed down by the wrap: {y_long}"
7165 );
7166
7167 // The SAME structure/styles, different content, the SAME runtime (a warm cache): the
7168 // measure key must invalidate the cache — otherwise the rects would be stale.
7169 let ui2 = build_ui(
7170 &tree("short"),
7171 Size::new(120.0, 600.0),
7172 &rt,
7173 &Theme::default(),
7174 );
7175 let y_short = follower_y(&ui2);
7176 assert!(
7177 y_short < y_long,
7178 "shorter content → a higher follower (the cache was invalidated): {y_short} vs {y_long}"
7179 );
7180 }
7181
7182 #[test]
7183 fn relayout_cache_reuses_the_root_layout_across_frames() {
7184 let rt = Runtime::default();
7185 let size = Size::new(400.0, 100.0);
7186 // Frame 1: nothing cached → a recomputation (at least the root).
7187 let _ = build_ui(&clickable_sample(), size, &rt, &Theme::default());
7188 let (hits1, misses1) = rt.layout_cache.borrow().last_frame_stats();
7189 assert_eq!(hits1, 0, "1st frame: nothing reused");
7190 assert!(misses1 >= 1, "1st frame: at least one computation");
7191
7192 // Frame 2: the same tree, the same constraints → the root is reused.
7193 let _ = build_ui(&clickable_sample(), size, &rt, &Theme::default());
7194 let (hits2, misses2) = rt.layout_cache.borrow().last_frame_stats();
7195 assert_eq!(hits2, 1, "2nd frame: the root is reused");
7196 assert_eq!(misses2, 0, "2nd frame: nothing recomputed");
7197
7198 // Frame 3: the window is resized → the constraints change → a recomputation.
7199 let _ = build_ui(
7200 &clickable_sample(),
7201 Size::new(500.0, 100.0),
7202 &rt,
7203 &Theme::default(),
7204 );
7205 let (hits3, misses3) = rt.layout_cache.borrow().last_frame_stats();
7206 assert_eq!((hits3, misses3), (0, 1), "redimensionnement → recalcul");
7207 }
7208
7209 #[test]
7210 fn modal_traps_tab_arrows_and_pointer_focus() {
7211 use crate::portal::{OverlayPortal, Placement};
7212 // The background: two focusable buttons; the open modal: two buttons as well.
7213 let dialog = Flex::<Msg>::row()
7214 .child(Button::new("ok").on_press(Msg::C))
7215 .child(Button::new("no").on_press(Msg::D));
7216 let tree: Flex<Msg> = Flex::column()
7217 .child(Button::new("bg1").on_press(Msg::A))
7218 .child(Button::new("bg2").on_press(Msg::B))
7219 .child(
7220 OverlayPortal::new(Container::new())
7221 .overlay(dialog, Placement::Center)
7222 .dismiss(Msg::A),
7223 );
7224 let ui = build_ui(
7225 &tree,
7226 Size::new(400.0, 300.0),
7227 &Runtime::default(),
7228 &Theme::default(),
7229 );
7230
7231 // Tab enters the trap (the modal's first focusable) and cycles inside it.
7232 let first = ui.focus_next(None, true).expect("the scope's first");
7233 assert_eq!(ui.msg_for(first), Some(Msg::C));
7234 let second = ui.focus_next(Some(first), true).expect("suivant");
7235 assert_eq!(ui.msg_for(second), Some(Msg::D));
7236 let wrapped = ui.focus_next(Some(second), true).expect("boucle");
7237 assert_eq!(
7238 ui.msg_for(wrapped),
7239 Some(Msg::C),
7240 "Tab cycles inside the modal"
7241 );
7242
7243 // The arrows stay inside the scope: there is nothing above the dialog.
7244 assert_eq!(ui.focus_directional(first, FocusDirection::Up), None);
7245 let right = ui
7246 .focus_directional(first, FocusDirection::Right)
7247 .expect("to the right");
7248 assert_eq!(ui.msg_for(right), Some(Msg::D));
7249
7250 // Click-to-focus ignores the background (the bg1 button is at the top left).
7251 assert_eq!(
7252 ui.focus_hit(Point::new(10.0, 10.0)),
7253 None,
7254 "fond hors scope"
7255 );
7256
7257 // Without a modal: no trap, Tab starts at the background.
7258 let open_less: Flex<Msg> = Flex::column()
7259 .child(Button::new("bg1").on_press(Msg::A))
7260 .child(Button::new("bg2").on_press(Msg::B));
7261 let ui2 = build_ui(
7262 &open_less,
7263 Size::new(400.0, 300.0),
7264 &Runtime::default(),
7265 &Theme::default(),
7266 );
7267 let f = ui2.focus_next(None, true).expect("premier");
7268 assert_eq!(ui2.msg_for(f), Some(Msg::A));
7269 }
7270
7271 #[test]
7272 fn open_menu_traps_focus_in_its_items() {
7273 use crate::PopupMenuButton;
7274 // A focusable background plus an **open** menu (anchor + two items).
7275 let menu = PopupMenuButton::new(Button::new("open").on_press(Msg::A), true, Msg::D)
7276 .item("one", Msg::B)
7277 .item("two", Msg::C);
7278 let tree: Flex<Msg> = Flex::column()
7279 .child(Button::new("bg").on_press(Msg::A))
7280 .child(menu);
7281 let ui = build_ui(
7282 &tree,
7283 Size::new(400.0, 300.0),
7284 &Runtime::default(),
7285 &Theme::default(),
7286 );
7287
7288 // Tab enters the menu's items and **cycles** inside (background and anchor are out of scope).
7289 let first = ui.focus_next(None, true).expect("the scope's first");
7290 assert_eq!(ui.msg_for(first), Some(Msg::B));
7291 let second = ui.focus_next(Some(first), true).expect("suivant");
7292 assert_eq!(ui.msg_for(second), Some(Msg::C));
7293 let wrapped = ui.focus_next(Some(second), true).expect("boucle");
7294 assert_eq!(
7295 ui.msg_for(wrapped),
7296 Some(Msg::B),
7297 "Tab cycles inside the open menu"
7298 );
7299 // The background (top left) is out of scope while the menu is open.
7300 assert_eq!(
7301 ui.focus_hit(Point::new(10.0, 10.0)),
7302 None,
7303 "fond hors scope"
7304 );
7305
7306 // The menu **closed**: no trap, Tab starts at the background.
7307 let closed: Flex<Msg> = Flex::column()
7308 .child(Button::new("bg").on_press(Msg::A))
7309 .child(PopupMenuButton::new(
7310 Button::new("open").on_press(Msg::B),
7311 false,
7312 Msg::D,
7313 ));
7314 let ui2 = build_ui(
7315 &closed,
7316 Size::new(400.0, 300.0),
7317 &Runtime::default(),
7318 &Theme::default(),
7319 );
7320 let f = ui2.focus_next(None, true).expect("premier");
7321 assert_eq!(ui2.msg_for(f), Some(Msg::A), "with no open menu, no trap");
7322 }
7323
7324 #[test]
7325 fn escape_infrastructure_finds_path_and_topmost_dismiss() {
7326 use crate::portal::{OverlayPortal, Placement};
7327 // An open portal (a modal with a dismissal) around a clickable anchor.
7328 let anchor = Container::<Msg>::new()
7329 .width(50.0)
7330 .height(30.0)
7331 .on_click(Msg::A);
7332 let content = Container::<Msg>::new()
7333 .width(80.0)
7334 .height(40.0)
7335 .on_click(Msg::B);
7336 let portal = OverlayPortal::new(anchor)
7337 .overlay(content, Placement::Center)
7338 .dismiss(Msg::C);
7339 let tree: Flex<Msg> = Flex::column().child(portal);
7340
7341 let ui = build_ui(
7342 &tree,
7343 Size::new(300.0, 200.0),
7344 &Runtime::default(),
7345 &Theme::default(),
7346 );
7347 // The topmost dismissal is the portal's.
7348 assert_eq!(ui.top_dismiss(), Some(Msg::C));
7349
7350 // Focus "inside the dialog": the root→content path goes through the portal, which
7351 // consumes Escape while bubbling. (The Center content, 80×40, is centred in 300×200 →
7352 // its centre is at (150, 100).)
7353 let inner = ui
7354 .hit(Point::new(150.0, 100.0))
7355 .expect("contenu de la modale");
7356 let path = find_path(&tree, inner);
7357 assert!(path.len() >= 3, "root, portal, content: {}", path.len());
7358 assert_eq!(
7359 path.last().unwrap().on_click(),
7360 Some(Msg::B),
7361 "the target closes the path"
7362 );
7363 let consumed = path
7364 .iter()
7365 .rev()
7366 .find_map(|w| match w.on_key(&crate::Key::Escape) {
7367 crate::KeyResponse::Handled(msg) => Some(msg),
7368 _ => None,
7369 });
7370 assert_eq!(
7371 consumed,
7372 Some(Some(Msg::C)),
7373 "the portal consumes Escape while bubbling"
7374 );
7375
7376 // Chemin introuvable → vide ; pas d'overlay → pas de fermeture.
7377 assert!(find_path(&tree, WidgetId::ROOT.child(99)).is_empty());
7378 let closed: Flex<Msg> = Flex::column().child(Container::new().on_click(Msg::A));
7379 let ui2 = build_ui(
7380 &closed,
7381 Size::new(300.0, 200.0),
7382 &Runtime::default(),
7383 &Theme::default(),
7384 );
7385 assert_eq!(ui2.top_dismiss(), None);
7386 }
7387
7388 #[test]
7389 fn long_press_targets_are_collected_topmost_first() {
7390 // A long-press container holding a long-press child: a point inside the child returns
7391 // the child's message (the topmost one).
7392 let tree: Container<Msg> = Container::new()
7393 .width(200.0)
7394 .height(100.0)
7395 .on_long_press(Msg::A)
7396 .child(
7397 Container::new()
7398 .width(50.0)
7399 .height(50.0)
7400 .on_long_press(Msg::B),
7401 );
7402 let ui = build_ui(
7403 &tree,
7404 Size::new(200.0, 100.0),
7405 &Runtime::default(),
7406 &Theme::default(),
7407 );
7408 assert_eq!(ui.long_press_at(Point::new(25.0, 25.0)), Some(Msg::B));
7409 assert_eq!(ui.long_press_at(Point::new(150.0, 80.0)), Some(Msg::A));
7410 assert_eq!(ui.long_press_at(Point::new(500.0, 500.0)), None);
7411 }
7412
7413 #[test]
7414 fn hit_and_msg_for_route_correctly() {
7415 let rt = Runtime::default();
7416 let ui = build_ui(
7417 &clickable_sample(),
7418 Size::new(400.0, 100.0),
7419 &rt,
7420 &Theme::default(),
7421 );
7422 let id_a = ui.hit(Point::new(50.0, 50.0)).expect("A");
7423 let id_b = ui.hit(Point::new(300.0, 50.0)).expect("B");
7424 assert_ne!(id_a, id_b);
7425 assert_eq!(ui.msg_for(id_a), Some(Msg::A));
7426 assert_eq!(ui.msg_for(id_b), Some(Msg::B));
7427 assert_eq!(ui.hit(Point::new(3.0, 3.0)), None);
7428 }
7429
7430 /// Every rectangle a frame paints down the right-hand edge of a 200-wide viewport.
7431 /// The bar is drawn last, over the content, and it is the only thing over there.
7432 fn painted(ui: &Ui<()>) -> Vec<(Rect, Color)> {
7433 ui.scene()
7434 .primitives()
7435 .iter()
7436 .filter_map(|p| match p {
7437 Primitive::Rect { rect, color, .. }
7438 if rect.x >= 200.0 - BAR_SIZE - BAR_MARGIN * 2.0 =>
7439 {
7440 Some((*rect, *color))
7441 }
7442 _ => None,
7443 })
7444 .collect()
7445 }
7446
7447 /// A runtime, and a scroll behaviour in force for as long as the guard lives, that
7448 /// draws scrollbars — one that follows a desktop — or does not.
7449 fn bars_runtime(how: Scrollbars) -> (Runtime, crate::ScrollBehaviorGuard) {
7450 let behavior = match how {
7451 Scrollbars::Always => {
7452 crate::ScrollBehavior::new().with_platform(frus_core::TargetPlatform::Linux)
7453 }
7454 Scrollbars::Never => crate::ScrollBehavior::new().with_scrollbars(false),
7455 };
7456 (
7457 Runtime::default(),
7458 crate::ScrollConfiguration::install(behavior),
7459 )
7460 }
7461
7462 /// A tall page in a short window, its bar **pinned**: whether a bar fades is the
7463 /// business of the tests below, and these are about whether there is one to fade.
7464 fn bars(area: SingleChildScrollView<()>, how: Scrollbars) -> Vec<(Rect, Color)> {
7465 let (runtime, _bars) = bars_runtime(how);
7466 let ui = build_ui(
7467 &area.thumb_visibility(true),
7468 Size::new(200.0, 100.0),
7469 &runtime,
7470 &Theme::dark(),
7471 );
7472 painted(&ui)
7473 }
7474
7475 /// **Under an Android theme, the platform's own bar** (`scrollbar.dart:331`, milestone
7476 /// 617): 4 px instead of 8, flush with the edge instead of 2 px in, square instead of
7477 /// a pill, an opaque grey at rest instead of a fade of the content colour, and nothing
7478 /// a pointer can take hold of — whatever this test runs on.
7479 #[test]
7480 fn an_android_theme_draws_the_android_bar() {
7481 let (runtime, _bars) = bars_runtime(Scrollbars::Always);
7482 let laid = |platform: frus_core::TargetPlatform| {
7483 let ui = build_ui(
7484 &tall().thumb_visibility(true),
7485 Size::new(200.0, 100.0),
7486 &runtime,
7487 &Theme::dark().with_platform(platform),
7488 );
7489 let thumb = ui
7490 .scene()
7491 .primitives()
7492 .iter()
7493 .find_map(|p| match p {
7494 Primitive::Rect {
7495 rect,
7496 color,
7497 radius,
7498 ..
7499 } if rect.x >= 200.0 - BAR_SIZE - BAR_MARGIN * 2.0 => {
7500 Some((*rect, *color, radius.top_left))
7501 }
7502 _ => None,
7503 })
7504 .expect("a thumb");
7505 let bar = ui.scrollbars()[0];
7506 let center = Point::new(
7507 bar.thumb.x + bar.thumb.width / 2.0,
7508 bar.thumb.y + bar.thumb.height / 2.0,
7509 );
7510 (
7511 thumb,
7512 bar.interactive,
7513 ui.scrollbar_at(center).is_some(),
7514 ui.scrollbar_near(center).is_some(),
7515 )
7516 };
7517 let ((rect, color, radius), interactive, grabbed, woken) =
7518 laid(frus_core::TargetPlatform::Android);
7519 assert_eq!(
7520 (rect.width, rect.x + rect.width),
7521 (4.0, 200.0),
7522 "4 px, flush"
7523 );
7524 assert_eq!(radius, 0.0, "square ends");
7525 assert_eq!(color, ANDROID_THUMB_ON_DARK, "an opaque grey at rest");
7526 assert!(
7527 !interactive && !grabbed && !woken,
7528 "nothing to take hold of"
7529 );
7530
7531 let ((rect, color, radius), interactive, grabbed, woken) =
7532 laid(frus_core::TargetPlatform::Linux);
7533 assert_eq!(
7534 (rect.width, rect.x + rect.width),
7535 (8.0, 198.0),
7536 "8 px, 2 px in"
7537 );
7538 assert_eq!(radius, 4.0, "a pill");
7539 assert!(color.a < 1.0, "a fade of the content colour");
7540 assert!(
7541 interactive && grabbed && woken,
7542 "a pointer takes hold of it"
7543 );
7544 }
7545
7546 /// **A theme's word outranks the platform's** on an Android theme too: a bar it makes
7547 /// interactive is one, and a colour it chose is the one at rest.
7548 #[test]
7549 fn an_android_bar_takes_the_theme_s_word() {
7550 let (runtime, _bars) = bars_runtime(Scrollbars::Always);
7551 let mut theme = Theme::dark().with_platform(frus_core::TargetPlatform::Android);
7552 theme.widgets.scrollbar.interactive = Some(true);
7553 theme.widgets.scrollbar.thumb_color = Some(Color::rgb(1.0, 0.0, 0.0));
7554 let ui = build_ui(
7555 &tall().thumb_visibility(true),
7556 Size::new(200.0, 100.0),
7557 &runtime,
7558 &theme,
7559 );
7560 let bar = ui.scrollbars()[0];
7561 assert!(bar.interactive);
7562 let color = painted(&ui)[0].1;
7563 assert_eq!((color.r, color.g, color.b), (1.0, 0.0, 0.0));
7564 }
7565
7566 /// A tall column in a short viewport: something to scroll.
7567 fn tall() -> SingleChildScrollView<()> {
7568 SingleChildScrollView::new()
7569 .width(200.0)
7570 .height(100.0)
7571 .child(Container::<()>::new().width(200.0).height(1000.0))
7572 }
7573
7574 /// **A touch screen gets no scrollbar** (`app.dart:870`).
7575 ///
7576 /// This drew one on every platform, on both axes, always — a permanent bar over the
7577 /// content of every scrolling page on a phone, where the reference draws nothing at
7578 /// all. A finger already knows where it is on the page.
7579 #[test]
7580 fn a_touch_screen_gets_no_scrollbar() {
7581 assert_eq!(bars(tall(), Scrollbars::Never), vec![]);
7582 assert_eq!(
7583 bars(tall(), Scrollbars::Always).len(),
7584 1,
7585 "and a desktop gets one"
7586 );
7587
7588 // Nothing to drag, either: a bar that is not drawn is not a target.
7589 let (runtime, _bars) = bars_runtime(Scrollbars::Never);
7590 let ui = build_ui(&tall(), Size::new(200.0, 100.0), &runtime, &Theme::dark());
7591 assert!(ui.scrollbars().is_empty());
7592 }
7593
7594 /// **A bar takes the scheme's word for the brightness, not the surface's luminance.**
7595 ///
7596 /// The opacities either side of this are three times apart at rest (`scrollbar.dart:242`,
7597 /// `:248`), so getting the question wrong is not a shade — it is a bar that reads as
7598 /// a smudge, or one that reads as a stripe.
7599 ///
7600 /// A **dimmed light** scheme is where the two answers part. The measurement it used
7601 /// to make says dark; the scheme says light, and the scheme is the one that knows.
7602 #[test]
7603 fn a_bar_reads_the_scheme_s_brightness_and_not_its_luminance() {
7604 use crate::media::Brightness;
7605 let dimmed = |brightness| {
7606 let mut theme = Theme::light();
7607 theme.scheme.surface = Color::rgb8(110, 110, 110);
7608 theme.scheme.brightness = brightness;
7609 theme
7610 };
7611 let thumb = |theme: &Theme| {
7612 let (runtime, _bars) = bars_runtime(Scrollbars::Always);
7613 let ui = build_ui(
7614 &tall().thumb_visibility(true),
7615 Size::new(200.0, 100.0),
7616 &runtime,
7617 theme,
7618 );
7619 painted(&ui)[0].1
7620 };
7621
7622 let light = dimmed(Brightness::Light);
7623 assert!(
7624 light.scheme.surface.compute_luminance() < 0.5,
7625 "the luminance the old line measured says dark"
7626 );
7627 assert_eq!(
7628 thumb(&light),
7629 light.scheme.on_surface.fade(THUMB_ON_LIGHT),
7630 "and the bar takes the scheme's word instead"
7631 );
7632
7633 let dark = dimmed(Brightness::Dark);
7634 assert_eq!(thumb(&dark), dark.scheme.on_surface.fade(THUMB_ON_DARK));
7635 }
7636
7637 /// **A theme reaches every number the bar is made of**, not only its colour. A
7638 /// scrollbar themed in its colour and sized from a constant is themed in the half
7639 /// that shows and unthemed in the half a pointer has to hit.
7640 ///
7641 /// There is no per-call override to outrank here — nobody configures a scrollbar at a
7642 /// call site, the walk draws it — so the chain is `theme ?? framework`, and the second
7643 /// half of this test is that the framework still answers where the theme says nothing.
7644 #[test]
7645 fn a_theme_reaches_every_number_the_scrollbar_is_made_of() {
7646 const THUMB: Color = Color::rgb(0.2, 0.7, 0.9);
7647 let mut theme = Theme::dark();
7648 theme.widgets.scrollbar.thickness = Some(14.0);
7649 theme.widgets.scrollbar.margin = Some(5.0);
7650 theme.widgets.scrollbar.min_thumb_length = Some(90.0);
7651 theme.widgets.scrollbar.radius = Some(2.0);
7652 theme.widgets.scrollbar.thumb_color = Some(THUMB);
7653 theme.widgets.scrollbar.opacity = Some(0.8);
7654
7655 let (runtime, _bars) = bars_runtime(Scrollbars::Always);
7656 let ui = build_ui(
7657 &tall().thumb_visibility(true),
7658 Size::new(200.0, 100.0),
7659 &runtime,
7660 &theme,
7661 );
7662 // `painted` filters on the strip a bar of the **default** thickness occupies, so
7663 // it cannot see a themed one — which is the point of the theme. This asks for the
7664 // strip the themed bar is in.
7665 let over_there: Vec<(Rect, Color)> = ui
7666 .scene()
7667 .primitives()
7668 .iter()
7669 .filter_map(|p| match p {
7670 Primitive::Rect { rect, color, .. } if rect.x >= 200.0 - 14.0 - 5.0 * 2.0 => {
7671 Some((*rect, *color))
7672 }
7673 _ => None,
7674 })
7675 .collect();
7676 assert_eq!(over_there.len(), 1, "still one rectangle: the thumb");
7677 let (thumb, color) = over_there[0];
7678 assert!(
7679 (thumb.width - 14.0).abs() < 0.01,
7680 "the theme's thickness: {thumb:?}"
7681 );
7682 assert!(
7683 ((thumb.x + thumb.width) - (200.0 - 5.0)).abs() < 0.01,
7684 "held clear by the theme's margin, not shrunk to fit: {thumb:?}"
7685 );
7686 assert!(
7687 thumb.height >= 90.0,
7688 "and never shorter than the theme's floor: {thumb:?}"
7689 );
7690 assert_eq!(
7691 color,
7692 THUMB.fade(0.8),
7693 "the theme's colour, faded to the theme's resting level"
7694 );
7695
7696 // The radius is not in `painted`, which reads the rectangles' colours; take it
7697 // from the primitive itself. A pill is what an unthemed bar is, and this one is
7698 // not a pill.
7699 let radius = ui.scene().primitives().iter().find_map(|p| match p {
7700 Primitive::Rect { radius, .. } if radius.top_left > 0.0 => Some(radius.top_left),
7701 _ => None,
7702 });
7703 assert_eq!(radius, Some(2.0), "the theme's corner radius");
7704
7705 // And with a theme that says nothing, the framework still answers.
7706 let plain = bars(tall(), Scrollbars::Always);
7707 let (thumb, color) = plain[0];
7708 assert!((thumb.width - BAR_SIZE).abs() < 0.01);
7709 assert_eq!(color, Theme::dark().scheme.on_surface.fade(THUMB_ON_DARK));
7710 }
7711
7712 /// **And it is a thumb and nothing else** (`scrollbar.dart:281`): the reference's
7713 /// track is transparent unless a caller asks for one, and even then it is 3 to 5 %.
7714 /// This painted an 18 % stripe down every scrolling page, under a 55 % thumb.
7715 #[test]
7716 fn a_bar_is_a_thumb_and_no_track() {
7717 let painted = bars(tall(), Scrollbars::Always);
7718 assert_eq!(painted.len(), 1, "one rectangle: the thumb");
7719 let (thumb, color) = painted[0];
7720 assert!(
7721 (thumb.width - BAR_SIZE).abs() < 0.01,
7722 "at the reference's thickness: {thumb:?}"
7723 );
7724 assert!(
7725 ((thumb.x + thumb.width) - (200.0 - BAR_MARGIN)).abs() < 0.01,
7726 "and clear of the edge by `crossAxisMargin`, not shrunk to fit: {thumb:?}"
7727 );
7728 assert!(
7729 thumb.height >= MIN_THUMB,
7730 "and never shorter than a pointer can catch: {thumb:?}"
7731 );
7732 let theme = Theme::dark();
7733 assert_eq!(color, theme.scheme.on_surface.fade(THUMB_ON_DARK));
7734 }
7735
7736 /// **Along the horizontal axis, never** (`app.dart:861`) — on any platform, under any
7737 /// setting. A sideways strip is scrolled by whatever made it sideways, and a bar
7738 /// under it takes room from content that is short of it already.
7739 #[test]
7740 fn a_sideways_strip_never_gets_one() {
7741 let strip = SingleChildScrollView::new()
7742 .axis(crate::scroll::Axis::Horizontal)
7743 .width(200.0)
7744 .height(100.0)
7745 .child(Container::<()>::new().width(1000.0).height(100.0));
7746 let (runtime, _bars) = bars_runtime(Scrollbars::Always);
7747 let ui = build_ui(&strip, Size::new(200.0, 100.0), &runtime, &Theme::dark());
7748 assert!(
7749 ui.scrollbars().is_empty(),
7750 "a desktop draws none across, either"
7751 );
7752 }
7753
7754 /// And **one area may answer for itself**, over the application's answer — the
7755 /// per-area say the physics beside it already had.
7756 #[test]
7757 fn an_area_may_ask_for_its_own_answer() {
7758 assert_eq!(
7759 bars(tall().scrollbars(Scrollbars::Always), Scrollbars::Never).len(),
7760 1,
7761 "asked for one where the platform gives none"
7762 );
7763 assert_eq!(
7764 bars(tall().scrollbars(Scrollbars::Never), Scrollbars::Always),
7765 vec![],
7766 "and refused one where it does"
7767 );
7768 }
7769
7770 /// **A bar nobody has scrolled is not there at all.** The reference's fade starts
7771 /// closed and is opened only by movement (`scrollbar.dart:1960`); a list that has
7772 /// just appeared shows its content, not its furniture.
7773 ///
7774 /// It is **registered** even so, which a bar the platform does not draw is not. The
7775 /// difference is real: that one is not there, and this one is here and invisible —
7776 /// and a mouse coming near it is the only thing that can bring it back.
7777 #[test]
7778 fn an_untouched_area_shows_no_bar_but_can_still_be_reached_for() {
7779 let (runtime, _bars) = bars_runtime(Scrollbars::Always);
7780 let ui = build_ui(&tall(), Size::new(200.0, 100.0), &runtime, &Theme::dark());
7781 assert_eq!(painted(&ui), vec![], "nothing is painted for it");
7782
7783 let bar = *ui.scrollbars().first().expect("registered all the same");
7784 assert_eq!(bar.opacity, 0.0);
7785 let on_it = Point::new(194.0, 20.0);
7786 assert!(
7787 ui.scrollbar_at(on_it).is_none(),
7788 "and a bar at zero cannot be grabbed"
7789 );
7790 assert!(
7791 ui.scrollbar_near(on_it).is_some(),
7792 "but it can be reached for, which is what brings it back"
7793 );
7794 // The reach is wider than the bar: 8 pixels at a tenth of an opacity is not
7795 // something a hand aims at precisely.
7796 assert!(
7797 ui.scrollbar_near(Point::new(178.0, 20.0)).is_some(),
7798 "the reach takes in a tap target's room around the thumb: {:?}",
7799 bar.reach
7800 );
7801 }
7802
7803 /// **A pinned bar does not fade** (`scrollbar.dart:214`) — for an area whose content
7804 /// does not look scrollable, or one a reader should be able to aim at without
7805 /// scrolling first to make the bar appear.
7806 #[test]
7807 fn a_pinned_bar_does_not_fade() {
7808 let (runtime, _bars) = bars_runtime(Scrollbars::Always);
7809 let ui = build_ui(
7810 &tall().thumb_visibility(true),
7811 Size::new(200.0, 100.0),
7812 &runtime,
7813 &Theme::dark(),
7814 );
7815 assert_eq!(painted(&ui).len(), 1, "there without being scrolled to");
7816 assert_eq!(ui.scrollbars()[0].opacity, 1.0);
7817 }
7818
7819 /// **A thumb under a pointer, and a thumb in a hand**, are two different colours
7820 /// (`scrollbar.dart:245`, `:244`) — and neither is the resting one. The dragged
7821 /// value arrives at once: the hand is already on it.
7822 #[test]
7823 fn a_thumb_answers_the_pointer() {
7824 use crate::runtime::ScrollbarFade;
7825 let build = |prime: &dyn Fn(&mut Runtime)| {
7826 let (mut runtime, _bars) = bars_runtime(Scrollbars::Always);
7827 prime(&mut runtime);
7828 let ui = build_ui(
7829 &tall().thumb_visibility(true),
7830 Size::new(200.0, 100.0),
7831 &runtime,
7832 &Theme::dark(),
7833 );
7834 let id = ui.scrollbars()[0].id;
7835 (id, painted(&ui)[0].1)
7836 };
7837
7838 let theme = Theme::dark();
7839 let (id, resting) = build(&|_| {});
7840 assert_eq!(resting, theme.scheme.on_surface.fade(THUMB_ON_DARK));
7841
7842 let (_, warmed) = build(&|rt| {
7843 rt.scrollbar_fade.insert(
7844 id,
7845 ScrollbarFade {
7846 opacity: 1.0,
7847 hover: 1.0,
7848 ..ScrollbarFade::default()
7849 },
7850 );
7851 });
7852 assert_eq!(warmed, theme.scheme.on_surface.fade(THUMB_HOVER_ON_DARK));
7853
7854 let (_, grabbed) = build(&|rt| rt.scrollbar_dragged = Some(id));
7855 assert_eq!(grabbed, theme.scheme.on_surface.fade(THUMB_DRAG_ON_DARK));
7856 }
7857
7858 #[test]
7859 fn hover_progress_interpolates_color() {
7860 let rt = Runtime::default();
7861 let base = build_ui(
7862 &clickable_sample(),
7863 Size::new(400.0, 100.0),
7864 &rt,
7865 &Theme::default(),
7866 );
7867 let id_a = base.hit(Point::new(50.0, 50.0)).unwrap();
7868
7869 // Without any progress: the base colour (red).
7870 if let Primitive::Rect { color, .. } = base.scene().primitives()[0] {
7871 assert_eq!(color, Color::rgb(1.0, 0.0, 0.0));
7872 } else {
7873 panic!("expected a rect");
7874 }
7875
7876 // Full progress: the hover colour (green).
7877 let mut rt = Runtime::default();
7878 rt.input.hovered = Some(id_a);
7879 rt.anims.insert(
7880 id_a,
7881 crate::Anim {
7882 hover: 1.0,
7883 ..Default::default()
7884 },
7885 );
7886 let ui = build_ui(
7887 &clickable_sample(),
7888 Size::new(400.0, 100.0),
7889 &rt,
7890 &Theme::default(),
7891 );
7892 if let Primitive::Rect { color, .. } = ui.scene().primitives()[0] {
7893 assert_eq!(color, Color::rgb(0.0, 1.0, 0.0));
7894 } else {
7895 panic!("expected a rect");
7896 }
7897 }
7898
7899 #[test]
7900 fn multiline_field_registers_as_scrollable_when_overflowing() {
7901 // A multi-line field whose content exceeds `rows` registers itself as a scrollable
7902 // area (with `max_y > 0`) — which is what the wheel and the scrollbar target. A short
7903 // field does not register.
7904 let tall = TextField::<Msg>::new("a\nb\nc\nd\ne\nf")
7905 .on_input(Msg::Edited)
7906 .rows(2)
7907 .width(200.0);
7908 let tree: Flex<Msg> = Flex::column().child(tall);
7909 let rt = Runtime::default();
7910 let ui = build_ui(&tree, Size::new(220.0, 240.0), &rt, &Theme::default());
7911 let maxes = ui.scrollable_maxes();
7912 assert_eq!(maxes.len(), 1, "the overflowing field registers");
7913 assert!(maxes[0].2 > 0.0, "max_y > 0 (overflowing content)");
7914
7915 let short = TextField::<Msg>::new("a\nb")
7916 .on_input(Msg::Edited)
7917 .rows(4)
7918 .width(200.0);
7919 let tree: Flex<Msg> = Flex::column().child(short);
7920 let ui = build_ui(&tree, Size::new(220.0, 240.0), &rt, &Theme::default());
7921 assert!(
7922 ui.scrollable_maxes().is_empty(),
7923 "a short field does not scroll"
7924 );
7925 }
7926
7927 #[test]
7928 fn only_text_inputs_place_a_cursor() {
7929 // The invariant of the click fix (milestone 39): a focusable button returns NO cursor
7930 // (`cursor_at` = None), so the shell does not start a text selection on it and does not
7931 // capture the click. Only text fields place one.
7932 let button = Button::<Msg>::new("x").on_press(Msg::A);
7933 assert_eq!(Widget::<Msg>::cursor_at(&button, 10.0, 5.0, 200.0, 0), None);
7934 let input = TextField::<Msg>::new("hi").on_input(Msg::Edited);
7935 assert!(Widget::<Msg>::cursor_at(&input, 10.0, 5.0, 200.0, 0).is_some());
7936 }
7937
7938 /// A form taller than its window: six buttons of 60 px in a 150 px scroll.
7939 fn long_form() -> SingleChildScrollView<Msg> {
7940 let mut column = Flex::<Msg>::column();
7941 for i in 0..6 {
7942 column = column.child(
7943 Button::new(format!("field {i}"))
7944 .on_press(Msg::A)
7945 .height(60.0),
7946 );
7947 }
7948 SingleChildScrollView::new()
7949 .width(200.0)
7950 .height(150.0)
7951 .child(column)
7952 }
7953
7954 fn form_ui(runtime: &Runtime) -> Ui<Msg> {
7955 build_ui(
7956 &long_form(),
7957 Size::new(200.0, 150.0),
7958 runtime,
7959 &Theme::default(),
7960 )
7961 }
7962
7963 /// **Every** field is a focus stop, not only the two and a half on screen.
7964 ///
7965 /// Registering what the eye can see is how a long form came to have most of its
7966 /// fields unreachable from the keyboard: Tab walked the visible ones and wrapped.
7967 #[test]
7968 fn tab_reaches_the_fields_below_the_fold() {
7969 let ui = form_ui(&Runtime::default());
7970 assert_eq!(ui.focusables.len(), 6, "all six, not the visible ones");
7971 // And the traversal is in tree order, so Tab walks down the form.
7972 let ids: Vec<_> = ui.focusables.iter().map(|f| f.id).collect();
7973 assert_eq!(ui.focus_next(Some(ids[2]), true), Some(ids[3]));
7974 }
7975
7976 /// A stop clipped away by a scroll keeps its **real** box, and an empty visible one.
7977 /// The two are different questions and the registry answers both.
7978 #[test]
7979 fn a_stop_out_of_sight_keeps_its_geometry() {
7980 let ui = form_ui(&Runtime::default());
7981 let fifth = ui.focusables[5];
7982 assert!((fifth.bounds.y - 300.0).abs() < 0.5, "{:?}", fifth.bounds);
7983 assert_eq!(fifth.rect.height, 0.0, "none of it is on screen");
7984 assert!(fifth.scroll.is_some(), "something around it scrolls");
7985 }
7986
7987 /// A click still lands only on what can be seen. The unclipped box of a field
7988 /// scrolled out of sight overlaps the window, and a tap there must not focus it.
7989 #[test]
7990 fn a_click_still_only_reaches_what_is_visible() {
7991 let ui = form_ui(&Runtime::default());
7992 // The second field, in the middle of its own visible box.
7993 let second = ui.focusables[1];
7994 let inside = Point::new(
7995 second.rect.x + second.rect.width / 2.0,
7996 second.rect.y + second.rect.height / 2.0,
7997 );
7998 assert_eq!(ui.focus_hit(inside).map(|(id, _)| id), Some(second.id));
7999 // The sixth field's **real** box overlaps nothing on screen, and a tap where it
8000 // would be if the form were not scrolled hits nothing at all.
8001 let sixth = ui.focusables[5];
8002 let past = Point::new(
8003 sixth.bounds.x + sixth.bounds.width / 2.0,
8004 sixth.bounds.y + sixth.bounds.height / 2.0,
8005 );
8006 assert!(past.y > 150.0, "past the fold: {past:?}");
8007 assert_eq!(ui.focus_hit(past), None);
8008 }
8009
8010 /// Focus lands below the fold and the view follows: the least movement that brings
8011 /// the field's foot to the window's.
8012 #[test]
8013 fn revealing_a_stop_below_the_fold_scrolls_to_it() {
8014 let runtime = Runtime::default();
8015 let ui = form_ui(&runtime);
8016 let area = ui.scroll_regions()[0];
8017 let fifth = ui.focusables[5].id;
8018 let moves = ui.reveal(fifth, &runtime);
8019 // Field 5 sits at 300..360 and the window is 150 tall: its foot reaches the
8020 // window's foot at 360 - 150 = 210.
8021 assert_eq!(moves, vec![(area.id, (0.0, 210.0))]);
8022 }
8023
8024 /// Already in view, nothing moves — the answer is the empty list rather than a
8025 /// zero, so no caller has to ask twice.
8026 #[test]
8027 fn revealing_something_already_in_view_moves_nothing() {
8028 let runtime = Runtime::default();
8029 let ui = form_ui(&runtime);
8030 assert!(ui.reveal(ui.focusables[0].id, &runtime).is_empty());
8031 assert!(ui.reveal(ui.focusables[1].id, &runtime).is_empty());
8032 // And a stop the frame does not contain at all is not an error.
8033 assert!(ui.reveal(WidgetId::from_u64(7), &runtime).is_empty());
8034 }
8035
8036 /// Coming back **up** the form scrolls the other way, and only as far as it must.
8037 #[test]
8038 fn revealing_a_stop_above_the_fold_scrolls_back() {
8039 let mut runtime = Runtime::default();
8040 let area_id = form_ui(&runtime).scroll_regions()[0].id;
8041 runtime.scroll.insert(area_id, (0.0, 210.0));
8042 let ui = form_ui(&runtime);
8043 let first = ui.focusables[0].id;
8044 // Field 0 sits at 0..60: its head reaches the window's head at offset 0.
8045 assert_eq!(ui.reveal(first, &runtime), vec![(area_id, (0.0, 0.0))]);
8046 // At offset 210 the window shows 210..360, so field 4 (240..300) is wholly in
8047 // it and does not move. Field 3 (180..240) hangs over the top edge and would.
8048 assert!(ui.reveal(ui.focusables[4].id, &runtime).is_empty());
8049 assert!(!ui.reveal(ui.focusables[3].id, &runtime).is_empty());
8050 }
8051
8052 /// The arithmetic on its own: the least movement, the leading edge for anything too
8053 /// big to fit, and the clamp at the ends of the travel.
8054 #[test]
8055 fn the_reveal_moves_as_little_as_it_can() {
8056 let area = Scrollable {
8057 id: WidgetId::ROOT,
8058 viewport: Rect::new(0.0, 0.0, 100.0, 100.0),
8059 max_x: 0.0,
8060 max_y: 400.0,
8061 axis: crate::scroll::Axis::Vertical,
8062 physics: crate::ScrollPhysics::CLAMPING,
8063 overscroll: Some(crate::OverscrollIndicator::Glow),
8064 fling: frus_core::VelocityStrategy::Regression,
8065 refresh: None,
8066 page: None,
8067 reverse_x: false,
8068 reverse_y: false,
8069 host: None,
8070 keep_visible: None,
8071 };
8072 // Inside: nothing.
8073 assert_eq!(
8074 area.reveal(Rect::new(0.0, 10.0, 50.0, 20.0), (0.0, 0.0)).1,
8075 (0.0, 0.0)
8076 );
8077 // Below: its foot comes to the window's foot, and no further.
8078 assert_eq!(
8079 area.reveal(Rect::new(0.0, 140.0, 50.0, 20.0), (0.0, 0.0)).0,
8080 (0.0, 60.0)
8081 );
8082 // Above: its head comes to the window's head.
8083 assert_eq!(
8084 area.reveal(Rect::new(0.0, -30.0, 50.0, 20.0), (0.0, 50.0))
8085 .0,
8086 (0.0, 20.0)
8087 );
8088 // Taller than the window: the **start** edge wins, because that is what is read.
8089 assert_eq!(
8090 area.reveal(Rect::new(0.0, -10.0, 50.0, 300.0), (0.0, 40.0))
8091 .0,
8092 (0.0, 30.0)
8093 );
8094 // And the travel is a wall: an offset past the end is not asked for.
8095 assert_eq!(
8096 area.reveal(Rect::new(0.0, 900.0, 50.0, 20.0), (0.0, 0.0)).0,
8097 (0.0, 400.0)
8098 );
8099 }
8100
8101 /// A reversed axis numbers its offsets from the far end, so the same movement of the
8102 /// content is the opposite movement of the offset. One function knows that, and the
8103 /// reveal asks it rather than keeping a second opinion.
8104 #[test]
8105 fn the_reveal_follows_a_reversed_axis() {
8106 let area = Scrollable {
8107 id: WidgetId::ROOT,
8108 viewport: Rect::new(0.0, 0.0, 100.0, 100.0),
8109 max_x: 0.0,
8110 max_y: 400.0,
8111 axis: crate::scroll::Axis::Vertical,
8112 physics: crate::ScrollPhysics::CLAMPING,
8113 overscroll: Some(crate::OverscrollIndicator::Glow),
8114 fling: frus_core::VelocityStrategy::Regression,
8115 refresh: None,
8116 page: None,
8117 reverse_x: false,
8118 reverse_y: true,
8119 host: None,
8120 keep_visible: None,
8121 };
8122 // A box below the window: the content must move **up** by 60 either way. On a
8123 // reversed axis a rising offset pushes the content down (`content_origin` is
8124 // `viewport - content + offset` there), so the offset comes **down** by 60 to do
8125 // what a plain axis would need it to go up by.
8126 let (offset, shift) = area.reveal(Rect::new(0.0, 140.0, 50.0, 20.0), (0.0, 100.0));
8127 assert_eq!(shift, (0.0, -60.0));
8128 assert_eq!(offset, (0.0, 40.0));
8129 }
8130
8131 /// Centring puts the box in the middle of the window, and the clamp is what keeps
8132 /// the first and last items of a row where they belong rather than half off it.
8133 #[test]
8134 fn centring_is_clamped_to_the_travel() {
8135 let area = Scrollable {
8136 id: WidgetId::ROOT,
8137 viewport: Rect::new(0.0, 0.0, 300.0, 50.0),
8138 max_x: 700.0,
8139 max_y: 0.0,
8140 axis: crate::scroll::Axis::Vertical,
8141 physics: crate::ScrollPhysics::CLAMPING,
8142 overscroll: Some(crate::OverscrollIndicator::Glow),
8143 fling: frus_core::VelocityStrategy::Regression,
8144 refresh: None,
8145 page: None,
8146 reverse_x: false,
8147 reverse_y: false,
8148 host: None,
8149 keep_visible: None,
8150 };
8151 // A box at 400..500 has its middle at 450; the window's is at 150.
8152 assert_eq!(
8153 area.centre(Rect::new(400.0, 0.0, 100.0, 50.0), (0.0, 0.0))
8154 .0,
8155 (300.0, 0.0)
8156 );
8157 // The first item cannot be centred without scrolling backwards, so it stays.
8158 assert_eq!(
8159 area.centre(Rect::new(0.0, 0.0, 100.0, 50.0), (0.0, 0.0)).0,
8160 (0.0, 0.0)
8161 );
8162 // Nor the last past the end of the travel.
8163 assert_eq!(
8164 area.centre(Rect::new(900.0, 0.0, 100.0, 50.0), (0.0, 0.0))
8165 .0,
8166 (700.0, 0.0)
8167 );
8168 }
8169
8170 /// **A lone child could widen the box it was given, and take the screen with it.**
8171 ///
8172 /// A flex item's automatic minimum is its **min-content** size, so agreeing to shrink
8173 /// buys nothing on its own: the item shrinks willingly and then refuses to go below
8174 /// the widest thing inside it. A column holding one over-wide row came out at that
8175 /// row's width; the padded box around it came out at the same; and every sibling,
8176 /// stretched across the column, came out at the same again — a whole screen laid out
8177 /// to the width of its widest line and then clipped by the window.
8178 ///
8179 /// Reported from a real application: a shop whose banner, search field, filter row
8180 /// and product grid were all cut off at the same edge, because one segmented control
8181 /// four segments wide had decided how wide the screen was.
8182 ///
8183 /// The over-wide child still overflows — that is the reference's behaviour and the
8184 /// honest one — but it no longer drags its parent and its siblings out with it.
8185 #[test]
8186 fn a_lone_child_cannot_widen_the_box_it_was_given() {
8187 let ordinary = Color::rgb(0.1, 0.2, 0.3);
8188 let overwide = Color::rgb(0.9, 0.8, 0.7);
8189 let screen: SingleChildScrollView<Msg> = SingleChildScrollView::new().width(300.0).child(
8190 Container::new().padding(24.0).child(
8191 Flex::column()
8192 .align(frus_layout::Align::Stretch)
8193 .child(Container::new().height(20.0).color(ordinary))
8194 .child(Container::new().width(600.0).height(20.0).color(overwide)),
8195 ),
8196 );
8197 let ui = build_ui(
8198 &screen,
8199 Size::new(300.0, 800.0),
8200 &Runtime::default(),
8201 &Theme::default(),
8202 );
8203 let width_of = |wanted: Color| {
8204 ui.scene()
8205 .primitives()
8206 .iter()
8207 .find_map(|p| match p {
8208 Primitive::Rect { color, rect, .. } if *color == wanted => Some(rect.width),
8209 _ => None,
8210 })
8211 .expect("the box is drawn")
8212 };
8213 // 300 less the padding either side, and not 600.
8214 assert_eq!(width_of(ordinary), 252.0);
8215 // The one that asked to be too wide still is: it overflows rather than being
8216 // squeezed, and rather than resizing everything around it.
8217 assert_eq!(width_of(overwide), 600.0);
8218 }
8219
8220 #[test]
8221 fn tab_cycles_focusables_in_order() {
8222 let tree = Flex::<Msg>::column()
8223 .child(Button::new("un").on_press(Msg::A))
8224 .child(Button::new("deux").on_press(Msg::B));
8225 let ui = build_ui(
8226 &tree,
8227 Size::new(200.0, 200.0),
8228 &Runtime::default(),
8229 &Theme::default(),
8230 );
8231 assert_eq!(ui.focusables.len(), 2, "both buttons are focusable");
8232 let first = ui.focusables[0].id;
8233 let second = ui.focusables[1].id;
8234
8235 // With no focus: Tab -> first, Shift+Tab -> last.
8236 assert_eq!(ui.focus_next(None, true), Some(first));
8237 assert_eq!(ui.focus_next(None, false), Some(second));
8238 // Wrap-around.
8239 assert_eq!(ui.focus_next(Some(first), true), Some(second));
8240 assert_eq!(ui.focus_next(Some(second), true), Some(first));
8241 assert_eq!(ui.focus_next(Some(first), false), Some(second));
8242 }
8243
8244 #[test]
8245 fn focus_ring_for_button_not_for_textinput() {
8246 let theme = Theme::default();
8247 let ring = theme.focus.fade(0.4); // focus_progress = 0 → alpha 0.4
8248 let count_ring = |tree: &dyn Widget<Msg>, keyboard: bool| -> usize {
8249 let mut rt = Runtime::default();
8250 let probe = build_ui(tree, Size::new(200.0, 200.0), &rt, &theme);
8251 rt.input.focused = probe.focusables.first().map(|f| f.id);
8252 rt.focus_visible = keyboard;
8253 let ui = build_ui(tree, Size::new(200.0, 200.0), &rt, &theme);
8254 ui.scene()
8255 .primitives()
8256 .iter()
8257 .filter(
8258 |p| matches!(p, Primitive::Rect { border_color, .. } if *border_color == ring),
8259 )
8260 .count()
8261 };
8262 let with_button = Flex::<Msg>::column().child(Button::new("x").on_press(Msg::A));
8263 assert!(
8264 count_ring(&with_button, true) >= 1,
8265 "a button focused with the keyboard has a generic ring"
8266 );
8267 // Focus taken with the pointer: no ring (FocusHighlightMode).
8268 assert_eq!(
8269 count_ring(&with_button, false),
8270 0,
8271 "a click does not flash a ring"
8272 );
8273
8274 let with_input = Flex::<Msg>::column().child(TextField::new("hi").on_input(Msg::Edited));
8275 assert_eq!(
8276 count_ring(&with_input, true),
8277 0,
8278 "the field draws its own focus"
8279 );
8280 }
8281
8282 #[test]
8283 fn arrow_focus_navigates_geometrically() {
8284 // A 2×2 grid of buttons; each target is identified by its message.
8285 let grid: Flex<Msg> = Flex::column()
8286 .child(
8287 Flex::row()
8288 .child(Button::new("a").on_press(Msg::A))
8289 .child(Button::new("b").on_press(Msg::B)),
8290 )
8291 .child(
8292 Flex::row()
8293 .child(Button::new("c").on_press(Msg::C))
8294 .child(Button::new("d").on_press(Msg::D)),
8295 );
8296 let ui = build_ui(
8297 &grid,
8298 Size::new(300.0, 200.0),
8299 &Runtime::default(),
8300 &Theme::default(),
8301 );
8302 let top_left = ui.focus_next(None, true).expect("premier focusable");
8303 assert_eq!(ui.msg_for(top_left), Some(Msg::A));
8304
8305 // Right: a → b; down: a → c; and nothing to the left of a.
8306 let right = ui
8307 .focus_directional(top_left, FocusDirection::Right)
8308 .expect("to the right");
8309 assert_eq!(ui.msg_for(right), Some(Msg::B));
8310 let down = ui
8311 .focus_directional(top_left, FocusDirection::Down)
8312 .expect("downwards");
8313 assert_eq!(ui.msg_for(down), Some(Msg::C));
8314 assert_eq!(ui.focus_directional(top_left, FocusDirection::Left), None);
8315 // The diagonal is kept in check: from b, down → d (aligned), not c.
8316 let down_right = ui
8317 .focus_directional(right, FocusDirection::Down)
8318 .expect("down from b");
8319 assert_eq!(ui.msg_for(down_right), Some(Msg::D));
8320 }
8321
8322 #[test]
8323 fn keyed_identity_survives_middle_removal() {
8324 let colored = |c: Color| Container::<Msg>::new().width(50.0).height(20.0).color(c);
8325 let red = Color::rgb(1.0, 0.0, 0.0);
8326 let green = Color::rgb(0.0, 1.0, 0.0);
8327 let blue = Color::rgb(0.0, 0.0, 1.0);
8328
8329 let owner_of = |ui: &Ui<Msg>, c: Color| -> u64 {
8330 ui.scene()
8331 .primitives()
8332 .iter()
8333 .find_map(|p| match p {
8334 Primitive::Rect { color, owner, .. } if *color == c => Some(*owner),
8335 _ => None,
8336 })
8337 .expect("the primitive is there")
8338 };
8339
8340 // The list [red(key 1), green(key 2), blue(key 3)].
8341 let full = Flex::<Msg>::column()
8342 .child(Keyed::new(1u64, colored(red)))
8343 .child(Keyed::new(2u64, colored(green)))
8344 .child(Keyed::new(3u64, colored(blue)));
8345 let ui_full = build_ui(
8346 &full,
8347 Size::new(200.0, 200.0),
8348 &Runtime::default(),
8349 &Theme::default(),
8350 );
8351
8352 // The list [red(1), blue(3)]: the green in the middle is removed → blue goes from index 2 to 1.
8353 let removed = Flex::<Msg>::column()
8354 .child(Keyed::new(1u64, colored(red)))
8355 .child(Keyed::new(3u64, colored(blue)));
8356 let ui_removed = build_ui(
8357 &removed,
8358 Size::new(200.0, 200.0),
8359 &Runtime::default(),
8360 &Theme::default(),
8361 );
8362
8363 // Blue's identity (owner, key 3) is UNCHANGED despite the shift in position.
8364 assert_eq!(owner_of(&ui_full, blue), owner_of(&ui_removed, blue));
8365
8366 // Without a key, the 2nd child's positional identity DOES change (index 2 vs 1).
8367 let unkeyed_full = Flex::<Msg>::column()
8368 .child(colored(red))
8369 .child(colored(green))
8370 .child(colored(blue));
8371 let unkeyed_removed = Flex::<Msg>::column()
8372 .child(colored(red))
8373 .child(colored(blue));
8374 let u1 = build_ui(
8375 &unkeyed_full,
8376 Size::new(200.0, 200.0),
8377 &Runtime::default(),
8378 &Theme::default(),
8379 );
8380 let u2 = build_ui(
8381 &unkeyed_removed,
8382 Size::new(200.0, 200.0),
8383 &Runtime::default(),
8384 &Theme::default(),
8385 );
8386 assert_ne!(owner_of(&u1, blue), owner_of(&u2, blue));
8387 }
8388
8389 #[test]
8390 fn center_overlay_scrim_click_dismisses() {
8391 // A Center modal with `.dismiss`: clicking the scrim (outside the content) returns the
8392 // dismissal message; clicking the content does not.
8393 let modal = Container::<Msg>::new()
8394 .width(100.0)
8395 .height(60.0)
8396 .color(Color::WHITE);
8397 let portal: OverlayPortal<Msg> =
8398 OverlayPortal::new(Container::<Msg>::new().width(20.0).height(20.0))
8399 .overlay(modal, Placement::Center)
8400 .dismiss(Msg::A);
8401 let ui = build_ui(
8402 &portal,
8403 Size::new(400.0, 300.0),
8404 &Runtime::default(),
8405 &Theme::default(),
8406 );
8407
8408 // The top-left corner: on the scrim → it dismisses.
8409 let corner = ui.hit(Point::new(5.0, 5.0)).expect("a clickable scrim");
8410 assert_eq!(ui.msg_for(corner), Some(Msg::A));
8411 }
8412
8413 /// **The shell's mark reaches the field** (milestone 511): a focused field whose
8414 /// selection the runtime marks as a touch one paints its two handles; the same
8415 /// selection unmarked paints none, and so does a marked field that lost the focus.
8416 #[test]
8417 fn a_touch_selection_reaches_the_field_as_handles() {
8418 let tree = Flex::column().width(300.0).height(80.0).child(
8419 TextField::new("hello world")
8420 .width(200.0)
8421 .on_input(Msg::Edited),
8422 );
8423 let theme = Theme::default();
8424 let size = Size::new(300.0, 80.0);
8425 let (id, _) = build_ui(&tree, size, &Runtime::default(), &theme)
8426 .focus_hit(Point::new(10.0, 10.0))
8427 .expect("the field");
8428 let handles = |focused: bool, marked: bool| {
8429 let mut rt = Runtime::default();
8430 rt.edits.insert(
8431 id,
8432 Edit {
8433 cursor: 11,
8434 anchor: Some(6),
8435 composing: None,
8436 },
8437 );
8438 rt.input.focused = focused.then_some(id);
8439 rt.selection_handles = marked.then_some(id);
8440 let ui = build_ui(&tree, size, &rt, &theme);
8441 ui.scene()
8442 .primitives()
8443 .iter()
8444 .filter(|p| {
8445 matches!(p, Primitive::Path { fill: Some(c), .. } if *c == theme.scheme.primary)
8446 })
8447 .count()
8448 };
8449 assert_eq!(handles(true, true), 2);
8450 assert_eq!(handles(true, false), 0, "a selection made with a mouse");
8451 assert_eq!(handles(false, true), 0, "a field left behind");
8452 }
8453
8454 /// A wrapper that fuses with its field shares its identity, so it is the wrapper the
8455 /// shell asks for the handles — and a wrapper that forgot to pass the question on
8456 /// would leave a finger nothing to take (the three silent bugs `Responsive` has
8457 /// already cost, milestones 477 to 495).
8458 #[test]
8459 fn the_wrappers_that_fuse_with_a_field_pass_its_handles_on() {
8460 let field = || TextField::<Msg>::new("hello world").width(200.0);
8461 let edit = Edit {
8462 cursor: 11,
8463 anchor: Some(6),
8464 composing: None,
8465 };
8466 let direct = Widget::<Msg>::selection_handles(&field(), 200.0, &edit, 0.0);
8467 assert!(direct.is_some());
8468 let keyed = crate::Keyed::new(1, field());
8469 let responsive = crate::Responsive::new(crate::SizeClass::Compact).compact(field());
8470 let boxed: Box<dyn Widget<Msg>> = Box::new(field());
8471 assert_eq!(keyed.selection_handles(200.0, &edit, 0.0), direct);
8472 assert_eq!(responsive.selection_handles(200.0, &edit, 0.0), direct);
8473 assert_eq!(boxed.selection_handles(200.0, &edit, 0.0), direct);
8474 }
8475
8476 /// The tree the selection-bar tests share: a field with `spacer` px of room above it, so
8477 /// the bar has somewhere to go above the selection (or not, at 0).
8478 fn bar_tree(spacer: f32, value: &str) -> impl Widget<Msg> {
8479 Flex::column()
8480 .width(300.0)
8481 .height(400.0)
8482 .child(Container::new().width(300.0).height(spacer))
8483 .child(
8484 TextField::new(value.to_string())
8485 .width(200.0)
8486 .on_input(Msg::Edited),
8487 )
8488 }
8489
8490 /// The identity of the field in [`bar_tree`].
8491 fn bar_field_id(tree: &impl Widget<Msg>) -> WidgetId {
8492 let ui = build_ui(
8493 tree,
8494 Size::new(300.0, 400.0),
8495 &Runtime::default(),
8496 &Theme::default(),
8497 );
8498 let id = ui.focusable_ids().next().expect("the field");
8499 id
8500 }
8501
8502 /// A frame with the field's bar open: `edit` selected, the runtime's mark saying
8503 /// `can_paste`, and the field focused and marked as the two flags say.
8504 fn bar_frame(
8505 tree: &impl Widget<Msg>,
8506 edit: Edit,
8507 can_paste: bool,
8508 focused: bool,
8509 marked: bool,
8510 ) -> Ui<Msg> {
8511 let id = bar_field_id(tree);
8512 let mut rt = Runtime::default();
8513 rt.edits.insert(id, edit);
8514 rt.input.focused = focused.then_some(id);
8515 rt.selection_toolbar = marked.then_some(crate::runtime::ToolbarMark {
8516 id,
8517 can_paste,
8518 at: None,
8519 });
8520 // The pill's tests: under a theme that follows Android, where the bar is a pill
8521 // (milestone 618), whatever this test runs on.
8522 build_ui(tree, Size::new(300.0, 400.0), &rt, &pill_theme())
8523 }
8524
8525 /// **On the desktops the bar is a menu, and a right-click puts it at the pointer**
8526 /// (milestone 618; `editable_text.dart:3211`): opened any other way it hangs from the
8527 /// top of the selection, at its middle; opened by a right-click it hangs from where the
8528 /// pointer was; either way it is pushed back inside the window.
8529 #[test]
8530 fn a_desktop_menu_hangs_from_the_pointer_or_the_selection() {
8531 let tree = bar_tree(150.0, "hello world");
8532 let id = bar_field_id(&tree);
8533 let theme = Theme::default().with_platform(frus_core::TargetPlatform::Linux);
8534 let frame = |at: Option<Point>| {
8535 let mut rt = Runtime::default();
8536 rt.edits.insert(id, selected(5, Some(0)));
8537 rt.input.focused = Some(id);
8538 rt.selection_toolbar = Some(crate::runtime::ToolbarMark {
8539 id,
8540 can_paste: false,
8541 at,
8542 });
8543 build_ui(&tree, Size::new(300.0, 400.0), &rt, &theme)
8544 };
8545 let window = Rect::new(0.0, 0.0, 300.0, 400.0);
8546
8547 let ui = frame(Some(Point::new(40.0, 60.0)));
8548 let menu = ui.toolbars[0];
8549 assert_eq!(menu.width, 222.0, "the menu, not the pill");
8550 assert_eq!((menu.x, menu.y), (40.0, 60.0), "at the pointer");
8551
8552 let ui = frame(Some(Point::new(250.0, 380.0)));
8553 let menu = ui.toolbars[0];
8554 let expected = crate::selectiontoolbar::place_menu(
8555 Point::new(250.0, 380.0),
8556 Size::new(menu.width, menu.height),
8557 window,
8558 );
8559 assert_eq!((menu.x, menu.y), expected, "pushed back inside");
8560 assert!(menu.x + menu.width <= 292.0 && menu.y + menu.height <= 392.0);
8561
8562 let ui = frame(None);
8563 let menu = ui.toolbars[0];
8564 let field = ui.widget_rect(id).expect("the field");
8565 assert!(
8566 (menu.y - field.y).abs() < field.height,
8567 "hangs from the selection's top, not above it: {menu:?} {field:?}"
8568 );
8569 }
8570
8571 /// A theme whose selection bar is the pill.
8572 fn pill_theme() -> Theme {
8573 Theme::default().with_platform(frus_core::TargetPlatform::Android)
8574 }
8575
8576 fn selected(cursor: usize, anchor: Option<usize>) -> Edit {
8577 Edit {
8578 cursor,
8579 anchor,
8580 composing: None,
8581 }
8582 }
8583
8584 /// The editing actions a finger can reach on the bar, found the way a finger finds
8585 /// them: pressing along a line through the bar and asking what is there.
8586 fn reachable_actions(ui: &Ui<Msg>) -> Vec<crate::EditAction> {
8587 let Some(bar) = ui.toolbars.first().copied() else {
8588 return Vec::new();
8589 };
8590 let mut found = Vec::new();
8591 let mut x = bar.x + 1.0;
8592 while x < bar.x + bar.width {
8593 let at = Point::new(x, bar.y + bar.height * 0.5);
8594 if let Some(action) = ui.hit(at).and_then(|id| ui.edit_action_for(id)) {
8595 if !found.contains(&action) {
8596 found.push(action);
8597 }
8598 }
8599 x += 2.0;
8600 }
8601 found
8602 }
8603
8604 fn sorted_actions(mut actions: Vec<crate::EditAction>) -> Vec<crate::EditAction> {
8605 actions.sort_by_key(|action| format!("{action:?}"));
8606 actions
8607 }
8608
8609 /// **The bar floats above a selection** (milestone 568): a focused field the shell has
8610 /// marked, with a word selected and something on the clipboard, gets Cut, Copy, Paste and
8611 /// Select all — in a box **above** the field, and each one a target a press can find.
8612 #[test]
8613 fn a_marked_field_floats_its_selection_bar_above_it() {
8614 use crate::EditAction::{Copy, Cut, Paste, SelectAll};
8615 let tree = bar_tree(200.0, "hello world");
8616 let ui = bar_frame(&tree, selected(11, Some(6)), true, true, true);
8617 assert_eq!(ui.toolbars.len(), 1, "one bar");
8618 let bar = ui.toolbars[0];
8619 // Above the **selection**, a gap clear of it: not above the field, whose top edge a
8620 // bar over a line of text a few px down may overlap.
8621 let field = ui.widget_rect(bar_field_id(&tree)).expect("the field");
8622 let selection = Widget::<Msg>::selection_anchor(
8623 &TextField::<Msg>::new("hello world").width(200.0),
8624 field.width,
8625 &selected(11, Some(6)),
8626 0.0,
8627 )
8628 .expect("a selection box");
8629 let gap = field.y + selection.y - (bar.y + bar.height);
8630 assert!(
8631 (gap - crate::selectiontoolbar::GAP).abs() < 0.01,
8632 "the bar ends {gap} px above the selection: {bar:?}, selection at {selection:?} in {field:?}"
8633 );
8634 assert!(
8635 bar.x >= 0.0 && bar.x + bar.width <= 300.0,
8636 "inside the window"
8637 );
8638 assert_eq!(
8639 sorted_actions(reachable_actions(&ui)),
8640 sorted_actions(vec![Cut, Copy, Paste, SelectAll])
8641 );
8642 }
8643
8644 /// With no room above the field, the bar goes **below** the selection — clear of the
8645 /// handles that hang there.
8646 #[test]
8647 fn with_no_room_above_the_bar_goes_below_the_field() {
8648 let tree = bar_tree(0.0, "hello world");
8649 let ui = bar_frame(&tree, selected(11, Some(6)), true, true, true);
8650 let bar = *ui.toolbars.first().expect("a bar");
8651 let field = ui.widget_rect(bar_field_id(&tree)).expect("the field");
8652 assert!(
8653 bar.y >= field.y + field.height * 0.5,
8654 "below the selection's line: bar {bar:?}, field {field:?}"
8655 );
8656 }
8657
8658 /// **Only a marked, focused field shows its bar**: the same selection unmarked (made
8659 /// with a mouse or the keyboard) shows none, and neither does a marked field that lost
8660 /// the focus.
8661 #[test]
8662 fn the_bar_shows_only_for_the_marked_focused_field() {
8663 let tree = bar_tree(200.0, "hello world");
8664 let edit = selected(11, Some(6));
8665 let shown = |focused, marked| bar_frame(&tree, edit, true, focused, marked).toolbars.len();
8666 assert_eq!(shown(true, true), 1);
8667 assert_eq!(shown(true, false), 0, "not marked");
8668 assert_eq!(shown(false, true), 0, "a field left behind");
8669 }
8670
8671 /// **The list follows the three conditions**: nothing selected offers Paste and Select
8672 /// all; everything selected offers Cut and Copy; and a field with nothing in it and
8673 /// nothing to paste has no bar at all.
8674 #[test]
8675 fn the_bar_offers_what_the_situation_allows() {
8676 use crate::EditAction::{Copy, Cut, Paste, SelectAll};
8677 let tree = bar_tree(200.0, "hello world");
8678 // A caret, no selection, something to paste.
8679 let caret = bar_frame(&tree, selected(3, None), true, true, true);
8680 assert_eq!(
8681 sorted_actions(reachable_actions(&caret)),
8682 sorted_actions(vec![Paste, SelectAll])
8683 );
8684 // A caret and an empty clipboard: only Select all.
8685 let bare = bar_frame(&tree, selected(3, None), false, true, true);
8686 assert_eq!(reachable_actions(&bare), vec![SelectAll]);
8687 // Everything selected (`usize::MAX` is "the end", as Select all leaves it).
8688 let all = bar_frame(&tree, selected(usize::MAX, Some(0)), false, true, true);
8689 assert_eq!(
8690 sorted_actions(reachable_actions(&all)),
8691 sorted_actions(vec![Cut, Copy])
8692 );
8693 // An empty field with nothing to paste has nothing to offer.
8694 let empty = bar_tree(200.0, "");
8695 let nothing = bar_frame(&empty, selected(0, None), false, true, true);
8696 assert!(nothing.toolbars.is_empty());
8697 }
8698
8699 /// A press on the bar is known for one — the shell must not treat it as a press
8700 /// elsewhere, which puts the selection away.
8701 #[test]
8702 fn a_press_on_the_bar_is_a_press_on_the_bar() {
8703 let tree = bar_tree(200.0, "hello world");
8704 let ui = bar_frame(&tree, selected(11, Some(6)), true, true, true);
8705 let bar = ui.toolbars[0];
8706 assert!(ui.toolbar_contains(Point::new(bar.x + 3.0, bar.y + 3.0)));
8707 assert!(!ui.toolbar_contains(Point::new(bar.x - 5.0, bar.y - 5.0)));
8708 let unmarked = bar_frame(&tree, selected(11, Some(6)), true, true, false);
8709 assert!(!unmarked.toolbar_contains(Point::new(bar.x + 3.0, bar.y + 3.0)));
8710 }
8711
8712 /// The same, **by identity** — how an assistive technology names what it clicks: a button
8713 /// of the bar is on the bar, the field it acts on is not.
8714 #[test]
8715 fn a_target_named_by_identity_is_on_the_bar_or_not() {
8716 let tree = bar_tree(200.0, "hello world");
8717 let ui = bar_frame(&tree, selected(11, Some(6)), true, true, true);
8718 let bar = ui.toolbars[0];
8719 let button = ui
8720 .hit(Point::new(
8721 bar.x + bar.width * 0.5,
8722 bar.y + bar.height * 0.5,
8723 ))
8724 .filter(|id| ui.edit_action_for(*id).is_some())
8725 .or_else(|| {
8726 let mut x = bar.x + 1.0;
8727 let mut found = None;
8728 while x < bar.x + bar.width && found.is_none() {
8729 found = ui
8730 .hit(Point::new(x, bar.y + bar.height * 0.5))
8731 .filter(|id| ui.edit_action_for(*id).is_some());
8732 x += 2.0;
8733 }
8734 found
8735 })
8736 .expect("a button of the bar");
8737 assert!(ui.hit_is_on_toolbar(button));
8738 assert!(!ui.hit_is_on_toolbar(bar_field_id(&tree)));
8739 }
8740
8741 /// **The hooks pass through the wrappers that fuse with a field** — the same three silent
8742 /// bugs `Responsive` has already cost (milestones 477 to 495), for the bar's hooks.
8743 #[test]
8744 fn the_wrappers_that_fuse_with_a_field_pass_its_bar_on() {
8745 use crate::ToolbarContext;
8746 let field = || TextField::<Msg>::new("hello world").width(200.0);
8747 let context = ToolbarContext {
8748 has_selection: true,
8749 can_paste: true,
8750 all_selected: false,
8751 };
8752 let edit = selected(11, Some(6));
8753 let count = |widget: &dyn Widget<Msg>| {
8754 widget
8755 .selection_toolbar(context)
8756 .map(|bar| bar.children().len())
8757 };
8758 let direct = count(&field());
8759 assert_eq!(direct, Some(4));
8760 let anchor = Widget::<Msg>::selection_anchor(&field(), 200.0, &edit, 0.0);
8761 assert!(anchor.is_some());
8762 let keyed = crate::Keyed::new(1, field());
8763 let responsive = crate::Responsive::new(crate::SizeClass::Compact).compact(field());
8764 let boxed: Box<dyn Widget<Msg>> = Box::new(field());
8765 assert_eq!(count(&keyed), direct);
8766 assert_eq!(count(&responsive), direct);
8767 assert_eq!(count(&boxed), direct);
8768 assert_eq!(keyed.selection_anchor(200.0, &edit, 0.0), anchor);
8769 assert_eq!(responsive.selection_anchor(200.0, &edit, 0.0), anchor);
8770 assert_eq!(boxed.selection_anchor(200.0, &edit, 0.0), anchor);
8771 }
8772
8773 /// Two fields, one marked: **one bar**, over the marked one — not one over each field
8774 /// that happens to hold a selection.
8775 #[test]
8776 fn of_two_fields_only_the_marked_one_shows_a_bar() {
8777 let tree = Flex::column()
8778 .width(300.0)
8779 .height(400.0)
8780 .child(Container::new().width(300.0).height(150.0))
8781 .child(
8782 TextField::new("first field")
8783 .width(200.0)
8784 .on_input(Msg::Edited),
8785 )
8786 .child(
8787 TextField::new("second field")
8788 .width(200.0)
8789 .on_input(Msg::Edited),
8790 );
8791 let theme = pill_theme();
8792 let size = Size::new(300.0, 400.0);
8793 let ids: Vec<WidgetId> = build_ui(&tree, size, &Runtime::default(), &theme)
8794 .focusable_ids()
8795 .collect();
8796 assert_eq!(ids.len(), 2);
8797 let mut rt = Runtime::default();
8798 for id in &ids {
8799 rt.edits.insert(*id, selected(5, Some(0)));
8800 }
8801 // Focus follows the marked field; the other holds a selection too, unfocused.
8802 rt.input.focused = Some(ids[1]);
8803 rt.selection_toolbar = Some(crate::runtime::ToolbarMark {
8804 id: ids[1],
8805 can_paste: true,
8806 at: None,
8807 });
8808 let ui = build_ui(&tree, size, &rt, &theme);
8809 assert_eq!(ui.toolbars.len(), 1);
8810 let second = ui.widget_rect(ids[1]).expect("the second field");
8811 let bar = ui.toolbars[0];
8812 // Above the second field's line and below the first: it belongs to the second.
8813 assert!(
8814 bar.y + bar.height <= second.y + second.height,
8815 "{bar:?} vs {second:?}"
8816 );
8817 assert!(
8818 bar.y > 100.0,
8819 "not over the first field's row, which is at 150: {bar:?}"
8820 );
8821 // The bar is the *marked* field's: focus on the other one opens nothing.
8822 rt.input.focused = Some(ids[0]);
8823 let elsewhere = build_ui(&tree, size, &rt, &theme);
8824 assert!(elsewhere.toolbars.is_empty());
8825 }
8826
8827 /// **The paint cache tells a field with its bar open from one without** (milestone 568).
8828 /// The fingerprint of a cached subtree is the statuses inside it, so two statuses that
8829 /// differ only in the bar — or in the handles, which it had never held — must hash apart,
8830 /// or the cache would replay the frame from before the bar opened.
8831 #[test]
8832 fn the_bar_and_the_handles_are_part_of_a_fields_fingerprint() {
8833 let hashed = |status: &Status| {
8834 let mut hasher = std::collections::hash_map::DefaultHasher::new();
8835 hash_status(status, &mut hasher);
8836 hasher.finish()
8837 };
8838 let plain = Status::default();
8839 let open = Status {
8840 toolbar: Some(true),
8841 ..Status::default()
8842 };
8843 let open_without_paste = Status {
8844 toolbar: Some(false),
8845 ..Status::default()
8846 };
8847 let handled = Status {
8848 handles: true,
8849 ..Status::default()
8850 };
8851 assert_ne!(hashed(&plain), hashed(&open), "the bar opening");
8852 assert_ne!(
8853 hashed(&open),
8854 hashed(&open_without_paste),
8855 "the clipboard emptying"
8856 );
8857 assert_ne!(hashed(&plain), hashed(&handled), "the handles appearing");
8858 }
8859
8860 /// **A field knows the form it is part of** (milestone 512): the stops inside one
8861 /// [`crate::AutofillGroup`] are that form, in tree order, whichever of them asks; two
8862 /// groups side by side are two forms; a field in no group is a form of one; and a
8863 /// group sits in the focus registry, so a cached subtree replays it like any stop.
8864 #[test]
8865 fn a_field_knows_the_form_it_is_part_of() {
8866 use crate::{AutofillGroup, AutofillHint};
8867 let field = |value: &str, hint: AutofillHint| {
8868 TextField::new(value)
8869 .width(200.0)
8870 .on_input(Msg::Edited)
8871 .autofill([hint])
8872 };
8873 let tree = Flex::column()
8874 .width(300.0)
8875 .height(400.0)
8876 .child(AutofillGroup::new(
8877 Flex::column()
8878 .child(field("someone", AutofillHint::Username))
8879 .child(field("secret", AutofillHint::Password)),
8880 ))
8881 .child(AutofillGroup::new(field(
8882 "123456",
8883 AutofillHint::OneTimeCode,
8884 )))
8885 .child(TextField::new("search").width(200.0).on_input(Msg::Edited));
8886 let ui = build_ui(
8887 &tree,
8888 Size::new(300.0, 400.0),
8889 &Runtime::default(),
8890 &Theme::default(),
8891 );
8892 let ids: Vec<WidgetId> = ui.focusable_ids().collect();
8893 assert_eq!(ids.len(), 4);
8894 let form = |id: WidgetId| ui.form_of(id).iter().map(|(f, _)| *f).collect::<Vec<_>>();
8895 assert_eq!(form(ids[0]), vec![ids[0], ids[1]], "the sign-in pair");
8896 assert_eq!(
8897 form(ids[1]),
8898 vec![ids[0], ids[1]],
8899 "whichever of the two asks"
8900 );
8901 assert_eq!(
8902 form(ids[2]),
8903 vec![ids[2]],
8904 "a second group is a second form"
8905 );
8906 assert_eq!(
8907 form(ids[3]),
8908 vec![ids[3]],
8909 "a field in no group is a form of one"
8910 );
8911 assert!(ui.form_of(WidgetId::from_u64(7)).is_empty());
8912 // The two groups are told apart by identity, and a lone field has none.
8913 let (first, second) = (ui.form_group(ids[0]), ui.form_group(ids[2]));
8914 assert!(first.is_some() && second.is_some() && first != second);
8915 assert_eq!(ui.form_group(ids[3]), None);
8916 assert_eq!(ui.group_stops(first.unwrap()).len(), 2);
8917 // Each stop carries its box, which is what the platform anchors its offer to.
8918 assert!(ui.form_of(ids[1]).iter().all(|(_, r)| r.height > 0.0));
8919 }
8920
8921 #[test]
8922 fn find_widget_and_edit_types() {
8923 let tree = Flex::column()
8924 .width(300.0)
8925 .height(80.0)
8926 .child(TextField::new("hi").width(200.0).on_input(Msg::Edited));
8927 let rt = Runtime::default();
8928 let ui = build_ui(&tree, Size::new(300.0, 80.0), &rt, &Theme::default());
8929 let (id, _rect) = ui.focus_hit(Point::new(10.0, 10.0)).expect("the field");
8930
8931 let widget = find_widget(&tree, id).expect("the widget was found");
8932 let mut edit = Edit {
8933 cursor: 2,
8934 anchor: None,
8935 composing: None,
8936 };
8937 assert_eq!(
8938 widget.on_edit(&mut edit, &Key::Text("!".to_string())),
8939 Some(Msg::Edited("hi!".to_string()))
8940 );
8941 }
8942
8943 #[test]
8944 fn a_traversal_can_arrive_before_the_layout_pass() {
8945 // Milestone 415. A `ThemeBuilder`'s subtree does not exist until something has
8946 // called `build_themed` on it, and `children()` says so in its own doc: "a
8947 // traversal that arrives first should call `build_themed`, not this".
8948 //
8949 // The shell has such a traversal. On a **burst of keystrokes** — a software
8950 // keyboard, `adb input text`, auto-repeat — it rebuilds the tree from `view` alone
8951 // and reads it immediately, with no layout pass in between, so every deferred
8952 // subtree in it is empty and every widget inside one is unreachable.
8953 let deferred = |value: &str| {
8954 let value = value.to_string();
8955 crate::ThemeBuilder::new(move |_: &Theme| {
8956 TextField::new(value.clone())
8957 .width(200.0)
8958 .on_input(Msg::Edited)
8959 })
8960 };
8961 let laid_out = Flex::column()
8962 .width(300.0)
8963 .height(80.0)
8964 .child(deferred("hi"));
8965 let rt = Runtime::default();
8966 let ui = build_ui(&laid_out, Size::new(300.0, 80.0), &rt, &Theme::default());
8967 let (id, _) = ui.focus_hit(Point::new(10.0, 10.0)).expect("the field");
8968 assert!(
8969 find_widget(&laid_out, id).is_some(),
8970 "after a layout pass the field is where the hit-test said it was"
8971 );
8972
8973 // The same tree, freshly built and not yet laid out. `build_deferred` is what the
8974 // layout pass does on its way down, and nothing else.
8975 let fresh = Flex::column()
8976 .width(300.0)
8977 .height(80.0)
8978 .child(deferred("hi"));
8979 build_deferred(&fresh, &Theme::default(), &Runtime::default());
8980 assert!(
8981 find_widget(&fresh, id).is_some(),
8982 "prepared the same way, the same traversal reaches the same widget"
8983 );
8984 }
8985
8986 #[test]
8987 #[should_panic(expected = "before it was built")]
8988 fn reading_an_unbuilt_deferred_subtree_trips_the_wire() {
8989 // Without the preparation the traversal used to return `None` and the caller
8990 // shrugged: no field found, no caret revealed, nothing said. The failure mode was
8991 // **silence**, so the guard is a panic in debug rather than a comment.
8992 let fresh =
8993 Flex::<Msg>::column()
8994 .width(300.0)
8995 .height(80.0)
8996 .child(crate::ThemeBuilder::new(|_: &Theme| {
8997 Container::<Msg>::new().width(10.0).height(10.0)
8998 }));
8999 let _ = find_widget(&fresh, WidgetId::ROOT.child(9));
9000 }
9001
9002 #[test]
9003 fn a_deferred_subtree_is_built_under_its_own_theme() {
9004 // The layout pass swaps the theme before it builds (`theme_override`), so a
9005 // builder inside a `Themed` sees the subtree's theme and not the frame's.
9006 // `build_deferred` has to make the same swap or it would build the wrong subtree
9007 // — and being *nearly* the walk is worse than not being it at all.
9008 let seen = std::rc::Rc::new(std::cell::Cell::new(0.0f32));
9009 let recorder = {
9010 let seen = seen.clone();
9011 crate::ThemeBuilder::new(move |theme: &Theme| {
9012 seen.set(theme.radius);
9013 Container::<Msg>::new().width(10.0).height(10.0)
9014 })
9015 };
9016 let inner = Theme {
9017 radius: 99.0,
9018 ..Theme::default()
9019 };
9020 let tree = Flex::<Msg>::column().child(crate::Themed::new(inner, recorder));
9021 build_deferred(&tree, &Theme::default(), &Runtime::default());
9022 assert_eq!(seen.get(), 99.0, "the subtree's theme, not the frame's");
9023 }
9024
9025 #[test]
9026 fn find_by_key_resolves_a_keyed_field_to_its_focus_id() {
9027 // Two named fields: `find_by_key` finds each one's focus identity (the one the
9028 // hit-test would assign), and it tells the keys apart.
9029 fn hash(k: &str) -> u64 {
9030 use std::hash::{Hash, Hasher};
9031 let mut h = std::collections::hash_map::DefaultHasher::new();
9032 k.hash(&mut h);
9033 h.finish()
9034 }
9035 let tree: Flex<Msg> = Flex::column()
9036 .child(Keyed::new(
9037 "email",
9038 TextField::new("").on_input(Msg::Edited),
9039 ))
9040 .child(Keyed::new(
9041 "password",
9042 TextField::new("").on_input(Msg::Edited),
9043 ));
9044
9045 let email = find_by_key(&tree, hash("email")).expect("email was found");
9046 let password = find_by_key(&tree, hash("password")).expect("password was found");
9047 assert_ne!(email, password, "distinct keys → distinct identities");
9048 // The identity resolved is indeed the by-key identity (stable, independent of the
9049 // position).
9050 assert_eq!(email, WidgetId::ROOT.keyed(hash("email")));
9051 assert!(
9052 find_by_key(&tree, hash("absent")).is_none(),
9053 "an unknown key → None"
9054 );
9055
9056 // And above all: it is **exactly** the identity the focus hit-test would assign to
9057 // that field — so setting this focus really does route to it.
9058 let rt = Runtime::default();
9059 let ui = build_ui(&tree, Size::new(300.0, 200.0), &rt, &Theme::default());
9060 let (hit_id, _) = ui
9061 .focus_hit(Point::new(10.0, 10.0))
9062 .expect("the email field under the cursor");
9063 assert_eq!(email, hit_id, "find_by_key == the field's focus identity");
9064 }
9065
9066 #[test]
9067 fn ink_is_painted_over_the_surface_and_under_its_child() {
9068 use crate::ink::InkWell;
9069 let tree = Container::<Msg>::new().width(120.0).height(60.0).child(
9070 InkWell::<Msg>::new().radius(8.0).on_click(Msg::A).child(
9071 Container::<Msg>::new()
9072 .width(120.0)
9073 .height(60.0)
9074 .color(frus_core::Color::WHITE),
9075 ),
9076 );
9077 let size = Size::new(120.0, 60.0);
9078
9079 // Dry: the well paints nothing of its own.
9080 let rt = Runtime::default();
9081 let dry = build_ui(&tree, size, &rt, &Theme::default());
9082 let plain = dry.scene.len();
9083 let id = dry.inks.first().expect("the well registered its box").0;
9084 assert_eq!(
9085 dry.ink_box(id).map(|r| (r.width, r.height)),
9086 Some((120.0, 60.0)),
9087 "the registry carries the **whole** box, which is what sizes the splash"
9088 );
9089
9090 // A finger lands in the top-left corner, and is still down.
9091 let mut rt = Runtime::default();
9092 rt.input.pressed = Some(id);
9093 rt.ink_press(id, Point::new(10.0, 10.0), Size::new(120.0, 60.0));
9094 rt.advance_ink(1.0 / 60.0);
9095 let ui = build_ui(&tree, size, &rt, &Theme::default());
9096 assert_eq!(ui.scene.len(), plain + 1, "the splash adds one layer");
9097
9098 // It is a shape-clipped layer, so the ink cannot escape the rounded corners…
9099 let (index, clip_shape) = ui
9100 .scene
9101 .primitives()
9102 .iter()
9103 .enumerate()
9104 .find_map(|(i, p)| match p {
9105 frus_core::Primitive::Layer { clip_shape, .. } => Some((i, clip_shape.clone())),
9106 _ => None,
9107 })
9108 .expect("the ink is a composited layer");
9109 assert_eq!(clip_shape, frus_core::ClipShape::RRect(8.0.into()));
9110
9111 // …and it is painted **before** the child that sits on top of it: the ink is
9112 // under the content, the way a material surface holds it.
9113 let child_index = ui
9114 .scene
9115 .primitives()
9116 .iter()
9117 .rposition(|p| matches!(p, frus_core::Primitive::Rect { .. }))
9118 .expect("the child painted a rectangle");
9119 assert!(
9120 index < child_index,
9121 "ink at {index} must come before the content at {child_index}"
9122 );
9123 }
9124
9125 #[test]
9126 fn a_splash_left_unconfirmed_does_not_wait_for_ever() {
9127 // The finger came down and never came back up on this widget — it slid off, or
9128 // the widget went away mid-press. Nothing will ever confirm the splash, and
9129 // without the sweep in `advance_ink` the ink would sit there for good.
9130 let mut rt = Runtime::default();
9131 let id = WidgetId::from_u64(42);
9132 rt.input.pressed = Some(id);
9133 rt.ink_press(id, Point::new(5.0, 5.0), Size::new(100.0, 40.0));
9134 // Held for a fifth of a second: the ink is up, and it stays up.
9135 for _ in 0..12 {
9136 rt.advance_ink(1.0 / 60.0);
9137 }
9138 assert!(rt.ink.contains_key(&id), "the splash waits for the finger");
9139
9140 // The finger is gone and nothing confirmed the tap.
9141 rt.input.pressed = None;
9142 let mut frames = 0;
9143 while rt.advance_ink(1.0 / 60.0) && frames < 200 {
9144 frames += 1;
9145 }
9146 assert!(rt.ink.is_empty(), "the ink is gone after {frames} frames");
9147 assert!(frames <= 6, "and quickly: a cancel is 75 ms, took {frames}");
9148 }
9149
9150 #[test]
9151 fn an_overscroll_glow_is_painted_over_its_scroll_area() {
9152 let tree = SingleChildScrollView::<Msg>::new()
9153 .width(200.0)
9154 .height(100.0)
9155 .child(Container::<Msg>::new().width(100.0).height(400.0));
9156 let size = Size::new(200.0, 100.0);
9157
9158 // Quiet: nothing extra is drawn.
9159 let rt = Runtime::default();
9160 let plain = build_ui(&tree, size, &rt, &Theme::default()).scene.len();
9161
9162 // A fling has just landed on the bottom edge.
9163 let mut rt = Runtime::default();
9164 let id = build_ui(&tree, size, &rt, &Theme::default()).scroll_regions()[0].id;
9165 rt.glow_absorb(
9166 id,
9167 Some(crate::physics::OverscrollIndicator::Glow),
9168 crate::overscroll::GlowEdge::Bottom,
9169 4000.0,
9170 );
9171 rt.advance_glow(0.02);
9172 let ui = build_ui(&tree, size, &rt, &Theme::default());
9173 assert_eq!(
9174 ui.scene.len(),
9175 plain + 1,
9176 "the glow adds exactly one shape to the frame"
9177 );
9178 // The glow sits under the scrollbars, so it is not the last primitive: it is
9179 // the only filled path in the frame.
9180 let clip = ui
9181 .scene
9182 .primitives()
9183 .iter()
9184 .find_map(|p| match p {
9185 frus_core::Primitive::Path { clip, .. } => Some(*clip),
9186 _ => None,
9187 })
9188 .expect("the glow is a filled path");
9189 // Drawn against the bottom of the viewport, and inside it.
9190 assert!(
9191 (clip.y + clip.height - 100.0).abs() < 1.0,
9192 "clip = {clip:?}"
9193 );
9194 assert!(clip.width <= 200.0 + 1e-3);
9195 }
9196
9197 /// **A list's rows go through the same walk as the rest of the tree** (milestone 592): a
9198 /// carousel in each row is a scroll area of its own. The reduced walk the rows used to take
9199 /// knew no scroll area.
9200 #[test]
9201 fn a_list_row_is_walked_like_the_rest_of_the_tree() {
9202 let tree = crate::ListView::<Msg>::new(20, 60.0, |_| {
9203 SingleChildScrollView::<Msg>::new()
9204 .axis(crate::scroll::Axis::Horizontal)
9205 .height(60.0)
9206 .child(Container::<Msg>::new().width(900.0).height(60.0))
9207 })
9208 .width(300.0)
9209 .height(200.0);
9210 let ui = build_ui(
9211 &tree,
9212 Size::new(300.0, 200.0),
9213 &Runtime::default(),
9214 &Theme::default(),
9215 );
9216 let regions = ui.scroll_regions();
9217 assert!(
9218 regions.len() >= 4,
9219 "the list and a carousel per visible row: {}",
9220 regions.len()
9221 );
9222 assert!(
9223 regions.iter().any(|r| r.max_x > 0.0),
9224 "a row scrolls across: {regions:?}"
9225 );
9226 }
9227
9228 /// The stretch layers of a frame: their owner and their transform.
9229 fn stretch_layers(ui: &Ui<Msg>) -> Vec<(u64, frus_core::Affine)> {
9230 ui.scene
9231 .primitives()
9232 .iter()
9233 .filter_map(|p| match p {
9234 frus_core::Primitive::Layer {
9235 transform: Some(transform),
9236 owner,
9237 ..
9238 } => Some((*owner, transform.affine)),
9239 _ => None,
9240 })
9241 .collect()
9242 }
9243
9244 /// **Where the indicator is the stretch, a pull deforms the content and draws no
9245 /// glow** (milestone 591): one layer holds the whole content, scaled down the axis
9246 /// from the top it is pulled past, and a button in it is touched where it is drawn.
9247 #[test]
9248 fn a_stretch_scales_the_content_from_the_edge_pulled() {
9249 let tree = SingleChildScrollView::<Msg>::new()
9250 .width(200.0)
9251 .height(100.0)
9252 .child(
9253 Flex::<Msg>::column()
9254 .child(Container::<Msg>::new().width(100.0).height(50.0))
9255 .child(crate::Button::new("Go").on_press(Msg::A))
9256 .child(Container::<Msg>::new().width(100.0).height(400.0)),
9257 );
9258 let size = Size::new(200.0, 100.0);
9259 let mut rt = Runtime::default();
9260 let quiet = build_ui(&tree, size, &rt, &Theme::default());
9261 let id = quiet.scroll_regions()[0].id;
9262 assert!(
9263 stretch_layers(&quiet).is_empty(),
9264 "at rest, nothing is deformed"
9265 );
9266 let button = quiet
9267 .focusable_ids()
9268 .find_map(|b| quiet.widget_rect(b))
9269 .expect("the button");
9270
9271 rt.glow_pull(
9272 id,
9273 Some(crate::physics::OverscrollIndicator::Stretch),
9274 crate::overscroll::GlowEdge::Top,
9275 100.0,
9276 100.0,
9277 0.0,
9278 200.0,
9279 );
9280 assert!(
9281 rt.scroll_glow.is_empty(),
9282 "no glow where the content stretches"
9283 );
9284 let ui = build_ui(&tree, size, &rt, &Theme::default());
9285 let layers = stretch_layers(&ui);
9286 assert_eq!(layers.len(), 1, "{layers:?}");
9287 let (owner, m) = layers[0];
9288 assert_eq!(owner, id.as_u64());
9289 let top = m.apply(Point::new(0.0, 0.0));
9290 let low = m.apply(Point::new(200.0, 100.0));
9291 assert!(top.y.abs() < 1e-3, "the top holds: {top:?}");
9292 assert!((low.x - 200.0).abs() < 1e-3, "not across: {low:?}");
9293 assert!(low.y > 100.5, "down the axis: {low:?}");
9294 let moved = ui
9295 .focusable_ids()
9296 .find_map(|b| ui.widget_rect(b))
9297 .expect("the button");
9298 assert!(
9299 moved.y > button.y && moved.height > button.height,
9300 "touched where drawn: {button:?} -> {moved:?}"
9301 );
9302
9303 // Letting go springs it back, and the layer goes with it.
9304 rt.glow_scroll_end(id);
9305 while rt.advance_glow(1.0 / 60.0) {}
9306 assert!(rt.scroll_stretch.is_empty());
9307 assert!(stretch_layers(&build_ui(&tree, size, &rt, &Theme::default())).is_empty());
9308 }
9309
9310 /// A list stretches the same way, from the bottom when pulled past its end.
9311 #[test]
9312 fn a_list_stretches_from_its_end() {
9313 let tree = crate::ListView::<Msg>::new(50, 20.0, |i| crate::Text::new(format!("{i}")))
9314 .width(200.0)
9315 .height(100.0);
9316 let size = Size::new(200.0, 100.0);
9317 let mut rt = Runtime::default();
9318 let id = build_ui(&tree, size, &rt, &Theme::default()).scroll_regions()[0].id;
9319 rt.glow_absorb(
9320 id,
9321 Some(crate::physics::OverscrollIndicator::Stretch),
9322 crate::overscroll::GlowEdge::Bottom,
9323 4000.0,
9324 );
9325 rt.advance_glow(0.02);
9326 let ui = build_ui(&tree, size, &rt, &Theme::default());
9327 let layers = stretch_layers(&ui);
9328 assert_eq!(layers.len(), 1);
9329 let m = layers[0].1;
9330 assert!((m.apply(Point::new(0.0, 100.0)).y - 100.0).abs() < 1e-3);
9331 assert!(m.apply(Point::new(0.0, 0.0)).y < -0.5);
9332 }
9333
9334 /// **An area's own physics lie over its behaviour's** (milestone 649): never or always
9335 /// scrollable keeps the platform's feel, and a feel given keeps the platform's rule.
9336 #[test]
9337 fn an_area_s_physics_lie_over_its_behaviour_s() {
9338 use crate::physics::{ScrollFeel, UserScroll};
9339 use frus_core::TargetPlatform as P;
9340 let scroll = |physics: ScrollPhysics, height: f32| {
9341 SingleChildScrollView::new()
9342 .width(200.0)
9343 .height(100.0)
9344 .physics(physics)
9345 .child(Container::<Msg>::new().width(100.0).height(height))
9346 };
9347 let rt = Runtime::default();
9348 let _root = crate::ScrollConfiguration::install(crate::ScrollBehavior::material());
9349 let region = |tree: &dyn Widget<Msg>, platform: P| {
9350 let theme = Theme::default().with_platform(platform);
9351 build_ui(tree, Size::new(200.0, 100.0), &rt, &theme).scroll_regions()[0]
9352 };
9353 let never = region(&scroll(ScrollPhysics::NEVER, 400.0), P::Ios);
9354 assert_eq!(never.physics.user(), UserScroll::Never);
9355 assert_eq!(
9356 never.physics.feel(),
9357 ScrollFeel::Bouncing(crate::physics::ScrollDecelerationRate::Normal),
9358 "the platform's feel"
9359 );
9360 assert!(
9361 !never.accepts_user_offset((0.0, 0.0)),
9362 "content to reveal, and no finger"
9363 );
9364 let always = region(&scroll(ScrollPhysics::ALWAYS, 50.0), P::Android);
9365 assert_eq!(always.physics.feel(), ScrollFeel::Clamping);
9366 assert!(
9367 always.accepts_user_offset((0.0, 0.0)),
9368 "nothing to reveal, and a finger"
9369 );
9370 assert_eq!(always.axis, crate::scroll::Axis::Vertical);
9371 let bouncing = region(&scroll(ScrollPhysics::BOUNCING, 400.0), P::Android);
9372 assert_eq!(
9373 bouncing.physics,
9374 ScrollPhysics::BOUNCING,
9375 "a feel given, the default rule"
9376 );
9377 }
9378
9379 /// **What an area does is its scroll behaviour's, settled when it is registered**
9380 /// (milestone 616): under the application's behaviour it follows the theme's
9381 /// platform, a `ScrollConfiguration` changes it for its subtree, and the area's own
9382 /// physics win over both.
9383 #[test]
9384 fn a_scroll_area_carries_its_physics_into_the_registry() {
9385 use frus_core::TargetPlatform as P;
9386 let tall = || Container::<Msg>::new().width(100.0).height(400.0);
9387 let plain = || {
9388 SingleChildScrollView::new()
9389 .width(200.0)
9390 .height(100.0)
9391 .child(tall())
9392 };
9393 let rt = Runtime::default();
9394 let _root = crate::ScrollConfiguration::install(crate::ScrollBehavior::material());
9395 let region = |tree: &dyn Widget<Msg>, platform: P| {
9396 let theme = Theme::default().with_platform(platform);
9397 build_ui(tree, Size::new(200.0, 100.0), &rt, &theme).scroll_regions()[0]
9398 };
9399
9400 // Untold: the theme's platform decides, through the behaviour.
9401 let ios = region(&plain(), P::Ios);
9402 assert_eq!(ios.physics, ScrollPhysics::BOUNCING);
9403 assert_eq!(ios.overscroll, None, "nothing past the edge on iOS");
9404 assert_eq!(
9405 ios.fling,
9406 frus_core::VelocityStrategy::RecentAverage(frus_core::BOUNCING_FLING_WEIGHTS)
9407 );
9408 let android = region(&plain(), P::Android);
9409 assert_eq!(android.physics, ScrollPhysics::CLAMPING);
9410 assert_eq!(
9411 android.overscroll,
9412 Some(crate::OverscrollIndicator::Stretch)
9413 );
9414 assert_eq!(android.fling, frus_core::VelocityStrategy::Regression);
9415
9416 // A subtree under another behaviour scrolls as that one says.
9417 let configured = crate::ScrollConfiguration::new(
9418 crate::ScrollBehavior::material()
9419 .with_physics(ScrollPhysics::BOUNCING)
9420 .with_overscroll(false),
9421 plain(),
9422 );
9423 let inside = region(&configured, P::Android);
9424 assert_eq!(inside.physics, ScrollPhysics::BOUNCING);
9425 assert_eq!(inside.overscroll, None);
9426 // And only that subtree: the behaviour is put back after it.
9427 assert_eq!(
9428 crate::ScrollConfiguration::of(),
9429 crate::ScrollBehavior::material()
9430 );
9431
9432 // Told: the area's own choice wins over the behaviour's.
9433 let bouncy = SingleChildScrollView::new()
9434 .width(200.0)
9435 .height(100.0)
9436 .physics(ScrollPhysics::BOUNCING)
9437 .child(tall());
9438 let area = region(&bouncy, P::Android);
9439 assert_eq!(area.physics, ScrollPhysics::BOUNCING);
9440 // And the metrics it hands the physics describe the right axis.
9441 let metrics = area.metrics_y(10.0);
9442 assert_eq!(metrics.pixels, 10.0);
9443 assert_eq!(metrics.max, 300.0); // 400 of content in a 100 viewport
9444 assert_eq!(metrics.viewport, 100.0);
9445 }
9446
9447 #[test]
9448 fn scroll_translates_and_clips_content() {
9449 let content = Flex::<Msg>::column()
9450 .gap(0.0)
9451 .child(
9452 Container::new()
9453 .height(60.0)
9454 .color(Color::rgb(1.0, 0.0, 0.0)),
9455 )
9456 .child(
9457 Container::new()
9458 .height(60.0)
9459 .color(Color::rgb(0.0, 1.0, 0.0)),
9460 )
9461 .child(
9462 Container::new()
9463 .height(60.0)
9464 .color(Color::rgb(0.0, 0.0, 1.0)),
9465 );
9466 let tree = SingleChildScrollView::new()
9467 .width(200.0)
9468 .height(100.0)
9469 .child(content);
9470
9471 let rt = Runtime::default();
9472 let ui = build_ui(&tree, Size::new(200.0, 100.0), &rt, &Theme::default());
9473 let area = ui.scrollables[0];
9474 let sid = area.id;
9475 assert_eq!(area.max_y, 80.0); // 180 - 100
9476 assert_eq!(area.max_x, 0.0);
9477 assert_eq!(ui.first_rect().0.y, 0.0);
9478 assert_eq!(ui.first_rect().1, Rect::new(0.0, 0.0, 200.0, 100.0));
9479
9480 let mut rt = Runtime::default();
9481 rt.scroll.insert(sid, (0.0, 50.0));
9482 let ui2 = build_ui(&tree, Size::new(200.0, 100.0), &rt, &Theme::default());
9483 assert_eq!(ui2.first_rect().0.y, -50.0);
9484 }
9485
9486 #[test]
9487 fn kanban_cards_are_reorderable_without_being_clickable() {
9488 use crate::Kanban;
9489 let board = Kanban::new(|_, _, _, _| Msg::A).column("To do", ["Card A"]);
9490 let rt = Runtime::default();
9491 let ui = build_ui(&board, Size::new(400.0, 300.0), &rt, &Theme::default());
9492 // The card **and** the drop zone are registered as reorderables.
9493 assert!(
9494 ui.reorderables.len() >= 2,
9495 "the card + the drop zone are in the reorderables registry"
9496 );
9497 // A point on the card can be **picked up** (it is reorderable) but is **not clickable**
9498 // — which is the whole point of the registry: without it, `ui.hit` alone would never
9499 // find the card.
9500 let (card_id, card) = ui.reorderables[0];
9501 let p = Point::new(card.x + 5.0, card.y + 5.0);
9502 assert!(
9503 ui.reorderable_at(p).is_some(),
9504 "the card can be picked up at that point"
9505 );
9506 assert!(
9507 ui.hit(p).is_none(),
9508 "…but it is not clickable (absent from the hit registry)"
9509 );
9510 // `widget_rect` must find the card **through the reorderable fallback** (it is not
9511 // focusable): otherwise the shell's vertical drag preview never starts.
9512 assert_eq!(
9513 ui.widget_rect(card_id),
9514 Some(card),
9515 "widget_rect falls back to the reorderables registry"
9516 );
9517 }
9518
9519 #[test]
9520 fn reorderables_inside_a_scroll_are_still_registered() {
9521 // The milestone 258/260 scenario: the board (with its reorderable cards) is **wrapped
9522 // in a `SingleChildScrollView`**. The reorderables must stay registered — otherwise dragging a card
9523 // stops engaging as soon as the board scrolls.
9524 use crate::{Axis, Kanban, SingleChildScrollView};
9525 let board = Kanban::new(|_, _, _, _| Msg::A).column("To do", ["Card A"]);
9526 let scrolled = SingleChildScrollView::new()
9527 .axis(Axis::Horizontal)
9528 .width(400.0)
9529 .height(300.0)
9530 .child(board);
9531 let rt = Runtime::default();
9532 let ui = build_ui(&scrolled, Size::new(400.0, 300.0), &rt, &Theme::default());
9533 assert!(
9534 ui.reorderables.len() >= 2,
9535 "the card + the drop zone stay reorderable even inside a SingleChildScrollView (got {})",
9536 ui.reorderables.len()
9537 );
9538 }
9539
9540 #[test]
9541 fn reorderables_inside_a_per_column_card_scroll_are_still_registered() {
9542 // Milestone 264: `card_area_height` puts each column's cards inside a **vertical
9543 // `SingleChildScrollView` with an explicit height**. This is exactly the case that *collapsed* at
9544 // milestone 263 (a flex scroll with no defined ancestor height → cards clipped to zero
9545 // and no longer reorderable). With a **defined** height the visible cards must stay
9546 // registered as reorderables — a guard against a regression of per-column dragging.
9547 use crate::Kanban;
9548 let board = Kanban::new(|_, _, _, _| Msg::A)
9549 .card_area_height(220.0)
9550 .column("To do", ["Card A", "Card B"]);
9551 let rt = Runtime::default();
9552 let ui = build_ui(&board, Size::new(400.0, 300.0), &rt, &Theme::default());
9553 assert!(
9554 ui.reorderables.len() >= 3,
9555 "2 cards + the drop zone stay reorderable inside a vertical scroll with a defined height \
9556 (got {})",
9557 ui.reorderables.len()
9558 );
9559 }
9560
9561 #[test]
9562 fn scrollable_columns_fill_the_board_height_then_scroll() {
9563 // Milestone 266: `Kanban::scrollable_columns()` makes the columns **fill** the board's
9564 // height (laid out in an ancestor with a defined height), and each column scrolls its
9565 // cards vertically **with no explicit height** (the flex does the arithmetic, so the
9566 // milestone 264 stopgap is gone). This checks that at least one column has a vertical
9567 // `SingleChildScrollView` whose viewport **fills** the height (far more than the default 200) and
9568 // **scrolls** (max_y > 0), which is the proof of fill-then-scroll.
9569 use crate::{Axis, Container, Flex, Kanban, SingleChildScrollView};
9570 // Enough cards to **overflow** the filled viewport (otherwise max_y = 0: nothing to scroll).
9571 let long: Vec<String> = (0..24).map(|i| format!("card {i}")).collect();
9572 let board = Kanban::new(|_, _, _, _| Msg::A)
9573 .scrollable_columns()
9574 .column("To do", long);
9575 // `board_screen`'s nesting: the board inside a **plain `Container` with padding** (the
9576 // visual margin), itself inside a horizontal `flex(1)` SingleChildScrollView, inside a screen (a Flex
9577 // column) of bounded height. That `Auto` `Container` **collapsed** at milestone 266
9578 // (hence the `Flex` `flex(1)` workaround); since `compute_scroll` **fills the
9579 // constrained axis**, it fills the viewport's height and the board follows — no filler
9580 // container needed any more.
9581 let padded = Container::<Msg>::new().padding(24.0).child(board);
9582 let scroll_h = SingleChildScrollView::new()
9583 .axis(Axis::Horizontal)
9584 .width(360.0)
9585 .flex(1.0)
9586 .child(padded);
9587 let root: Flex<Msg> = Flex::column().width(400.0).height(600.0).child(scroll_h);
9588 let ui = build_ui(
9589 &root,
9590 Size::new(400.0, 600.0),
9591 &Runtime::default(),
9592 &Theme::default(),
9593 );
9594 // A column's **vertical** scroll: a tall viewport (it fills) and it scrolls.
9595 let filled = ui
9596 .scrollables
9597 .iter()
9598 .any(|area| area.viewport.height > 300.0 && area.max_y > 0.0);
9599 assert!(
9600 filled,
9601 "a column fills the board's height then scrolls (scrollables: {:?})",
9602 ui.scrollables
9603 );
9604 }
9605
9606 #[test]
9607 fn subtree_ids_covers_a_widget_and_its_descendants() {
9608 use crate::Text;
9609 // Root (Container) > Flex(column) > [Text, Text].
9610 let tree: Container<Msg> = Container::new().child(
9611 Flex::<Msg>::column()
9612 .child(Text::new("a"))
9613 .child(Text::new("b")),
9614 );
9615 let all = collect_ids(&tree);
9616 // From the root: identical to `collect_ids` (the same positional walk).
9617 assert_eq!(subtree_ids(&tree, WidgetId::ROOT), all);
9618 // A child's subtree starts with its own identity and stays a **strict** subset of the
9619 // tree (which is how a rich card's ghost captures all of its content).
9620 let flex_id = all[1];
9621 let sub = subtree_ids(Widget::children(&tree)[0].as_ref(), flex_id);
9622 assert_eq!(
9623 sub[0], flex_id,
9624 "the subtree starts with the identity supplied"
9625 );
9626 assert!(
9627 sub.len() < all.len() && sub.iter().all(|i| all.contains(i)),
9628 "a subset of the tree including the descendants"
9629 );
9630 }
9631
9632 impl<Msg: Clone> Ui<Msg> {
9633 fn first_rect(&self) -> (Rect, Rect) {
9634 for primitive in self.scene.primitives() {
9635 if let Primitive::Rect { rect, clip, .. } = primitive {
9636 return (*rect, *clip);
9637 }
9638 }
9639 panic!("no rect at all");
9640 }
9641 }
9642
9643 /// A static subtree under `RepaintBoundary` (mixed content: text + boxes) — enough to
9644 /// produce several primitives.
9645 fn boundary_tree() -> Container<Msg> {
9646 use crate::Text;
9647 Container::new().repaint_boundary().child(
9648 Flex::<Msg>::column()
9649 .child(Text::new("Statique"))
9650 .child(Container::new().width(30.0).height(20.0).on_click(Msg::A))
9651 .child(Button::new("ok").on_press(Msg::B)),
9652 )
9653 }
9654
9655 #[test]
9656 fn repaint_boundary_reuses_a_static_subtree_bit_identical() {
9657 let tree = boundary_tree();
9658 let size = Size::new(200.0, 200.0);
9659 let theme = Theme::default();
9660 let rt = Runtime::default();
9661
9662 // Frame 1: nothing cached → 1 miss (a full paint), the subtree is captured.
9663 let ui1 = build_ui(&tree, size, &rt, &theme);
9664 // The **primitives** are what is compared (the scene's transient ambient state — the
9665 // current clip/owner — is not rendered).
9666 let dbg1 = format!("{:?}", ui1.scene().primitives());
9667 assert_eq!(rt.paint_cache.borrow().last_frame_stats(), (0, 1));
9668
9669 // Frame 2: the same generation + the same state → the boundary is reused.
9670 let ui2 = build_ui(&tree, size, &rt, &theme);
9671 let dbg2 = format!("{:?}", ui2.scene().primitives());
9672 assert_eq!(
9673 rt.paint_cache.borrow().last_frame_stats(),
9674 (1, 0),
9675 "the boundary is reused"
9676 );
9677 assert_eq!(
9678 dbg1, dbg2,
9679 "the replayed scene is bit-for-bit identical to the full repaint"
9680 );
9681 // The interaction maps are replayed too (a click stays routable).
9682 assert_eq!(ui1.hits.len(), ui2.hits.len());
9683 assert!(!ui2.hits.is_empty(), "the subtree's hit is indeed replayed");
9684 }
9685
9686 #[test]
9687 fn repaint_boundary_invalidated_by_generation_bump() {
9688 let tree = boundary_tree();
9689 let size = Size::new(200.0, 200.0);
9690 let theme = Theme::default();
9691 let rt = Runtime::default();
9692
9693 build_ui(&tree, size, &rt, &theme); // frame 1 : miss + capture
9694
9695 // The `view` is rebuilt (the config may have changed).
9696 rt.paint_cache.borrow_mut().bump_generation();
9697 build_ui(&tree, size, &rt, &theme); // frame 2
9698 assert_eq!(
9699 rt.paint_cache.borrow().last_frame_stats(),
9700 (0, 1),
9701 "a stale generation → a full repaint"
9702 );
9703 }
9704
9705 #[test]
9706 fn repaint_boundary_invalidated_by_interaction_change() {
9707 let tree = boundary_tree();
9708 let size = Size::new(200.0, 200.0);
9709 let theme = Theme::default();
9710 let mut rt = Runtime::default();
9711
9712 build_ui(&tree, size, &rt, &theme); // frame 1 : miss + capture
9713
9714 // No rebuild, but a descendant's interaction state changes (the boundary's animated
9715 // hover) → the fingerprint differs → a repaint.
9716 rt.anims.insert(
9717 WidgetId::ROOT,
9718 crate::Anim {
9719 hover: 0.5,
9720 ..Default::default()
9721 },
9722 );
9723 build_ui(&tree, size, &rt, &theme); // frame 2
9724 assert_eq!(
9725 rt.paint_cache.borrow().last_frame_stats(),
9726 (0, 1),
9727 "the interaction state changed → a full repaint"
9728 );
9729
9730 // Once the state has settled, the boundary is reused again.
9731 build_ui(&tree, size, &rt, &theme); // frame 3
9732 assert_eq!(rt.paint_cache.borrow().last_frame_stats(), (1, 0));
9733 }
9734}