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/// Parallax factor of the screen behind during a transition (0 = fixed, 1 = follows
31/// exactly). It is what gives a native navigation its depth.
32const NAV_PARALLAX: f32 = 0.3;
33
34/// Thickness of a scrollbar, in pixels (`scrollbar.dart:12`).
35const BAR_SIZE: f32 = 8.0;
36/// Minimum length of a thumb (`scrollbar.dart:15`). It is the tap target: a thumb has to
37/// be catchable by a pointer, whatever fraction of the content the viewport shows.
38const MIN_THUMB: f32 = 48.0;
39/// The thumb's opacity at rest, over `on_surface` (`scrollbar.dart:248`, `:242`). The two
40/// numbers are the reference's, and which one applies follows the surface's own
41/// brightness — this crate's scheme carries no brightness flag, so it is read off the
42/// surface rather than declared.
43/// The gap between the thumb and the edge of the viewport (`scrollbar.dart:14`).
44const BAR_MARGIN: f32 = 2.0;
45const THUMB_ON_DARK: f32 = 0.30;
46const THUMB_ON_LIGHT: f32 = 0.10;
47/// And under a pointer (`scrollbar.dart:245`, `:239`) — a five-fold change on a light
48/// surface, which is what makes a bar you can barely see worth reaching for.
49const THUMB_HOVER_ON_DARK: f32 = 0.65;
50const THUMB_HOVER_ON_LIGHT: f32 = 0.50;
51/// And while it is being dragged (`scrollbar.dart:244`, `:238`), which arrives at once:
52/// the hand is already on it, and a fade would only lag behind the grab.
53const THUMB_DRAG_ON_DARK: f32 = 0.75;
54const THUMB_DRAG_ON_LIGHT: f32 = 0.60;
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}
102
103/// A scrollable area of the frame: where it is, how far it may scroll, and how it
104/// is meant to behave at its edges.
105///
106/// The registry the shell and the runtime both read. It carries the area's own
107/// [`ScrollPhysics`] only when the widget asked for one — resolving the default is
108/// the application's job, through [`Scrollable::physics_or`].
109#[derive(Copy, Clone, Debug)]
110pub struct Scrollable {
111 /// The scrollable area's identity.
112 pub id: WidgetId,
113 /// The viewport, in absolute coordinates.
114 pub viewport: Rect,
115 /// Largest horizontal offset the content may rest at (0 = nothing to scroll).
116 pub max_x: f32,
117 /// Largest vertical offset the content may rest at.
118 pub max_y: f32,
119 /// The area's own physics, when it asked for one.
120 pub physics: Option<ScrollPhysics>,
121 /// The [`crate::RefreshIndicator`] area this scrollable sits inside, when there is one.
122 /// Movement refused at its **top** edge feeds that area's pull instead of the
123 /// overscroll glow.
124 pub refresh: Option<WidgetId>,
125 /// Set when this area rests **only on pages** ([`crate::PageView`]): the release
126 /// springs to a page boundary instead of flinging.
127 pub page: Option<PageSnap>,
128 /// Whether the horizontal axis is **reversed** — offset 0 is the right-hand end of
129 /// the content rather than the left.
130 pub reverse_x: bool,
131 /// Whether the vertical axis is **reversed** — offset 0 is the **bottom** of the
132 /// content rather than the top.
133 ///
134 /// Offsets are measured from the end an axis starts at, which is what makes a
135 /// reversed list stay where it is when content arrives: a conversation resting at
136 /// offset 0 is resting at the newest message, and the newest message is wherever the
137 /// end now happens to be. Measuring from the top instead would leave the view
138 /// drifting away from the end every time something was appended, which is the one
139 /// thing this exists to prevent.
140 ///
141 /// Everything the *user* does is unchanged by it. A finger pushes the content the
142 /// way it moves, in either direction; only the arithmetic between that push and the
143 /// number changes sign, in one place.
144 pub reverse_y: bool,
145 /// What the content asked to be kept in view, in the frame's coordinates.
146 pub keep_visible: Option<KeepVisible>,
147 /// The scroll region this one sits **inside**, when there is one.
148 ///
149 /// Scroll areas nest — a sideways strip inside a page that scrolls down — and
150 /// bringing something into view means moving every one of them, from the nearest
151 /// outwards. Identities are hashes and carry no ancestry, so the walk records it.
152 pub host: Option<WidgetId>,
153}
154
155impl Scrollable {
156 /// This area's physics, falling back to the application's choice.
157 pub fn physics_or(&self, default: ScrollPhysics) -> ScrollPhysics {
158 self.physics.unwrap_or(default)
159 }
160
161 /// Whether a finger may move this area at all, resting at `offset`.
162 ///
163 /// The rule the reference states plainly: the user can change the offset **if, and
164 /// only if, there is content outside the viewport to reveal** — or the content is
165 /// already displaced, having shrunk under an offset it no longer reaches. An area that
166 /// fails this takes no drag at all: not a short one, not a refused one, and so no
167 /// end-of-content glow either. An edge effect where there is no edge to meet is a
168 /// statement about the content that is not true.
169 ///
170 /// A [`crate::RefreshIndicator`] listening above is the exception, and the same one the
171 /// reference makes: a list of two items must still pull down to reload.
172 pub fn accepts_user_offset(&self, offset: (f32, f32)) -> bool {
173 self.max_x > 0.0
174 || self.max_y > 0.0
175 || offset.0 != 0.0
176 || offset.1 != 0.0
177 || self.refresh.is_some()
178 }
179
180 /// The horizontal axis as the physics sees it, at `offset`.
181 pub fn metrics_x(&self, offset: f32) -> ScrollMetrics {
182 ScrollMetrics::new(offset, self.max_x, self.viewport.width)
183 }
184
185 /// The vertical axis as the physics sees it, at `offset`.
186 pub fn metrics_y(&self, offset: f32) -> ScrollMetrics {
187 ScrollMetrics::new(offset, self.max_y, self.viewport.height)
188 }
189
190 /// Turns a **screen** delta — how far the content was pushed, in pixels — into the
191 /// change it makes to this area's offsets.
192 ///
193 /// Two sign changes, and both have a reason. The content moves *opposite* the
194 /// number: dragging down reveals what is above, which is a smaller offset. And a
195 /// reversed axis counts from the other end, so the same drag raises it instead.
196 ///
197 /// One function rather than a minus sign at each of the five places a delta becomes
198 /// an offset — the wheel, the drag, the release fling, and both axes of each — so
199 /// that a reversed scroll is right in all of them or in none.
200 pub fn offset_delta(&self, screen: (f32, f32)) -> (f32, f32) {
201 (
202 if self.reverse_x { screen.0 } else { -screen.0 },
203 if self.reverse_y { screen.1 } else { -screen.1 },
204 )
205 }
206
207 /// The edge a refusal happened at, given the refused movement **in offset space**.
208 ///
209 /// A negative refusal means the content was pulled towards the axis's start, and
210 /// where that is on screen is exactly what reversing changes: the start of a
211 /// reversed vertical axis is the **bottom**. Without this the glow would flash at
212 /// the far end of a conversation the moment it refused to go further back.
213 pub fn refused_edge(&self, vertical: bool, refused: f32) -> crate::overscroll::GlowEdge {
214 let reversed = if vertical {
215 self.reverse_y
216 } else {
217 self.reverse_x
218 };
219 crate::overscroll::edge_for(vertical, if reversed { -refused } else { refused })
220 }
221
222 /// The edge an axis **starts** at, which is where a pull-to-refresh listens.
223 pub fn start_edge(&self, vertical: bool) -> crate::overscroll::GlowEdge {
224 self.refused_edge(vertical, -1.0)
225 }
226
227 /// The offset that brings `target` fully inside this viewport, and how far the
228 /// content moves to get there.
229 ///
230 /// The **least** movement that does it: a box already in view does not budge, one
231 /// off the near edge comes to that edge and no further. Centring it instead would
232 /// make Tab through a form scroll on every stop, which is motion nobody asked for.
233 ///
234 /// A target **too big** for the viewport gets its leading edge aligned, which falls
235 /// out of asking about that edge first: a tall field half off the top comes down to
236 /// the top rather than up to the bottom, because its start is what you read.
237 ///
238 /// Both halves are returned because they are not the same number. The offset is
239 /// clamped to what this region can actually do, and an outer region has to be told
240 /// how far the target really moved — not how far it was asked to.
241 pub fn reveal(&self, target: Rect, current: (f32, f32)) -> ((f32, f32), (f32, f32)) {
242 let shift = |lo: f32, hi: f32, near: f32, far: f32| {
243 if near < lo {
244 lo - near
245 } else if far > hi {
246 hi - far
247 } else {
248 0.0
249 }
250 };
251 let wanted = (
252 shift(
253 self.viewport.x,
254 self.viewport.x + self.viewport.width,
255 target.x,
256 target.x + target.width,
257 ),
258 shift(
259 self.viewport.y,
260 self.viewport.y + self.viewport.height,
261 target.y,
262 target.y + target.height,
263 ),
264 );
265 // `offset_delta` turns a movement of the content into a movement of the offset,
266 // and it is its own inverse — it negates or it does not — so the same function
267 // turns the offset that was allowed back into the content movement it buys.
268 let delta = self.offset_delta(wanted);
269 let next = (
270 (current.0 + delta.0).clamp(0.0, self.max_x),
271 (current.1 + delta.1).clamp(0.0, self.max_y),
272 );
273 let applied = (next.0 - current.0, next.1 - current.1);
274 (next, self.offset_delta(applied))
275 }
276
277 /// The offset that puts `target` in the **middle** of this viewport, and how far the
278 /// content moves to get there.
279 ///
280 /// The same pair as [`Scrollable::reveal`], and the same clamp — which is what makes
281 /// the first item of a row rest at the start rather than half off it. A target
282 /// bigger than the viewport centres too, which leaves it hanging over both edges,
283 /// and that is the honest answer: there is no offset that shows all of it.
284 pub fn centre(&self, target: Rect, current: (f32, f32)) -> ((f32, f32), (f32, f32)) {
285 let wanted = (
286 (self.viewport.x + self.viewport.width / 2.0) - (target.x + target.width / 2.0),
287 (self.viewport.y + self.viewport.height / 2.0) - (target.y + target.height / 2.0),
288 );
289 let delta = self.offset_delta(wanted);
290 let next = (
291 (current.0 + delta.0).clamp(0.0, self.max_x),
292 (current.1 + delta.1).clamp(0.0, self.max_y),
293 );
294 let applied = (next.0 - current.0, next.1 - current.1);
295 (next, self.offset_delta(applied))
296 }
297
298 /// The offset that brings `keep` into view under its own policy, and how far the
299 /// content moves from **rest** to get there.
300 ///
301 /// Absolute, because [`KeepVisible::rect`] is recorded at rest: the base is offset
302 /// zero, not wherever the region currently is.
303 pub fn keep_offset(&self, keep: KeepVisible) -> ((f32, f32), (f32, f32)) {
304 match keep.centre {
305 true => self.centre(keep.rect, (0.0, 0.0)),
306 false => self.reveal(keep.rect, (0.0, 0.0)),
307 }
308 }
309
310 /// Where the content's leading edge sits, relative to the viewport's, at `offset`.
311 ///
312 /// Normally minus the offset. On a reversed axis the content is anchored to the far
313 /// end, so offset 0 puts the content's *end* against the viewport's — which is also
314 /// the answer for content too short to scroll, where it sits at the bottom of the
315 /// box rather than the top.
316 pub fn content_origin(&self, offset: (f32, f32), content: Size) -> (f32, f32) {
317 let axis = |reverse: bool, offset: f32, viewport: f32, content: f32| {
318 if reverse {
319 viewport - content + offset
320 } else {
321 -offset
322 }
323 };
324 (
325 axis(self.reverse_x, offset.0, self.viewport.width, content.width),
326 axis(
327 self.reverse_y,
328 offset.1,
329 self.viewport.height,
330 content.height,
331 ),
332 )
333 }
334}
335
336/// A box a scroll region has been asked to **keep in view**, and which box it is.
337///
338/// A tab bar wider than its window is what this is for. Declared by the content through
339/// [`Widget::keep_visible`], carried on the region so it survives a repaint boundary
340/// like everything else about the region, and acted on by
341/// [`crate::Runtime::sync_visible`].
342#[derive(Copy, Clone, Debug, PartialEq)]
343pub struct KeepVisible {
344 /// Which box this is. The region acts when this **changes** and not otherwise: the
345 /// box moves as the region scrolls, so a region chasing the box itself would pin
346 /// the content in place and no finger could move it.
347 pub key: u64,
348 /// The box: in the declaring widget's own coordinates when it comes back from
349 /// [`Widget::keep_visible`], and in the frame's — **as it would sit at offset zero**
350 /// — when it reaches the region.
351 ///
352 /// At rest rather than where it is actually drawn, and that is the whole reason the
353 /// answer can be trusted. A box recorded where it is drawn is a box whose position
354 /// already contains the current offset, so the offset that would centre it can only
355 /// be worked out **relative** to wherever the region happens to be — and on the
356 /// first frame, before anything has been retained, there is no wherever to be
357 /// relative to. Recorded at rest, the answer is absolute: the same number on the
358 /// first frame and the hundredth.
359 pub rect: Rect,
360 /// Put it in the **middle** of the window rather than merely inside it. What a tab
361 /// bar wants: a selected tab flush against the edge reads as the end of the row.
362 /// Clamped either way, so the first tab still rests at the start.
363 pub centre: bool,
364}
365
366/// Direction of arrow-key focus navigation.
367#[derive(Clone, Copy, Debug, PartialEq, Eq)]
368pub enum FocusDirection {
369 /// The up arrow: focus moves to the widget above.
370 Up,
371 /// The down arrow: focus moves to the widget below.
372 Down,
373 /// The left arrow: focus moves to the widget on the left.
374 Left,
375 /// The right arrow: focus moves to the widget on the right.
376 Right,
377}
378
379#[derive(Clone)]
380struct Hit<Msg> {
381 id: WidgetId,
382 rect: Rect,
383 /// The message the target emits, or `None` for a target that **swallows** input
384 /// without doing anything — an [`crate::AbsorbPointer`]. Such a target still wins
385 /// the hit test, which is exactly what stops the click reaching what is behind it.
386 msg: Option<Msg>,
387 /// **Inverse** transform (screen → the flat frame) of a transformed subtree
388 /// (`Transform::scale`/`rotate`/a composition): the test point is passed through it to
389 /// get back to the untransformed frame where `rect` lives. `None` = an axis-aligned,
390 /// untransformed rect (the common case).
391 xform: Option<Affine>,
392}
393
394impl<Msg> Hit<Msg> {
395 /// `true` when `point` (in screen coordinates) falls inside the target, taking any
396 /// transform on the subtree into account.
397 fn contains(&self, point: Point) -> bool {
398 let p = self.xform.map_or(point, |inv| inv.apply(point));
399 self.rect.contains(p)
400 }
401}
402
403/// The painted output of a repaint-boundary subtree, cached (see `paintcache.rs`) and
404/// replayed as is on a *hit*. Erased behind a `Box<dyn Any>` in the cache; brought back down
405/// to `Msg` here (one `Msg` instance per app → the `downcast` always succeeds).
406#[derive(Clone)]
407struct BoundaryData<Msg> {
408 prims: Vec<frus_core::Primitive>,
409 hits: Vec<Hit<Msg>>,
410 long_presses: Vec<Hit<Msg>>,
411 dismisses: Vec<Msg>,
412 focusables: Vec<Focusable>,
413 scrollables: Vec<Scrollable>,
414 draggables: Vec<(WidgetId, Rect)>,
415 drag_sources: Vec<DragSource>,
416 drop_zones: Vec<DropZone>,
417 inks: Vec<(WidgetId, Rect)>,
418 semantics: Vec<(WidgetId, Rect, frus_core::SemanticsProperties)>,
419 system_ui: Vec<(Rect, crate::SystemUiOverlayStyle)>,
420}
421
422/// Lengths of the builder's collections on entering a boundary: the lower bounds of the
423/// slices to capture on the way out.
424struct Snapshot {
425 scene: usize,
426 hits: usize,
427 long_presses: usize,
428 dismisses: usize,
429 focusables: usize,
430 scrollables: usize,
431 draggables: usize,
432 drag_sources: usize,
433 drop_zones: usize,
434 inks: usize,
435 semantics: usize,
436 system_ui: usize,
437 overlays: usize,
438 focus_scope_start: Option<usize>,
439}
440
441/// Lower bounds of the **interaction** registries at the start of a transformed composition
442/// (a scale/rotation layer): only what the subtree has just added is re-mapped. Distinct from
443/// [`Snapshot`] (the boundary cache) because it **includes `reorderables`** — those are never
444/// cached but do have to be transformed. See [`transform_interaction_registries`].
445/// Lengths of the registries a [`ModalBarrier`] withholds from, on entering its subtree: the
446/// point each is truncated back to on the way out. Distinct from [`XformBase`] because a
447/// barrier also covers the **scene** and the **scrollbars**, and distinct from [`Snapshot`]
448/// because it covers the registries that are never cached.
449/// What is withdrawn from a layer of an [`crate::IndexedStack`] that is not the shown one:
450/// its paint, its input and its place in the accessibility tree. Not `absorb` — the layer
451/// is not a scrim over what is behind it, it is simply not there for the reader.
452const HIDDEN_LAYER: crate::barrier::ModalBarrier = crate::barrier::ModalBarrier {
453 pointer: true,
454 absorb: false,
455 paint: true,
456 semantics: true,
457};
458
459struct BarrierBase {
460 scene: usize,
461 hits: usize,
462 long_presses: usize,
463 focusables: usize,
464 scrollables: usize,
465 scrollbars: usize,
466 draggables: usize,
467 drag_sources: usize,
468 drop_zones: usize,
469 inks: usize,
470 reorderables: usize,
471 interactives: usize,
472 semantics: usize,
473}
474
475struct XformBase {
476 hits: usize,
477 long_presses: usize,
478 focusables: usize,
479 scrollables: usize,
480 draggables: usize,
481 drag_sources: usize,
482 drop_zones: usize,
483 inks: usize,
484 reorderables: usize,
485 semantics: usize,
486}
487
488/// Node count of the subtree **if** it is "plain" — that is, if the boundary and **all** its
489/// descendants take the default walk branch (children in prefix order): not scrollable, not a
490/// navigator, not a virtualised list, no `layout_builder`, not a stack, no overlay, no
491/// continuous animation. The count then follows the walk's rect order **exactly**, which is
492/// what makes the fingerprint and the bit-for-bit replay correct. `None` = a subtree that
493/// cannot be cached (it is repainted in full — the safe fallback).
494fn plain_subtree_len<Msg>(widget: &dyn Widget<Msg>) -> Option<usize> {
495 if widget.continuous()
496 || widget.scroll_content().is_some()
497 || widget.interactive().is_some()
498 || widget.fitted().is_some()
499 || widget.rotated_quarter_turns().is_some()
500 || widget.navigator().is_some()
501 // Only *whether*, never *what*: these two ask if the widget windows its
502 // children at all, and a size it will not read is the honest argument.
503 || widget.virtual_list(Size::ZERO).is_some()
504 || widget.page_view().is_some()
505 || widget.overflow_box().is_some()
506 || widget.constraints_transform().is_some()
507 || widget.layout_builder().is_some()
508 || widget.stack()
509 || widget.overlay().is_some()
510 // A reorderable (a Kanban card, a draggable header): its bounds feed the reorderables
511 // registry, which is not cached — so its subtree is not put in the paint cache.
512 || widget.reorder_index().is_some()
513 {
514 return None;
515 }
516 let mut n = 1;
517 for child in widget.children() {
518 n += plain_subtree_len(child.as_ref())?;
519 }
520 Some(n)
521}
522
523/// Quantises a `[0,1]` float for the fingerprint (independent of tiny binary differences: two
524/// visually identical progresses ⇒ the same fingerprint).
525fn quant(x: f32) -> i32 {
526 (x * 4096.0).round() as i32
527}
528
529/// Adds to `h` everything the paint reads from a `Status`, **except** the time (excluded: a
530/// cacheable boundary holds no `continuous` widget).
531fn hash_status<H: Hasher>(s: &Status, h: &mut H) {
532 (s.interaction as u8).hash(h);
533 s.focused.hash(h);
534 s.cursor.hash(h);
535 s.selection.hash(h);
536 s.composing.hash(h);
537 quant(s.hover_progress).hash(h);
538 quant(s.focus_progress).hash(h);
539 quant(s.press_progress).hash(h);
540 quant(s.opacity).hash(h);
541 quant(s.value).hash(h);
542 // Sub-region hover (milestone 208): a change of position repaints the highlight.
543 s.hover_cursor.map(|p| (quant(p.x), quant(p.y))).hash(h);
544}
545
546/// The result of building an interface for one frame.
547/// A **shortcut / action scope**: a subtree that binds keystrokes, answers intents, or
548/// both, together with the range of focus stops it contains.
549///
550/// The range is how "focus is inside this subtree" is answered without an ancestor test:
551/// the walk is depth-first, so a subtree's focus stops are contiguous, and the focused
552/// stop's index either falls inside the range or does not.
553struct Scope<Msg> {
554 strokes: Vec<(KeyStroke, Intent)>,
555 callbacks: Vec<(KeyStroke, Msg)>,
556 actions: Vec<(Intent, Msg)>,
557 listeners: Vec<(Intent, Msg)>,
558 /// The focus stops this subtree contains, as indices into `focusables`.
559 range: std::ops::Range<usize>,
560}
561
562/// A **focus stop**: somewhere the keyboard can land, and what the traversal should make
563/// of it.
564#[derive(Clone, Copy, Debug)]
565pub struct Focusable {
566 /// The widget's identity.
567 pub id: WidgetId,
568 /// Its **visible** box this frame: the box it was given, clipped by whatever
569 /// encloses it. Empty when it is scrolled out of sight, which is what a click test
570 /// wants — a tap on empty space must not land on something nobody can see.
571 pub rect: Rect,
572 /// Its box this frame, **unclipped**. What the geometry is really about: where the
573 /// widget is, whether or not any of it is on screen. Arrow navigation places
574 /// candidates by this, and [`Ui::reveal`] works out what to scroll from it.
575 pub bounds: Rect,
576 /// The [`crate::AutofillGroup`] this stop sits in — the form it is part of — when
577 /// there is one (milestone 512).
578 pub form: Option<WidgetId>,
579 /// The nearest enclosing scroll region, when there is one. What could bring this
580 /// stop into view, and the first link of the chain [`Ui::reveal`] walks.
581 pub scroll: Option<WidgetId>,
582 /// Focusable by a click, but **passed over by Tab** — the reference's
583 /// `ExcludeFocusTraversal`. The two are separate questions.
584 pub skip: bool,
585 /// An explicit traversal position, smallest first. `None` means tree order, which is
586 /// where everything sits until someone says otherwise.
587 pub order: Option<f32>,
588 /// The traversal group it belongs to, within which its order is resolved.
589 pub group: Option<WidgetId>,
590}
591
592/// The **interface** of one frame: what the walk over a widget tree produced.
593///
594/// Besides the [`Scene`] to paint it holds everything the shell asks of a frame afterwards —
595/// what a click, a long press or a dismissal answers, what can take the focus, which areas
596/// scroll, which widgets can be dragged or dropped on — so that events are routed against
597/// the frame the reader saw. It is made by [`build_ui`] and read until the next frame
598/// replaces it.
599pub struct Ui<Msg = crate::callback::Callback> {
600 scene: Scene,
601 hits: Vec<Hit<Msg>>,
602 /// Long-press targets (id, visible bounds, message).
603 long_presses: Vec<Hit<Msg>>,
604 /// Overlay dismissal messages, from the bottom to the **top**.
605 dismisses: Vec<Msg>,
606 focusables: Vec<Focusable>,
607 /// **Focus scope**: index of the topmost modal overlay's first focusable —
608 /// Tab/arrows/click-to-focus are trapped from there on (`None` = no modal, every
609 /// focusable takes part).
610 focus_scope_start: Option<usize>,
611 /// (id, viewport, offset max x, offset max y)
612 scrollables: Vec<Scrollable>,
613 scrollbars: Vec<Scrollbar>,
614 draggables: Vec<(WidgetId, Rect)>,
615 drag_sources: Vec<DragSource>,
616 drop_zones: Vec<DropZone>,
617 /// **Inked surfaces**: (id, the box the ink is clipped to). Tracked separately from
618 /// clicking because the splash needs the surface's **whole** box — where the finger
619 /// landed inside it, and how far the circle has to travel to cover it — which a
620 /// click target, recorded as its *visible* part, does not give.
621 inks: Vec<(WidgetId, Rect)>,
622 /// **Reorderables** (column headers, Kanban cards): (id, visible bounds). Tracked
623 /// independently of clicking — a card is not clickable but can still be picked up and
624 /// dropped onto.
625 reorderables: Vec<(WidgetId, Rect)>,
626 /// Interactive viewports (`InteractiveViewer`): (id, the screen viewport). The shell
627 /// routes panning (dragging) and zooming (wheel / pinch) to them.
628 interactives: Vec<(WidgetId, Rect)>,
629 /// Pull-to-refresh areas of the frame, with their configuration.
630 refreshes: Vec<Refreshable>,
631 /// Swipe-to-dismiss items of the frame, with their configuration.
632 dismissables: Vec<Dismissable>,
633 /// Draggable sheets of the frame, with their configuration and the scroll areas
634 /// walked inside each.
635 sheets: Vec<crate::sheet::SheetArea>,
636 wants_animation: bool,
637 /// The accessibility tree: semantic nodes (id, bounds, annotation), in paint order. The
638 /// shell maps it onto AccessKit.
639 semantics: Vec<(WidgetId, Rect, frus_core::SemanticsProperties)>,
640 /// The regions saying what the system bars over them should look like (paint order).
641 system_ui: Vec<(Rect, crate::SystemUiOverlayStyle)>,
642 /// Boxes whose children ran outside them, with the edge and the amount.
643 overflows: Vec<Overflowing>,
644 /// Shortcut and action scopes, in the order the walk closed them — innermost first
645 /// among any that overlap, which is the order the resolution wants.
646 scopes: Vec<Scope<Msg>>,
647 /// Keystroke listeners, with the focus stops they cover.
648 #[allow(clippy::type_complexity)]
649 listeners: Vec<(
650 std::ops::Range<usize>,
651 std::rc::Rc<dyn Fn(KeyStroke) -> Option<Msg>>,
652 )>,
653}
654
655impl<Msg: Clone> Ui<Msg> {
656 /// The scene to hand to the renderer.
657 /// Boxes in this frame whose children did not fit inside them.
658 ///
659 /// Empty is the normal answer. A non-empty one means content is being drawn outside
660 /// its parent — invisible where it is clipped, unhittable where it leaves the window,
661 /// and silent either way until something asks. That silence is what let a task row's
662 /// delete button sit off-screen through three milestones.
663 pub fn overflows(&self) -> &[Overflowing] {
664 &self.overflows
665 }
666
667 /// The scrollbars this frame drew, and therefore the ones a pointer may drag. Empty
668 /// where the platform draws none — see [`crate::Scrollbars`].
669 pub fn scrollbars(&self) -> &[Scrollbar] {
670 &self.scrollbars
671 }
672
673 /// The [`Scene`] this frame painted.
674 pub fn scene(&self) -> &Scene {
675 &self.scene
676 }
677
678 /// The frame's **accessibility tree**: every meaningful node with its bounds on screen and
679 /// its annotation ([`frus_core::SemanticsProperties`]). Paint order (= reading order). The shell
680 /// pushes it to AccessKit.
681 pub fn semantics(&self) -> &[(WidgetId, Rect, frus_core::SemanticsProperties)] {
682 &self.semantics
683 }
684
685 /// What the system bars should look like over this frame (#46): the regions under
686 /// `status_bar` answer for the status bar and those under `navigation_bar` for the
687 /// other, the one drawn last first and field by field. What none states is left
688 /// `None`, for [`crate::SystemUiOverlayStyle::resolve`] to take from the theme.
689 ///
690 /// The shell picks the two points: the first row of the screen's content under the
691 /// status bar, and the last one above the navigation bar.
692 pub fn system_ui_style(
693 &self,
694 status_bar: Point,
695 navigation_bar: Point,
696 ) -> crate::SystemUiOverlayStyle {
697 crate::system_ui::style_under(&self.system_ui, status_bar, navigation_bar)
698 }
699
700 /// `true` when a widget animates continuously (the framework must redraw).
701 /// Whether **this tree's widgets** want the next frame: a spinner turning, an ink
702 /// ripple spreading, a pull being held.
703 ///
704 /// It answers for the widgets and nothing else. A fetch still in flight also owes the
705 /// screen a frame — see [`frus_core::images_in_flight`] — and that is the *process's*
706 /// business, asked by whoever drives the loop. It used to be folded in here, which
707 /// made this method's answer depend on what an unrelated part of the program happened
708 /// to be doing: in the test suite, where everything shares one process, a refresh area
709 /// with nothing pulled asked for frames whenever another test was loading an image
710 /// (milestone 411).
711 pub fn wants_animation(&self) -> bool {
712 self.wants_animation
713 }
714
715 /// Identity of the topmost clickable widget containing `point`.
716 pub fn hit(&self, point: Point) -> Option<WidgetId> {
717 self.hits
718 .iter()
719 .rev()
720 .find(|hit| hit.contains(point))
721 .map(|hit| hit.id)
722 }
723
724 /// The message tied to a given clickable widget. `None` for a target that swallows
725 /// input without emitting anything.
726 ///
727 /// It is always paired with [`hit`](Self::hit), so the two have to agree on which
728 /// target an identity means: every registered target therefore has one of its own,
729 /// the dismissal barrier below an overlay included.
730 pub fn msg_for(&self, id: WidgetId) -> Option<Msg> {
731 self.hits
732 .iter()
733 .find(|hit| hit.id == id)
734 .and_then(|hit| hit.msg.clone())
735 }
736
737 /// Dismissal message of the **topmost** overlay (for Escape).
738 pub fn top_dismiss(&self) -> Option<Msg> {
739 self.dismisses.last().cloned()
740 }
741
742 /// **Long-press** message of the topmost target containing `point`.
743 pub fn long_press_at(&self, point: Point) -> Option<Msg> {
744 self.long_presses
745 .iter()
746 .rev()
747 .find(|hit| hit.contains(point))
748 .and_then(|hit| hit.msg.clone())
749 }
750
751 /// The frame's **pull-to-refresh areas**, with the configuration each was built
752 /// with. The shell steps them and reads their `refreshing` flag from here.
753 pub fn refresh_areas(&self) -> &[Refreshable] {
754 &self.refreshes
755 }
756
757 /// The frame's **dismissible items**, with the configuration each was built with.
758 pub fn dismissables(&self) -> &[Dismissable] {
759 &self.dismissables
760 }
761
762 /// The frame's **sheets**, with the configuration each was built with and the scroll
763 /// areas walked inside it.
764 pub fn sheets(&self) -> &[crate::sheet::SheetArea] {
765 &self.sheets
766 }
767
768 /// The sheet whose panel is under `point`, the last one walked winning — which is
769 /// the one painted on top.
770 pub fn sheet_at(&self, point: Point) -> Option<&crate::sheet::SheetArea> {
771 self.sheets.iter().rev().find(|sheet| sheet.contains(point))
772 }
773
774 /// The sheet `id`, when it is in the frame.
775 pub fn sheet(&self, id: WidgetId) -> Option<&crate::sheet::SheetArea> {
776 self.sheets.iter().find(|sheet| sheet.id == id)
777 }
778
779 /// The sheet the scroll area `area` was walked inside, when there is one — the
780 /// innermost, should sheets nest, since an outer one counts the inner one's areas too.
781 pub fn sheet_holding(&self, area: WidgetId) -> Option<&crate::sheet::SheetArea> {
782 self.sheets
783 .iter()
784 .filter(|sheet| sheet.areas.contains(&area))
785 .min_by_key(|sheet| sheet.areas.len())
786 }
787
788 /// The topmost dismissible item containing `point` — the candidate a press has to
789 /// weigh against the scrollable underneath it.
790 pub fn dismissable_at(&self, point: Point) -> Option<Dismissable> {
791 self.dismissables
792 .iter()
793 .rev()
794 .find(|item| item.rect.contains(point))
795 .copied()
796 }
797
798 /// Topmost focusable widget containing `point`: (id, its bounds).
799 pub fn focus_hit(&self, point: Point) -> Option<(WidgetId, Rect)> {
800 self.focus_pool()
801 .iter()
802 .rev()
803 .find(|f| f.rect.contains(point))
804 .map(|f| (f.id, f.rect))
805 }
806
807 /// The **participating** focusables: those of the topmost modal scope when there is one
808 /// (a focus trap), otherwise all of them.
809 fn focus_pool(&self) -> &[Focusable] {
810 match self.focus_scope_start {
811 Some(start) => &self.focusables[start.min(self.focusables.len())..],
812 None => &self.focusables,
813 }
814 }
815
816 /// Nearest focusable in the given **direction** (arrow navigation, a geometric policy):
817 /// among the focusables whose centre lies on the right side, it minimises the distance
818 /// along the main axis with a penalty on the cross-axis offset. `None` when there is
819 /// nothing in that direction.
820 pub fn focus_directional(
821 &self,
822 current: WidgetId,
823 direction: FocusDirection,
824 ) -> Option<WidgetId> {
825 // The starting point may be outside the scope (focus from before it opened); the
826 // **candidates**, though, are trapped inside it.
827 let from = self
828 .focusables
829 .iter()
830 .find(|f| f.id == current)
831 .map(|f| &f.rect)?;
832 let center = |r: &Rect| (r.x + r.width * 0.5, r.y + r.height * 0.5);
833 let (fx, fy) = center(from);
834
835 let mut best: Option<(WidgetId, f32)> = None;
836 for candidate in self.focus_pool() {
837 let (id, rect) = (candidate.id, &candidate.rect);
838 if id == current {
839 continue;
840 }
841 let (cx, cy) = center(rect);
842 // (how far ahead in the direction, the cross-axis offset)
843 let (ahead, cross) = match direction {
844 FocusDirection::Right => (cx - fx, (cy - fy).abs()),
845 FocusDirection::Left => (fx - cx, (cy - fy).abs()),
846 FocusDirection::Down => (cy - fy, (cx - fx).abs()),
847 FocusDirection::Up => (fy - cy, (cx - fx).abs()),
848 };
849 // Inside a **cone** around the direction (not a plain half-plane): a candidate
850 // almost aligned on the cross axis but barely "ahead" (slightly different widths)
851 // is not a directional target.
852 if ahead <= 0.5 || cross > ahead * 3.0 {
853 continue;
854 }
855 let score = ahead + cross * 3.0;
856 if best.map(|(_, s)| score < s).unwrap_or(true) {
857 best = Some((id, score));
858 }
859 }
860 best.map(|(id, _)| id)
861 }
862
863 /// Next/previous focus stop for Tab, wrapping. With no current focus, the first (or
864 /// the last).
865 ///
866 /// **Tree order, unless something said otherwise.** Stops carrying an explicit order
867 /// are sorted ahead of those that do not, smallest first, and the sort is applied
868 /// **within a traversal group** rather than across the whole frame — so a reordered
869 /// dialog does not reshuffle the page behind it. Stops marked `skip` are passed over
870 /// here while remaining reachable by a click: the keyboard's order and the pointer's
871 /// reach are two different questions.
872 pub fn focus_next(&self, current: Option<WidgetId>, forward: bool) -> Option<WidgetId> {
873 let order = self.traversal_order();
874 if order.is_empty() {
875 return None;
876 }
877 let n = order.len();
878 // A current focus **outside the scope** (taken before the modal opened) is treated
879 // as "none": Tab enters the trap.
880 match current.and_then(|c| order.iter().position(|id| *id == c)) {
881 Some(i) => {
882 let j = if forward {
883 (i + 1) % n
884 } else {
885 (i + n - 1) % n
886 };
887 Some(order[j])
888 }
889 None => Some(order[if forward { 0 } else { n - 1 }]),
890 }
891 }
892
893 /// The Tab order: the participating stops, minus those that asked to be skipped,
894 /// with each traversal group's members sorted among themselves.
895 ///
896 /// A **stable** sort on the order key, so everything without one keeps tree order and
897 /// ties do too — the property that makes an explicit order a local statement rather
898 /// than a rearrangement of the whole frame.
899 pub fn traversal_order(&self) -> Vec<WidgetId> {
900 let pool: Vec<&Focusable> = self.focus_pool().iter().filter(|f| !f.skip).collect();
901 if pool.iter().all(|f| f.order.is_none()) {
902 return pool.iter().map(|f| f.id).collect();
903 }
904 let mut out: Vec<WidgetId> = Vec::with_capacity(pool.len());
905 let mut i = 0;
906 while i < pool.len() {
907 // A group's members are contiguous: the walk is depth-first, so a subtree's
908 // stops arrive together.
909 let group = pool[i].group;
910 let mut j = i;
911 while j < pool.len() && pool[j].group == group {
912 j += 1;
913 }
914 let mut run: Vec<&Focusable> = pool[i..j].to_vec();
915 run.sort_by(|a, b| {
916 let key = |f: &Focusable| f.order.unwrap_or(f32::INFINITY);
917 key(a).total_cmp(&key(b))
918 });
919 out.extend(run.iter().map(|f| f.id));
920 i = j;
921 }
922 out
923 }
924
925 /// What a keystroke means here: the messages it sends, in the order they should be
926 /// applied.
927 ///
928 /// The resolution, in one place because it is the whole feature:
929 ///
930 /// 1. Only scopes **containing the focused stop** are candidates; with nothing
931 /// focused, all of them are. Innermost first — `scopes` is filled on the way *out*
932 /// of the walk, so that order is already the one wanted.
933 /// 2. A [`crate::KeyboardListener`] gets the first refusal, and a `None` from it lets
934 /// the stroke carry on.
935 /// 3. A direct binding ([`crate::CallbackShortcuts`]) sends its message.
936 /// 4. Otherwise a [`crate::Shortcuts`] binding names an **intent**, and the innermost
937 /// [`crate::Actions`] that answers it supplies the message. An intent nobody
938 /// answers does nothing — deliberately: a key bound to a meaning the current screen
939 /// has no answer for should be inert, not an error.
940 /// 5. Every [`crate::ActionListener`] watching that intent adds its message too.
941 ///
942 /// Empty means *nobody wanted it*, and the caller should carry on to whatever it would
943 /// have done with the key.
944 pub fn keystroke(&self, stroke: KeyStroke, focused: Option<WidgetId>) -> Vec<Msg> {
945 let at = focused.and_then(|id| self.focusables.iter().position(|f| f.id == id));
946 let covers = |range: &std::ops::Range<usize>| match at {
947 Some(i) => range.contains(&i),
948 None => true,
949 };
950
951 for (range, on_key) in self.listeners.iter().rev() {
952 if covers(range) {
953 if let Some(msg) = on_key(stroke) {
954 return vec![msg];
955 }
956 }
957 }
958
959 let mut out = Vec::new();
960 for scope in self.scopes.iter().filter(|s| covers(&s.range)) {
961 if let Some((_, msg)) = scope.callbacks.iter().find(|(s, _)| stroke.matches(s)) {
962 out.push(msg.clone());
963 return out;
964 }
965 let Some((_, intent)) = scope.strokes.iter().find(|(s, _)| stroke.matches(s)) else {
966 continue;
967 };
968 let answer = self
969 .scopes
970 .iter()
971 .filter(|s| covers(&s.range))
972 .find_map(|s| {
973 s.actions
974 .iter()
975 .find(|(i, _)| i == intent)
976 .map(|(_, m)| m.clone())
977 });
978 if let Some(msg) = answer {
979 out.push(msg);
980 for watcher in self.scopes.iter().filter(|s| covers(&s.range)) {
981 for (i, m) in &watcher.listeners {
982 if i == intent {
983 out.push(m.clone());
984 }
985 }
986 }
987 }
988 return out;
989 }
990 out
991 }
992
993 /// Topmost scrollable area containing `point`.
994 pub fn scroll_hit(&self, point: Point) -> Option<Scrollable> {
995 self.scrollables
996 .iter()
997 .rev()
998 .find(|area| area.viewport.contains(point))
999 .copied()
1000 }
1001
1002 /// **Every** scrollable area containing `point`, innermost first — the whole stack a
1003 /// finger has under it.
1004 ///
1005 /// [`Self::scroll_hit`] answers *which one is on top*, which is the right question for
1006 /// a wheel or a scrollbar. A finger asks a different one: a strip that only slides
1007 /// across, sitting in a page that only scrolls down, cannot take a drag downwards, and
1008 /// the page behind it can. Deciding that needs the ones behind as well.
1009 pub fn scroll_chain(&self, point: Point) -> impl Iterator<Item = Scrollable> + '_ {
1010 self.scrollables
1011 .iter()
1012 .rev()
1013 .filter(move |area| area.viewport.contains(point))
1014 .copied()
1015 }
1016
1017 /// Frame of a **focusable** widget `id`, when it is present this frame — so the shell can
1018 /// find the focused field's geometry (vertical caret movement).
1019 pub fn widget_rect(&self, id: WidgetId) -> Option<Rect> {
1020 self.focusables
1021 .iter()
1022 .find(|f| f.id == id)
1023 .map(|f| f.rect)
1024 // Falls back to the **reorderables** registry. Kanban cards (and headers that are
1025 // reorderable but **not** sortable) are not focusable: without this fallback the
1026 // shell cannot find their bounds, and the whole **vertical** drag preview (ghost,
1027 // insertion line, reflow) as well as the *insert-after* routing stay dead.
1028 .or_else(|| {
1029 self.reorderables
1030 .iter()
1031 .find(|(rid, _)| *rid == id)
1032 .map(|(_, rect)| *rect)
1033 })
1034 }
1035
1036 /// The box of the inked surface `id`, when the frame has one — what the shell needs
1037 /// to start a splash: where inside it the finger landed, and how big it is.
1038 ///
1039 /// The **topmost** match wins, as the hit-test does: two inked surfaces can overlap,
1040 /// and the one that took the tap is the one drawn last.
1041 pub fn ink_box(&self, id: WidgetId) -> Option<Rect> {
1042 self.inks
1043 .iter()
1044 .rev()
1045 .find(|(iid, _)| *iid == id)
1046 .map(|(_, rect)| *rect)
1047 }
1048
1049 /// The identities of **every** focusable widget of the frame (the scope included) — so
1050 /// the shell can detect the focus **disappearing** (an overlay closed) and restore it.
1051 pub fn focusable_ids(&self) -> impl Iterator<Item = WidgetId> + '_ {
1052 self.focusables.iter().map(|f| f.id)
1053 }
1054
1055 /// The focus stops of the **form** `id` is part of, in tree order, each with its box
1056 /// this frame (unclipped: a field scrolled out of sight is still a field of the form).
1057 /// A stop in no [`crate::AutofillGroup`] is a form of one. Empty when `id` is not a
1058 /// focus stop this frame.
1059 ///
1060 /// Stops, not fields: which of them take part in autofill is a question for the
1061 /// widgets ([`crate::Widget::autofill_hints`]), which the shell has and this does not.
1062 pub fn form_of(&self, id: WidgetId) -> Vec<(WidgetId, Rect)> {
1063 let Some(stop) = self.focusables.iter().find(|f| f.id == id) else {
1064 return Vec::new();
1065 };
1066 match stop.form {
1067 Some(form) => self.group_stops(form),
1068 None => vec![(stop.id, stop.bounds)],
1069 }
1070 }
1071
1072 /// The [`crate::AutofillGroup`] focus stop `id` sits in, if it is a stop this frame
1073 /// and sits in one.
1074 pub fn form_group(&self, id: WidgetId) -> Option<WidgetId> {
1075 self.focusables.iter().find(|f| f.id == id)?.form
1076 }
1077
1078 /// Every focus stop inside the [`crate::AutofillGroup`] `group`, in tree order, with
1079 /// its unclipped box. Empty once the group has left the screen — which is the moment
1080 /// a form is finished with, and the platform may offer to save what was typed in it.
1081 pub fn group_stops(&self, group: WidgetId) -> Vec<(WidgetId, Rect)> {
1082 self.focusables
1083 .iter()
1084 .filter(|f| f.form == Some(group))
1085 .map(|f| (f.id, f.bounds))
1086 .collect()
1087 }
1088
1089 /// Frame (viewport) of the scrollable area `id`, when there is one — so the shell can find
1090 /// a multi-line field's width/height (caret following).
1091 pub fn scrollable_viewport(&self, id: WidgetId) -> Option<Rect> {
1092 self.scrollables
1093 .iter()
1094 .find(|area| area.id == id)
1095 .map(|area| area.viewport)
1096 }
1097
1098 /// The scroll offsets that would bring `id` **into view**, nearest region first.
1099 ///
1100 /// Empty when it is already in view, when the frame does not contain it, or when
1101 /// nothing around it scrolls. Nobody has to check first: an empty answer is the
1102 /// answer that nothing needs to move.
1103 ///
1104 /// Nested regions are walked from the inside out, and each is told where the target
1105 /// **ended up** rather than where it started. A sideways strip inside a page that
1106 /// scrolls down moves the target across, and the page then decides how far down it
1107 /// still is; asking both about the original box would have them fight over the same
1108 /// pixels and land somewhere neither wanted.
1109 pub fn reveal(&self, id: WidgetId, runtime: &crate::Runtime) -> Vec<(WidgetId, (f32, f32))> {
1110 let Some(stop) = self.focusables.iter().find(|f| f.id == id) else {
1111 return Vec::new();
1112 };
1113 let mut target = stop.bounds;
1114 let mut host = stop.scroll;
1115 let mut moves = Vec::new();
1116 // Regions nest a handful deep at most. The bound is a backstop against a chain
1117 // that loops, which cannot happen and would hang the frame if it did.
1118 for _ in 0..16 {
1119 let Some(hid) = host else { break };
1120 let Some(area) = self.scrollables.iter().find(|area| area.id == hid) else {
1121 break;
1122 };
1123 let current = runtime.scroll.get(&hid).copied().unwrap_or((0.0, 0.0));
1124 let (offset, shift) = area.reveal(target, current);
1125 if shift.0 != 0.0 || shift.1 != 0.0 {
1126 target = target.translate(shift.0, shift.1);
1127 moves.push((hid, offset));
1128 }
1129 host = area.host;
1130 }
1131 moves
1132 }
1133
1134 /// Every scrollable area of the frame — geometry, bounds and physics — which is
1135 /// what the runtime needs to drive momentum and edges.
1136 pub fn scroll_regions(&self) -> &[Scrollable] {
1137 &self.scrollables
1138 }
1139
1140 /// The area `id`, when it is present this frame.
1141 pub fn scroll_region(&self, id: WidgetId) -> Option<Scrollable> {
1142 self.scrollables.iter().find(|area| area.id == id).copied()
1143 }
1144
1145 /// SingleChildScrollView bounds `(id, max_x, max_y)` of every scrollable area.
1146 pub fn scrollable_maxes(&self) -> Vec<(WidgetId, f32, f32)> {
1147 self.scrollables
1148 .iter()
1149 .map(|area| (area.id, area.max_x, area.max_y))
1150 .collect()
1151 }
1152
1153 /// Scrollbar thumb under `point` (to start a drag).
1154 pub fn scrollbar_at(&self, point: Point) -> Option<Scrollbar> {
1155 self.scrollbars
1156 .iter()
1157 .rev()
1158 .find(|bar| bar.opacity > 0.0 && bar.thumb.contains(point))
1159 .copied()
1160 }
1161
1162 /// The scrollbar a **pointer is near enough to wake**: inside its track, or within a
1163 /// tap target's reach of its thumb.
1164 ///
1165 /// Wider than [`Ui::scrollbar_at`], and — unlike it — it answers for a bar that has
1166 /// faded out entirely, which is the whole point. A bar nobody can see is a bar nobody
1167 /// can grab, so the reference lets a mouse coming near one bring it back first
1168 /// (`scrollbar.dart:766`); only then is it something to take hold of.
1169 pub fn scrollbar_near(&self, point: Point) -> Option<Scrollbar> {
1170 self.scrollbars
1171 .iter()
1172 .rev()
1173 .find(|bar| bar.reach.contains(point))
1174 .copied()
1175 }
1176
1177 /// Topmost draggable widget under `point`: (id, its bounds).
1178 pub fn draggable_at(&self, point: Point) -> Option<(WidgetId, Rect)> {
1179 self.draggables
1180 .iter()
1181 .rev()
1182 .find(|(_, rect)| rect.contains(point))
1183 .map(|(id, rect)| (*id, *rect))
1184 }
1185
1186 /// Topmost **drag source** under `point`, with what it carries.
1187 pub fn drag_source_at(&self, point: Point) -> Option<DragSource> {
1188 self.drag_sources
1189 .iter()
1190 .rev()
1191 .find(|source| source.rect.contains(point))
1192 .copied()
1193 }
1194
1195 /// Topmost **drop target** under `point`.
1196 pub fn drop_zone_at(&self, point: Point) -> Option<DropZone> {
1197 self.drop_zones
1198 .iter()
1199 .rev()
1200 .find(|zone| zone.rect.contains(point))
1201 .copied()
1202 }
1203
1204 /// Topmost **reorderable** widget under `point`: its id. The basis of reordering
1205 /// drag-and-drop (the source on press, the target on drop) — independent of clickability.
1206 pub fn reorderable_at(&self, point: Point) -> Option<WidgetId> {
1207 self.reorderables
1208 .iter()
1209 .rev()
1210 .find(|(_, rect)| rect.contains(point))
1211 .map(|(id, _)| *id)
1212 }
1213
1214 /// **Every** reorderable under `point`, topmost first.
1215 ///
1216 /// [`Self::reorderable_at`] answers *which one is on top*, which is the right question
1217 /// for a press: whatever is nearest the finger is what the finger grabbed. A **drop**
1218 /// asks a different one, because the thing on top may be a grip that nothing can be
1219 /// dropped on — see [`Widget::reorder_droppable`] — and the row it belongs to, which
1220 /// can, is the one behind it.
1221 pub fn reorderables_at(&self, point: Point) -> impl Iterator<Item = WidgetId> + '_ {
1222 self.reorderables
1223 .iter()
1224 .rev()
1225 .filter(move |(_, rect)| rect.contains(point))
1226 .map(|(id, _)| *id)
1227 }
1228
1229 /// Every **reorderable** of the frame, with its box, in paint order.
1230 pub fn reorderables(&self) -> &[(WidgetId, Rect)] {
1231 &self.reorderables
1232 }
1233
1234 /// Topmost **interactive** viewport (`InteractiveViewer`) under `point`: (id, its screen
1235 /// viewport). The shell routes panning and zooming to it.
1236 pub fn interactive_at(&self, point: Point) -> Option<(WidgetId, Rect)> {
1237 self.interactives
1238 .iter()
1239 .rev()
1240 .find(|(_, rect)| rect.contains(point))
1241 .map(|(id, rect)| (*id, *rect))
1242 }
1243
1244 /// Interactive viewports `(id, screen viewport)`, to drive the inertia (fling) and the
1245 /// clamping of the pan on the framework's side.
1246 pub fn interactive_bounds(&self) -> Vec<(WidgetId, Rect)> {
1247 self.interactives.clone()
1248 }
1249}
1250
1251/// Paints a **striped band** along the edge of every box whose children ran past it.
1252///
1253/// Milestone 335 taught the layout to notice an overflow and the shell to say so in the
1254/// console. That is the half a developer reads; this is the half a *screenshot* shows —
1255/// and a screenshot is what a bug report is made of. Three of this framework's own
1256/// milestones went looking for defects that a band would have pointed at: a delete button
1257/// laid out past the card (333, 334), a segmented control 221 px outside its parent (335).
1258///
1259/// The look is the reference's, because the point of it is to be recognised on sight:
1260/// black and yellow diagonal stripes, three quarters opaque, over a tenth of the box along
1261/// the offending edge, with the edge and the number written across it. Not the mechanism,
1262/// though — the reference paints one repeating gradient and this paints the stripes,
1263/// because a repeating diagonal gradient is a shader feature and a parallelogram is four
1264/// points.
1265///
1266/// **Debug builds only.** A band is a message to whoever is building the application, and
1267/// in a release build there is nobody to read it — the reference draws the same line.
1268fn paint_overflow_bands(scene: &mut Scene, overflows: &[Overflowing]) {
1269 if !cfg!(debug_assertions) || overflows.is_empty() {
1270 return;
1271 }
1272 // The reference's two colours and its 10 % band, exactly.
1273 const BLACK: Color = Color::rgba(0.0, 0.0, 0.0, 0.75);
1274 const YELLOW: Color = Color::rgba(1.0, 1.0, 0.0, 0.75);
1275 const FRACTION: f32 = 0.1;
1276 /// The stripes run at 45°; this is their period measured along the x axis.
1277 const PERIOD: f32 = 14.0;
1278
1279 let (outer, outer_bounds) = (scene.current_clip(), scene.current_bounds());
1280 scene.set_clip(Rect::UNBOUNDED);
1281 for over in overflows {
1282 let r = over.rect;
1283 let band = match over.side {
1284 Side::Left => Rect::new(r.x, r.y, r.width * FRACTION, r.height),
1285 Side::Right => Rect::new(
1286 r.x + r.width * (1.0 - FRACTION),
1287 r.y,
1288 r.width * FRACTION,
1289 r.height,
1290 ),
1291 Side::Top => Rect::new(r.x, r.y, r.width, r.height * FRACTION),
1292 Side::Bottom => Rect::new(
1293 r.x,
1294 r.y + r.height * (1.0 - FRACTION),
1295 r.width,
1296 r.height * FRACTION,
1297 ),
1298 };
1299 if band.width <= 0.0 || band.height <= 0.0 {
1300 continue;
1301 }
1302 scene.set_clip(band);
1303 scene.fill_rect(band, BLACK);
1304 // One parallelogram per stripe, sheared by the band's height so that every one
1305 // of them crosses it at 45° whatever shape the band is.
1306 let shear = band.height;
1307 let mut x = band.x - shear;
1308 while x < band.x + band.width {
1309 let stripe = Path::new()
1310 .move_to(Point::new(x, band.y))
1311 .line_to(Point::new(x + PERIOD / 2.0, band.y))
1312 .line_to(Point::new(x + PERIOD / 2.0 + shear, band.y + band.height))
1313 .line_to(Point::new(x + shear, band.y + band.height))
1314 .close();
1315 scene.fill_path(&stripe, YELLOW);
1316 x += PERIOD;
1317 }
1318 // The label is **not** clipped to the band: a vertical one is usually longer than
1319 // the box is tall, and half a sentence is worse than a sentence written over
1320 // whatever is beside it. The reference does not clip it either.
1321 scene.set_clip(Rect::UNBOUNDED);
1322 scene.set_bounds(Rect::UNBOUNDED);
1323 paint_overflow_label(scene, band, over);
1324 }
1325 scene.set_clip(outer);
1326 scene.set_bounds(outer_bounds);
1327}
1328
1329/// Writes the reference's sentence across a band: which edge overflowed, and by how much.
1330///
1331/// The console already says it — but the console is on the developer's machine and the
1332/// band is on the device, and a photograph of a phone is what half the bug reports in the
1333/// world are made of. A striped edge says *something* is too big; `RIGHT OVERFLOWED BY 86
1334/// PIXELS` says which edge, and by enough that you can tell a forgotten padding from a
1335/// missing wrap without running anything.
1336///
1337/// The words are the reference's, exactly, so that searching for them finds the same
1338/// answers. So are the numbers: 7.5 px, the heaviest weight there is, dark red on an
1339/// opaque white plate — a label that must stay readable over stripes cannot be subtle —
1340/// centred on the band's outer edge and turned a quarter turn on the vertical ones.
1341fn paint_overflow_label(scene: &mut Scene, band: Rect, over: &Overflowing) {
1342 /// The reference's label metrics.
1343 const SIZE: f32 = 7.5;
1344 /// The gap between the label and the outer edge of the band.
1345 const PAD: f32 = 1.0;
1346 /// `0xFF900000`.
1347 const INK: Color = Color::rgb(0.5647059, 0.0, 0.0);
1348
1349 let side = match over.side {
1350 Side::Left => "LEFT",
1351 Side::Right => "RIGHT",
1352 Side::Top => "TOP",
1353 Side::Bottom => "BOTTOM",
1354 };
1355 let text = format!("{side} OVERFLOWED BY {} PIXELS", format_pixels(over.amount));
1356 // The reference asks for an 800 weight; the heaviest this framework has is bold.
1357 let style = TextStyle::new(SIZE).weight(FontWeight::Bold);
1358 let size = frus_text::measure_styled(&text, SIZE, FontWeight::Bold, false);
1359
1360 // Where the middle of the label's leading edge goes, before any rotation, and by how
1361 // much the whole thing then turns about that point.
1362 let (anchor, angle) = match over.side {
1363 Side::Left => (
1364 Point::new(band.x + SIZE + PAD, band.y + band.height / 2.0),
1365 std::f32::consts::FRAC_PI_2,
1366 ),
1367 Side::Right => (
1368 Point::new(band.x + band.width - SIZE - PAD, band.y + band.height / 2.0),
1369 -std::f32::consts::FRAC_PI_2,
1370 ),
1371 Side::Top => (Point::new(band.x + band.width / 2.0, band.y + PAD), 0.0),
1372 Side::Bottom => (
1373 Point::new(band.x + band.width / 2.0, band.y + band.height - SIZE - PAD),
1374 0.0,
1375 ),
1376 };
1377 if angle == 0.0 {
1378 let at = Point::new(anchor.x - size.width / 2.0, anchor.y);
1379 scene.fill_rect(Rect::from_point_size(at, size), Color::WHITE);
1380 scene.text(at, text, &style.resolved(), INK);
1381 return;
1382 }
1383 // A rotation of a group, not of a glyph: the plate and the sentence turn together and
1384 // neither has to know it is turning.
1385 //
1386 // The group is painted **at the origin** rather than where it lands, because a layer
1387 // is rendered flat into a window-sized texture before it is composited: a vertical
1388 // label is written across a box near the right edge, so laid out flat it would run
1389 // off the texture and lose its last word before the rotation ever brought it back
1390 // inside. The transform is the one that carries the origin to where the label goes —
1391 // shift it half its width to the left, turn it, and land it on the anchor.
1392 let flat = Rect::from_point_size(Point::ZERO, size);
1393 let matrix = Affine::translation(-size.width / 2.0, 0.0)
1394 .then(Affine::rotation(angle))
1395 .then(Affine::translation(anchor.x, anchor.y));
1396 scene.transformed(LayerTransform::new(matrix), move |scene: &mut Scene| {
1397 scene.fill_rect(flat, Color::WHITE);
1398 scene.text(flat.origin(), text, &style.resolved(), INK);
1399 });
1400}
1401
1402/// How the reference writes an overflow: whole pixels past ten, one decimal past one, and
1403/// three significant figures below that — because the difference between 0.5 px and
1404/// 0.0001 px is the difference between a layout bug and a rounding error.
1405fn format_pixels(value: f32) -> String {
1406 if value > 10.0 {
1407 format!("{value:.0}")
1408 } else if value > 1.0 {
1409 format!("{value:.1}")
1410 } else {
1411 // Three significant figures: the first one sits at the first non-zero decimal.
1412 let leading = if value > 0.0 {
1413 (-value.abs().log10().floor()) as i32
1414 } else {
1415 0
1416 };
1417 format!("{value:.*}", (leading + 2).clamp(0, 12) as usize)
1418 }
1419}
1420
1421/// Identity of the `index`-th child: **by key** when the child declares one (stable whatever
1422/// its position), **positional** otherwise. It must be used everywhere a child identity is
1423/// derived (render, collection, lookup, animations) so they all stay consistent.
1424pub(crate) fn child_id<Msg>(parent: WidgetId, index: usize, child: &dyn Widget<Msg>) -> WidgetId {
1425 match child.key() {
1426 Some(key) => parent.keyed(key),
1427 None => parent.child(index),
1428 }
1429}
1430
1431/// A widget's **effective** style for layout: its `style()`, whose size is **replaced by the
1432/// runtime's interpolated size** when the widget is animated (`Widget::anim_size` +
1433/// `Container::animated_size`). This is the only place where an animated size enters layout —
1434/// used **identically** by [`build_layout`] and by the cache fingerprint (`hash_node`), which
1435/// is what keeps them consistent (the cache stays invalid for as long as the size moves).
1436pub(crate) fn effective_style<Msg>(
1437 widget: &dyn Widget<Msg>,
1438 id: WidgetId,
1439 runtime: &Runtime,
1440 theme: &Theme,
1441) -> frus_layout::Style {
1442 // The theme's say on size and spacing (milestone 309), before the runtime's
1443 // animated overrides below — an animation in flight wins over a resting default.
1444 let mut style = widget.style_themed(theme);
1445 if widget.anim_size().is_some() {
1446 if let Some(size) = runtime.anim_size(id) {
1447 style.width = frus_layout::Dimension::Length(size.width);
1448 style.height = frus_layout::Dimension::Length(size.height);
1449 }
1450 }
1451 if widget.anim_padding().is_some() {
1452 if let Some(padding) = runtime.anim_padding(id) {
1453 style.padding = padding;
1454 }
1455 }
1456 // A share of the parent, growing or shrinking. `Percent` and not `Length`, because
1457 // the whole point of the quantity is that it does not know what it will come to — and
1458 // an axis whose factor is unset goes back to following its content rather than to
1459 // nought, which is what "unset" has always meant here.
1460 if widget.anim_fractions().is_some() {
1461 if let Some((width, height)) = runtime.anim_fractions(id) {
1462 let dim = |factor: Option<f32>| match factor {
1463 Some(f) => frus_layout::Dimension::Percent(f),
1464 None => frus_layout::Dimension::Auto,
1465 };
1466 style.width = dim(width);
1467 style.height = dim(height);
1468 }
1469 }
1470 // A dismissed item closing its gap. It collapses along the axis it was **not**
1471 // swiped along — a row swiped sideways loses its height — which is what makes the
1472 // neighbours slide up rather than the row narrow to a sliver. Only an explicit
1473 // length can shrink: an `Auto` box has no number to scale, which is why
1474 // `Dismissible` asks for a size (see its docs).
1475 // A sheet's panel, as tall as the retained share of its box. The same `Percent` as a
1476 // fraction above, for the same reason: the share is the quantity, and the box it is a
1477 // share of is the layout's to know. Hashed through here as well, so the cache misses
1478 // on every frame the sheet moves.
1479 if let Some(spec) = widget.sheet() {
1480 style.height = frus_layout::Dimension::Percent(runtime.sheet_size(id, spec));
1481 }
1482 if let Some(spec) = widget.dismissible() {
1483 if let Some(factor) = runtime.dismiss_extent_factor(id) {
1484 let axis = if spec.axis.is_horizontal() {
1485 &mut style.height
1486 } else {
1487 &mut style.width
1488 };
1489 if let frus_layout::Dimension::Length(extent) = *axis {
1490 *axis = frus_layout::Dimension::Length(extent * factor);
1491 }
1492 }
1493 }
1494 style
1495}
1496
1497/// Builds the main layout tree (a scrollable is a **leaf**). `id`/`runtime` are what inject
1498/// the animated size through [`effective_style`], following the walk's identity scheme
1499/// (`child_id`) **exactly**.
1500pub(crate) fn build_layout<'a, Msg>(
1501 widget: &'a dyn Widget<Msg>,
1502 id: WidgetId,
1503 runtime: &'a Runtime,
1504 theme: &Theme,
1505 layout: &mut Layout<'a, BaselineData>,
1506) -> NodeId {
1507 let (node, fills) = build_layout_scoped(widget, id, runtime, theme, layout, true);
1508 // A root that wants to fill has no parent to fill: the flex machinery below needs one
1509 // on each side of the question. A percentage of the room the layout is being computed
1510 // in is the same answer without a parent, and it degrades to hugging the content when
1511 // that room is unbounded — a scrollable measuring its content, a natural size.
1512 if fills.horizontal {
1513 layout.fill_root(node, true);
1514 }
1515 if fills.vertical {
1516 layout.fill_root(node, false);
1517 }
1518 node
1519}
1520
1521/// The axes a subtree asks to **fill** rather than shrink-wrap.
1522///
1523/// It is not a property of one widget. A column whose row fills the width is itself as
1524/// wide as the room it was given, because the row inside it took that room — so the
1525/// request travels up as the layout is built, and each container answers it with the one
1526/// thing a container can do about it: grow along its own axis, or stretch across it.
1527#[derive(Clone, Copy, Default)]
1528pub(crate) struct Fills {
1529 horizontal: bool,
1530 vertical: bool,
1531}
1532
1533impl Fills {
1534 /// What a widget asks for on its own account.
1535 ///
1536 /// A straight copy since milestone 405: the widget's answer already has a flag per
1537 /// axis, where it used to be one direction and a container that wanted both had no way
1538 /// to say so.
1539 fn own<Msg>(widget: &dyn Widget<Msg>, theme: &Theme) -> Self {
1540 let asked = widget.fill_axes(theme);
1541 Fills {
1542 horizontal: asked.horizontal,
1543 vertical: asked.vertical,
1544 }
1545 }
1546
1547 fn merge(self, other: Fills) -> Self {
1548 Fills {
1549 horizontal: self.horizontal || other.horizontal,
1550 vertical: self.vertical || other.vertical,
1551 }
1552 }
1553
1554 /// The half of a child's request a container passes on to **its** parent, given
1555 /// which way the container runs. Along its own axis a container divides the room up
1556 /// and the request stops there; across it, the container's size *is* the child's, so
1557 /// the request has to keep going.
1558 /// Whether one axis is asked for.
1559 fn wants(self, horizontal: bool) -> bool {
1560 if horizontal {
1561 self.horizontal
1562 } else {
1563 self.vertical
1564 }
1565 }
1566
1567 /// Drops any axis this box was **given a size on**: it has already been told how big
1568 /// to be, and a request to fill cannot reach past that answer.
1569 fn bounded_by(self, style: &frus_layout::Style) -> Self {
1570 let auto = |d| matches!(d, frus_layout::Dimension::Auto);
1571 Fills {
1572 horizontal: self.horizontal && auto(style.width),
1573 vertical: self.vertical && auto(style.height),
1574 }
1575 }
1576
1577 fn across(self, direction: frus_layout::FlexDirection) -> Self {
1578 if direction.is_horizontal() {
1579 Fills {
1580 horizontal: false,
1581 vertical: self.vertical,
1582 }
1583 } else {
1584 Fills {
1585 horizontal: self.horizontal,
1586 vertical: false,
1587 }
1588 }
1589 }
1590}
1591
1592/// A layout leaf's **text baseline**, when it has one: the distance from the top of its
1593/// box down to the line its letters sit on.
1594///
1595/// It rides on the layout tree rather than being asked of the widgets afterwards because
1596/// only the layout knows where a node ended up, and only the walk that built it knows
1597/// which branch it took — a scrollable, a stack and a page view are all *leaves* here,
1598/// with their contents laid out elsewhere, and a second walk guessing at that would be a
1599/// copy of this one waiting to drift out of step.
1600pub(crate) type BaselineData = Option<f32>;
1601
1602/// An owned copy of a theme, **made in a frame of its own**.
1603///
1604/// `#[inline(never)]` is the whole point: inlined, the eleven-kilobyte temporary is built
1605/// in the caller's frame, and the caller here is the recursive walk that pays for its
1606/// frame once per level of the tree. Behind the call it is built, copied into the
1607/// allocation, and gone before the walk recurses.
1608#[inline(never)]
1609fn owned_theme(theme: &Theme) -> std::rc::Rc<Theme> {
1610 std::rc::Rc::new(theme.clone())
1611}
1612
1613/// The body of [`build_layout`], carrying whether baselines are still being collected.
1614/// An [`crate::IgnoreBaseline`] turns them off for its subtree — the widgets inside still
1615/// have baselines, and the point is that nothing above may see them.
1616fn build_layout_scoped<'a, Msg>(
1617 widget: &'a dyn Widget<Msg>,
1618 id: WidgetId,
1619 runtime: &'a Runtime,
1620 theme: &Theme,
1621 layout: &mut Layout<'a, BaselineData>,
1622 baselines: bool,
1623) -> (NodeId, Fills) {
1624 // A component is built into what it stands for before anything is asked of the node,
1625 // the scope it introduces included — see `Widget::expand`.
1626 widget.expand(id, runtime, theme);
1627 // A themed subtree lays out under **its** theme, not the frame's: a theme reaches
1628 // sizes and spacing (milestone 309), so this has to happen here and not only at paint
1629 // time. `hash_node`, which fingerprints this same walk for the relayout cache, makes
1630 // the same swap — the two staying in step is what keeps the cache honest.
1631 let scoped = scoped_theme(widget, id, runtime, theme);
1632 let theme = scoped.as_deref().unwrap_or(theme);
1633 // And a **scoped surface**, the same idea one milestone later: a shell that hands a slot
1634 // a narrowed description narrows it for everything the slot builds, the deferred build
1635 // below included. Held for this subtree and put back on the way out.
1636 let _surface = widget
1637 .media_override(crate::MediaQuery::of())
1638 .map(crate::MediaQuery::install);
1639 // And the **shell**, the third inherited thing (milestone 422): a slot told what shell
1640 // it stands in composes differently — a bar grows a menu button — so it has to be in
1641 // force before `build_themed` below, exactly as the surface is.
1642 let _shell = widget.scaffold_override().map(crate::ScaffoldInfo::install);
1643 // A subtree that could not be composed until the theme was known (`ThemeBuilder`).
1644 // It has to happen **before** anything reads `children()`, and under the subtree's
1645 // own theme, which is why it sits after the swap above rather than at the call site.
1646 widget.build_in(id, runtime, theme);
1647 // Everything below an `IgnoreBaseline` keeps its baseline and loses the right to be
1648 // seen: the flag is narrowed once, here, so no branch below has to remember.
1649 let baselines = baselines && !widget.ignores_baseline();
1650 // A leaf's own baseline, or nothing when this subtree is being ignored.
1651 let own_baseline = if baselines {
1652 widget.text_baseline(theme)
1653 } else {
1654 None
1655 };
1656 // `Baseline`: the child is pushed down until **its** baseline lands `target` pixels
1657 // from the top of this box, and this box grows to contain it. If the child's own
1658 // baseline is already lower than that, there is nowhere to push it up to and the
1659 // child is top-aligned instead — the same answer the reference gives.
1660 if let Some(target) = widget.baseline_target() {
1661 let mut style = effective_style(widget, id, runtime, theme);
1662 if let Some(child) = widget.children().first() {
1663 let child = child.as_ref();
1664 let cid = child_id(id, 0, child);
1665 // A child with no baseline of its own is treated as if its baseline were its
1666 // bottom edge, which is what a box with no text amounts to.
1667 let child_baseline = natural_baseline(child, cid, runtime, theme)
1668 .unwrap_or_else(|| natural_size(child, cid, runtime, theme).height);
1669 style.padding.top += (target - child_baseline).max(0.0);
1670 let (node, fills) = build_layout_scoped(child, cid, runtime, theme, layout, baselines);
1671 return (layout.container(style, &[node]), fills);
1672 }
1673 return (layout.leaf(style, None), Fills::default());
1674 }
1675 // `RotatedBox`: a leaf whose box is the child's **natural** size, with its dimensions
1676 // **swapped** for an odd quarter turn (the rotation does affect layout). The child itself
1677 // is laid out separately (at render time).
1678 if let Some(q) = widget.rotated_quarter_turns() {
1679 let mut style = effective_style(widget, id, runtime, theme);
1680 if let Some(child) = widget.children().first() {
1681 let nat = natural_size(
1682 child.as_ref(),
1683 child_id(id, 0, child.as_ref()),
1684 runtime,
1685 theme,
1686 );
1687 let (w, h) = if q.rem_euclid(4) % 2 != 0 {
1688 (nat.height, nat.width)
1689 } else {
1690 (nat.width, nat.height)
1691 };
1692 style.width = frus_layout::Dimension::Length(w);
1693 style.height = frus_layout::Dimension::Length(h);
1694 }
1695 return (layout.leaf(style, own_baseline), Fills::default());
1696 }
1697 // `Intrinsic`: one axis is taken from what the content would **like** to be, not from
1698 // the space on offer. The content is measured once, unconstrained, and the answer is
1699 // written into this node's style as a length — the same trick `RotatedBox` uses, except
1700 // that here the child stays inside, laid out normally within the size it asked for.
1701 if let Some((axis, step)) = widget.intrinsic() {
1702 let mut style = effective_style(widget, id, runtime, theme);
1703 if let Some(child) = widget.children().first() {
1704 let child = child.as_ref();
1705 let cid = child_id(id, 0, child);
1706 let nat = natural_size(child, cid, runtime, theme);
1707 let quantise = |extent: f32| match step {
1708 Some(step) if step > 0.0 => (extent / step).ceil() * step,
1709 _ => extent,
1710 };
1711 match axis {
1712 crate::constraints::IntrinsicAxis::Width => {
1713 style.width = frus_layout::Dimension::Length(quantise(nat.width));
1714 }
1715 crate::constraints::IntrinsicAxis::Height => {
1716 style.height = frus_layout::Dimension::Length(quantise(nat.height));
1717 }
1718 }
1719 let (node, fills) = build_layout_scoped(child, cid, runtime, theme, layout, baselines);
1720 return (layout.container(style, &[node]), fills);
1721 }
1722 return (layout.leaf(style, own_baseline), Fills::default());
1723 }
1724 // `OverflowBox`: a leaf, because its child is laid out **separately**, to constraints of
1725 // its own. That separation is the whole feature — a child sharing this node's layout
1726 // could never be bigger than it.
1727 if widget.overflow_box().is_some() {
1728 return (
1729 layout.leaf(effective_style(widget, id, runtime, theme), own_baseline),
1730 Fills::default(),
1731 );
1732 }
1733 // A box that **transforms the space on offer**: its child is laid out in a box derived
1734 // from this one's, in a tree of its own, and this box is then as big as what came back
1735 // — held to what it was itself offered, which is the reference's
1736 // `constraints.constrain(child.size)`. `UnconstrainedBox` is this branch with the
1737 // constraint taken away.
1738 if let Some(transform) = widget.constraints_transform() {
1739 let style = effective_style(widget, id, runtime, theme);
1740 let child = widget.children().first().map(|c| c.as_ref());
1741 let owned = owned_theme(theme);
1742 let measure: frus_layout::MeasureFn<'a> = Box::new(move |w, h| {
1743 let Some(child) = child else {
1744 return Size::new(0.0, 0.0);
1745 };
1746 let (aw, free_x) = transform.width.offered(w);
1747 let (ah, free_y) = transform.height.offered(h);
1748 let mut inner: Layout<BaselineData> = Layout::new();
1749 let node = build_layout(child, child_id(id, 0, child), runtime, &owned, &mut inner);
1750 // **Handed** every axis that has a number, **asked** about every axis that does
1751 // not. `compute_scroll` already fills a lone constrained axis, for the reason a
1752 // scrollable's cross axis needs it; with numbers on both, `compute_filled` is
1753 // that same rule. Merely constraining them instead would leave a child with no
1754 // size of its own — a plain container — hugging at nothing, and a box asked how
1755 // big its child wanted to be would come back empty.
1756 match (free_x, free_y) {
1757 (false, false) => inner.compute_filled(node, aw, ah),
1758 _ => inner.compute_scroll(node, aw, ah, free_x, free_y),
1759 }
1760 let size = inner.size_of(node);
1761 // Held to the offer: a child too big for the room spills, and the box stays the
1762 // size the room was.
1763 Size::new(
1764 size.width.min(w.unwrap_or(f32::INFINITY)),
1765 size.height.min(h.unwrap_or(f32::INFINITY)),
1766 )
1767 });
1768 return (
1769 layout.measured_leaf(style, own_baseline, measure),
1770 Fills::default(),
1771 );
1772 }
1773 // A `LayoutBuilder` **is measured**: it builds its content from the space offered and
1774 // is then as big as what it built, which is the reference's
1775 // `size = constraints.constrain(child.size)`. It cannot be a plain leaf, because a
1776 // leaf answers before it has been asked and there is nothing to answer with until the
1777 // box is known; and it cannot be a container, because its child does not exist yet.
1778 //
1779 // Taffy calls the closure during the computation with the space actually available —
1780 // the same moment the reference runs its layout callback — and the closure builds the
1781 // subtree, lays it out in a `Layout` of its own, and hands back the size. An axis the
1782 // style pins is still the style's: taffy does not ask about a dimension it already
1783 // knows, so `LayoutBuilder::height(200.0)` behaves exactly as it did.
1784 if let Some(build) = widget.layout_builder() {
1785 let style = effective_style(widget, id, runtime, theme);
1786 // The closure needs an **owned** theme: a themed subtree's theme is a local in
1787 // this function, so it cannot be borrowed for as long as the closure lives. The
1788 // runtime and the widget are borrowed, which is the whole reason `Layout` carries
1789 // a lifetime.
1790 //
1791 // This is a clone of eleven kilobytes, and it used to be a silent one — `Theme`
1792 // was `Copy` until milestone 448, so `*theme` read like a pointer copy and was
1793 // not. It happens once per `LayoutBuilder`, not once per node.
1794 //
1795 // **Behind an `Rc`, and made somewhere else**, because the cost that mattered was
1796 // not the copy. A `Theme` local here and a closure that captures one are two
1797 // slots of eleven kilobytes in *this* function's frame, and a frame is charged to
1798 // every level of the walk whether or not the branch is taken: 24 736 bytes a
1799 // level, which overflowed a Windows main thread's megabyte at about forty. The
1800 // `Rc` makes the capture a pointer, and `owned_theme` keeps the copy in a frame
1801 // that pops before the recursion goes any deeper.
1802 let owned = owned_theme(theme);
1803 let cid = id.child(0);
1804 let measure: frus_layout::MeasureFn<'a> = Box::new(move |w, h| {
1805 // What taffy offers. `None` is an *intrinsic* question — how big would you
1806 // be with no limit, or with none at all — and there is no honest answer to
1807 // it here: finding out means running the application's callback
1808 // speculatively. The reference refuses these outright (`computeDryLayout`
1809 // asserts); we build at the offered numbers, substituting zero for what is
1810 // not offered, and leave the unoffered axis free in the nested computation
1811 // so the content's own size comes back.
1812 let offered = Size::new(w.unwrap_or(0.0), h.unwrap_or(0.0));
1813 let child = build(offered);
1814 let mut inner: Layout<BaselineData> = Layout::new();
1815 let node = build_layout(child.as_ref(), cid, runtime, &owned, &mut inner);
1816 // The content is **handed** the offered box, not asked what it would like:
1817 // it was built from that box. An axis that was not offered is the one being
1818 // asked about, and stays free so the content's own size comes back.
1819 match (w, h) {
1820 (Some(w), Some(h)) => inner.compute_filled(node, w, h),
1821 _ => inner.compute_scroll(
1822 node,
1823 offered.width,
1824 offered.height,
1825 w.is_none(),
1826 h.is_none(),
1827 ),
1828 }
1829 inner.size_of(node)
1830 });
1831 return (
1832 layout.measured_leaf(style, own_baseline, measure),
1833 Fills::own(widget, theme),
1834 );
1835 }
1836 // **A viewport is sized from its content on the axis it does not scroll.**
1837 //
1838 // A scrollable area declares a size on the axis it scrolls — that is what a viewport
1839 // is, a window shorter than what is behind it. On the *other* axis there is nothing to
1840 // choose: the content is as tall as it is, and the viewport is that tall. Until
1841 // milestone 485 a horizontal strip took the 200 px that belongs to a vertical one, so a
1842 // scrollable tab bar 48 px tall claimed 200 and pushed its panel a hundred and fifty
1843 // pixels into empty space (#65).
1844 //
1845 // It cannot be answered by the widget alone, and that is the whole reason this lives
1846 // here. A scroll host lays its content out **against** the viewport on the axis that
1847 // does not scroll — the content of a horizontal strip is given the viewport's height —
1848 // so a viewport that said `Auto` there would hand the content nothing and come back
1849 // nothing tall, the circle closing at zero. Breaking it takes a measurement, at the one
1850 // moment the runtime and the theme are both to hand.
1851 if let Some(content) = widget.scroll_content() {
1852 let style = effective_style(widget, id, runtime, theme);
1853 let axis = widget.scroll_axis();
1854 // Only an axis that **neither scrolls nor was given a size**. A caller's number is
1855 // a caller's number, and an axis that scrolls has already answered.
1856 let hug_x = !axis.free_x() && matches!(style.width, frus_layout::Dimension::Auto);
1857 let hug_y = !axis.free_y() && matches!(style.height, frus_layout::Dimension::Auto);
1858 if hug_x || hug_y {
1859 let owned = owned_theme(theme);
1860 let cid = child_id(id, 0, content);
1861 let measure: frus_layout::MeasureFn<'a> = Box::new(move |w, h| {
1862 let mut inner: Layout<BaselineData> = Layout::new();
1863 let node = build_layout(content, cid, runtime, &owned, &mut inner);
1864 // The content is unconstrained on every axis that scrolls — that is what
1865 // scrolling means — and on the one being measured. What is left is an axis
1866 // with a real number on it, and the content is held to it.
1867 inner.compute_scroll(
1868 node,
1869 w.unwrap_or(0.0),
1870 h.unwrap_or(0.0),
1871 hug_x || axis.free_x() || w.is_none(),
1872 hug_y || axis.free_y() || h.is_none(),
1873 );
1874 let content = inner.size_of(node);
1875 let pad = widget.scroll_padding();
1876 // **Only the hugged axis is answered.** On the axis that scrolls, a
1877 // viewport is precisely *not* as big as its content — answering with the
1878 // content's size there would make a flexible viewport ask for the whole
1879 // length of what is behind it as its basis, and a column holding one would
1880 // overflow by however much there was to scroll. Nought is what a leaf
1881 // answered before this branch existed, and it is still the right answer.
1882 Size::new(
1883 match hug_x {
1884 true => content.width + pad.left + pad.right,
1885 false => 0.0,
1886 },
1887 match hug_y {
1888 true => content.height + pad.top + pad.bottom,
1889 false => 0.0,
1890 },
1891 )
1892 });
1893 return (
1894 layout.measured_leaf(style, own_baseline, measure),
1895 Fills::own(widget, theme),
1896 );
1897 }
1898 }
1899
1900 // Scrollables, interactive viewports, fitters (`FittedBox`), navigators, virtualised
1901 // lists and stacks: their content is laid out separately (independent layers / screens /
1902 // items, or a child laid out at its natural size).
1903 if widget.scroll_content().is_some()
1904 || widget.interactive().is_some()
1905 || widget.fitted().is_some()
1906 || widget.navigator().is_some()
1907 || widget.virtual_list(Size::ZERO).is_some()
1908 || widget.page_view().is_some()
1909 || widget.stack()
1910 {
1911 // Nothing bubbles out of these: what is inside is laid out somewhere else, to
1912 // constraints of its own, and cannot have an opinion about this box.
1913 return (
1914 layout.leaf(effective_style(widget, id, runtime, theme), own_baseline),
1915 Fills::own(widget, theme),
1916 );
1917 }
1918 // A portal only lays out its anchor (child 0); the overlay is deferred.
1919 if widget.overlay().is_some() {
1920 let anchor_w = widget.children()[0].as_ref();
1921 let (anchor, fills) = build_layout_scoped(
1922 anchor_w,
1923 child_id(id, 0, anchor_w),
1924 runtime,
1925 theme,
1926 layout,
1927 baselines,
1928 );
1929 return (
1930 layout.container(effective_style(widget, id, runtime, theme), &[anchor]),
1931 fills,
1932 );
1933 }
1934 let children = widget.children();
1935 if children.is_empty() {
1936 // A leaf measured under constraints (a paragraph that wraps…): taffy calls the
1937 // closure during the computation.
1938 if let Some(measure) = widget.measure(theme) {
1939 return (
1940 layout.measured_leaf(
1941 effective_style(widget, id, runtime, theme),
1942 own_baseline,
1943 measure,
1944 ),
1945 Fills::own(widget, theme),
1946 );
1947 }
1948 (
1949 layout.leaf(effective_style(widget, id, runtime, theme), own_baseline),
1950 Fills::own(widget, theme),
1951 )
1952 } else {
1953 let style = effective_style(widget, id, runtime, theme);
1954 let built: Vec<(NodeId, Fills)> = children
1955 .iter()
1956 .enumerate()
1957 .map(|(i, child)| {
1958 build_layout_scoped(
1959 child.as_ref(),
1960 child_id(id, i, child.as_ref()),
1961 runtime,
1962 theme,
1963 layout,
1964 baselines,
1965 )
1966 })
1967 .collect();
1968 let child_ids: Vec<NodeId> = built.iter().map(|(node, _)| *node).collect();
1969 // `MainAxisSize::Max`: a run that fills the room its parent leaves it. Only the
1970 // parent knows what that means — grow along an axis it shares, stretch across
1971 // one it does not — which is why the child asks rather than saying.
1972 //
1973 // The request is the child's **and its subtree's**: a column whose row fills the
1974 // width is itself as wide as the room it was given.
1975 let alone = children.len() == 1;
1976 for (node, fills) in &built {
1977 for horizontal in [true, false] {
1978 if !fills.wants(horizontal) {
1979 continue;
1980 }
1981 let parallel = horizontal == style.flex_direction.is_horizontal();
1982 // Along an axis the parent runs too, the reference leaves the main axis
1983 // **unbounded** and `Max` quietly degrades to `Min`: there is no maximum
1984 // to take. The exception is a parent with a single child — a padding, an
1985 // alignment, a decorated box — which passes its own constraints straight
1986 // down, and there the run does fill.
1987 if parallel && !alone {
1988 continue;
1989 }
1990 layout.fill_parent(*node, horizontal, parallel);
1991 }
1992 }
1993 // A **lone** child gives way; several do not.
1994 //
1995 // The reference hands an inflexible child of a row or a column an unbounded main
1996 // axis: it is never squeezed, and a line that does not fit overflows and says so.
1997 // That is the default here now, because a framework that quietly crushes a button
1998 // instead of reporting a layout that does not fit is one whose bugs are invisible
1999 // — milestone 333 spent three milestones on exactly that crushed button.
2000 //
2001 // But a box with one child is not dividing a line up: it is handing its own
2002 // constraints down, and the reference bounds a lone child by them. The same
2003 // exception the fill request and the floor below both make, for the same reason.
2004 if alone {
2005 for node in &child_ids {
2006 layout.allow_shrink(*node);
2007 }
2008 }
2009 // A grid's tiles are all the same shape. The ratio is the *container's*, since a
2010 // tile cannot know how wide its column came out, and the reference says the same:
2011 // a grid's delegate hands every child a tight box derived from the track, square
2012 // unless the application asked for another ratio.
2013 if let Some(ratio) = widget.tile_shape() {
2014 for node in &child_ids {
2015 layout.set_tile_shape(*node, ratio);
2016 }
2017 }
2018 // A child that refuses to be squeezed along the row. Only a row of **several**
2019 // children may say so. Across a column the same floor would refuse a width the
2020 // column was handing it, which is how a paragraph is told how wide to be; and a
2021 // box with a single child — a padding, an alignment, a decorated box — is handing
2022 // one down rather than dividing a line up, whichever way it nominally runs.
2023 if style.flex_direction.is_horizontal() && !alone {
2024 for (child, node) in children.iter().zip(&child_ids) {
2025 if let Some(floor) = child.main_axis_floor(theme) {
2026 layout.set_min_width(*node, floor);
2027 }
2028 }
2029 }
2030 // **Baseline** cross-alignment: the children are pushed down until their
2031 // baselines meet, which is the only alignment that makes two runs of different
2032 // sizes read as one line rather than as one row.
2033 //
2034 // It is resolved here rather than by taffy, which cannot: taffy asks a leaf for
2035 // a size and a leaf can only answer with one. The measurement a baseline needs
2036 // is the font's, so the answer has to come from up here, where the widgets are.
2037 //
2038 // The **natural** baseline is the right one to measure even though the children
2039 // will be laid out at some other width: the first line of a piece of text sits
2040 // in the same place however narrow the box gets, since narrowing it adds lines
2041 // below rather than moving the first one.
2042 if style.align == frus_layout::Align::Baseline && style.flex_direction.is_horizontal() {
2043 let own: Vec<Option<f32>> = children
2044 .iter()
2045 .enumerate()
2046 .map(|(i, child)| {
2047 natural_baseline(
2048 child.as_ref(),
2049 child_id(id, i, child.as_ref()),
2050 runtime,
2051 theme,
2052 )
2053 })
2054 .collect();
2055 if let Some(deepest) = own.iter().flatten().copied().reduce(f32::max) {
2056 for (node, b) in child_ids.iter().zip(&own) {
2057 // A child with no baseline keeps its place: it is a box, and a box
2058 // has nothing to line up.
2059 if let Some(b) = b {
2060 layout.add_margin_top(*node, deepest - b);
2061 }
2062 }
2063 }
2064 }
2065 // What this container passes on to **its** parent. Across its own axis the
2066 // container's size is its child's, so the request keeps going; along it the
2067 // container divides the room up and the request stops. A container with a single
2068 // child divides nothing, and passes the whole request on.
2069 let mut fills = Fills::own(widget, theme);
2070 for (_, child) in &built {
2071 fills = fills.merge(if alone {
2072 *child
2073 } else {
2074 child.across(style.flex_direction)
2075 });
2076 }
2077 (
2078 layout.container(style, &child_ids),
2079 fills.bounded_by(&style),
2080 )
2081 }
2082}
2083
2084/// The distance from the top of `widget`'s **natural** box to the first baseline inside
2085/// it, or `None` when there is no text in there to have one.
2086///
2087/// It lays the subtree out on its own, exactly as [`natural_size`] does, and takes the
2088/// first baseline the layout collected. First in *layout* order, which is the order a
2089/// reader would find it in — and it is the leaves that carry them, so an intervening
2090/// padding or alignment is already counted in the rectangle it comes with.
2091pub(crate) fn natural_baseline<Msg>(
2092 widget: &dyn Widget<Msg>,
2093 id: WidgetId,
2094 runtime: &Runtime,
2095 theme: &Theme,
2096) -> Option<f32> {
2097 let mut layout = Layout::new();
2098 let node = build_layout(widget, id, runtime, theme, &mut layout);
2099 layout.compute_scroll(node, 0.0, 0.0, true, true);
2100 let rects = layout.absolute_rects(node);
2101 let top = rects.first()?.0.y;
2102 rects
2103 .iter()
2104 .find_map(|(rect, data)| data.copied().flatten().map(|b| rect.y + b - top))
2105}
2106
2107/// **Natural** (intrinsic) size of a subtree: laid out under free axes (`MaxContent`), with
2108/// no imposed constraint. Used by `RotatedBox` (the dimensions to swap) and — at render time
2109/// — by `FittedBox` (the fit factor).
2110pub(crate) fn natural_size<Msg>(
2111 widget: &dyn Widget<Msg>,
2112 id: WidgetId,
2113 runtime: &Runtime,
2114 theme: &Theme,
2115) -> Size {
2116 let mut layout = Layout::new();
2117 let node = build_layout(widget, id, runtime, theme, &mut layout);
2118 layout.compute_scroll(node, 0.0, 0.0, true, true);
2119 layout
2120 .absolute_rects(node)
2121 .first()
2122 .map(|(r, _)| Size::new(r.width, r.height))
2123 .unwrap_or(Size::new(0.0, 0.0))
2124}
2125
2126/// A deferred overlay: `(content, id, the anchor's bounds, placement, dismissal,
2127/// progress 0..=1, whether it takes the pointer, the scrim's colour, the theme it was
2128/// declared under)`. The progress animates the appearance — a drawer sliding in, a
2129/// scrim fading — and is `1.0` for overlays that are not animated.
2130///
2131/// The **theme travels with it** because an overlay is painted long after the walk has
2132/// left the node that declared it: a dialog opened from inside a [`crate::Themed`]
2133/// subtree would otherwise come out in the root's theme, which is the surprise the
2134/// reference had to grow a whole mechanism to avoid. The scrim's colour travels for the
2135/// same reason — by the time it is painted, the widget that asked for it is long
2136/// behind.
2137type Overlay<'a, Msg> = (
2138 &'a dyn Widget<Msg>,
2139 WidgetId,
2140 Rect,
2141 Placement,
2142 Option<Msg>,
2143 f32,
2144 bool,
2145 Option<frus_core::Color>,
2146 std::rc::Rc<Theme>,
2147);
2148
2149struct Builder<'a, Msg> {
2150 scene: Scene,
2151 hits: Vec<Hit<Msg>>,
2152 long_presses: Vec<Hit<Msg>>,
2153 dismisses: Vec<Msg>,
2154 focusables: Vec<Focusable>,
2155 /// Start of the topmost modal overlay's focus scope.
2156 focus_scope_start: Option<usize>,
2157 /// Set while inside an `ExcludeFocus`: nothing in here registers a focus stop.
2158 focus_excluded: bool,
2159 /// The identity of the nearest enclosing backdrop group — the key a backdrop
2160 /// asking to be shared takes. Pushed and popped by the walk, like the focus flags.
2161 backdrop_group: Option<u64>,
2162 /// Set while inside an `ExcludeFocusTraversal`: stops register, Tab passes them by.
2163 focus_skipped: bool,
2164 /// The traversal order in force, from the nearest enclosing `FocusTraversalOrder`.
2165 focus_order: Option<f32>,
2166 /// The nearest enclosing `FocusTraversalGroup`, within which an order is resolved.
2167 focus_group: Option<WidgetId>,
2168 /// The nearest enclosing [`crate::AutofillGroup`]: the form a field is part of.
2169 autofill_group: Option<WidgetId>,
2170 /// Shortcut and action scopes closed so far this frame.
2171 scopes: Vec<Scope<Msg>>,
2172 /// Keystroke listeners, with the focus stops they cover.
2173 #[allow(clippy::type_complexity)]
2174 listeners: Vec<(
2175 std::ops::Range<usize>,
2176 std::rc::Rc<dyn Fn(KeyStroke) -> Option<Msg>>,
2177 )>,
2178 scrollables: Vec<Scrollable>,
2179 scrollbars: Vec<Scrollbar>,
2180 draggables: Vec<(WidgetId, Rect)>,
2181 drag_sources: Vec<DragSource>,
2182 drop_zones: Vec<DropZone>,
2183 inks: Vec<(WidgetId, Rect)>,
2184 reorderables: Vec<(WidgetId, Rect)>,
2185 interactives: Vec<(WidgetId, Rect)>,
2186 /// Deferred overlays: (content, id, the anchor's bounds, placement, dismissal, progress
2187 /// `0..=1`). The progress animates the appearance (a drawer sliding in, a scrim fading);
2188 /// it is `1.0` for overlays that are not animated.
2189 overlays: Vec<Overlay<'a, Msg>>,
2190 /// A widget is asking for a continuous, time-driven animation.
2191 wants_animation: bool,
2192 available: Size,
2193 runtime: &'a Runtime,
2194 /// The theme **in force at this point of the walk** — the root's, unless a
2195 /// [`crate::Themed`] ancestor replaced it. Owned rather than borrowed because a
2196 /// subtree's theme is derived from the one above it and outlives nothing.
2197 ///
2198 /// Owned **behind an `Rc`**: a `Theme` is eleven kilobytes, and this field is swapped
2199 /// on the way into a themed subtree and swapped back on the way out. By value that is
2200 /// two copies of eleven kilobytes standing in the walk's frame — one being moved in,
2201 /// one held across the recursion waiting to be put back — on every level of the tree,
2202 /// taken or not. Behind a pointer the swap is a pointer's width, and the walk's frame
2203 /// went from 28 144 bytes a level to 5 264.
2204 theme: std::rc::Rc<Theme>,
2205 /// The inspector's collection (`Some` only while it is on): one node per painted widget,
2206 /// in paint order.
2207 inspector: Option<Vec<crate::inspector::InspectorNode>>,
2208 /// Current depth of the walk (for the dump's indentation and the palette of the
2209 /// inspector's outlines).
2210 depth: usize,
2211 /// The [`crate::RefreshIndicator`] area currently being walked, if any: every scrollable
2212 /// registered under it records it, so the shell knows where to send the movement
2213 /// its physics refuses. Saved and restored around the subtree, so sibling areas do
2214 /// not inherit one another's.
2215 refresh_host: Option<WidgetId>,
2216 /// The scroll region currently being walked, if any: the nearest one enclosing
2217 /// whatever the walk is looking at. Focus stops record it so the shell can reveal
2218 /// them, and nested regions record one another. Saved and restored around the
2219 /// subtree, like [`Builder::refresh_host`].
2220 scroll_host: Option<WidgetId>,
2221 /// Which screen of a route transition the walk is inside (`0` = leaving, `1` =
2222 /// entering); `None` outside one. Recorded on each hero so the two sides of a
2223 /// flight are told apart rather than guessed from paint order.
2224 hero_screen: Option<u8>,
2225 /// The shared elements seen so far, each with the widget that declared it (needed
2226 /// to lift its subtree's painting) and the transition screen it belongs to.
2227 heroes: Vec<(HeroSpot, &'a dyn Widget<Msg>)>,
2228 /// The frame's refresh areas, in paint order.
2229 refreshes: Vec<Refreshable>,
2230 /// The frame's dismissible items, in paint order.
2231 dismissables: Vec<Dismissable>,
2232 /// The frame's sheets, in the order their subtrees finished walking.
2233 sheets: Vec<crate::sheet::SheetArea>,
2234 /// Accessibility nodes collected during the walk (paint order).
2235 semantics: Vec<(WidgetId, Rect, frus_core::SemanticsProperties)>,
2236 /// The parts of the screen saying what the system bars over them should look like,
2237 /// with their boxes, in paint order.
2238 system_ui: Vec<(Rect, crate::SystemUiOverlayStyle)>,
2239 /// Boxes whose children did not fit, screen-positioned, from every sub-root walked
2240 /// this frame.
2241 overflows: std::cell::RefCell<Vec<Overflowing>>,
2242 /// The same, still in their sub-root's own coordinates, waiting for the walk to reach
2243 /// that sub-root and say where on screen it ended up.
2244 pending_overflows: std::cell::RefCell<std::collections::HashMap<WidgetId, Vec<Overflowing>>>,
2245}
2246
2247impl<'a, Msg: Clone + 'static> Builder<'a, Msg> {
2248 /// A layout root's rects, through the relayout cache retained in the runtime (it only
2249 /// recomputes through taffy when the style/structure/constraints have changed). A brief
2250 /// mutable borrow: the `Vec` returned is owned.
2251 ///
2252 /// In **RTL**, taffy computes in LTR (canonical, and cached), then each rect is
2253 /// **mirrored** about the root's width (the 1st rect): the rows reverse, the alignment
2254 /// and the margins flip — without touching the widgets. The text itself is drawn
2255 /// normally inside its moved box (the *internal* bidi is handled by cosmic-text).
2256 fn cached_rects(&self, key: WidgetId, root: &dyn Widget<Msg>, c: Constraints) -> Vec<Rect> {
2257 let (mut rects, overflows) =
2258 self.runtime
2259 .layout_cache
2260 .borrow_mut()
2261 .rects(key, root, self.runtime, &self.theme, c);
2262 self.mirror(&mut rects);
2263 self.record_overflows(key, &rects, overflows);
2264 rects
2265 }
2266
2267 /// Files this sub-root's overflowing boxes under its identity, mirrored with
2268 /// everything else in RTL — where an edge changes name as well as position, since the
2269 /// layout is the mirror image and "ran past the right" becomes "ran past the left".
2270 ///
2271 /// They wait here rather than going straight out because a sub-root's rectangles are
2272 /// in its **own** coordinates: a scrollable's content, a page, a list item and a
2273 /// stack's layer each get their own taffy pass, and where that pass lands on screen is
2274 /// only known when the walk reaches it. [`Self::claim_overflows`] is the other half.
2275 /// Records a child that came out **bigger than the box holding it**, one entry per
2276 /// edge it ran past — the same yellow and black band a row too full of children gets,
2277 /// from a box that laid its child out itself and so was never measured against it.
2278 ///
2279 /// The rectangle is already on the screen, this being the walk rather than a sub-root's
2280 /// own pass, so it goes straight out rather than through [`Self::record_overflows`].
2281 fn report_spill(&self, own: Rect, child: Rect) {
2282 // Per edge, from where the child actually landed: a centred child that is too wide
2283 // runs past **both** sides, and a band on one of them would be a half-truth.
2284 let over = [
2285 (Side::Left, own.x - child.x),
2286 (Side::Top, own.y - child.y),
2287 (Side::Right, (child.x + child.width) - (own.x + own.width)),
2288 (
2289 Side::Bottom,
2290 (child.y + child.height) - (own.y + own.height),
2291 ),
2292 ];
2293 let spills: Vec<Overflowing> = over
2294 .into_iter()
2295 .filter(|(_, amount)| *amount > 0.5)
2296 .map(|(side, amount)| Overflowing {
2297 rect: own,
2298 side,
2299 amount,
2300 })
2301 .collect();
2302 self.overflows.borrow_mut().extend(spills);
2303 }
2304
2305 fn record_overflows(&self, key: WidgetId, mirrored: &[Rect], overflows: Vec<Overflowing>) {
2306 if overflows.is_empty() {
2307 return;
2308 }
2309 let rtl = self.rtl();
2310 let root = mirrored.first().copied();
2311 let list: Vec<Overflowing> = overflows
2312 .into_iter()
2313 .map(|mut o| {
2314 if let (true, Some(root)) = (rtl, root) {
2315 o.rect.x = root.x + (root.width - (o.rect.x - root.x) - o.rect.width);
2316 o.side = match o.side {
2317 Side::Left => Side::Right,
2318 Side::Right => Side::Left,
2319 other => other,
2320 };
2321 }
2322 o
2323 })
2324 .collect();
2325 self.pending_overflows.borrow_mut().insert(key, list);
2326 }
2327
2328 /// The walk has reached sub-root `id` and knows where it sits: its overflowing boxes
2329 /// become screen rectangles.
2330 fn claim_overflows(&self, id: WidgetId, translation: (f32, f32)) {
2331 let taken = self.pending_overflows.borrow_mut().remove(&id);
2332 if let Some(list) = taken {
2333 self.overflows
2334 .borrow_mut()
2335 .extend(list.into_iter().map(|o| Overflowing {
2336 rect: o.rect.translate(translation.0, translation.1),
2337 ..o
2338 }));
2339 }
2340 }
2341
2342 /// The layout direction **in force here** — read from the ambient theme rather than
2343 /// from a field set once at the root, so a [`crate::Themed`] subtree carries its own.
2344 ///
2345 /// What that reaches is everything decided during the walk: which edge a drawer
2346 /// slides from, which way an anchored overlay opens, the sign of a rotation, the
2347 /// mirroring of any **layout root** inside the subtree (a scrollable, an overlay).
2348 /// What it does not reach is ordinary flow inside the subtree: the frame's rects are
2349 /// computed once at the root and mirrored there as a whole, so a direction set part
2350 /// way down does not reverse the rows around it. Setting the direction on the theme
2351 /// handed to `build_ui` is still the way to flip an application.
2352 fn rtl(&self) -> bool {
2353 self.theme.direction.is_rtl()
2354 }
2355
2356 /// Mirrors a root's rects horizontally (RTL). The first rect is the root itself: its
2357 /// width is the axis of symmetry.
2358 fn mirror(&self, rects: &mut [Rect]) {
2359 if !self.rtl() {
2360 return;
2361 }
2362 let Some(root) = rects.first().copied() else {
2363 return;
2364 };
2365 // The axis: the rects are relative to the root (origin at root.x).
2366 for r in rects.iter_mut() {
2367 r.x = root.x + (root.width - (r.x - root.x) - r.width);
2368 }
2369 }
2370
2371 /// 64-bit fingerprint of everything painting a boundary subtree reads **without**
2372 /// rebuilding the `view`: each descendant's `Status` (in walk order) and the subtree's
2373 /// absolute rects. An unchanged fingerprint + generation ⇒ a bit-for-bit identical paint.
2374 /// The time is excluded (a cacheable boundary holds no `continuous` widget).
2375 fn boundary_fingerprint(
2376 &self,
2377 widget: &dyn Widget<Msg>,
2378 id: WidgetId,
2379 translation: (f32, f32),
2380 sub: &[Rect],
2381 ) -> u64 {
2382 let mut h = std::collections::hash_map::DefaultHasher::new();
2383 self.rtl().hash(&mut h);
2384 translation.0.to_bits().hash(&mut h);
2385 translation.1.to_bits().hash(&mut h);
2386 for r in sub {
2387 r.x.to_bits().hash(&mut h);
2388 r.y.to_bits().hash(&mut h);
2389 r.width.to_bits().hash(&mut h);
2390 r.height.to_bits().hash(&mut h);
2391 }
2392 self.hash_statuses(widget, id, &mut h);
2393 h.finish()
2394 }
2395
2396 /// Adds `widget`'s `Status` to `h`, then its children's, recursively — following the
2397 /// walk's identity scheme (`child_id`) **exactly**, so it lines up with the order in which
2398 /// `walk_node` paints them.
2399 fn hash_statuses<H: Hasher>(&self, widget: &dyn Widget<Msg>, id: WidgetId, h: &mut H) {
2400 hash_status(&self.full_status(widget, id), h);
2401 // Live ink is part of what the boundary paints, and it moves every frame while a
2402 // splash is alive. Without it here the cached primitives would be replayed and
2403 // the ripple would stand still — and once the ink dries the hash goes back to
2404 // what it was, so the boundary starts hitting again.
2405 if let Some(ripples) = self.runtime.ink.get(&id) {
2406 ripples.hash_state(h);
2407 }
2408 for (i, child) in widget.children().iter().enumerate() {
2409 self.hash_statuses(child.as_ref(), child_id(id, i, child.as_ref()), h);
2410 }
2411 }
2412
2413 /// Current lengths of the builder's collections: the lower bound of the slices to capture
2414 /// for a boundary (see [`capture_since`](Self::capture_since)).
2415 fn snapshot(&self) -> Snapshot {
2416 Snapshot {
2417 scene: self.scene.primitives().len(),
2418 hits: self.hits.len(),
2419 long_presses: self.long_presses.len(),
2420 dismisses: self.dismisses.len(),
2421 focusables: self.focusables.len(),
2422 scrollables: self.scrollables.len(),
2423 draggables: self.draggables.len(),
2424 drag_sources: self.drag_sources.len(),
2425 drop_zones: self.drop_zones.len(),
2426 inks: self.inks.len(),
2427 semantics: self.semantics.len(),
2428 system_ui: self.system_ui.len(),
2429 overlays: self.overlays.len(),
2430 focus_scope_start: self.focus_scope_start,
2431 }
2432 }
2433
2434 /// Captures the output produced since `snap` (the collections' *tail slices*) into a
2435 /// [`BoundaryData`]. Returns `None` — hence **not cacheable** — when the subtree pushed an
2436 /// overlay or touched the modal focus scope (global state that cannot be captured here).
2437 fn capture_since(&self, snap: &Snapshot) -> Option<BoundaryData<Msg>> {
2438 if self.overlays.len() != snap.overlays || self.focus_scope_start != snap.focus_scope_start
2439 {
2440 return None;
2441 }
2442 Some(BoundaryData {
2443 prims: self.scene.primitives()[snap.scene..].to_vec(),
2444 hits: self.hits[snap.hits..].to_vec(),
2445 long_presses: self.long_presses[snap.long_presses..].to_vec(),
2446 dismisses: self.dismisses[snap.dismisses..].to_vec(),
2447 focusables: self.focusables[snap.focusables..].to_vec(),
2448 scrollables: self.scrollables[snap.scrollables..].to_vec(),
2449 draggables: self.draggables[snap.draggables..].to_vec(),
2450 drag_sources: self.drag_sources[snap.drag_sources..].to_vec(),
2451 drop_zones: self.drop_zones[snap.drop_zones..].to_vec(),
2452 inks: self.inks[snap.inks..].to_vec(),
2453 semantics: self.semantics[snap.semantics..].to_vec(),
2454 system_ui: self.system_ui[snap.system_ui..].to_vec(),
2455 })
2456 }
2457
2458 /// Replays a boundary from the cache: primitives already formed (clip/owner baked in) and
2459 /// interaction maps, appended as is.
2460 fn splice_boundary(&mut self, data: BoundaryData<Msg>) {
2461 for p in data.prims {
2462 self.scene.push_primitive(p);
2463 }
2464 self.hits.extend(data.hits);
2465 self.long_presses.extend(data.long_presses);
2466 self.dismisses.extend(data.dismisses);
2467 self.focusables.extend(data.focusables);
2468 self.scrollables.extend(data.scrollables);
2469 self.draggables.extend(data.draggables);
2470 self.drag_sources.extend(data.drag_sources);
2471 self.drop_zones.extend(data.drop_zones);
2472 self.inks.extend(data.inks);
2473 self.semantics.extend(data.semantics);
2474 self.system_ui.extend(data.system_ui);
2475 }
2476
2477 /// Lengths of every registry a [`ModalBarrier`] can withhold from, taken before its subtree
2478 /// is walked.
2479 fn barrier_base(&self) -> BarrierBase {
2480 BarrierBase {
2481 scene: self.scene.primitives().len(),
2482 hits: self.hits.len(),
2483 long_presses: self.long_presses.len(),
2484 focusables: self.focusables.len(),
2485 scrollables: self.scrollables.len(),
2486 scrollbars: self.scrollbars.len(),
2487 draggables: self.draggables.len(),
2488 drag_sources: self.drag_sources.len(),
2489 drop_zones: self.drop_zones.len(),
2490 inks: self.inks.len(),
2491 reorderables: self.reorderables.len(),
2492 interactives: self.interactives.len(),
2493 semantics: self.semantics.len(),
2494 }
2495 }
2496
2497 /// Drops what the subtree added since `base`, according to `barrier`, and — for an
2498 /// absorbing barrier — puts a message-less hit target in its place so that input stops
2499 /// at the barrier instead of reaching whatever is painted behind it.
2500 /// Resolves the **shared elements** of a route transition: every tag that appears
2501 /// on both screens flies from where it was to where it is going.
2502 ///
2503 /// Called once, after both screens have been drawn, because that is the first
2504 /// moment both boxes are known. `hero_base` and `scene_base` mark where those two
2505 /// screens started.
2506 ///
2507 /// What flies is the **destination**'s own painting, mapped onto the box it is
2508 /// travelling through. Both originals are taken out of the frame: a thing that is
2509 /// flying is not also sitting at either end, and leaving them in is the difference
2510 /// between a shared element and three copies of one.
2511 fn fly_heroes(&mut self, hero_base: usize, scene_base: usize, progress: f32) {
2512 let spots: Vec<(HeroSpot, &'a dyn Widget<Msg>)> = self.heroes.split_off(hero_base);
2513 if spots.len() < 2 {
2514 return;
2515 }
2516 // The pairs: one tag, one hero on each side. A tag on one side only has nothing
2517 // to fly to, and a tag used more than once per side is ambiguous — both are
2518 // left alone rather than guessed at.
2519 let mut flights: Vec<(Rect, Rect, &'a dyn Widget<Msg>, WidgetId, WidgetId)> = Vec::new();
2520 for (from, _) in spots.iter().filter(|(s, _)| s.screen == 0) {
2521 let mut matching = spots
2522 .iter()
2523 .filter(|(s, _)| s.screen == 1 && s.tag == from.tag);
2524 let Some((to, widget)) = matching.next() else {
2525 continue;
2526 };
2527 if matching.next().is_some() {
2528 continue;
2529 }
2530 flights.push((from.rect, to.rect, *widget, from.id, to.id));
2531 }
2532 if flights.is_empty() {
2533 return;
2534 }
2535
2536 // Everything the two screens painted, so the originals can be taken out and the
2537 // travelling copies laid on top.
2538 let painted = self.scene.split_off(scene_base);
2539 let mut hidden: std::collections::HashSet<u64> = std::collections::HashSet::new();
2540 for (_, _, widget, from_id, to_id) in &flights {
2541 for id in subtree_ids(*widget, *to_id) {
2542 hidden.insert(id.as_u64());
2543 }
2544 // The source's subtree is the *other* screen's widget, whose identity is
2545 // rooted at `from_id`; the same relative shape, so the ids follow.
2546 for id in subtree_ids(*widget, *from_id) {
2547 hidden.insert(id.as_u64());
2548 }
2549 }
2550 for primitive in &painted {
2551 if !hidden.contains(&primitive.owner()) {
2552 self.scene.push_primitive(primitive.clone());
2553 }
2554 }
2555
2556 self.scene.set_clip(Rect::UNBOUNDED);
2557 for (from, to, _, _, to_id) in &flights {
2558 let travelling = lerp_rect(*from, *to, progress);
2559 if to.width <= 0.0 || to.height <= 0.0 {
2560 continue;
2561 }
2562 let (sx, sy) = (travelling.width / to.width, travelling.height / to.height);
2563 let pivot = Point::new(to.x, to.y);
2564 let owners: std::collections::HashSet<u64> = subtree_ids(
2565 flights
2566 .iter()
2567 .find(|(_, _, _, _, id)| id == to_id)
2568 .map(|(_, _, w, _, _)| *w)
2569 .expect("the flight just matched"),
2570 *to_id,
2571 )
2572 .iter()
2573 .map(|id| id.as_u64())
2574 .collect();
2575 for primitive in painted.iter().filter(|p| owners.contains(&p.owner())) {
2576 let flown = primitive
2577 .scaled_about_xy(pivot, sx, sy)
2578 .translated(travelling.x - to.x, travelling.y - to.y)
2579 .with_clip(Rect::UNBOUNDED);
2580 self.scene.push_primitive(flown);
2581 }
2582 }
2583 }
2584
2585 /// Reconciles what a [`crate::Semantics`] wrapper's subtree announced with what the
2586 /// wrapper says about it.
2587 ///
2588 /// The wrapper's own answers win, and the subtree's show through wherever it said
2589 /// nothing — a container that knows a child is a *heading* rarely knows what the words
2590 /// are, and dropping them would replace a spoken title with silence.
2591 ///
2592 /// The merged node goes in **where the subtree's first one was**, not at the end. The
2593 /// accessibility tree is flat and its order is reading order, so a wrapper appended
2594 /// after its own contents would be read after them.
2595 fn apply_description(
2596 &mut self,
2597 described: crate::semantics::Description,
2598 base: usize,
2599 id: WidgetId,
2600 own: Rect,
2601 ) {
2602 let mut props = described.props;
2603 // Merging swallows every child's own entry, so **something** has to answer for
2604 // this node when it is activated — the wrapper itself never does (see
2605 // `Semantics::on_click`'s own reasoning: an annotation that swallowed a click
2606 // would put a hit target over the widget it describes). Found testing #18 in a
2607 // real browser and real screen reader: a `Checkbox::new(..).on_toggle(..)`
2608 // merged with its row's caption read correctly and did **nothing** on
2609 // activation, on every platform, because the merged entry carried the row's own
2610 // id, which nothing routes a click through. When exactly one child in the
2611 // swallowed subtree was clickable, its id is who a click or a keyboard
2612 // activation has to reach, so the merged entry keys on that id instead — the
2613 // wrapper's id is kept only when there is no such child, or more than one and
2614 // "the one that gets clicked" has no single answer.
2615 let mut operable = None;
2616 if described.merging {
2617 let mut clickable_count = 0;
2618 for (child_id, _, child) in self.semantics.drain(base..).collect::<Vec<_>>() {
2619 if child.clickable {
2620 clickable_count += 1;
2621 operable = Some(child_id);
2622 }
2623 props = props.over(&child);
2624 }
2625 if clickable_count != 1 {
2626 operable = None;
2627 }
2628 }
2629 let id = operable.unwrap_or(id);
2630 if props.is_meaningful() && own.width > 0.0 && own.height > 0.0 {
2631 self.semantics.insert(base, (id, own, props));
2632 }
2633 }
2634
2635 fn apply_barrier(
2636 &mut self,
2637 barrier: ModalBarrier,
2638 base: &BarrierBase,
2639 id: WidgetId,
2640 own: Rect,
2641 ) {
2642 if barrier.pointer {
2643 self.hits.truncate(base.hits);
2644 self.long_presses.truncate(base.long_presses);
2645 self.focusables.truncate(base.focusables);
2646 self.scrollables.truncate(base.scrollables);
2647 self.scrollbars.truncate(base.scrollbars);
2648 self.draggables.truncate(base.draggables);
2649 self.drag_sources.truncate(base.drag_sources);
2650 self.drop_zones.truncate(base.drop_zones);
2651 self.inks.truncate(base.inks);
2652 self.reorderables.truncate(base.reorderables);
2653 self.interactives.truncate(base.interactives);
2654 // The modal focus scope is an index **into** `focusables`. A barrier that cut
2655 // away the focusable it pointed at would leave it dangling past the end, and
2656 // every later slice of the pool would then be empty — no focusable at all,
2657 // rather than the ones the barrier meant to spare.
2658 if let Some(start) = self.focus_scope_start {
2659 self.focus_scope_start = Some(start.min(self.focusables.len()));
2660 }
2661 }
2662 if barrier.absorb && own.width > 0.0 && own.height > 0.0 {
2663 self.hits.push(Hit {
2664 id,
2665 rect: own,
2666 msg: None,
2667 xform: None,
2668 });
2669 }
2670 if barrier.paint {
2671 // The clip is baked into each primitive when it is pushed, so dropping the tail
2672 // leaves nothing for the next sibling to repair.
2673 let _hidden = self.scene.split_off(base.scene);
2674 }
2675 if barrier.semantics {
2676 self.semantics.truncate(base.semantics);
2677 }
2678 }
2679
2680 /// Lower bounds of the interaction registries **before** a transformed composited subtree.
2681 fn xform_base(&self) -> XformBase {
2682 XformBase {
2683 hits: self.hits.len(),
2684 long_presses: self.long_presses.len(),
2685 focusables: self.focusables.len(),
2686 scrollables: self.scrollables.len(),
2687 draggables: self.draggables.len(),
2688 drag_sources: self.drag_sources.len(),
2689 drop_zones: self.drop_zones.len(),
2690 inks: self.inks.len(),
2691 reorderables: self.reorderables.len(),
2692 semantics: self.semantics.len(),
2693 }
2694 }
2695
2696 /// Applies the layer transform `matrix` to the registry entries **added since** `base`:
2697 /// the test point of click / long-press targets goes through `M⁻¹` (without overwriting an
2698 /// inner transform already set); and when `matrix` preserves axis alignment
2699 /// (scale/translation, no rotation), the **rects** of focus / scrolling / dragging /
2700 /// **reordering** / accessibility are mapped exactly by `M` (otherwise they are left as
2701 /// they are — the click stays correct through `M⁻¹`). The **shared factor** of the two
2702 /// transformed-composition sites (`walk`'s boundary and `emit_transformed_child`): one
2703 /// place to keep up to date, which forgets no registry (milestone 250 had forgotten
2704 /// `reorderables` in exactly one of the two).
2705 fn transform_interaction_registries(&mut self, base: &XformBase, matrix: Affine) {
2706 let inverse = matrix.inverse();
2707 for h in &mut self.hits[base.hits..] {
2708 h.xform.get_or_insert(inverse);
2709 }
2710 for h in &mut self.long_presses[base.long_presses..] {
2711 h.xform.get_or_insert(inverse);
2712 }
2713 if matrix.is_axis_aligned() {
2714 for f in &mut self.focusables[base.focusables..] {
2715 f.rect = matrix.apply_rect(f.rect);
2716 f.bounds = matrix.apply_rect(f.bounds);
2717 }
2718 for area in &mut self.scrollables[base.scrollables..] {
2719 area.viewport = matrix.apply_rect(area.viewport);
2720 }
2721 for source in &mut self.drag_sources[base.drag_sources..] {
2722 source.rect = matrix.apply_rect(source.rect);
2723 }
2724 for zone in &mut self.drop_zones[base.drop_zones..] {
2725 zone.rect = matrix.apply_rect(zone.rect);
2726 }
2727 for (_, r) in &mut self.inks[base.inks..] {
2728 *r = matrix.apply_rect(*r);
2729 }
2730 for (_, r) in &mut self.draggables[base.draggables..] {
2731 *r = matrix.apply_rect(*r);
2732 }
2733 for (_, r) in &mut self.reorderables[base.reorderables..] {
2734 *r = matrix.apply_rect(*r);
2735 }
2736 for (_, r, _) in &mut self.semantics[base.semantics..] {
2737 *r = matrix.apply_rect(*r);
2738 }
2739 }
2740 }
2741
2742 /// Entry point for walking a node. When the node is a cacheable **repaint boundary**, it
2743 /// tries to replay its subtree from the paint cache; otherwise (or on a *miss*) it
2744 /// delegates to the full walk [`walk_node`](Self::walk_node), capturing its output for the
2745 /// next frame. Every recursion goes through here → nested boundaries get cached too.
2746 /// The focus flags a subtree carries — exclusion, Tab-skipping, an order, a group —
2747 /// are **subtree-scoped**, so they are pushed here and popped on the way out rather
2748 /// than asked of each widget at the moment it registers. The body of the walk has
2749 /// early returns in a dozen branches; wrapping it is the only way to be sure the
2750 /// scope is closed on every one of them.
2751 fn walk(
2752 &mut self,
2753 widget: &'a dyn Widget<Msg>,
2754 id: WidgetId,
2755 translation: (f32, f32),
2756 clip: Rect,
2757 rects: &[Rect],
2758 index: &mut usize,
2759 ) {
2760 let outer = (
2761 self.focus_excluded,
2762 self.focus_skipped,
2763 self.focus_order,
2764 self.focus_group,
2765 );
2766 if !widget.descendants_focusable() {
2767 self.focus_excluded = true;
2768 }
2769 if widget.focus_skip_traversal() {
2770 self.focus_skipped = true;
2771 }
2772 if let Some(order) = widget.focus_order() {
2773 self.focus_order = Some(order);
2774 }
2775 if widget.focus_group() {
2776 self.focus_group = Some(id);
2777 }
2778 // The backdrop group a subtree sits in, scoped the same way and for the same
2779 // reason. Its own identity is the key: stable across frames, and unique
2780 // without anything having to hand one out.
2781 let outer_group = self.backdrop_group;
2782 if widget.backdrop_group() {
2783 self.backdrop_group = Some(id.as_u64());
2784 }
2785 // The form a field belongs to, scoped the same way (milestone 512).
2786 let outer_form = self.autofill_group;
2787 if widget.autofill_group() {
2788 self.autofill_group = Some(id);
2789 }
2790 // The focus stops this subtree contains start here; the walk is depth-first, so
2791 // they are contiguous and the range closes below.
2792 let stops_before = self.focusables.len();
2793 self.walk_scoped(widget, id, translation, clip, rects, index);
2794 (
2795 self.focus_excluded,
2796 self.focus_skipped,
2797 self.focus_order,
2798 self.focus_group,
2799 ) = outer;
2800 self.backdrop_group = outer_group;
2801 self.autofill_group = outer_form;
2802 self.close_scope(widget, stops_before);
2803 }
2804
2805 /// Files whatever tables this subtree carried, with the focus stops it turned out to
2806 /// contain. Recorded **on the way out**, because the range is not known on the way in;
2807 /// so `scopes` ends up innermost-first among any that overlap, which is the order the
2808 /// resolution wants and the reason it is not sorted afterwards.
2809 fn close_scope(&mut self, widget: &'a dyn Widget<Msg>, stops_before: usize) {
2810 let range = stops_before..self.focusables.len();
2811 if let Some(on_key) = widget.on_keystroke() {
2812 self.listeners.push((range.clone(), on_key));
2813 }
2814 let (strokes, callbacks) = (widget.shortcut_bindings(), widget.shortcut_callbacks());
2815 let (actions, listeners) = (widget.action_bindings(), widget.action_listeners());
2816 if strokes.is_empty() && callbacks.is_empty() && actions.is_empty() && listeners.is_empty()
2817 {
2818 return;
2819 }
2820 self.scopes.push(Scope {
2821 strokes: strokes.to_vec(),
2822 callbacks: callbacks.to_vec(),
2823 actions: actions.to_vec(),
2824 listeners: listeners.to_vec(),
2825 range,
2826 });
2827 }
2828
2829 fn walk_scoped(
2830 &mut self,
2831 widget: &'a dyn Widget<Msg>,
2832 id: WidgetId,
2833 translation: (f32, f32),
2834 clip: Rect,
2835 rects: &[Rect],
2836 index: &mut usize,
2837 ) {
2838 self.claim_overflows(id, translation);
2839 // A **sheet's panel**: the subtree is walked as usual, and every scroll area it
2840 // registered is recorded against the sheet, so the shell knows which lists share a
2841 // finger with it. By range, since a subtree's registrations are contiguous —
2842 // a replayed boundary splices its own in the same place.
2843 if let Some(spec) = widget.sheet() {
2844 let panel = rects[*index].translate(translation.0, translation.1);
2845 let first = self.scrollables.len();
2846 self.walk_node(widget, id, translation, clip, rects, index);
2847 let areas = self.scrollables[first..]
2848 .iter()
2849 .map(|area| area.id)
2850 .collect();
2851 // What the share is a share of. At nought there is nothing to divide by, so the
2852 // height last measured stands in: a finger that lowered the panel to nothing
2853 // may still be holding it.
2854 let size = self.runtime.sheet_size(id, spec);
2855 let available = if size > 0.0 {
2856 panel.height / size
2857 } else {
2858 self.runtime
2859 .sheets
2860 .get(&id)
2861 .map_or(0.0, |sheet| sheet.available())
2862 };
2863 self.sheets.push(crate::sheet::SheetArea {
2864 id,
2865 panel,
2866 available,
2867 spec: spec.clone(),
2868 areas,
2869 });
2870 return;
2871 }
2872
2873 // A **refresh area**: the subtree is walked with this widget named as the host, so
2874 // every scrollable inside records where to send the movement its physics refuses at
2875 // the top edge. The indicator is then painted **over** the subtree — painting it in
2876 // the widget's own `paint` would put it under the list it belongs to.
2877 if let Some(spec) = widget.refresh() {
2878 let viewport = rects[*index]
2879 .translate(translation.0, translation.1)
2880 .intersect(clip);
2881 let outer = self.refresh_host.replace(id);
2882 self.walk_node(widget, id, translation, clip, rects, index);
2883 self.refresh_host = outer;
2884
2885 let area = Refreshable { id, viewport, spec };
2886 if let Some(pull) = self.runtime.refresh.get(&id) {
2887 crate::refresh::paint_refresh(&mut self.scene, &area, pull, &self.theme, clip);
2888 // A pull that is settling, spinning or fading away drives itself, so the
2889 // frame after this one has to happen.
2890 self.wants_animation = true;
2891 }
2892 self.refreshes.push(area);
2893 return;
2894 }
2895
2896 // A **barrier** (`IgnorePointer`, `AbsorbPointer`, a hidden `Visibility`,
2897 // `ExcludeSemantics`): the subtree is walked exactly as usual, and then whatever it
2898 // added to the withheld registries is dropped again.
2899 //
2900 // Removing afterwards rather than skipping the walk is what makes it exact. A widget
2901 // deep inside registers its click target, focus stop, scrollable area or accessibility
2902 // node without knowing that something above is holding the subtree out of the frame;
2903 // truncating at the barrier catches every one of them, including those added by
2904 // widgets written after this code. It also keeps the walk's rect indexing untouched,
2905 // which a skipped subtree would break.
2906 // A **description** (`Semantics`): something above states what the subtree *is*,
2907 // which the subtree cannot state for itself. Same shape as the barrier below and for
2908 // the same reason — walk first, reconcile after, so a widget deep inside annotates
2909 // itself without knowing anything is speaking for it.
2910 if let Some(described) = widget.describes() {
2911 let base = self.semantics.len();
2912 let own = rects[*index]
2913 .translate(translation.0, translation.1)
2914 .intersect(clip);
2915 self.walk_node(widget, id, translation, clip, rects, index);
2916 self.apply_description(described, base, id, own);
2917 return;
2918 }
2919
2920 if let Some(barrier) = widget.barrier().filter(|b| !b.is_none()) {
2921 let base = self.barrier_base();
2922 // The barrier's own box, before `walk_node` advances the index past it.
2923 let own = rects[*index]
2924 .translate(translation.0, translation.1)
2925 .intersect(clip);
2926 self.walk_node(widget, id, translation, clip, rects, index);
2927 self.apply_barrier(barrier, &base, id, own);
2928 return;
2929 }
2930
2931 // An opacity group: the subtree is painted normally, then its range of primitives is
2932 // **drained** into a composited layer at the group's opacity — so overlaps do not
2933 // double-blend. The animated opacity is the value the runtime tweened; otherwise the
2934 // fixed target. Opaque (≈1): no layer at all (zero cost).
2935 if let Some(target) = widget.opacity_group() {
2936 let opacity = self.runtime.value_or(id, target).clamp(0.0, 1.0);
2937 if opacity < 0.999 {
2938 let start = self.scene.primitives().len();
2939 self.walk_node(widget, id, translation, clip, rects, index);
2940 let group = self.scene.split_off(start);
2941 self.scene.push_primitive(Primitive::Layer {
2942 primitives: group,
2943 opacity,
2944 clip,
2945 clip_shape: ClipShape::Rect,
2946 transform: None,
2947 filter: LayerFilter::NONE,
2948 owner: id.as_u64(),
2949 });
2950 return;
2951 }
2952 // Fully opaque: an ordinary render (no pointless layer).
2953 }
2954
2955 // A **pixel effect** over the subtree (`ColorFiltered`, `ImageFiltered`,
2956 // `ShaderMask`): painted normally, then drained into a composited layer that
2957 // carries the effect. The renderer applies it when it composites — the image
2958 // filter as a pre-pass over the layer texture, the colour filter and the mask
2959 // in the compositing fragment.
2960 //
2961 // The box is passed in because a mask is written in fractions of the box it
2962 // covers, and this is the first point at which that box is a place on screen.
2963 if let Some(filter) = widget.layer_filter(crate::widget::FilterContext {
2964 box_rect: rects[*index].translate(translation.0, translation.1),
2965 backdrop_group: self.backdrop_group,
2966 }) {
2967 // The layer keeps the **ambient** clip rather than its own box: an image
2968 // filter reaches past the pixels it came from, and cutting it at the box
2969 // would shave the edge off every blur.
2970 let start = self.scene.primitives().len();
2971 self.walk_node(widget, id, translation, clip, rects, index);
2972 let group = self.scene.split_off(start);
2973 let (group, filter, clip_box) = fold_filter(group, filter, clip);
2974 self.scene.push_primitive(Primitive::Layer {
2975 primitives: group,
2976 opacity: 1.0,
2977 clip: clip_box,
2978 clip_shape: ClipShape::Rect,
2979 transform: None,
2980 filter,
2981 owner: id.as_u64(),
2982 });
2983 return;
2984 }
2985
2986 // **Shape** clipping: the subtree is painted normally, bounded to the widget's box,
2987 // then its primitives are **drained** into a composited layer whose shape (rounded
2988 // corners / ellipse) modulates the alpha — the overflowing corners are erased at
2989 // compositing time. The shape's box is the layer's clip rect. Two sources: `ClipPath`
2990 // (an arbitrary path, taking priority) or `ClipRRect`/`ClipOval` (an analytic shape).
2991 // The local path is **offset to the box's screen position**.
2992 if widget.clip_path().is_some() || widget.clip_shape().is_some() {
2993 let box_rect = rects[*index].translate(translation.0, translation.1);
2994 let shape = widget
2995 .clip_path()
2996 .map(|p| ClipShape::Path(p.translated(box_rect.x, box_rect.y)))
2997 .or_else(|| widget.clip_shape())
2998 .expect("clip_path or clip_shape is Some");
2999 let clip_box = clip.intersect(box_rect);
3000 let start = self.scene.primitives().len();
3001 self.walk_node(widget, id, translation, clip_box, rects, index);
3002 let group = self.scene.split_off(start);
3003 // A clip around a filter is **one** layer, not two. It matters most for a
3004 // backdrop: the reference's own advice is to wrap one in a clip to give it
3005 // its shape, and a backdrop pushed a level down would be filtering the
3006 // clip's contents rather than the frame.
3007 let (group, filter, clip_box) = fold_filter(group, LayerFilter::NONE, clip_box);
3008 self.scene.push_primitive(Primitive::Layer {
3009 primitives: group,
3010 opacity: 1.0,
3011 clip: clip_box,
3012 clip_shape: shape,
3013 transform: None,
3014 filter,
3015 owner: id.as_u64(),
3016 });
3017 return;
3018 }
3019
3020 // Composable paint transforms (`Transform`: scale and/or rotation; translation goes
3021 // through the child offset instead). Scale and rotation are melted into **a single
3022 // affine matrix** `M`, the subtree is painted **flat**, then wrapped in a composited
3023 // layer transformed by `M`. The hit-test applies `M⁻¹` to the point. The translation,
3024 // applied upstream through `child_offset`, is the innermost one.
3025 //
3026 // A transform that was told to **animate** hands over its numbers, and only its
3027 // numbers: the pivots stay the widget's, because a pivot is a choice of origin
3028 // rather than a quantity and interpolating it would slide the subtree across the
3029 // screen with nothing describing it having changed. Where the runtime has never
3030 // heard of this widget the static value stands, which is the same rule every
3031 // other animated property follows on mount.
3032 let moving = self.runtime.anim_transform(id);
3033 let scale = widget
3034 .transform_scale()
3035 .map(|(sx, sy, pivot)| {
3036 moving.map_or((sx, sy, pivot), |m| (m.scale_x, m.scale_y, pivot))
3037 })
3038 .filter(|(sx, sy, _)| (sx - 1.0).abs() > 1e-4 || (sy - 1.0).abs() > 1e-4);
3039 let rotate = widget
3040 .transform_rotate()
3041 .map(|(angle, pivot)| moving.map_or((angle, pivot), |m| (m.rotation, pivot)))
3042 .filter(|(a, _)| a.abs() > 1e-4);
3043 if scale.is_some() || rotate.is_some() {
3044 // The pivots are taken on the **child's** box (the next node in prefix order): it
3045 // is the child that gets transformed, and its box hugs the content even when the
3046 // Transform's own box is stretched by the parent.
3047 let basis = rects.get(*index + 1).copied().unwrap_or(rects[*index]);
3048 let box_rect = basis.translate(translation.0, translation.1);
3049 let pivot_of = |align: frus_core::Alignment| {
3050 Point::new(
3051 box_rect.x + box_rect.width * align.fraction_x(),
3052 box_rect.y + box_rect.height * align.fraction_y(),
3053 )
3054 };
3055 // The composition: scale (about its own pivot) **then** rotation (about its own).
3056 // In RTL the world is mirrored → the rotation direction is inverted.
3057 let mut matrix = Affine::IDENTITY;
3058 if let Some((sx, sy, pivot_align)) = scale {
3059 matrix = matrix.then(Affine::scale(sx, sy).about(pivot_of(pivot_align)));
3060 }
3061 if let Some((angle, pivot_align)) = rotate {
3062 let angle = if self.rtl() { -angle } else { angle };
3063 matrix = matrix.then(Affine::rotation(angle).about(pivot_of(pivot_align)));
3064 }
3065
3066 let p0 = self.scene.primitives().len();
3067 let base = self.xform_base();
3068 self.walk_node(widget, id, translation, clip, rects, index);
3069 // Wraps the range of primitives — painted flat — in a layer transformed by `M`
3070 // (scale/rotation applied at compositing time).
3071 let group = self.scene.split_off(p0);
3072 self.scene.push_primitive(Primitive::Layer {
3073 primitives: group,
3074 opacity: 1.0,
3075 clip,
3076 clip_shape: ClipShape::Rect,
3077 transform: Some(LayerTransform::new(matrix)),
3078 filter: LayerFilter::NONE,
3079 owner: id.as_u64(),
3080 });
3081 // Counter-transforms the hit-test and maps the subtree's interaction rects.
3082 self.transform_interaction_registries(&base, matrix);
3083 return;
3084 }
3085
3086 // The inspector wants a full walk (it collects every node); caching only happens
3087 // outside inspection.
3088 if self.inspector.is_none() && widget.repaint_boundary() {
3089 if let Some(count) = plain_subtree_len(widget) {
3090 let start = *index;
3091 let sub = &rects[start..start + count];
3092 let fp = self.boundary_fingerprint(widget, id, translation, sub);
3093
3094 // Hit: the same generation (config) + the same fingerprint (state+geometry)
3095 // ⇒ the paint would be identical. The output is cloned and replayed.
3096 let hit = {
3097 let mut pc = self.runtime.paint_cache.borrow_mut();
3098 pc.get(id, fp).and_then(|(rc, any)| {
3099 any.downcast_ref::<BoundaryData<Msg>>()
3100 .map(|d| (rc, d.clone()))
3101 })
3102 };
3103 if let Some((rc, data)) = hit {
3104 self.runtime.paint_cache.borrow_mut().note_hit();
3105 self.splice_boundary(data);
3106 *index += rc;
3107 return;
3108 }
3109
3110 // Miss: paints normally, capturing the subtree's output (primitives +
3111 // interaction maps) for the next frame.
3112 let snap = self.snapshot();
3113 self.walk_node(widget, id, translation, clip, rects, index);
3114 self.runtime.paint_cache.borrow_mut().note_miss();
3115 if let Some(data) = self.capture_since(&snap) {
3116 self.runtime
3117 .paint_cache
3118 .borrow_mut()
3119 .put(id, fp, count, Box::new(data));
3120 }
3121 return;
3122 }
3123 }
3124 self.walk_node(widget, id, translation, clip, rects, index);
3125 }
3126
3127 /// Paints a child **flat** (laid out separately, at `translation`, with its own rects)
3128 /// then wraps it in a composited layer transformed by `matrix` and clipped to `clip`. The
3129 /// hit-test counter-transforms the point (`M⁻¹`); when `matrix` stays axis-aligned
3130 /// (scale/translation), the focus / scroll / drag / accessibility bounds are transformed
3131 /// too. The shared factor of `InteractiveViewer`, `RotatedBox` and `FittedBox`.
3132 // The child, its identity, and the four things that place it. Splitting them would
3133 // only mean passing the same values in two hops.
3134 #[allow(clippy::too_many_arguments)]
3135 fn emit_transformed_child(
3136 &mut self,
3137 child: &'a dyn Widget<Msg>,
3138 cid: WidgetId,
3139 translation: (f32, f32),
3140 clip: Rect,
3141 child_rects: &[Rect],
3142 matrix: Affine,
3143 owner: WidgetId,
3144 ) {
3145 let p0 = self.scene.primitives().len();
3146 let base = self.xform_base();
3147 let mut child_index = 0;
3148 self.walk(child, cid, translation, clip, child_rects, &mut child_index);
3149 let group = self.scene.split_off(p0);
3150 self.scene.push_primitive(Primitive::Layer {
3151 primitives: group,
3152 opacity: 1.0,
3153 clip,
3154 clip_shape: ClipShape::Rect,
3155 transform: Some(LayerTransform::new(matrix)),
3156 filter: LayerFilter::NONE,
3157 owner: owner.as_u64(),
3158 });
3159 self.transform_interaction_registries(&base, matrix);
3160 }
3161
3162 /// Paints one node, **under the theme it asks for**. A [`crate::Themed`] replaces the
3163 /// ambient theme for itself and everything below it; it is put back on the way out, so
3164 /// the sibling that follows is untouched. Everything the subtree reads — its paint,
3165 /// its ink, its focus ring, its layout through `cached_rects`, and any overlay it
3166 /// defers — goes through this field, which is what makes one swap enough.
3167 fn walk_node(
3168 &mut self,
3169 widget: &'a dyn Widget<Msg>,
3170 id: WidgetId,
3171 translation: (f32, f32),
3172 clip: Rect,
3173 rects: &[Rect],
3174 index: &mut usize,
3175 ) {
3176 // The box this node is about to be given, read **before** the walk consumes the
3177 // index: a decoration painted over the children needs it once they are done, and
3178 // by then `*index` has moved past the whole subtree.
3179 let over = rects
3180 .get(*index)
3181 .copied()
3182 .map(|r| r.translate(translation.0, translation.1));
3183 let outer = scoped_theme(widget, id, self.runtime, &self.theme)
3184 .map(|theme| std::mem::replace(&mut self.theme, std::rc::Rc::from(theme)));
3185 // The scoped surface, held for this subtree exactly as the layout walk holds it —
3186 // a widget that paints from `MediaQuery::of()` must see the same description it
3187 // was measured against, or the two disagree by whatever the scope removed.
3188 let _surface = widget
3189 .media_override(crate::MediaQuery::of())
3190 .map(crate::MediaQuery::install);
3191 let _shell = widget.scaffold_override().map(crate::ScaffoldInfo::install);
3192 self.walk_node_themed(widget, id, translation, clip, rects, index);
3193 // Over its own children: the reference's `foregroundDecoration`, and the only
3194 // point in the walk where a widget paints after its subtree. Still under this
3195 // node's own theme, since it is this node's decoration.
3196 if let Some(bounds) = over {
3197 if let Some(decoration) = widget.foreground(&self.theme) {
3198 let status = self.full_status(widget, id);
3199 self.scene.set_clip(clip);
3200 self.scene.set_owner(id.as_u64());
3201 self.scene.set_bounds(bounds);
3202 decoration.paint_into(&mut self.scene, bounds, status.opacity);
3203 self.scene.set_clip(clip);
3204 }
3205 }
3206 if let Some(outer) = outer {
3207 self.theme = outer;
3208 }
3209 }
3210
3211 /// The walk proper, with the theme already in place — split out so that no early
3212 /// return inside it can skip putting the outer theme back.
3213 fn walk_node_themed(
3214 &mut self,
3215 widget: &'a dyn Widget<Msg>,
3216 id: WidgetId,
3217 translation: (f32, f32),
3218 clip: Rect,
3219 rects: &[Rect],
3220 index: &mut usize,
3221 ) {
3222 let rect = rects[*index];
3223 *index += 1;
3224 let draw_rect = rect.translate(translation.0, translation.1);
3225 self.inspect_enter(widget, id, draw_rect);
3226
3227 let status = self.full_status(widget, id);
3228 if widget.continuous() {
3229 self.wants_animation = true;
3230 }
3231
3232 self.scene.set_clip(clip);
3233 self.scene.set_owner(id.as_u64());
3234 // The box this widget was given. Text primitives record it: a line of text
3235 // says only where it starts, and the renderer has to know what it covers to
3236 // order it against anything else (milestone 295).
3237 self.scene.set_bounds(draw_rect);
3238 widget.paint(draw_rect, status, &self.theme, &mut self.scene);
3239 // A widget may have tightened the clip (TextField, for one): it is restored here.
3240 self.scene.set_clip(clip);
3241 // A part of the screen saying what the system bars over it should look like, kept
3242 // with its box: the shell asks which ones lie under each bar (#46).
3243 if let Some(style) = widget.system_ui_style() {
3244 self.system_ui.push((draw_rect, style));
3245 }
3246 // The ink a tap left on this surface: over the surface's own paint, under its
3247 // children — where a material surface puts it. The box is recorded too, so the
3248 // shell can start the next splash at the right place, in the right size.
3249 if let Some(style) = widget.ink(&self.theme) {
3250 self.inks.push((id, draw_rect));
3251 if let Some(ripples) = self.runtime.ink.get(&id) {
3252 ripples.paint(
3253 id.as_u64(),
3254 draw_rect,
3255 style.radius,
3256 style.color,
3257 &mut self.scene,
3258 );
3259 self.scene.set_clip(clip);
3260 }
3261 }
3262 self.draw_focus_ring(draw_rect, &status, widget);
3263
3264 // A shared element is recorded **whether or not it is on screen**: half way
3265 // through a transition the one being left behind has usually slid off the edge,
3266 // and its box is exactly what the flight starts from. Every other registry here
3267 // is about what a pointer can reach, which is why they are inside the guard.
3268 if let Some(screen) = self.hero_screen {
3269 if let Some(tag) = widget.hero_tag() {
3270 self.heroes.push((
3271 HeroSpot {
3272 tag,
3273 screen,
3274 id,
3275 rect: draw_rect,
3276 },
3277 widget,
3278 ));
3279 }
3280 }
3281
3282 let visible = draw_rect.intersect(clip);
3283 // A focus stop clipped entirely away by a **scroll** is still a focus stop.
3284 // Tab has to reach the field below the fold — the shell brings it into view
3285 // when it lands there — and registering only what the eye can see is how a long
3286 // form came to have half its fields unreachable from the keyboard. Clipped away
3287 // by anything else it is gone: nothing could reveal it, so focus would land
3288 // where the eye cannot follow.
3289 //
3290 // Only the focus registry does this. A click still needs the visible box, or a
3291 // tap on empty space would land on something scrolled out of sight.
3292 if widget.focusable()
3293 && !self.focus_excluded
3294 && (self.scroll_host.is_some() || (visible.width > 0.0 && visible.height > 0.0))
3295 {
3296 self.focusables.push(Focusable {
3297 id,
3298 rect: visible,
3299 bounds: draw_rect,
3300 form: self.autofill_group,
3301 scroll: self.scroll_host,
3302 skip: self.focus_skipped || widget.focus_skip_traversal(),
3303 order: widget.focus_order().or(self.focus_order),
3304 group: self.focus_group,
3305 });
3306 }
3307 if visible.width > 0.0 && visible.height > 0.0 {
3308 // A press that lands on a **surface** and on nothing inside it stops there.
3309 // Registered before the children, so anything inside still wins on the way
3310 // back out — see [`Widget::opaque`].
3311 if widget.opaque() {
3312 self.hits.push(Hit {
3313 id,
3314 rect: visible,
3315 msg: None,
3316 xform: None,
3317 });
3318 }
3319 if let Some(msg) = widget.on_click() {
3320 self.hits.push(Hit {
3321 id,
3322 rect: visible,
3323 msg: Some(msg),
3324 xform: None,
3325 });
3326 }
3327 if let Some(msg) = widget.on_long_press() {
3328 self.long_presses.push(Hit {
3329 id,
3330 rect: visible,
3331 msg: Some(msg),
3332 xform: None,
3333 });
3334 }
3335 if widget.draggable() {
3336 self.draggables.push((id, visible));
3337 }
3338 if let Some(payload) = widget.drag_payload() {
3339 self.drag_sources.push(DragSource {
3340 id,
3341 rect: visible,
3342 payload,
3343 });
3344 }
3345 if widget.drop_zone() {
3346 self.drop_zones.push(DropZone { id, rect: visible });
3347 }
3348 if widget.reorder_index().is_some() {
3349 self.reorderables.push((id, visible));
3350 }
3351 // The accessibility tree: nodes that carry meaning (a role or a label).
3352 if let Some(sem) = widget.semantics().filter(|s| s.is_meaningful()) {
3353 self.semantics.push((id, visible, sem));
3354 }
3355 // A **multi-line** field whose content overflows: a scrollable region (wheel and
3356 // inertia through the generic machinery; the shell follows the caret) plus a
3357 // draggable scrollbar (mouse and touch).
3358 if let Some(vp) = widget.text_viewport(draw_rect) {
3359 if let Some((content_h, visible_h, _, _)) = widget.text_metrics(draw_rect.width, 0)
3360 {
3361 let max_y = (content_h - visible_h).max(0.0);
3362 if max_y > 0.0 {
3363 self.scrollables.push(Scrollable {
3364 id,
3365 viewport: vp,
3366 max_x: 0.0,
3367 max_y,
3368 physics: widget.scroll_physics(),
3369 refresh: self.refresh_host,
3370 page: None,
3371 reverse_x: false,
3372 reverse_y: false,
3373 host: self.scroll_host,
3374 keep_visible: None,
3375 });
3376 let offset_y = self
3377 .runtime
3378 .scroll
3379 .get(&id)
3380 .map(|s| s.1)
3381 .unwrap_or(0.0)
3382 .clamp(0.0, max_y);
3383 self.add_scrollbar(
3384 id,
3385 vp,
3386 Bar {
3387 vertical: true,
3388 offset: offset_y,
3389 max: max_y,
3390 reverse: false,
3391 asked: widget.scrollbars(),
3392 visible: widget.thumb_visibility(),
3393 },
3394 );
3395 }
3396 }
3397 }
3398 }
3399
3400 if let Some((progress, forward)) = widget.navigator() {
3401 let bounds = draw_rect;
3402 let children = widget.children();
3403 let w = bounds.width;
3404 // **The pages stop at the navigator's edge.** A screen sliding in comes from
3405 // outside the box, and one sliding out goes outside it; before milestone 398
3406 // the only thing stopping them was the window, so a navigator that was not the
3407 // whole window painted its pages over its siblings — and even a full-window
3408 // one spent the frame drawing a screen nobody could see. The reference clips
3409 // by default (`Clip.hardEdge`) for the same reason.
3410 let clip = if widget.navigator_clips() {
3411 clip.intersect(bounds)
3412 } else {
3413 clip
3414 };
3415 // The pages under the one on show are kept in the tree and stay out of the picture.
3416 let hidden = widget.navigator_retained().min(children.len());
3417 let shown = &children[hidden..];
3418 if shown.len() >= 2 {
3419 // A transition: two offset screens. The screen "behind" (a negative offset)
3420 // moves less (parallax) → the sense of depth.
3421 let dir = if forward { 1.0 } else { -1.0 };
3422 let raw = [-progress * w * dir, (1.0 - progress) * w * dir];
3423 let off = [
3424 if raw[0] < 0.0 {
3425 raw[0] * NAV_PARALLAX
3426 } else {
3427 raw[0]
3428 },
3429 if raw[1] < 0.0 {
3430 raw[1] * NAV_PARALLAX
3431 } else {
3432 raw[1]
3433 },
3434 ];
3435 // Depth order: the one offset furthest left (the back one) goes first.
3436 let (back, front) = if off[0] <= off[1] { (0, 1) } else { (1, 0) };
3437 // Where the shared elements of this transition start, in both the
3438 // registry and the scene: everything after this point belongs to the
3439 // two screens, and the flight is resolved from it once both are drawn.
3440 let hero_base = self.heroes.len();
3441 let scene_base = self.scene.primitives().len();
3442 let outer_screen = self.hero_screen.replace(back as u8);
3443 // A screen being left keeps its floating layers to itself.
3444 // `process_overlays` draws every overlay above the **whole window**, after
3445 // both screens, so a menu left open on the departing screen is painted on
3446 // top of the screen that replaced it — opaque, and anchored to a bar the
3447 // window no longer shows. A device found it; the parallax is why it is not
3448 // self-correcting, since the outgoing screen travels only 30 % of the width
3449 // and its anchor never actually leaves.
3450 //
3451 // `Navigator::from` inserts the screen being left at index 0, so
3452 // `children[1]` is always the destination — on a push, on a pop, and under
3453 // a back gesture alike. Whatever the other one defers is dropped.
3454 let overlay_base = self.overlays.len();
3455 self.render_screen(
3456 shown[back].as_ref(),
3457 child_id(id, hidden + back, shown[back].as_ref()),
3458 bounds,
3459 off[back],
3460 clip,
3461 );
3462 if back != 1 {
3463 self.overlays.truncate(overlay_base);
3464 }
3465 // Darkens the screen behind in proportion to how far it is covered.
3466 let coverage = (off[back].abs() / (w * NAV_PARALLAX)).min(1.0);
3467 if coverage > 0.0 {
3468 let scrim =
3469 Rect::new(bounds.x + off[back], bounds.y, bounds.width, bounds.height);
3470 self.scene.set_owner(0);
3471 self.scene.set_clip(clip);
3472 self.scene
3473 .fill_rect(scrim, self.theme.scheme.scrim.with_alpha(0.22 * coverage));
3474 }
3475 self.hero_screen = Some(front as u8);
3476 let overlay_base = self.overlays.len();
3477 self.render_screen(
3478 shown[front].as_ref(),
3479 child_id(id, hidden + front, shown[front].as_ref()),
3480 bounds,
3481 off[front],
3482 clip,
3483 );
3484 if front != 1 {
3485 self.overlays.truncate(overlay_base);
3486 }
3487 self.hero_screen = outer_screen;
3488 self.fly_heroes(hero_base, scene_base, progress);
3489 } else if let Some(screen) = shown.first() {
3490 self.render_screen(
3491 screen.as_ref(),
3492 child_id(id, hidden, screen.as_ref()),
3493 bounds,
3494 0.0,
3495 clip,
3496 );
3497 }
3498 } else if widget.interactive().is_some() {
3499 // An **interactive** viewport (`InteractiveViewer`): the child fills the viewport
3500 // at scale 1, then the retained transform (scale + translation, from the shell's
3501 // gestures) is applied to it through **a single layer** carrying both the matrix
3502 // `M` and the clip to the viewport.
3503 let viewport = draw_rect;
3504 let content_clip = clip.intersect(viewport);
3505 self.interactives.push((id, viewport));
3506 if let Some(content) = widget.children().first() {
3507 let content = content.as_ref();
3508 let cid = child_id(id, 0, content);
3509 let content_rects = self.cached_rects(
3510 cid,
3511 content,
3512 Constraints::definite(Size::new(viewport.width, viewport.height)),
3513 );
3514 let matrix = self
3515 .runtime
3516 .interactive
3517 .get(&id)
3518 .copied()
3519 .unwrap_or_default()
3520 .matrix();
3521 self.emit_transformed_child(
3522 content,
3523 cid,
3524 (viewport.x, viewport.y),
3525 content_clip,
3526 &content_rects,
3527 matrix,
3528 id,
3529 );
3530 }
3531 } else if let Some(q) = widget.rotated_quarter_turns() {
3532 // `RotatedBox`: the child, measured at its **natural** size, is centred in the box
3533 // (with the dimensions swapped for an odd quarter, see `build_layout`) then rotated
3534 // about the centre. The rotation, applied at compositing time, does not stay
3535 // axis-aligned for an odd quarter (the focus bounds are left as they are — the
3536 // click itself stays correct through `M⁻¹`).
3537 let box_rect = draw_rect;
3538 if let Some(child) = widget.children().first() {
3539 let child = child.as_ref();
3540 let cid = child_id(id, 0, child);
3541 let child_rects =
3542 self.cached_rects(cid, child, Constraints::scroll(0.0, 0.0, true, true));
3543 let nat = child_rects
3544 .first()
3545 .copied()
3546 .unwrap_or(Rect::new(0.0, 0.0, 0.0, 0.0));
3547 let center = Point::new(
3548 box_rect.x + box_rect.width / 2.0,
3549 box_rect.y + box_rect.height / 2.0,
3550 );
3551 let translation = (center.x - nat.width / 2.0, center.y - nat.height / 2.0);
3552 let angle = q.rem_euclid(4) as f32 * std::f32::consts::FRAC_PI_2;
3553 let angle = if self.rtl() { -angle } else { angle };
3554 let matrix = Affine::rotation(angle).about(center);
3555 self.emit_transformed_child(
3556 child,
3557 cid,
3558 translation,
3559 clip,
3560 &child_rects,
3561 matrix,
3562 id,
3563 );
3564 }
3565 } else if let Some(fit) = widget.fitted() {
3566 // `FittedBox`: the child, measured at its **natural** size, is scaled according to
3567 // the `BoxFit` (uniformly, except for `Fill`), centred and clipped to the box. The
3568 // scale stays axis-aligned → the focus bounds follow.
3569 let box_rect = draw_rect;
3570 let content_clip = clip.intersect(box_rect);
3571 if let Some(child) = widget.children().first() {
3572 let child = child.as_ref();
3573 let cid = child_id(id, 0, child);
3574 let child_rects =
3575 self.cached_rects(cid, child, Constraints::scroll(0.0, 0.0, true, true));
3576 let nat = child_rects
3577 .first()
3578 .copied()
3579 .unwrap_or(Rect::new(0.0, 0.0, 0.0, 0.0));
3580 let (sx, sy) = fit.scale(
3581 Size::new(nat.width, nat.height),
3582 Size::new(box_rect.width, box_rect.height),
3583 );
3584 let center = Point::new(
3585 box_rect.x + box_rect.width / 2.0,
3586 box_rect.y + box_rect.height / 2.0,
3587 );
3588 let translation = (center.x - nat.width / 2.0, center.y - nat.height / 2.0);
3589 let matrix = Affine::scale(sx, sy).about(center);
3590 self.emit_transformed_child(
3591 child,
3592 cid,
3593 translation,
3594 content_clip,
3595 &child_rects,
3596 matrix,
3597 id,
3598 );
3599 }
3600 } else if let Some(content) = widget.scroll_content() {
3601 let axis = widget.scroll_axis();
3602 let viewport = draw_rect;
3603 let content_clip = clip.intersect(viewport);
3604 // `reverse` applies to the axis this scrolls along; a two-dimensional scroll
3605 // takes it on the vertical, which is the one a reversed view is ever about.
3606 let reverse = if widget.scroll_reverse() {
3607 (axis.free_x() && !axis.free_y(), axis.free_y())
3608 } else {
3609 (false, false)
3610 };
3611
3612 // The padding is **inside** the viewport and scrolls with the content:
3613 // room at the end of a feed is reachable by scrolling to it, room taken out
3614 // of the window would sit there for ever. The reference's `SliverPadding`.
3615 let pad = widget.scroll_padding();
3616 let content_rects = self.cached_rects(
3617 child_id(id, 0, content),
3618 content,
3619 Constraints::scroll(
3620 (viewport.width - pad.left - pad.right).max(0.0),
3621 (viewport.height - pad.top - pad.bottom).max(0.0),
3622 axis.free_x(),
3623 axis.free_y(),
3624 ),
3625 );
3626
3627 let content_size = content_rects
3628 .first()
3629 .copied()
3630 .unwrap_or(Rect::new(0.0, 0.0, 0.0, 0.0));
3631 // What scrolls is the content **and** its room, which is what makes the far
3632 // inset reachable rather than decorative.
3633 let scrolled = Size::new(
3634 content_size.width + pad.left + pad.right,
3635 content_size.height + pad.top + pad.bottom,
3636 );
3637 let max_x = (scrolled.width - viewport.width).max(0.0);
3638 let max_y = (scrolled.height - viewport.height).max(0.0);
3639
3640 // The geometry without the identity: enough to ask where the content sits at
3641 // an offset, and what offset would put a box where it is wanted.
3642 let frame = Scrollable {
3643 id,
3644 viewport,
3645 max_x,
3646 max_y,
3647 physics: None,
3648 refresh: None,
3649 page: None,
3650 reverse_x: reverse.0,
3651 reverse_y: reverse.1,
3652 host: None,
3653 keep_visible: None,
3654 };
3655 // What the content asks to be kept in view — a tab bar's selected tab — in
3656 // the content's own coordinates, which is the only frame it can speak in.
3657 let asked = content.keep_visible(
3658 Size::new(content_size.width, content_size.height),
3659 &self.theme,
3660 );
3661 // Where that box would sit if nobody had scrolled: the frame's coordinates,
3662 // with the offset taken out. See [`KeepVisible::rect`] for why it is stored
3663 // this way rather than where it is drawn.
3664 let keep = asked.map(|keep| {
3665 let rest = frame.content_origin((0.0, 0.0), scrolled);
3666 KeepVisible {
3667 rect: keep.rect.translate(
3668 viewport.x + rest.0 + pad.left,
3669 viewport.y + rest.1 + pad.top,
3670 ),
3671 ..keep
3672 }
3673 });
3674 // Inside the extent this frame has just measured, unless something owns the
3675 // offset: content that shrank is drawn where it rests (milestone 533).
3676 let (offset_x, offset_y) = match self.runtime.scroll_offset_within(id, (max_x, max_y)) {
3677 Some(offset) => offset,
3678 // Never scrolled, and something inside wants to be seen: **open** there
3679 // rather than sliding across a frame later. A tab bar restored on its
3680 // eighth tab should show the eighth tab, not show the first and then
3681 // travel. The same answer a paged view gives its initial page, and the
3682 // runtime retains the offset on that first frame so it stays.
3683 None => match keep {
3684 Some(keep) => frame.keep_offset(keep).0,
3685 None => (0.0, 0.0),
3686 },
3687 };
3688
3689 let origin = frame.content_origin((offset_x, offset_y), scrolled);
3690 let content_translation = (
3691 viewport.x + origin.0 + pad.left,
3692 viewport.y + origin.1 + pad.top,
3693 );
3694 self.scrollables.push(Scrollable {
3695 id,
3696 viewport,
3697 max_x,
3698 max_y,
3699 physics: widget.scroll_physics(),
3700 refresh: self.refresh_host,
3701 page: None,
3702 reverse_x: reverse.0,
3703 reverse_y: reverse.1,
3704 host: self.scroll_host,
3705 keep_visible: keep,
3706 });
3707 let mut content_index = 0;
3708 let outer = self.scroll_host.replace(id);
3709 self.walk(
3710 content,
3711 child_id(id, 0, content),
3712 content_translation,
3713 content_clip,
3714 &content_rects,
3715 &mut content_index,
3716 );
3717 self.scroll_host = outer;
3718
3719 // The overscroll glow, then the scrollbars, over the content (not
3720 // clipped by it).
3721 self.scene.set_clip(clip);
3722 self.add_overscroll_glow(id, viewport);
3723 if max_y > 0.0 {
3724 self.add_scrollbar(
3725 id,
3726 viewport,
3727 Bar {
3728 vertical: true,
3729 offset: offset_y,
3730 max: max_y,
3731 reverse: reverse.1,
3732 asked: widget.scrollbars(),
3733 visible: widget.thumb_visibility(),
3734 },
3735 );
3736 }
3737 if max_x > 0.0 {
3738 self.add_scrollbar(
3739 id,
3740 viewport,
3741 Bar {
3742 vertical: false,
3743 offset: offset_x,
3744 max: max_x,
3745 reverse: reverse.0,
3746 asked: widget.scrollbars(),
3747 visible: widget.thumb_visibility(),
3748 },
3749 );
3750 }
3751 } else if let Some(vlist) =
3752 widget.virtual_list(Size::new(draw_rect.width, draw_rect.height))
3753 {
3754 // A virtualised list: only build/lay out/paint the visible window.
3755 let viewport = draw_rect;
3756 let content_clip = clip.intersect(viewport);
3757 let reverse = widget.scroll_reverse();
3758 // Down the screen or across it. A list virtualises along **one** axis, which
3759 // is what lets it place item `n` without building the ones before it, so
3760 // `Axis::Both` has no meaning here and reads as vertical.
3761 let across = matches!(vlist.axis, crate::scroll::Axis::Horizontal);
3762 // Room around the items, inside the viewport and scrolling with them. The
3763 // **leading** inset is the one at the end the items start from, so a
3764 // reversed list clears the far end first and both keep their sides.
3765 let pad = widget.scroll_padding();
3766 let (lead, trail, cross_near, cross_far) = match (across, reverse) {
3767 (true, false) => (pad.left, pad.right, pad.top, pad.bottom),
3768 (true, true) => (pad.right, pad.left, pad.top, pad.bottom),
3769 (false, false) => (pad.top, pad.bottom, pad.left, pad.right),
3770 (false, true) => (pad.bottom, pad.top, pad.left, pad.right),
3771 };
3772 // From here down it is a main axis and a cross axis, and only the two
3773 // branches at the end have to know which is which. The alternative was a
3774 // second copy of the window arithmetic below, which is where a reversed
3775 // list gets its signs right -- and a fix applied to one copy and not the
3776 // other is exactly how the two would drift apart.
3777 let (extent, cross_extent) = match across {
3778 true => (viewport.width, viewport.height),
3779 false => (viewport.height, viewport.width),
3780 };
3781 let content = lead + trail + vlist.count as f32 * vlist.item_extent;
3782 let max = (content - extent).max(0.0);
3783 let bounds = if across { (max, 0.0) } else { (0.0, max) };
3784 let offset = self
3785 .runtime
3786 .scroll_offset_within(id, bounds)
3787 .map_or(0.0, |(x, y)| if across { x } else { y });
3788 self.scrollables.push(Scrollable {
3789 id,
3790 viewport,
3791 max_x: if across { max } else { 0.0 },
3792 max_y: if across { 0.0 } else { max },
3793 physics: widget.scroll_physics(),
3794 refresh: self.refresh_host,
3795 page: None,
3796 reverse_x: across && reverse,
3797 reverse_y: !across && reverse,
3798 host: self.scroll_host,
3799 keep_visible: None,
3800 });
3801
3802 let outer = self.scroll_host.replace(id);
3803 if vlist.item_extent > 0.0 && vlist.count > 0 {
3804 // The **window** is the same arithmetic either way, and that is not a
3805 // coincidence: a reversed list counts its indices from the end, and a
3806 // reversed offset counts its pixels from the end, so index and offset
3807 // agree about which way is forward. Only where an item lands differs.
3808 let first = ((offset - lead) / vlist.item_extent).floor().max(0.0) as usize;
3809 let last = ((((offset + extent - lead) / vlist.item_extent).ceil()).max(0.0)
3810 as usize)
3811 .min(vlist.count);
3812 let origin = if across { viewport.x } else { viewport.y };
3813 for i in first..last {
3814 let item = (vlist.build)(i);
3815 let along = if reverse {
3816 origin + extent - lead - (i + 1) as f32 * vlist.item_extent + offset
3817 } else {
3818 origin + lead + i as f32 * vlist.item_extent - offset
3819 };
3820 let (x, y) = match across {
3821 true => (along, viewport.y + cross_near),
3822 false => (viewport.x + cross_near, along),
3823 };
3824
3825 // **Filled**, not merely constrained: a list hands its children a
3826 // box rather than asking them how big they would like to be. The
3827 // reference gives a list's children a *tight* cross-axis extent, and
3828 // for a fixed-extent list a tight main-axis one too — which is
3829 // exactly the pair of numbers here.
3830 //
3831 // Constrained-but-not-filled, a row whose width nobody set hugged
3832 // its own text: a list of coloured rows painted a column of chips
3833 // down the left instead of rows across the list. Found on a device
3834 // in milestone 349.
3835 let free_cross = (cross_extent - cross_near - cross_far).max(0.0);
3836 let item_rects = self.cached_rects(
3837 id.child(i),
3838 item.as_ref(),
3839 Constraints::filled(match across {
3840 true => Size::new(vlist.item_extent, free_cross),
3841 false => Size::new(free_cross, vlist.item_extent),
3842 }),
3843 );
3844
3845 let mut item_index = 0;
3846 self.render_item(
3847 item.as_ref(),
3848 id.child(i),
3849 (x, y),
3850 content_clip,
3851 &item_rects,
3852 &mut item_index,
3853 );
3854 }
3855 }
3856
3857 self.scroll_host = outer;
3858 self.scene.set_clip(clip);
3859 self.add_overscroll_glow(id, viewport);
3860 if max > 0.0 {
3861 self.add_scrollbar(
3862 id,
3863 viewport,
3864 Bar {
3865 vertical: !across,
3866 offset,
3867 max,
3868 reverse,
3869 asked: widget.scrollbars(),
3870 visible: widget.thumb_visibility(),
3871 },
3872 );
3873 }
3874 } else if let Some(pages) = widget.page_view() {
3875 // A paged view: like a virtualised list turned on its side, and with the
3876 // page extent — the whole geometry — derived from the viewport rather than
3877 // given. Only the pages the viewport touches are built.
3878 let viewport = draw_rect;
3879 let content_clip = clip.intersect(viewport);
3880 let snap = pages.snap(viewport);
3881 let (viewport_along, page_across) = if snap.horizontal {
3882 (viewport.width, viewport.height)
3883 } else {
3884 (viewport.height, viewport.width)
3885 };
3886 let reverse = widget.scroll_reverse();
3887 // Room at both ends, so a page narrower than the viewport comes to rest
3888 // **centred** in it rather than jammed against the leading edge — and so
3889 // the last one can be reached at all: unpadded, the travel stops with the
3890 // last page's trailing edge on the viewport's, which is short of where that
3891 // page rests. Padded, the total works out to `extent × (count - 1)`,
3892 // the last page's own offset, exactly.
3893 let pad = pages.end_padding(viewport);
3894 let total = pages.count as f32 * snap.extent + 2.0 * pad;
3895 let max = (total - viewport_along).max(0.0);
3896 // With no retained offset this is the view's **first** frame, and it opens
3897 // on the page it was asked for. Reading the initial page here rather than
3898 // correcting it a frame later is what keeps page 0 from flashing past on
3899 // the way to page 3.
3900 let bounds = if snap.horizontal {
3901 (max, 0.0)
3902 } else {
3903 (0.0, max)
3904 };
3905 let along = match self.runtime.scroll_offset_within(id, bounds) {
3906 Some((x, y)) => {
3907 if snap.horizontal {
3908 x
3909 } else {
3910 y
3911 }
3912 }
3913 None => snap
3914 .offset_of(snap.requested.min(pages.count.saturating_sub(1)))
3915 .clamp(0.0, max),
3916 };
3917 self.scrollables.push(Scrollable {
3918 id,
3919 viewport,
3920 max_x: if snap.horizontal { max } else { 0.0 },
3921 max_y: if snap.horizontal { 0.0 } else { max },
3922 physics: widget.scroll_physics(),
3923 refresh: self.refresh_host,
3924 page: Some(snap),
3925 reverse_x: snap.horizontal && reverse,
3926 reverse_y: !snap.horizontal && reverse,
3927 host: self.scroll_host,
3928 keep_visible: None,
3929 });
3930
3931 let outer = self.scroll_host.replace(id);
3932 if pages.count > 0 {
3933 // The window is the same arithmetic whichever end index 0 is at: a
3934 // reversed view counts its indices from the far end and a reversed
3935 // offset counts its pixels from the same one, so the two agree about
3936 // which way forward is. Only where a page **lands** differs.
3937 let first = ((along - pad) / snap.extent).floor().max(0.0) as usize;
3938 let last = ((((along + viewport_along - pad) / snap.extent).ceil()).max(0.0)
3939 as usize)
3940 .min(pages.count)
3941 .max(first + 1);
3942 // A wheel shows **more** rows than a flat strip would: the ones a quarter
3943 // turn away are compressed into the last few pixels at either end, and
3944 // their flat positions are a whole quarter-circumference outside the
3945 // viewport. Widening the window in both directions keeps the reversed
3946 // arithmetic above untouched, and the rows that turn out to have gone
3947 // over the horizon are dropped below — before they are built.
3948 let (first, last) = match pages.wheel {
3949 Some(wheel) => {
3950 let span = wheel.radius(viewport_along) * std::f32::consts::FRAC_PI_2;
3951 let extra = (span / snap.extent).ceil().max(0.0) as usize + 1;
3952 (first.saturating_sub(extra), (last + extra).min(pages.count))
3953 }
3954 None => (first, last),
3955 };
3956 for index in first..last.min(pages.count) {
3957 let start = match reverse {
3958 true => viewport_along - pad - (index + 1) as f32 * snap.extent + along,
3959 false => pad + index as f32 * snap.extent - along,
3960 };
3961 // Where this row's centre would sit on the unrolled strip, measured
3962 // from the middle of the viewport — which is the whole of what the
3963 // cylinder needs to know about it.
3964 let on_wheel = match pages.wheel {
3965 Some(wheel) => {
3966 let flat = start + snap.extent / 2.0 - viewport_along / 2.0;
3967 match wheel.row(viewport_along, flat) {
3968 // Past the horizon: not drawn, and not built either.
3969 None => continue,
3970 Some(row) => Some((row, flat)),
3971 }
3972 }
3973 None => None,
3974 };
3975 let page = (pages.build)(index);
3976 let (size, origin) = if snap.horizontal {
3977 (
3978 Size::new(snap.extent, page_across),
3979 (viewport.x + start, viewport.y),
3980 )
3981 } else {
3982 (
3983 Size::new(page_across, snap.extent),
3984 (viewport.x, viewport.y + start),
3985 )
3986 };
3987
3988 // A page is **given** its box, not asked for one: it fills the
3989 // panel even when its content is a single centred line.
3990 let page_rects = self.cached_rects(
3991 id.child(index),
3992 page.as_ref(),
3993 Constraints::filled(size),
3994 );
3995
3996 let mut page_index = 0;
3997 // The row is drawn **flat**, then the whole of it is composited on
3998 // the cylinder — one layer per row, the way `Transform` does it, so
3999 // the text, the rules and the backgrounds all turn together instead
4000 // of each primitive having to know it is on a wheel.
4001 let before = self.scene.primitives().len();
4002 let base = self.xform_base();
4003 self.render_item(
4004 page.as_ref(),
4005 id.child(index),
4006 origin,
4007 content_clip,
4008 &page_rects,
4009 &mut page_index,
4010 );
4011 if let Some((row, flat)) = on_wheel {
4012 // The row's own centre on screen: the scale is about it, so a row
4013 // shrinks in place rather than creeping towards a corner.
4014 let centre = Point::new(
4015 viewport.x + viewport.width / 2.0,
4016 viewport.y + viewport_along / 2.0 + flat,
4017 );
4018 let matrix = Affine::scale(row.scale_x, row.scale_y)
4019 .about(centre)
4020 // And then up or down to where the cylinder actually put it.
4021 .then(Affine::translation(0.0, row.offset - flat));
4022 let group = self.scene.split_off(before);
4023 self.scene.push_primitive(Primitive::Layer {
4024 primitives: group,
4025 opacity: row.opacity.clamp(0.0, 1.0),
4026 clip: content_clip,
4027 clip_shape: ClipShape::Rect,
4028 transform: Some(LayerTransform::new(matrix)),
4029 filter: LayerFilter::NONE,
4030 owner: id.child(index).as_u64(),
4031 });
4032 self.transform_interaction_registries(&base, matrix);
4033 }
4034 }
4035 }
4036
4037 self.scroll_host = outer;
4038 self.scene.set_clip(clip);
4039 self.add_overscroll_glow(id, viewport);
4040 } else if let Some(overflow) = widget.overflow_box() {
4041 // The child is laid out to constraints of its own — which is why it can
4042 // come out bigger than the box holding it — then anchored inside it and
4043 // rendered **without** the box's clip. A spill that got clipped here would
4044 // be no spill at all; a `ClipRect` above is how a caller asks for one.
4045 let own = draw_rect;
4046 if let Some(child) = widget.children().first() {
4047 let child = child.as_ref();
4048 let cid = child_id(id, 0, child);
4049 let asked = overflow.child_size(Size::new(own.width, own.height));
4050 let child_rects = if overflow.unconstrained {
4051 // Unconstrained: the child is asked how big it wants to be, and
4052 // gets exactly that.
4053 self.cached_rects(cid, child, Constraints::scroll(0.0, 0.0, true, true))
4054 } else {
4055 self.cached_rects(cid, child, Constraints::filled(asked))
4056 };
4057 let size = child_rects
4058 .first()
4059 .map(|r| Size::new(r.width, r.height))
4060 .unwrap_or(asked);
4061 let origin = overflow.origin(own, size);
4062
4063 let mut child_index = 0;
4064 self.render_item(child, cid, origin, clip, &child_rects, &mut child_index);
4065 }
4066 } else if let Some(transform) = widget.constraints_transform() {
4067 // The child is laid out at **the same box the measurement gave it**, worked out
4068 // again from what is left of that measurement — the box it produced. Each of
4069 // the three things an axis can say survives the round trip; see
4070 // `AxisConstraint::at`.
4071 let own = draw_rect;
4072 if let Some(child) = widget.children().first() {
4073 let child = child.as_ref();
4074 let cid = child_id(id, 0, child);
4075 let (aw, free_x) = transform.width.at(own.width);
4076 let (ah, free_y) = transform.height.at(own.height);
4077 let constraints = match (free_x, free_y) {
4078 (false, false) => Constraints::filled(Size::new(aw, ah)),
4079 _ => Constraints::scroll(aw, ah, free_x, free_y),
4080 };
4081 let child_rects = self.cached_rects(cid, child, constraints);
4082 let size = child_rects
4083 .first()
4084 .map(|r| Size::new(r.width, r.height))
4085 .unwrap_or(Size::new(0.0, 0.0));
4086 let align = transform.alignment;
4087 let origin = (
4088 own.x + (own.width - size.width) * align.fraction_x(),
4089 own.y + (own.height - size.height) * align.fraction_y(),
4090 );
4091 if transform.report {
4092 self.report_spill(own, Rect::new(origin.0, origin.1, size.width, size.height));
4093 }
4094 let mut child_index = 0;
4095 self.render_item(child, cid, origin, clip, &child_rects, &mut child_index);
4096 }
4097 } else if let Some(build) = widget.layout_builder() {
4098 // Builds the content from the actual box, then lays it out and renders it inside
4099 // (like a list item: with no retained state).
4100 let bounds = draw_rect;
4101 let content_clip = clip.intersect(bounds);
4102 let child = build(Size::new(bounds.width, bounds.height));
4103
4104 // **Filled**, not merely constrained: the content is handed this box, and it
4105 // was built from this box in the first place. Constraining it instead let a
4106 // root with no width of its own hug its content — a grid built here laid its
4107 // columns out at nothing, which is what `GridView::extent`'s first tests found
4108 // (milestone 356). The same distinction a list's items and a paged view's
4109 // pages already draw.
4110 let child_rects = self.cached_rects(
4111 id.child(0),
4112 child.as_ref(),
4113 Constraints::filled(Size::new(bounds.width, bounds.height)),
4114 );
4115
4116 let mut child_index = 0;
4117 self.render_item(
4118 child.as_ref(),
4119 id.child(0),
4120 (bounds.x, bounds.y),
4121 content_clip,
4122 &child_rects,
4123 &mut child_index,
4124 );
4125 } else if let Some(build) = widget.scroll_overlay() {
4126 // The **body first**, exactly as an ordinary single child: it keeps its own
4127 // identity, registers itself as a scroll region in its own right, and consumes
4128 // its rects from this array the way the generic walk would. Nothing about it
4129 // changes for being watched.
4130 let children = widget.children();
4131 for (child_index, child) in children.iter().enumerate() {
4132 self.walk(
4133 child.as_ref(),
4134 child_id(id, child_index, child.as_ref()),
4135 translation,
4136 clip,
4137 rects,
4138 index,
4139 );
4140 }
4141 // Then the overlay, built from **where the body has got to this frame**. Read
4142 // from the retained offset rather than from the registry the walk is still
4143 // filling: the offset is written before the frame is drawn, so it is current,
4144 // where the extents beside it are not yet known — which is why the builder is
4145 // handed the one and not the other.
4146 let offset = children
4147 .first()
4148 .map(|body| child_id(id, 0, body.as_ref()))
4149 .and_then(|body| self.runtime.scroll.get(&body).copied())
4150 .unwrap_or((0.0, 0.0));
4151 let bounds = draw_rect;
4152 let over = build(offset, Size::new(bounds.width, bounds.height));
4153 // `child(1)`, beside the body's `child(0)` — or beside its key, which cannot
4154 // collide with a positional identity. The overlay is laid out in a tree of its
4155 // own, so it needs an identity of its own for hover, ink and the paint cache.
4156 let over_id = id.child(1);
4157 let over_rects = self.cached_rects(
4158 over_id,
4159 over.as_ref(),
4160 // **Filled**, not constrained: an overlay covers the region it is over, and
4161 // where inside that box its content sits is the caller's business — a
4162 // column pins it to the top, an `Align` puts it anywhere else. The same
4163 // rule a `layout_builder`'s content is given.
4164 Constraints::filled(Size::new(bounds.width, bounds.height)),
4165 );
4166 let mut over_index = 0;
4167 self.render_item(
4168 over.as_ref(),
4169 over_id,
4170 (bounds.x, bounds.y),
4171 clip.intersect(bounds),
4172 &over_rects,
4173 &mut over_index,
4174 );
4175 } else if let Some(spec) = widget.dismissible() {
4176 // A dismissible item is a stack whose **last** layer — the item itself — is
4177 // offset by however far it has been swiped, and whose earlier layers, the
4178 // backgrounds, show only through the strip the item has uncovered. Clipping
4179 // them is what makes a background read as something the item is sliding
4180 // *off*, rather than a block that was always there.
4181 let bounds = draw_rect;
4182 let state = self.runtime.dismiss.get(&id).copied();
4183 let progress = state.map(|s| s.progress()).unwrap_or(0.0);
4184 let layers = widget.children();
4185 let last = layers.len().saturating_sub(1);
4186 // Which background: the first covers a swipe towards the end, the second —
4187 // when there is one — the other way. With only one, it serves both.
4188 let shown_background = if progress < 0.0 && last >= 2 { 1 } else { 0 };
4189 let strip = crate::dismiss::revealed_strip(bounds, progress);
4190
4191 for (i, layer) in layers.iter().enumerate() {
4192 let is_item = i == last;
4193 if !is_item && (i != shown_background || progress == 0.0) {
4194 // Not the background this direction shows: skip it, and skip the
4195 // rects it would have consumed — it is laid out separately, so
4196 // there is no index to keep in step.
4197 continue;
4198 }
4199 let offset = if is_item {
4200 if spec.axis.is_horizontal() {
4201 (bounds.width * progress, 0.0)
4202 } else {
4203 (0.0, bounds.height * progress)
4204 }
4205 } else {
4206 (0.0, 0.0)
4207 };
4208 let layer_clip = if is_item {
4209 clip.intersect(bounds)
4210 } else {
4211 clip.intersect(strip)
4212 };
4213 if layer_clip.width <= 0.0 || layer_clip.height <= 0.0 {
4214 continue;
4215 }
4216 let cid = child_id(id, i, layer.as_ref());
4217 let layer_rects = self.cached_rects(
4218 cid,
4219 layer.as_ref(),
4220 Constraints::filled(Size::new(bounds.width, bounds.height)),
4221 );
4222 let mut layer_index = 0;
4223 self.walk(
4224 layer.as_ref(),
4225 cid,
4226 (bounds.x + offset.0, bounds.y + offset.1),
4227 layer_clip,
4228 &layer_rects,
4229 &mut layer_index,
4230 );
4231 }
4232
4233 self.dismissables.push(Dismissable {
4234 id,
4235 rect: bounds,
4236 spec,
4237 });
4238 // A swipe that is settling, flying or collapsing drives itself.
4239 if state.is_some_and(|s| s.phase() != DismissPhase::Drag) {
4240 self.wants_animation = true;
4241 }
4242 } else if widget.stack() {
4243 // A stack: the layers are rendered in order, bottom first.
4244 let bounds = draw_rect;
4245 let layer_clip = clip.intersect(bounds);
4246 let loose = widget.stack_loose();
4247 // An **indexed** stack shows one layer and lays out the rest. Which is a
4248 // question for here rather than for the widget: a layer that wrapped itself in
4249 // something to keep quiet would no longer be laid out the way a bare layer is,
4250 // and the layout is the whole promise.
4251 let shown = widget.stack_visible();
4252 // Where a layer smaller than the stack sits. Resolved once, against the
4253 // reading direction, so a start-anchored badge follows the script.
4254 let align = widget
4255 .alignment_geometry()
4256 .unwrap_or(frus_core::AlignmentGeometry::Physical(
4257 frus_core::Alignment::TOP_LEFT,
4258 ))
4259 .resolve(if self.rtl() {
4260 frus_core::TextDirection::Rtl
4261 } else {
4262 frus_core::TextDirection::Ltr
4263 });
4264 for (i, layer) in widget.children().iter().enumerate() {
4265 let cid = child_id(id, i, layer.as_ref());
4266 // What this layer asks for on each axis: a pinned pair gives a number, a
4267 // lone pin plus an explicit extent gives one, and neither leaves the axis
4268 // to the layer itself — or, under `Expand`, to the stack.
4269 // A layer moving between two sets of pins is read from the runtime rather
4270 // than from the widget: the widget carries where it is **going**, and a
4271 // layer that jumped there would be the whole bug this animates away.
4272 // Both the size and the place come from the one value, so a box cannot be
4273 // measured against one frame's pins and placed against another's.
4274 let pins = match layer.positioned() {
4275 Some(spec) if layer.anim_pins().is_some() => {
4276 Some(self.runtime.anim_pins(cid).unwrap_or(spec))
4277 }
4278 other => other,
4279 };
4280 let (want_w, want_h) = match pins {
4281 Some(p) => (
4282 p.resolved_width(bounds.width),
4283 p.resolved_height(bounds.height),
4284 ),
4285 None if loose => (None, None),
4286 // The historical behaviour, and still the default: a layer is
4287 // **given** the box rather than asked what size it would like. An
4288 // unsized layer that hugged its content would collapse to nothing —
4289 // invisibly, since a stack draws no box of its own.
4290 None => (Some(bounds.width), Some(bounds.height)),
4291 };
4292 // A pinned layer is **forced** into what its edges say: two opposite
4293 // pins are not a suggestion, and a width of its own is a contradiction
4294 // rather than a second opinion. An unpinned layer under `Expand` is
4295 // merely handed the box, which leaves a layer that chose a size alone —
4296 // that is what a badge sitting in a filled stack has always relied on.
4297 // An axis nobody pinned is left free, so the layer's own size comes back.
4298 let layer_rects = if pins.is_some() {
4299 self.cached_rects(
4300 cid,
4301 layer.as_ref(),
4302 Constraints::pinned(want_w, want_h, Size::new(bounds.width, bounds.height)),
4303 )
4304 } else {
4305 match (want_w, want_h) {
4306 (Some(w), Some(h)) => self.cached_rects(
4307 cid,
4308 layer.as_ref(),
4309 Constraints::filled(Size::new(w, h)),
4310 ),
4311 _ => self.cached_rects(
4312 cid,
4313 layer.as_ref(),
4314 Constraints::scroll(bounds.width, bounds.height, true, true),
4315 ),
4316 }
4317 };
4318 let own = layer_rects
4319 .first()
4320 .copied()
4321 .unwrap_or(Rect::new(0.0, 0.0, 0.0, 0.0));
4322 let (dx, dy) = match pins {
4323 Some(p) => (
4324 Positioning::place(
4325 p.left,
4326 p.right,
4327 bounds.width,
4328 own.width,
4329 align.fraction_x(),
4330 ),
4331 Positioning::place(
4332 p.top,
4333 p.bottom,
4334 bounds.height,
4335 own.height,
4336 align.fraction_y(),
4337 ),
4338 ),
4339 None => (
4340 (bounds.width - own.width).max(0.0) * align.fraction_x(),
4341 (bounds.height - own.height).max(0.0) * align.fraction_y(),
4342 ),
4343 };
4344 let mut layer_index = 0;
4345 // A layer that is not the shown one is walked in full — so it is measured,
4346 // and so the runtime keeps its scroll offset, its caret and its focus — and
4347 // then everything it added is dropped again. The same withdrawal a
4348 // `Visibility` that maintains its size performs, applied from outside.
4349 let hidden = shown.is_some_and(|which| which != i);
4350 let base = hidden.then(|| self.barrier_base());
4351 self.walk(
4352 layer.as_ref(),
4353 cid,
4354 (bounds.x + dx, bounds.y + dy),
4355 layer_clip,
4356 &layer_rects,
4357 &mut layer_index,
4358 );
4359 if let Some(base) = base {
4360 self.apply_barrier(HIDDEN_LAYER, &base, cid, Rect::new(0.0, 0.0, 0.0, 0.0));
4361 }
4362 }
4363 } else if let Some((content, placement)) = widget.overlay() {
4364 // The anchor (child 0) is rendered inline; the overlay (child 1) is deferred.
4365 self.walk(
4366 widget.children()[0].as_ref(),
4367 child_id(id, 0, widget.children()[0].as_ref()),
4368 translation,
4369 clip,
4370 rects,
4371 index,
4372 );
4373 // The appearance progress: for an animated overlay (a drawer), the value the
4374 // runtime interpolated; otherwise `1.0` (shown at once). With no value recorded
4375 // (an isolated render), the target is adopted immediately.
4376 let target = widget.anim_target().unwrap_or(1.0);
4377 let progress = self.runtime.value_or(id, target);
4378 // A tooltip only shows while the anchor is hovered; an animated overlay disappears
4379 // once its progress has fallen back to zero.
4380 let visible = match placement {
4381 Placement::Tooltip => self.runtime.input.hovered == Some(id.child(0)),
4382 _ => true,
4383 };
4384 if visible && progress > 0.001 {
4385 self.overlays.push((
4386 content,
4387 child_id(id, 1, content),
4388 draw_rect,
4389 placement,
4390 widget.overlay_dismiss(),
4391 progress,
4392 widget.overlay_traps_focus(),
4393 widget.overlay_scrim(&self.theme),
4394 self.theme.clone(),
4395 ));
4396 }
4397 } else {
4398 let children = widget.children();
4399 // Fractional alignment (`Container::alignment`) + paint offset
4400 // (`Transform::translate`): both offset the subtree. `(0, 0)` otherwise → the
4401 // ordinary flex walk.
4402 let extra = self.child_offset(widget, id, rect, rects, *index, children);
4403 for (child_index, child) in children.iter().enumerate() {
4404 self.walk(
4405 child.as_ref(),
4406 child_id(id, child_index, child.as_ref()),
4407 (translation.0 + extra.0, translation.1 + extra.1),
4408 clip,
4409 rects,
4410 index,
4411 );
4412 }
4413 }
4414 self.depth -= 1;
4415 }
4416
4417 /// Offset to apply to a child's subtree: the sum of the **fractional alignment**
4418 /// (`Widget::alignment`, a single child slid through the free space) and the **paint
4419 /// offset** (`Transform::translate`, the whole subtree offset without touching layout).
4420 /// `(0, 0)` by default. Since it is added to the walk's translation, this offset follows
4421 /// both the primitives **and** the hit-test.
4422 fn child_offset(
4423 &self,
4424 widget: &dyn Widget<Msg>,
4425 id: WidgetId,
4426 container: Rect,
4427 rects: &[Rect],
4428 child_index: usize,
4429 children: &[Box<dyn Widget<Msg>>],
4430 ) -> (f32, f32) {
4431 let mut off = (0.0, 0.0);
4432
4433 // Fractional alignment: it targets a single child. taffy laid the child out at the
4434 // top left (Start/Start) at its natural size; it is then slid by `free × fraction`.
4435 // A **stack** answers this question too, and means something else by it: where a
4436 // layer smaller than the box sits, which the stack branch applies per layer. It
4437 // must not also be applied to the stack itself, or a stack with exactly one layer
4438 // would shift twice.
4439 if let (Some(geo), 1, false) = (widget.alignment_geometry(), children.len(), widget.stack())
4440 {
4441 // An **animated** anchor moves through the runtime's pair rather than the
4442 // widget's own numbers, in the anchor's own coordinates — so the reading
4443 // direction is applied to the interpolated anchor below, and not to the two
4444 // it came from.
4445 let geo = match self.runtime.anim_offset(id) {
4446 Some((x, y)) => geo.with_fractions(x, y),
4447 None => geo,
4448 };
4449 // Resolves the alignment (physical or directional) against the reading direction;
4450 // `resolve` produces a physical `Alignment` that the rest (with its RTL correction)
4451 // handles uniformly.
4452 let direction = if self.rtl() {
4453 frus_core::TextDirection::Rtl
4454 } else {
4455 frus_core::TextDirection::Ltr
4456 };
4457 let align = geo.resolve(direction);
4458 let child = rects[child_index];
4459 let pad = effective_style(widget, id, self.runtime, &self.theme).padding;
4460 let free_w = (container.width - pad.left - pad.right - child.width).max(0.0);
4461 let free_h = (container.height - pad.top - pad.bottom - child.height).max(0.0);
4462 // In RTL, taffy lays the child out on the left and `mirror` has sent it back to
4463 // the right: the baseline is therefore already right-aligned. 1 is subtracted so
4464 // the fraction stays **physical** (x = +1 ⇒ the right in both directions).
4465 let fx = align.fraction_x() - if self.rtl() { 1.0 } else { 0.0 };
4466 off.0 += free_w * fx;
4467 off.1 += free_h * align.fraction_y();
4468 }
4469
4470 // Paint offset (`Transform::translate`): in RTL the world's x axis is mirrored, so a
4471 // logical +x offset ("towards the end") points left — the sign is inverted to stay
4472 // consistent with the reading direction.
4473 if let Some((tx, ty)) = widget.transform_translate() {
4474 off.0 += if self.rtl() { -tx } else { tx };
4475 off.1 += ty;
4476 }
4477
4478 // The same offset, given as a **fraction of the child's own box**
4479 // (`FractionalTranslation`). The child's rectangle is the one thing this function
4480 // has that the widget did not: a widget cannot multiply by a width it was never
4481 // told.
4482 if let Some((fx, fy)) = widget.translate_fraction() {
4483 // The same tween, on the other offset rule: a slide that animates reads the
4484 // runtime's pair, and one that does not reads its own.
4485 let (fx, fy) = self.runtime.anim_offset(id).unwrap_or((fx, fy));
4486 let child = rects.get(child_index).copied().unwrap_or(container);
4487 let tx = child.width * fx;
4488 off.0 += if self.rtl() { -tx } else { tx };
4489 off.1 += child.height * fy;
4490 }
4491
4492 off
4493 }
4494
4495 /// Records the widget in the inspector's collection (when it is on) and opens a depth
4496 /// level — every render path closes it again at the end of the function
4497 /// (`self.depth -= 1`).
4498 fn inspect_enter(&mut self, widget: &dyn Widget<Msg>, id: WidgetId, draw_rect: Rect) {
4499 if let Some(nodes) = &mut self.inspector {
4500 nodes.push(crate::inspector::InspectorNode {
4501 id,
4502 rect: draw_rect,
4503 name: widget.debug_name(),
4504 depth: self.depth,
4505 });
4506 }
4507 self.depth += 1;
4508 }
4509
4510 /// A widget's full status: pointer interaction + focus + animation progresses + the
4511 /// cursor/selection when there is one.
4512 fn full_status(&self, widget: &dyn Widget<Msg>, id: WidgetId) -> crate::interaction::Status {
4513 let mut status = self.runtime.input.status_for(id);
4514 status.hover_progress = self.runtime.hover_progress(id);
4515 status.focus_progress = self.runtime.focus_progress(id);
4516 // A press is a progression now, and where the runtime has never heard of this
4517 // widget the flag is adopted whole — the rule `value` follows just below. Without
4518 // it a frame built outside the loop would draw a held widget as untouched.
4519 status.press_progress = self.runtime.press_progress_or(
4520 id,
4521 if status.interaction == crate::interaction::Interaction::Pressed {
4522 1.0
4523 } else {
4524 0.0
4525 },
4526 );
4527 status.opacity = self.runtime.opacity(id);
4528 // A widget's own animated value, or **its target** where the runtime has never
4529 // heard of it: the same rule the runtime applies on mount (adopt, do not animate
4530 // in from zero). Without the fallback an isolated render — a test, a frame built
4531 // before the loop has advanced anything — draws every such widget at zero: a
4532 // switch that is on drawn off, an indicator under the wrong tab.
4533 status.value = match widget.anim_target() {
4534 Some(target) => self.runtime.value_or(id, target),
4535 None => self.runtime.value(id),
4536 };
4537 status.anim_color = self.runtime.anim_color(id);
4538 status.anim_radius = self.runtime.anim_radius(id);
4539 status.time = self.runtime.time;
4540 status.drag_over = self.runtime.drag_over == Some(id);
4541 status.scroll_y = self.runtime.scroll.get(&id).map(|s| s.1).unwrap_or(0.0);
4542 if status.focused {
4543 if let Some(edit) = self.runtime.edits.get(&id) {
4544 status.cursor = Some(edit.cursor);
4545 status.selection = edit.selection_range();
4546 status.composing = edit.composing;
4547 }
4548 status.handles = self.runtime.selection_handles == Some(id);
4549 status.caret_hidden = self.runtime.caret_hidden;
4550 }
4551 status
4552 }
4553
4554 /// The generic focus ring (for widgets that do not draw their own).
4555 fn draw_focus_ring(
4556 &mut self,
4557 draw_rect: Rect,
4558 status: &crate::interaction::Status,
4559 widget: &dyn Widget<Msg>,
4560 ) {
4561 // The generic ring only appears when the last interaction was a **keyboard** one
4562 // (`focus_visible`) — a click must not flash a ring.
4563 if status.focused
4564 && self.runtime.focus_visible
4565 && widget.focusable()
4566 && !widget.draws_own_focus()
4567 {
4568 let ring = Rect::new(
4569 draw_rect.x - 2.0,
4570 draw_rect.y - 2.0,
4571 draw_rect.width + 4.0,
4572 draw_rect.height + 4.0,
4573 );
4574 let alpha = 0.4 + 0.6 * status.focus_progress.clamp(0.0, 1.0);
4575 self.scene.draw_rect(
4576 ring,
4577 Color::TRANSPARENT,
4578 self.theme.radius + 2.0,
4579 2.0,
4580 self.theme.focus.fade(alpha),
4581 );
4582 }
4583 }
4584
4585 /// Renders a **virtualised list item**: built on the fly, it cannot defer an overlay
4586 /// (hence a render of its own, without the special branches).
4587 fn render_item(
4588 &mut self,
4589 widget: &dyn Widget<Msg>,
4590 id: WidgetId,
4591 translation: (f32, f32),
4592 clip: Rect,
4593 rects: &[Rect],
4594 index: &mut usize,
4595 ) {
4596 let rect = rects[*index];
4597 *index += 1;
4598 let draw_rect = rect.translate(translation.0, translation.1);
4599 self.inspect_enter(widget, id, draw_rect);
4600
4601 let status = self.full_status(widget, id);
4602 if widget.continuous() {
4603 self.wants_animation = true;
4604 }
4605 self.scene.set_clip(clip);
4606 self.scene.set_owner(id.as_u64());
4607 // The box this widget was given. Text primitives record it: a line of text
4608 // says only where it starts, and the renderer has to know what it covers to
4609 // order it against anything else (milestone 295).
4610 self.scene.set_bounds(draw_rect);
4611 widget.paint(draw_rect, status, &self.theme, &mut self.scene);
4612 self.scene.set_clip(clip);
4613 self.draw_focus_ring(draw_rect, &status, widget);
4614
4615 let visible = draw_rect.intersect(clip);
4616 // A focus stop clipped entirely away by a **scroll** is still a focus stop.
4617 // Tab has to reach the field below the fold — the shell brings it into view
4618 // when it lands there — and registering only what the eye can see is how a long
4619 // form came to have half its fields unreachable from the keyboard. Clipped away
4620 // by anything else it is gone: nothing could reveal it, so focus would land
4621 // where the eye cannot follow.
4622 //
4623 // Only the focus registry does this. A click still needs the visible box, or a
4624 // tap on empty space would land on something scrolled out of sight.
4625 if widget.focusable()
4626 && !self.focus_excluded
4627 && (self.scroll_host.is_some() || (visible.width > 0.0 && visible.height > 0.0))
4628 {
4629 self.focusables.push(Focusable {
4630 id,
4631 rect: visible,
4632 bounds: draw_rect,
4633 form: self.autofill_group,
4634 scroll: self.scroll_host,
4635 skip: self.focus_skipped || widget.focus_skip_traversal(),
4636 order: widget.focus_order().or(self.focus_order),
4637 group: self.focus_group,
4638 });
4639 }
4640 if visible.width > 0.0 && visible.height > 0.0 {
4641 // A press that lands on a **surface** and on nothing inside it stops there.
4642 // Registered before the children, so anything inside still wins on the way
4643 // back out — see [`Widget::opaque`].
4644 if widget.opaque() {
4645 self.hits.push(Hit {
4646 id,
4647 rect: visible,
4648 msg: None,
4649 xform: None,
4650 });
4651 }
4652 if let Some(msg) = widget.on_click() {
4653 self.hits.push(Hit {
4654 id,
4655 rect: visible,
4656 msg: Some(msg),
4657 xform: None,
4658 });
4659 }
4660 if let Some(msg) = widget.on_long_press() {
4661 self.long_presses.push(Hit {
4662 id,
4663 rect: visible,
4664 msg: Some(msg),
4665 xform: None,
4666 });
4667 }
4668 if widget.draggable() {
4669 self.draggables.push((id, visible));
4670 }
4671 if widget.reorder_index().is_some() {
4672 self.reorderables.push((id, visible));
4673 }
4674 // The accessibility tree: nodes that carry meaning (a role or a label).
4675 if let Some(sem) = widget.semantics().filter(|s| s.is_meaningful()) {
4676 self.semantics.push((id, visible, sem));
4677 }
4678 }
4679
4680 let children = widget.children();
4681 // Fractional alignment + paint offset, as in the main walk (a virtualised-list /
4682 // `layout_builder` child may itself be an aligned or transformed container).
4683 let extra = self.child_offset(widget, id, rect, rects, *index, children);
4684 for (child_index, child) in children.iter().enumerate() {
4685 self.render_item(
4686 child.as_ref(),
4687 child_id(id, child_index, child.as_ref()),
4688 (translation.0 + extra.0, translation.1 + extra.1),
4689 clip,
4690 rects,
4691 index,
4692 );
4693 }
4694 self.depth -= 1;
4695 }
4696
4697 /// Lays out a full-window screen and renders it offset by `off_x`.
4698 fn render_screen(
4699 &mut self,
4700 screen: &'a dyn Widget<Msg>,
4701 id: WidgetId,
4702 bounds: Rect,
4703 off_x: f32,
4704 clip: Rect,
4705 ) {
4706 let rects = self.cached_rects(
4707 id,
4708 screen,
4709 Constraints::definite(Size::new(bounds.width, bounds.height)),
4710 );
4711 let screen_clip = clip.intersect(bounds);
4712 let mut index = 0;
4713 self.walk(
4714 screen,
4715 id,
4716 (bounds.x + off_x, bounds.y),
4717 screen_clip,
4718 &rects,
4719 &mut index,
4720 );
4721 }
4722
4723 /// Processes the deferred overlays: sub-layout, positioning and rendering **above**
4724 /// everything (their clickable areas win). May spawn further overlays (nested portals).
4725 fn process_overlays(&mut self) {
4726 let window = Rect::new(0.0, 0.0, self.available.width, self.available.height);
4727 while let Some((content, oid, anchor, placement, dismiss, progress, traps, scrim, theme)) =
4728 self.overlays.pop()
4729 {
4730 // The theme this overlay was declared under, for the whole of its layout and
4731 // painting; whatever was in force when the walk ended is put back after it,
4732 // so two overlays declared under two themes do not bleed into each other.
4733 let outer = std::mem::replace(&mut self.theme, theme);
4734 // Drawers slide along a **spring curve** (a gentle arrival), not linearly; the
4735 // other overlays keep their raw progress.
4736 let progress = if matches!(
4737 placement,
4738 Placement::Left | Placement::Right | Placement::Bottom
4739 ) {
4740 crate::runtime::spring_ease(progress)
4741 } else {
4742 progress
4743 };
4744 // The content's natural size. A drawer (`Left`) is constrained in height to the
4745 // window (so its `Percent(1.0)` panel unfolds) with a free width; the other
4746 // overlays take their natural size.
4747 let (free_x, free_y) = match placement {
4748 Placement::Left | Placement::Right => (true, false),
4749 // The sheet is full-width (constrained to the window) with its natural height:
4750 // its `Percent(1.0)` panel unfolds across the width.
4751 Placement::Bottom => (false, true),
4752 _ => (true, true),
4753 };
4754 let mut rects = self.cached_rects(
4755 oid,
4756 content,
4757 Constraints::scroll(self.available.width, self.available.height, free_x, free_y),
4758 );
4759 let mut size = rects
4760 .first()
4761 .copied()
4762 .unwrap_or(Rect::new(0.0, 0.0, 0.0, 0.0));
4763 // **Nothing wider than the window.** A free axis asks the content how big it
4764 // would like to be, and a dialog answers with the width of its widest line —
4765 // which, at a reader's font size, is wider than the screen. Centred, that
4766 // spills off *both* edges, and a button drawn at a negative x is a button
4767 // nobody can press. So the answer is taken as a wish rather than a size: past
4768 // the room there is, the content is laid out again at that room, and whatever
4769 // can fold — a paragraph, a bar of actions — folds.
4770 //
4771 // The room is the window less the content's **own margin**, which is how an
4772 // overlay says how far off the edges it wants to be held: a dialog's inset
4773 // padding is exactly that, and a clamp that ignored it would push the surface
4774 // flat against both sides of the screen.
4775 //
4776 // The height is deliberately left alone. A dialog too tall for the screen
4777 // wants its content to scroll, which is a question of its own; squashing it
4778 // here would only move the spill inside the surface.
4779 let inset = content.style_themed(&self.theme).margin;
4780 let room = (self.available.width - inset.left - inset.right).max(0.0);
4781 if free_x && size.width > room {
4782 rects = self.cached_rects(
4783 oid,
4784 content,
4785 Constraints::scroll(room, self.available.height, false, free_y),
4786 );
4787 size = rects
4788 .first()
4789 .copied()
4790 .unwrap_or(Rect::new(0.0, 0.0, 0.0, 0.0));
4791 }
4792
4793 // A drawer's slide-in from the left / right edge.
4794 let from_left = -(1.0 - progress) * size.width;
4795 let from_right = self.available.width - progress * size.width;
4796 // An anchored menu/tooltip: aligned to the anchor's **start** edge — in RTL that
4797 // edge is the right one (the menu opens leftwards).
4798 let anchor_x = if self.rtl() {
4799 anchor.x + anchor.width - size.width
4800 } else {
4801 anchor.x
4802 };
4803 let mut pos = match placement {
4804 Placement::Below => (anchor_x, anchor.y + anchor.height + 4.0),
4805 Placement::Center => (
4806 (self.available.width - size.width) * 0.5,
4807 (self.available.height - size.height) * 0.5,
4808 ),
4809 Placement::Tooltip => (anchor_x, anchor.y - size.height - 6.0),
4810 // `Left` = a drawer on the **start** side; in RTL, start = the right.
4811 Placement::Left if self.rtl() => (from_right, 0.0),
4812 Placement::Left => (from_left, 0.0),
4813 // `Right` = the **end** side; in RTL, end = the left.
4814 Placement::Right if self.rtl() => (from_left, 0.0),
4815 Placement::Right => (from_right, 0.0),
4816 // The sheet slides up from the bottom: its bottom edge stays flush with the
4817 // window, offset downwards by `(1-progress)·height`.
4818 Placement::Bottom => (0.0, self.available.height - progress * size.height),
4819 };
4820
4821 // An **anchored** overlay follows a widget; the others (a modal, a drawer, a
4822 // sheet) are positioned against the window and have no anchor worth the name.
4823 // Only the first kind can find its anchor gone from the window: a screen
4824 // sliding out under a `Navigator`, a row scrolled sideways past the edge.
4825 let anchored = matches!(placement, Placement::Below | Placement::Tooltip);
4826 let anchor_on_screen = !anchored
4827 || (anchor.x < self.available.width
4828 && anchor.x + anchor.width > 0.0
4829 && anchor.y < self.available.height
4830 && anchor.y + anchor.height > 0.0);
4831
4832 // Auto-flip: when an anchored overlay overflows an edge, it is flipped / nudged
4833 // back inside the window.
4834 //
4835 // This is for a menu opened near the right margin, and it assumes the anchor is
4836 // something the window is showing. When the anchor has **left** the window the
4837 // nudge does the opposite of its job: it drags a departing screen's menu back
4838 // into view, fully opaque, over the screen that replaced it. An overlay whose
4839 // anchor is off screen goes off screen with it.
4840 if anchored && anchor_on_screen {
4841 // A vertical overflow → flip to the other side of the anchor.
4842 if placement == Placement::Below
4843 && pos.1 + size.height > self.available.height
4844 && anchor.y - size.height - 4.0 >= 0.0
4845 {
4846 pos.1 = anchor.y - size.height - 4.0;
4847 } else if placement == Placement::Tooltip
4848 && pos.1 < 0.0
4849 && anchor.y + anchor.height + size.height + 6.0 <= self.available.height
4850 {
4851 pos.1 = anchor.y + anchor.height + 6.0;
4852 }
4853 // A horizontal overflow → nudge back inside the window.
4854 if pos.0 + size.width > self.available.width {
4855 pos.0 = (self.available.width - size.width).max(0.0);
4856 }
4857 if pos.0 < 0.0 {
4858 pos.0 = 0.0;
4859 }
4860 }
4861
4862 if matches!(
4863 placement,
4864 Placement::Center | Placement::Left | Placement::Right | Placement::Bottom
4865 ) {
4866 // The scrim behind the modal / the drawer, modulated by the progress (a fade
4867 // synchronised with the slide). The colour is the overlay's own when it asked
4868 // for one — alpha included, since a scrim's transparency *is* the colour —
4869 // and the `scrim` role at half opacity otherwise.
4870 let base = scrim.unwrap_or_else(|| self.theme.scheme.scrim.with_alpha(0.5));
4871 self.scene.set_owner(0);
4872 self.scene.set_clip(window);
4873 self.scene
4874 .fill_rect(window, base.with_alpha(base.a * progress));
4875 }
4876
4877 // Dismissal on a click **outside** the content (a modal, a menu…): a full-screen
4878 // hit added **before** the content, so the content beats it where they overlap.
4879 // The dismissal is also remembered for Escape (the last one rendered = the
4880 // topmost).
4881 //
4882 // Gated on the same condition as the nudge, and for the same reason: a
4883 // window-wide barrier belonging to an overlay nobody can see would swallow the
4884 // next press anywhere on the screen that replaced it.
4885 if let Some(msg) = dismiss.filter(|_| anchor_on_screen) {
4886 self.dismisses.push(msg.clone());
4887 self.hits.push(Hit {
4888 id: oid.barrier(),
4889 rect: window,
4890 msg: Some(msg),
4891 xform: None,
4892 });
4893 }
4894
4895 // A **modal** (scrimmed) or **trapping anchored** (menu) overlay: its focusables
4896 // form a **scope** that traps Tab/arrows/click-to-focus. The last one rendered (the
4897 // topmost) wins; anchored overlays that do not trap (a tooltip, an autocomplete
4898 // list) leave the focus alone.
4899 let modal = matches!(
4900 placement,
4901 Placement::Center | Placement::Left | Placement::Right | Placement::Bottom
4902 );
4903 if modal || traps {
4904 self.focus_scope_start = Some(self.focusables.len());
4905 }
4906
4907 let mut index = 0;
4908 self.walk(content, oid, pos, window, &rects, &mut index);
4909 self.theme = outer;
4910 }
4911 }
4912}
4913
4914impl<Msg: Clone> Builder<'_, Msg> {
4915 /// Draws a scrollbar (track + thumb) and registers it for hit-testing a drag.
4916 /// Paints the region's overscroll glows over its viewport.
4917 ///
4918 /// The colour is the scheme's **secondary**: the edge feedback is an
4919 /// acknowledgement, not a call to action, so it must read as part of the surface
4920 /// rather than compete with whatever primary-coloured control sits nearby.
4921 fn add_overscroll_glow(&mut self, id: WidgetId, viewport: Rect) {
4922 if let Some(glows) = self.runtime.scroll_glow.get(&id) {
4923 glows.paint(viewport, self.theme.scheme.secondary, &mut self.scene);
4924 }
4925 }
4926
4927 fn add_scrollbar(&mut self, id: WidgetId, viewport: Rect, axis: Bar) {
4928 let Bar {
4929 vertical,
4930 offset,
4931 max,
4932 reverse,
4933 asked,
4934 visible,
4935 } = axis;
4936 // **Along the horizontal axis, never** (`app.dart:861`) — on any platform, under
4937 // any setting. A sideways strip is scrolled by the thing that made it sideways,
4938 // and a bar under it takes room from content that is short of it already.
4939 if !vertical {
4940 return;
4941 }
4942 // And down the vertical one, only where the platform's own scroll views have a
4943 // bar (`app.dart:865` against `:870`). A finger already knows where it is on the
4944 // page; a permanent bar over the content is an affordance for a pointer that
4945 // cannot feel the edges. This drew one everywhere, on both axes, always.
4946 if asked.unwrap_or(self.runtime.scrollbars) == crate::physics::Scrollbars::Never {
4947 return;
4948 }
4949 let (track_start, track_len, content_len) = if vertical {
4950 (viewport.y, viewport.height, viewport.height + max)
4951 } else {
4952 (viewport.x, viewport.width, viewport.width + max)
4953 };
4954 // Every metric below is `theme ?? framework`. There is no per-call override for a
4955 // scrollbar — it is drawn by the walk, not by a widget a caller configured — so
4956 // the chain is two terms here rather than three.
4957 let bar = &self.theme.widgets.scrollbar;
4958 let bar_size = bar.thickness.unwrap_or(BAR_SIZE);
4959 let bar_margin = bar.margin.unwrap_or(BAR_MARGIN);
4960 let thumb_len = (track_len * track_len / content_len)
4961 .max(bar.min_thumb_length.unwrap_or(MIN_THUMB))
4962 .min(track_len);
4963 let travel = track_len - thumb_len;
4964 // Along a reversed axis the numbers run the other way, so the thumb does too:
4965 // offset 0 is the far end, and that is where it rests.
4966 let fraction = if max > 0.0 { offset / max } else { 0.0 };
4967 let fraction = if reverse { 1.0 - fraction } else { fraction };
4968 let thumb_pos = track_start + fraction * travel;
4969
4970 // The track's rectangle is still worked out — it is the room a pointer has to be
4971 // in, and the geometry the thumb sits in — but nothing is painted for it.
4972 //
4973 // `BAR_SIZE` is the **thumb's** thickness, not a slot's: the reference keeps the
4974 // thumb clear of the edge with `crossAxisMargin` (`scrollbar.dart:357`) rather
4975 // than shrinking it to fit. This drew a 6-pixel thumb inside an 8-pixel slot,
4976 // which is the same arithmetic with the wrong number left over.
4977 let (track, thumb) = if vertical {
4978 let x = viewport.x + viewport.width - bar_margin - bar_size;
4979 (
4980 Rect::new(
4981 x - bar_margin,
4982 viewport.y,
4983 bar_size + bar_margin * 2.0,
4984 viewport.height,
4985 ),
4986 Rect::new(x, thumb_pos, bar_size, thumb_len),
4987 )
4988 } else {
4989 let y = viewport.y + viewport.height - bar_margin - bar_size;
4990 (
4991 Rect::new(
4992 viewport.x,
4993 y - bar_margin,
4994 viewport.width,
4995 bar_size + bar_margin * 2.0,
4996 ),
4997 Rect::new(thumb_pos, y, thumb_len, bar_size),
4998 )
4999 };
5000 // What a mouse has to be inside for the bar to notice it: the track, widened to
5001 // take in a tap target's worth of room around the thumb (`scrollbar.dart:762`).
5002 let reach = {
5003 let (cx, cy) = (thumb.x + thumb.width * 0.5, thumb.y + thumb.height * 0.5);
5004 let r = crate::theme::MIN_TAP_TARGET * 0.5;
5005 let x0 = track.x.min(cx - r);
5006 let y0 = track.y.min(cy - r);
5007 let x1 = (track.x + track.width).max(cx + r);
5008 let y1 = (track.y + track.height).max(cy + r);
5009 Rect::new(x0, y0, x1 - x0, y1 - y0)
5010 };
5011
5012 // **How present the bar is**, this frame. It arrives when the area moves and goes
5013 // again once the area has been still long enough; an area nobody has scrolled has
5014 // none at all. Unless it was asked to stay (`scrollbar.dart:214`).
5015 let fade = self.runtime.scrollbar_fade_of(id);
5016 let opacity = if visible.unwrap_or(false) {
5017 1.0
5018 } else {
5019 fade.opacity.clamp(0.0, 1.0)
5020 };
5021
5022 // **No track.** The reference's is transparent unless a caller asks for one
5023 // (`scrollbar.dart:281`), and even then it is 3 to 5 %.
5024 //
5025 // The thumb takes the reference's opacity for the surface's brightness: at rest
5026 // (`scrollbar.dart:242`, `:248`), warmed towards its hovered value while a
5027 // pointer is near it (`:239`, `:245`), and at its dragged value the moment the
5028 // thumb is held (`:238`, `:244`) — that one does not fade in, because the hand is
5029 // already on it and a fade would only lag behind the grab.
5030 // The scheme's own answer, which is what the reference reads here
5031 // (`scrollbar.dart:232`). This used to be `surface.compute_luminance() < 0.5`:
5032 // right on both built-in schemes and a guess on any other.
5033 let dark = self.theme.brightness().is_dark();
5034 let (rest, warm, grabbed) = if dark {
5035 (THUMB_ON_DARK, THUMB_HOVER_ON_DARK, THUMB_DRAG_ON_DARK)
5036 } else {
5037 (THUMB_ON_LIGHT, THUMB_HOVER_ON_LIGHT, THUMB_DRAG_ON_LIGHT)
5038 };
5039 // A theme's levels replace the scheme's, one at a time: naming the resting
5040 // opacity says nothing about the held one.
5041 let rest = bar.opacity.unwrap_or(rest);
5042 let warm = bar.hover_opacity.unwrap_or(warm);
5043 let grabbed = bar.drag_opacity.unwrap_or(grabbed);
5044 let level = if self.runtime.scrollbar_dragged == Some(id) {
5045 grabbed
5046 } else {
5047 rest + (warm - rest) * fade.hover.clamp(0.0, 1.0)
5048 };
5049 if opacity > 0.0 {
5050 let base = bar.thumb_color.unwrap_or(self.theme.scheme.on_surface);
5051 self.scene.draw_rect(
5052 thumb,
5053 base.fade(level * opacity),
5054 bar.radius.unwrap_or(bar_size * 0.5),
5055 0.0,
5056 Color::TRANSPARENT,
5057 );
5058 }
5059 // Registered even when nothing was painted for it — unlike a bar the platform
5060 // does not draw at all, which is not registered because it is not there. A mouse
5061 // coming near a faded bar is what brings it back (`scrollbar.dart:2132`), so
5062 // something has to be here for the mouse to find. What a bar at zero cannot be is
5063 // **grabbed**; see `Ui::scrollbar_at`.
5064 self.scrollbars.push(Scrollbar {
5065 id,
5066 vertical,
5067 thumb,
5068 track_start,
5069 track_len,
5070 thumb_len,
5071 reverse,
5072 max,
5073 opacity,
5074 reach,
5075 });
5076 }
5077}
5078
5079/// Turns a widget tree into a [`Ui`] for a given size, runtime state and theme.
5080pub fn build_ui<'a, Msg: Clone + 'static>(
5081 root: &'a dyn Widget<Msg>,
5082 available: Size,
5083 runtime: &'a Runtime,
5084 theme: &'a Theme,
5085) -> Ui<Msg> {
5086 build_ui_impl(root, available, runtime, theme, false).0
5087}
5088
5089/// Like [`build_ui`], but it also collects the **inspection nodes** (one per painted widget:
5090/// name, box, depth) — the raw material of the runtime inspector and of the diagnostic dump.
5091/// Reserve it for the frames where the inspector is on.
5092pub fn build_ui_inspected<'a, Msg: Clone + 'static>(
5093 root: &'a dyn Widget<Msg>,
5094 available: Size,
5095 runtime: &'a Runtime,
5096 theme: &'a Theme,
5097) -> (Ui<Msg>, Vec<crate::inspector::InspectorNode>) {
5098 let (ui, nodes) = build_ui_impl(root, available, runtime, theme, true);
5099 (ui, nodes.unwrap_or_default())
5100}
5101
5102/// Panics, **in debug builds only**, when the reader's font size is installed and the
5103/// surface it belongs to is not.
5104///
5105/// The two travel together — [`MediaQuery::install`] sets both — so a scale away from 1
5106/// with no description means somebody installed half a surface, and the half they left out
5107/// is about to be missing for the layout that follows. That is not a theory: for four
5108/// milestones the shell scoped only the build, the scale was 1 while text was measured and
5109/// painted, and the reader's setting reached a device without moving a pixel. Nothing said
5110/// a word — not the 91 goldens, not clippy, not the strict rustdoc.
5111///
5112/// Debug only, because it is a wiring mistake rather than a state a running application can
5113/// reach: it should stop a test, not a user's frame. A scale of exactly 1 is the neutral
5114/// value and says nothing either way, so it is never a failure.
5115#[cfg(debug_assertions)]
5116fn assert_surface_is_whole() {
5117 let scale = frus_core::text_scale();
5118 if (scale - 1.0).abs() > f32::EPSILON && !crate::MediaQuery::of().is_described() {
5119 panic!(
5120 "a text scale of {scale} is installed with no surface described: something \
5121 installed half a surface. Use `MediaQuery::install` (or `scope`), which sets \
5122 the description and the reader's font size together \u{2014} see milestone 408."
5123 );
5124 }
5125}
5126
5127fn build_ui_impl<'a, Msg: Clone + 'static>(
5128 root: &'a dyn Widget<Msg>,
5129 available: Size,
5130 runtime: &'a Runtime,
5131 theme: &'a Theme,
5132 inspect: bool,
5133) -> (Ui<Msg>, Option<Vec<crate::inspector::InspectorNode>>) {
5134 #[cfg(debug_assertions)]
5135 assert_surface_is_whole();
5136
5137 let (mut rects, overflows) = runtime.layout_cache.borrow_mut().rects(
5138 WidgetId::ROOT,
5139 root,
5140 runtime,
5141 theme,
5142 Constraints::definite(available),
5143 );
5144
5145 let mut builder = Builder {
5146 scene: Scene::new(),
5147 hits: Vec::new(),
5148 long_presses: Vec::new(),
5149 dismisses: Vec::new(),
5150 focusables: Vec::new(),
5151 focus_scope_start: None,
5152 scrollables: Vec::new(),
5153 scrollbars: Vec::new(),
5154 draggables: Vec::new(),
5155 drag_sources: Vec::new(),
5156 drop_zones: Vec::new(),
5157 inks: Vec::new(),
5158 reorderables: Vec::new(),
5159 interactives: Vec::new(),
5160 overlays: Vec::new(),
5161 wants_animation: false,
5162 available,
5163 runtime,
5164 theme: std::rc::Rc::new(theme.clone()),
5165 inspector: inspect.then(Vec::new),
5166 depth: 0,
5167 refresh_host: None,
5168 scroll_host: None,
5169 hero_screen: None,
5170 heroes: Vec::new(),
5171 refreshes: Vec::new(),
5172 dismissables: Vec::new(),
5173 sheets: Vec::new(),
5174 semantics: Vec::new(),
5175 system_ui: Vec::new(),
5176 focus_excluded: false,
5177 backdrop_group: None,
5178 focus_skipped: false,
5179 focus_order: None,
5180 focus_group: None,
5181 autofill_group: None,
5182 scopes: Vec::new(),
5183 listeners: Vec::new(),
5184 overflows: std::cell::RefCell::new(Vec::new()),
5185 pending_overflows: std::cell::RefCell::new(std::collections::HashMap::new()),
5186 };
5187 // The root is mirrored in RTL (like every layout root).
5188 builder.mirror(&mut rects);
5189 builder.record_overflows(WidgetId::ROOT, &rects, overflows);
5190 let mut index = 0;
5191 builder.walk(
5192 root,
5193 WidgetId::ROOT,
5194 (0.0, 0.0),
5195 Rect::UNBOUNDED,
5196 &rects,
5197 &mut index,
5198 );
5199
5200 // Overlays (floating menus, modals, tooltips) above everything else. (Their walk restarts
5201 // from depth 0: roots as far as the inspector is concerned.)
5202 builder.process_overlays();
5203
5204 // End of frame: forget the layout roots and repaint boundaries of widgets that have gone,
5205 // and freeze the caches' diagnostic counters.
5206 runtime.layout_cache.borrow_mut().end_frame();
5207 runtime.paint_cache.borrow_mut().end_frame();
5208
5209 // Replays the outgoing subtrees, fading out, over the current scene.
5210 builder.scene.set_clip(Rect::UNBOUNDED);
5211 for (primitives, opacity) in runtime.leaving.values() {
5212 for primitive in primitives {
5213 builder.scene.push_faded(primitive, *opacity);
5214 }
5215 }
5216
5217 // A box whose children ran past it now **says so on the screen**, not only in the
5218 // console — see `paint_overflow_bands`. Last, so nothing paints over it.
5219 paint_overflow_bands(&mut builder.scene, &builder.overflows.borrow());
5220
5221 let ui = Ui {
5222 scene: builder.scene,
5223 hits: builder.hits,
5224 long_presses: builder.long_presses,
5225 dismisses: builder.dismisses,
5226 focusables: builder.focusables,
5227 focus_scope_start: builder.focus_scope_start,
5228 scrollables: builder.scrollables,
5229 scrollbars: builder.scrollbars,
5230 draggables: builder.draggables,
5231 drag_sources: builder.drag_sources,
5232 drop_zones: builder.drop_zones,
5233 inks: builder.inks,
5234 reorderables: builder.reorderables,
5235 interactives: builder.interactives,
5236 refreshes: builder.refreshes,
5237 dismissables: builder.dismissables,
5238 sheets: builder.sheets,
5239 wants_animation: builder.wants_animation,
5240 semantics: builder.semantics,
5241 system_ui: builder.system_ui,
5242 overflows: builder.overflows.into_inner(),
5243 scopes: builder.scopes,
5244 listeners: builder.listeners,
5245 };
5246 (ui, builder.inspector)
5247}
5248
5249/// Collects the identities of every widget in the tree (prefix order), following the same
5250/// positional scheme as [`build_ui`]. Used to detect mounts/unmounts between two frames.
5251pub fn collect_ids<Msg>(root: &dyn Widget<Msg>) -> Vec<WidgetId> {
5252 fn walk<Msg>(widget: &dyn Widget<Msg>, id: WidgetId, out: &mut Vec<WidgetId>) {
5253 out.push(id);
5254 for (index, child) in widget.children().iter().enumerate() {
5255 walk(child.as_ref(), child_id(id, index, child.as_ref()), out);
5256 }
5257 }
5258 let mut out = Vec::new();
5259 walk(root, WidgetId::ROOT, &mut out);
5260 out
5261}
5262
5263/// Identities of the **subtree** rooted at `root_id` — `widget` being the widget with that
5264/// identity — that is `[root_id, …descendants]`, derived by the same positional scheme as
5265/// [`collect_ids`].
5266///
5267/// Used by the **drag ghost**: a **rich** card's primitives are painted by its children (owners
5268/// other than the card itself). To capture all of its visuals, the shell gathers the primitives
5269/// of the **whole** subtree, not only the card's own.
5270pub fn subtree_ids<Msg>(widget: &dyn Widget<Msg>, root_id: WidgetId) -> Vec<WidgetId> {
5271 fn walk<Msg>(widget: &dyn Widget<Msg>, id: WidgetId, out: &mut Vec<WidgetId>) {
5272 out.push(id);
5273 for (index, child) in widget.children().iter().enumerate() {
5274 walk(child.as_ref(), child_id(id, index, child.as_ref()), out);
5275 }
5276 }
5277 let mut out = Vec::new();
5278 walk(widget, root_id, &mut out);
5279 out
5280}
5281
5282/// The owners of everything a carried row, card or item may be **moved past** this frame:
5283/// every reorderable that can be dropped on, and all it paints.
5284///
5285/// What a reorder preview slides out of the way is decided by geometry — the band the
5286/// source sits in — and geometry alone moves whatever shares that band: a button floating
5287/// over a list, the navigation bar under it. This is the other half of the question, *is
5288/// it part of something that can be reordered*, asked of the tree. A grip is inside its
5289/// row, so it moves with it without being a target itself.
5290pub fn reorderable_owners<Msg>(
5291 ui: &Ui<Msg>,
5292 root: &dyn Widget<Msg>,
5293) -> std::collections::HashSet<u64> {
5294 ui.reorderables
5295 .iter()
5296 .filter_map(|(id, _)| {
5297 find_widget(root, *id)
5298 .filter(|widget| widget.reorder_droppable())
5299 .map(|widget| subtree_ids(widget, *id))
5300 })
5301 .flatten()
5302 .map(|id| id.as_u64())
5303 .collect()
5304}
5305
5306/// The slots a reorderable `id` can be dropped among **in its own list**: its siblings — the
5307/// children of its parent — that can be dropped on, with their boxes this frame, in order.
5308///
5309/// A list's rows are one widget's children, and so are a board column's cards and the drop
5310/// zone that ends it. Nothing else on the page is, which is what lets a drop that landed on
5311/// no row at all still mean *somewhere in this list* rather than anywhere reorderable.
5312/// Siblings the frame registered no box for are left out.
5313pub fn reorder_siblings<Msg>(
5314 ui: &Ui<Msg>,
5315 root: &dyn Widget<Msg>,
5316 id: WidgetId,
5317) -> Vec<(WidgetId, Rect)> {
5318 fn parent_of<Msg>(
5319 widget: &dyn Widget<Msg>,
5320 own: WidgetId,
5321 target: WidgetId,
5322 ) -> Option<(&dyn Widget<Msg>, WidgetId)> {
5323 for (index, child) in widget.children().iter().enumerate() {
5324 let id = child_id(own, index, child.as_ref());
5325 if id == target {
5326 return Some((widget, own));
5327 }
5328 if let Some(found) = parent_of(child.as_ref(), id, target) {
5329 return Some(found);
5330 }
5331 }
5332 None
5333 }
5334 let Some((parent, parent_id)) = parent_of(root, WidgetId::ROOT, id) else {
5335 return Vec::new();
5336 };
5337 parent
5338 .children()
5339 .iter()
5340 .enumerate()
5341 .filter(|(_, child)| child.reorder_index().is_some() && child.reorder_droppable())
5342 .filter_map(|(index, child)| {
5343 let id = child_id(parent_id, index, child.as_ref());
5344 ui.reorderables
5345 .iter()
5346 .find(|(registered, _)| *registered == id)
5347 .map(|(_, rect)| (id, *rect))
5348 })
5349 .collect()
5350}
5351
5352/// Identity of the **first** widget declaring the key `key` (a hash), or `None`. It is how the
5353/// shell resolves a focus-by-key request (`Command::focus`): the application wraps a field in
5354/// `keyed(k, …)`, then focuses by `k`.
5355pub fn find_by_key<Msg>(root: &dyn Widget<Msg>, key: u64) -> Option<WidgetId> {
5356 fn walk<Msg>(widget: &dyn Widget<Msg>, id: WidgetId, key: u64) -> Option<WidgetId> {
5357 if widget.key() == Some(key) {
5358 return Some(id);
5359 }
5360 for (index, child) in widget.children().iter().enumerate() {
5361 if let Some(found) = walk(child.as_ref(), child_id(id, index, child.as_ref()), key) {
5362 return Some(found);
5363 }
5364 }
5365 None
5366 }
5367 walk(root, WidgetId::ROOT, key)
5368}
5369
5370/// Identity of the **sheet** the key `key` names: the first sheet's panel at or under the
5371/// widget declaring it, which is the identity the sheet's state is kept under and the one the
5372/// frame's sheets are reported by. `None` when nothing declares the key, or nothing under it
5373/// is a sheet.
5374///
5375/// A sheet's state belongs to its panel, which the application never builds itself, so a
5376/// request by key (`Command::sheet`) needs this one step more than a focus request does.
5377pub fn find_sheet_by_key<Msg>(root: &dyn Widget<Msg>, key: u64) -> Option<WidgetId> {
5378 fn first_sheet<Msg>(widget: &dyn Widget<Msg>, id: WidgetId) -> Option<WidgetId> {
5379 if widget.sheet().is_some() {
5380 return Some(id);
5381 }
5382 widget
5383 .children()
5384 .iter()
5385 .enumerate()
5386 .find_map(|(index, child)| {
5387 first_sheet(child.as_ref(), child_id(id, index, child.as_ref()))
5388 })
5389 }
5390 fn walk<Msg>(widget: &dyn Widget<Msg>, id: WidgetId, key: u64) -> Option<WidgetId> {
5391 if widget.key() == Some(key) {
5392 return first_sheet(widget, id);
5393 }
5394 widget
5395 .children()
5396 .iter()
5397 .enumerate()
5398 .find_map(|(index, child)| {
5399 walk(child.as_ref(), child_id(id, index, child.as_ref()), key)
5400 })
5401 }
5402 walk(root, WidgetId::ROOT, key)
5403}
5404
5405/// Path from the **root down to the widget** with identity `target` (`[root, …, target]`), for
5406/// the leaf→root bubbling of keys. Empty when it cannot be found.
5407pub fn find_path<Msg>(root: &dyn Widget<Msg>, target: WidgetId) -> Vec<&dyn Widget<Msg>> {
5408 fn walk<'a, Msg>(
5409 widget: &'a dyn Widget<Msg>,
5410 id: WidgetId,
5411 target: WidgetId,
5412 path: &mut Vec<&'a dyn Widget<Msg>>,
5413 ) -> bool {
5414 path.push(widget);
5415 if id == target {
5416 return true;
5417 }
5418 for (index, child) in widget.children().iter().enumerate() {
5419 if walk(
5420 child.as_ref(),
5421 child_id(id, index, child.as_ref()),
5422 target,
5423 path,
5424 ) {
5425 return true;
5426 }
5427 }
5428 path.pop();
5429 false
5430 }
5431 let mut path = Vec::new();
5432 if !walk(root, WidgetId::ROOT, target, &mut path) {
5433 path.clear();
5434 }
5435 path
5436}
5437
5438/// The theme a subtree inherits at this node — the widget's own override, with any
5439/// **animated text style** the runtime is holding for it laid on top.
5440///
5441/// The one place the theme swap happens, and it is one place on purpose. The swap is made
5442/// four times over a frame — by the layout pass, by the relayout fingerprint that has to
5443/// agree with it or the cache lies, by the paint walk, and by [`build_deferred`] before
5444/// any of them — and every animated value in this framework is read from the runtime by
5445/// whoever consumes it. This one's consumer is the swap itself, so a copy of the rule at
5446/// each of the four sites is four chances to drift. `Responsive` cost three silent bugs
5447/// that way (milestone 495); this is the same lesson written down as a function.
5448///
5449/// Cheap where nothing is animated: no `anim_text_style`, no clone, and the widget's own
5450/// `theme_override` is returned untouched — which is every node but the rare one.
5451pub(crate) fn scoped_theme<Msg>(
5452 widget: &dyn Widget<Msg>,
5453 id: WidgetId,
5454 runtime: &Runtime,
5455 inherited: &Theme,
5456) -> Option<Box<Theme>> {
5457 let own = widget.theme_override(inherited);
5458 let Some(target) = widget.anim_text_style() else {
5459 return own;
5460 };
5461 // Mid-flight if the runtime has a value for this node, and the target otherwise —
5462 // which is the first frame, before the family has been advanced over this tree.
5463 let style = runtime.anim_text_style(id).unwrap_or(target);
5464 let mut theme = own.map_or_else(|| inherited.clone(), |boxed| *boxed);
5465 // **Merged over**, not assigned: the node's own `theme_override` has already put the
5466 // target style and the four unanimated answers (alignment, wrapping, overflow, line
5467 // count) in place, and this replaces field by field exactly what is moving.
5468 theme.widgets.text.style = theme.widgets.text.style.merge(style);
5469 Some(Box::new(theme))
5470}
5471
5472/// Runs the **deferred builds** over a tree, the way the layout pass does on its way down.
5473///
5474/// A [`ThemeBuilder`](crate::ThemeBuilder) — and everything built on one, an
5475/// [`AppBar`](crate::AppBar) included — has no children at all until something calls
5476/// [`Widget::build_themed`] on it. The layout pass does that first thing, so within a frame
5477/// nothing ever meets an unbuilt subtree.
5478///
5479/// **A tree built outside a frame does.** The shell rebuilds from `view` alone on a burst of
5480/// keystrokes, faster than a frame, so the next key sees the current value rather than the
5481/// retained tree's — and then reads that tree straight away. Every traversal that arrives
5482/// there ([`find_widget`], focus resolution, a caret being revealed) walks a tree whose
5483/// deferred subtrees are empty, and silently finds nothing.
5484///
5485/// This is that half of the walk and nothing else: the theme swap
5486/// ([`Widget::theme_override`]) then the build, top-down. The swap is not optional — a
5487/// builder inside a [`Themed`](crate::Themed) has to see its own subtree's theme, and a
5488/// preparation that is *nearly* the walk is worse than none, because what it builds is wrong
5489/// rather than absent.
5490///
5491/// **Which is why it takes a runtime.** A subtree under an animated text style
5492/// (`AnimatedDefaultTextStyle`) inherits a theme that is moving, and a builder inside one
5493/// composes against whatever theme reaches it *first* — this walk. Preparing it against
5494/// the target instead of the value in flight is the same failure as skipping the swap: what
5495/// it builds is wrong rather than absent, and only for as long as the movement lasts, which
5496/// is the hardest kind to see. The runtime is one frame behind here, because this runs
5497/// before the frame advances its animations, and one frame behind is the whole tree agreeing
5498/// with itself rather than a builder disagreeing with the layout around it.
5499pub fn build_deferred<Msg>(root: &dyn Widget<Msg>, theme: &Theme, runtime: &Runtime) {
5500 fn walk<Msg>(widget: &dyn Widget<Msg>, id: WidgetId, runtime: &Runtime, theme: &Theme) {
5501 widget.expand(id, runtime, theme);
5502 let scoped = scoped_theme(widget, id, runtime, theme);
5503 let theme = scoped.as_deref().unwrap_or(theme);
5504 let _surface = widget
5505 .media_override(crate::MediaQuery::of())
5506 .map(crate::MediaQuery::install);
5507 let _shell = widget.scaffold_override().map(crate::ScaffoldInfo::install);
5508 widget.build_in(id, runtime, theme);
5509 for (index, child) in widget.children().iter().enumerate() {
5510 walk(
5511 child.as_ref(),
5512 child_id(id, index, child.as_ref()),
5513 runtime,
5514 theme,
5515 );
5516 }
5517 }
5518 walk(root, WidgetId::ROOT, runtime, theme);
5519}
5520
5521/// The widget of the tree under `root` that has the identity `target`, or `None` if none
5522/// does. Identities are the ones the walk that built the frame gave, so an id read from a
5523/// [`Ui`] finds its widget here.
5524pub fn find_widget<Msg>(root: &dyn Widget<Msg>, target: WidgetId) -> Option<&dyn Widget<Msg>> {
5525 fn walk<Msg>(
5526 widget: &dyn Widget<Msg>,
5527 id: WidgetId,
5528 target: WidgetId,
5529 ) -> Option<&dyn Widget<Msg>> {
5530 if id == target {
5531 return Some(widget);
5532 }
5533 for (index, child) in widget.children().iter().enumerate() {
5534 if let Some(found) = walk(child.as_ref(), child_id(id, index, child.as_ref()), target) {
5535 return Some(found);
5536 }
5537 }
5538 None
5539 }
5540 walk(root, WidgetId::ROOT, target)
5541}
5542
5543/// Folds a filter into the single filtered layer its subtree turned out to be, so two
5544/// filter widgets wrapped one inside the other cost one layer rather than two.
5545///
5546/// It is not only a saving. Compositing renders a layer into a texture and composites
5547/// that texture; a layer *inside* that texture is not composited again, so a nested
5548/// filter would simply not be applied. Folding is what makes `ColorFiltered` around
5549/// `ImageFiltered` mean what it reads as.
5550///
5551/// Everything about the inner layer must be neutral — full opacity, a plain
5552/// rectangular clip, no transform — because those are applied at compositing time and
5553/// there is only one composite to apply them at. And the two filters must not want
5554/// the same slot: greyscale of an inverted picture is not the inversion of a
5555/// greyscale one, so there is no single layer that means both, and the two nest.
5556fn fold_filter(
5557 group: Vec<Primitive>,
5558 filter: LayerFilter,
5559 clip: Rect,
5560) -> (Vec<Primitive>, LayerFilter, Rect) {
5561 if group.len() != 1 {
5562 return (group, filter, clip);
5563 }
5564 let Primitive::Layer {
5565 opacity,
5566 clip: inner_clip,
5567 clip_shape,
5568 transform,
5569 filter: inner,
5570 ..
5571 } = &group[0]
5572 else {
5573 return (group, filter, clip);
5574 };
5575 if *opacity < 0.999 || !matches!(clip_shape, ClipShape::Rect) || transform.is_some() {
5576 return (group, filter, clip);
5577 }
5578 let (inner, inner_clip) = (*inner, *inner_clip);
5579 let Some(merged) = filter.merge(inner) else {
5580 return (group, filter, clip);
5581 };
5582 let Some(Primitive::Layer { primitives, .. }) = group.into_iter().next() else {
5583 unreachable!("checked just above")
5584 };
5585 (primitives, merged, clip.intersect(inner_clip))
5586}
5587
5588#[cfg(test)]
5589mod tests {
5590 use super::*;
5591 use crate::physics::Scrollbars;
5592 use crate::runtime::Edit;
5593 use crate::{
5594 Button, Container, Flex, Key, Keyed, OverlayPortal, Placement, SingleChildScrollView,
5595 TextField,
5596 };
5597 use frus_core::{Color, Point, Primitive, Rect, Size};
5598
5599 /// **A deep tree on the smallest stack any platform gives a frame.**
5600 ///
5601 /// Building a tree recurses twice over it — the layout walk and the scene walk — and
5602 /// what each level costs is a property of those two functions, not of the widgets.
5603 /// Two `Theme` copies in each frame made that cost 53 kilobytes a level, so a tree
5604 /// forty deep exhausted the megabyte a Windows main thread gets and the process died
5605 /// with no panic and no message. A real screen nests further than forty.
5606 ///
5607 /// One megabyte and 64 levels is what this holds to: the platform's smallest stack,
5608 /// against a tree deeper than a screen builds and half again the depth that used to
5609 /// die. It is deliberately not the deepest that fits — around 128 now, at about seven
5610 /// kilobytes a level — because a tripwire wants margin over the number it guards. An
5611 /// overflow aborts rather than fails, so this one kills the run outright, which is the
5612 /// right noise for what it watches.
5613 #[test]
5614 fn a_deep_tree_builds_on_a_platform_s_smallest_stack() {
5615 let built = std::thread::Builder::new()
5616 .stack_size(1024 * 1024)
5617 .spawn(|| {
5618 let mut node: Box<dyn Widget<Msg>> = Box::new(crate::Text::new("the bottom"));
5619 for _ in 0..64 {
5620 node = Box::new(Container::new().padding(1.0).child(node));
5621 }
5622 let ui = build_ui(
5623 node.as_ref(),
5624 Size::new(800.0, 600.0),
5625 &Runtime::default(),
5626 &Theme::default(),
5627 );
5628 ui.scene().primitives().len()
5629 })
5630 .expect("spawning the deep-tree thread")
5631 .join()
5632 .expect("the deep tree was built");
5633 assert!(built > 0, "and it painted something");
5634 }
5635
5636 #[derive(Clone, Debug, PartialEq)]
5637 enum Msg {
5638 A,
5639 B,
5640 C,
5641 D,
5642 Edited(String),
5643 }
5644
5645 #[test]
5646 fn semantics_tree_carries_roles_and_labels() {
5647 use crate::{Button, Checkbox, Role, Text};
5648 let rt = Runtime::default();
5649 let tree = crate::Flex::column()
5650 .child(Text::new("Titre"))
5651 .child(Button::new("Submit").on_press(Msg::A))
5652 .child(Checkbox::new(true).on_toggle(|_| Msg::B));
5653 let ui = build_ui(&tree, Size::new(300.0, 200.0), &rt, &Theme::default());
5654 let sem = ui.semantics();
5655 // One node per meaningful widget (the containing Flex is skipped).
5656 let roles: Vec<Role> = sem.iter().map(|(_, _, s)| s.role).collect();
5657 assert!(roles.contains(&Role::Label));
5658 assert!(roles.contains(&Role::Button));
5659 assert!(roles.contains(&Role::CheckBox));
5660 // The button carries its label and is actionable.
5661 let button = sem.iter().find(|(_, _, s)| s.role == Role::Button).unwrap();
5662 assert_eq!(button.2.label.as_deref(), Some("Submit"));
5663 assert!(button.2.clickable);
5664 // The checked box mirrors its state.
5665 let check = sem
5666 .iter()
5667 .find(|(_, _, s)| s.role == Role::CheckBox)
5668 .unwrap();
5669 assert_eq!(check.2.toggled, crate::Toggled::True);
5670 }
5671
5672 fn clickable_sample() -> Flex<Msg> {
5673 Flex::row()
5674 .width(400.0)
5675 .height(100.0)
5676 .padding(10.0)
5677 .gap(8.0)
5678 .child(
5679 Container::new()
5680 .width(120.0)
5681 .color(Color::rgb(1.0, 0.0, 0.0))
5682 .hover_color(Color::rgb(0.0, 1.0, 0.0))
5683 .on_click(Msg::A),
5684 )
5685 .child(
5686 Container::new()
5687 .flex(1.0)
5688 .color(Color::rgb(0.0, 0.0, 1.0))
5689 .on_click(Msg::B),
5690 )
5691 }
5692
5693 /// A box whose children ran past it is **marked on the screen**, over a tenth of it,
5694 /// on the edge they ran past — and a box whose children fit is left alone.
5695 #[test]
5696 fn an_overflow_paints_a_band_on_the_edge_it_ran_past() {
5697 use crate::{Container, Flex};
5698 // Two children, because a row with one is not dividing a line up — it hands its
5699 // constraints down, and a lone child is bounded by them rather than overflowing.
5700 let row = |child_width: f32| {
5701 let root: Flex<()> = Flex::row()
5702 .width(100.0)
5703 .height(40.0)
5704 .child(
5705 Container::new()
5706 .width(child_width)
5707 .height(20.0)
5708 .color(Color::WHITE),
5709 )
5710 .child(Container::new().width(20.0).height(20.0));
5711 let rt = crate::runtime::Runtime::default();
5712 build_ui(
5713 &root,
5714 Size::new(200.0, 100.0),
5715 &rt,
5716 &crate::theme::Theme::dark(),
5717 )
5718 };
5719 // The stripes are paths; nothing else in this tree draws one.
5720 let stripes = |ui: &Ui<()>| {
5721 ui.scene()
5722 .primitives()
5723 .iter()
5724 .filter(|p| matches!(p, Primitive::Path { .. }))
5725 .count()
5726 };
5727 let over = row(300.0);
5728 assert!(!over.overflows().is_empty(), "the fixture has to overflow");
5729 assert!(stripes(&over) > 2, "striped: {}", stripes(&over));
5730 // The band is on the right-hand tenth: 90..100 of a 100-wide row.
5731 let band = over
5732 .scene()
5733 .primitives()
5734 .iter()
5735 .rev()
5736 .find_map(|p| match p {
5737 Primitive::Rect { rect, color, .. } if color.a > 0.7 && color.r == 0.0 => {
5738 Some(*rect)
5739 }
5740 _ => None,
5741 })
5742 .expect("the band under the stripes");
5743 assert!((band.x - 90.0).abs() < 0.5, "at the right edge: {band:?}");
5744 assert!((band.width - 10.0).abs() < 0.5, "a tenth of it: {band:?}");
5745
5746 let fits = row(40.0);
5747 assert!(fits.overflows().is_empty(), "the fixture has to fit");
5748 assert!(fits.overflows().is_empty());
5749 assert_eq!(stripes(&fits), 0, "nothing to mark");
5750 }
5751
5752 /// The band says **which edge and by how much**, in the reference's words. A striped
5753 /// edge tells you something is too big; the sentence tells you what to look for, and
5754 /// it is the only half of the report that survives being photographed off a device.
5755 #[test]
5756 fn a_band_writes_which_edge_overflowed_and_by_how_much() {
5757 use crate::{Container, Flex};
5758 let root: Flex<()> = Flex::row()
5759 .width(100.0)
5760 .height(40.0)
5761 .child(
5762 Container::new()
5763 .width(300.0)
5764 .height(20.0)
5765 .color(Color::WHITE),
5766 )
5767 .child(Container::new().width(20.0).height(20.0));
5768 let ui = build_ui(
5769 &root,
5770 Size::new(200.0, 100.0),
5771 &Runtime::default(),
5772 &Theme::dark(),
5773 );
5774 // The label is inside the rotated layer, which is why the search is recursive.
5775 fn labels(primitives: &[Primitive], out: &mut Vec<String>) {
5776 for p in primitives {
5777 match p {
5778 Primitive::Text { text, .. } => out.push(text.clone()),
5779 Primitive::Layer { primitives, .. } => labels(primitives, out),
5780 _ => {}
5781 }
5782 }
5783 }
5784 let mut found = Vec::new();
5785 labels(ui.scene().primitives(), &mut found);
5786 assert!(
5787 found.iter().any(|t| t == "RIGHT OVERFLOWED BY 220 PIXELS"),
5788 "the sentence, in the reference's words: {found:?}"
5789 );
5790 }
5791
5792 /// The reference's precision rule: whole pixels when there are many, a decimal when
5793 /// there are few, three significant figures when there is almost nothing — which is
5794 /// the difference between a layout bug and a rounding error.
5795 #[test]
5796 fn an_overflow_is_written_to_the_precision_it_deserves() {
5797 assert_eq!(format_pixels(86.0), "86");
5798 // Past ten pixels the decimal is noise; at ten exactly it is not yet.
5799 assert_eq!(format_pixels(10.4), "10");
5800 assert_eq!(format_pixels(10.0), "10.0");
5801 assert_eq!(format_pixels(4.5), "4.5");
5802 assert_eq!(format_pixels(1.0), "1.00");
5803 assert_eq!(format_pixels(0.5), "0.500");
5804 assert_eq!(format_pixels(0.0123), "0.0123");
5805 }
5806
5807 #[test]
5808 fn rtl_mirrors_row_horizontally() {
5809 let size = Size::new(400.0, 100.0);
5810 // A = a fixed 120 px button (on the left in LTR), B = the flexible remainder.
5811 let ltr = build_ui(
5812 &clickable_sample(),
5813 size,
5814 &Runtime::default(),
5815 &Theme::default(),
5816 );
5817 let rtl = build_ui(
5818 &clickable_sample(),
5819 size,
5820 &Runtime::default(),
5821 &Theme::default().rtl(),
5822 );
5823 let hit = |ui: &Ui<Msg>, x: f32| ui.hit(Point::new(x, 50.0)).and_then(|id| ui.msg_for(id));
5824
5825 // LTR: button A takes the left edge.
5826 assert_eq!(hit(<r, 40.0), Some(Msg::A));
5827 assert_eq!(hit(<r, 360.0), Some(Msg::B));
5828 // RTL: everything is mirrored — A moves to the right, B takes the left.
5829 assert_eq!(hit(&rtl, 360.0), Some(Msg::A), "A on the right in RTL");
5830 assert_eq!(hit(&rtl, 40.0), Some(Msg::B), "B on the left in RTL");
5831 }
5832
5833 #[test]
5834 fn wrapped_text_wraps_in_layout_and_invalidates_the_cache() {
5835 let tree = |text: &str| {
5836 crate::Flex::column()
5837 .width(120.0)
5838 .child(crate::Text::new(text).wrap())
5839 .child(Container::new().height(10.0).on_click(Msg::A))
5840 };
5841 // Position of the clickable follower: the first y hit while sweeping.
5842 let follower_y = |ui: &Ui<Msg>| {
5843 (0..600)
5844 .map(|y| y as f32)
5845 .find(|&y| ui.hit(Point::new(60.0, y)).is_some())
5846 .expect("suiveur cliquable")
5847 };
5848
5849 let rt = Runtime::default();
5850 let long = "a rather long paragraph that will wrap onto several lines";
5851 let ui = build_ui(&tree(long), Size::new(120.0, 600.0), &rt, &Theme::default());
5852
5853 // The paragraph's render carries its wrap width (≤ the column).
5854 let max_w = ui
5855 .scene()
5856 .primitives()
5857 .iter()
5858 .find_map(|p| match p {
5859 Primitive::Text { max_width, .. } => *max_width,
5860 _ => None,
5861 })
5862 .expect("a wrapped paragraph");
5863 assert!(max_w <= 120.5, "wrapped to the column width: {max_w}");
5864
5865 // The wrapped text takes several lines: the follower is pushed down.
5866 let y_long = follower_y(&ui);
5867 assert!(
5868 y_long > 30.0,
5869 "the follower is pushed down by the wrap: {y_long}"
5870 );
5871
5872 // The SAME structure/styles, different content, the SAME runtime (a warm cache): the
5873 // measure key must invalidate the cache — otherwise the rects would be stale.
5874 let ui2 = build_ui(
5875 &tree("short"),
5876 Size::new(120.0, 600.0),
5877 &rt,
5878 &Theme::default(),
5879 );
5880 let y_short = follower_y(&ui2);
5881 assert!(
5882 y_short < y_long,
5883 "shorter content → a higher follower (the cache was invalidated): {y_short} vs {y_long}"
5884 );
5885 }
5886
5887 #[test]
5888 fn relayout_cache_reuses_the_root_layout_across_frames() {
5889 let rt = Runtime::default();
5890 let size = Size::new(400.0, 100.0);
5891 // Frame 1: nothing cached → a recomputation (at least the root).
5892 let _ = build_ui(&clickable_sample(), size, &rt, &Theme::default());
5893 let (hits1, misses1) = rt.layout_cache.borrow().last_frame_stats();
5894 assert_eq!(hits1, 0, "1st frame: nothing reused");
5895 assert!(misses1 >= 1, "1st frame: at least one computation");
5896
5897 // Frame 2: the same tree, the same constraints → the root is reused.
5898 let _ = build_ui(&clickable_sample(), size, &rt, &Theme::default());
5899 let (hits2, misses2) = rt.layout_cache.borrow().last_frame_stats();
5900 assert_eq!(hits2, 1, "2nd frame: the root is reused");
5901 assert_eq!(misses2, 0, "2nd frame: nothing recomputed");
5902
5903 // Frame 3: the window is resized → the constraints change → a recomputation.
5904 let _ = build_ui(
5905 &clickable_sample(),
5906 Size::new(500.0, 100.0),
5907 &rt,
5908 &Theme::default(),
5909 );
5910 let (hits3, misses3) = rt.layout_cache.borrow().last_frame_stats();
5911 assert_eq!((hits3, misses3), (0, 1), "redimensionnement → recalcul");
5912 }
5913
5914 #[test]
5915 fn modal_traps_tab_arrows_and_pointer_focus() {
5916 use crate::portal::{OverlayPortal, Placement};
5917 // The background: two focusable buttons; the open modal: two buttons as well.
5918 let dialog = Flex::<Msg>::row()
5919 .child(Button::new("ok").on_press(Msg::C))
5920 .child(Button::new("no").on_press(Msg::D));
5921 let tree: Flex<Msg> = Flex::column()
5922 .child(Button::new("bg1").on_press(Msg::A))
5923 .child(Button::new("bg2").on_press(Msg::B))
5924 .child(
5925 OverlayPortal::new(Container::new())
5926 .overlay(dialog, Placement::Center)
5927 .dismiss(Msg::A),
5928 );
5929 let ui = build_ui(
5930 &tree,
5931 Size::new(400.0, 300.0),
5932 &Runtime::default(),
5933 &Theme::default(),
5934 );
5935
5936 // Tab enters the trap (the modal's first focusable) and cycles inside it.
5937 let first = ui.focus_next(None, true).expect("the scope's first");
5938 assert_eq!(ui.msg_for(first), Some(Msg::C));
5939 let second = ui.focus_next(Some(first), true).expect("suivant");
5940 assert_eq!(ui.msg_for(second), Some(Msg::D));
5941 let wrapped = ui.focus_next(Some(second), true).expect("boucle");
5942 assert_eq!(
5943 ui.msg_for(wrapped),
5944 Some(Msg::C),
5945 "Tab cycles inside the modal"
5946 );
5947
5948 // The arrows stay inside the scope: there is nothing above the dialog.
5949 assert_eq!(ui.focus_directional(first, FocusDirection::Up), None);
5950 let right = ui
5951 .focus_directional(first, FocusDirection::Right)
5952 .expect("to the right");
5953 assert_eq!(ui.msg_for(right), Some(Msg::D));
5954
5955 // Click-to-focus ignores the background (the bg1 button is at the top left).
5956 assert_eq!(
5957 ui.focus_hit(Point::new(10.0, 10.0)),
5958 None,
5959 "fond hors scope"
5960 );
5961
5962 // Without a modal: no trap, Tab starts at the background.
5963 let open_less: Flex<Msg> = Flex::column()
5964 .child(Button::new("bg1").on_press(Msg::A))
5965 .child(Button::new("bg2").on_press(Msg::B));
5966 let ui2 = build_ui(
5967 &open_less,
5968 Size::new(400.0, 300.0),
5969 &Runtime::default(),
5970 &Theme::default(),
5971 );
5972 let f = ui2.focus_next(None, true).expect("premier");
5973 assert_eq!(ui2.msg_for(f), Some(Msg::A));
5974 }
5975
5976 #[test]
5977 fn open_menu_traps_focus_in_its_items() {
5978 use crate::PopupMenuButton;
5979 // A focusable background plus an **open** menu (anchor + two items).
5980 let menu = PopupMenuButton::new(Button::new("open").on_press(Msg::A), true, Msg::D)
5981 .item("one", Msg::B)
5982 .item("two", Msg::C);
5983 let tree: Flex<Msg> = Flex::column()
5984 .child(Button::new("bg").on_press(Msg::A))
5985 .child(menu);
5986 let ui = build_ui(
5987 &tree,
5988 Size::new(400.0, 300.0),
5989 &Runtime::default(),
5990 &Theme::default(),
5991 );
5992
5993 // Tab enters the menu's items and **cycles** inside (background and anchor are out of scope).
5994 let first = ui.focus_next(None, true).expect("the scope's first");
5995 assert_eq!(ui.msg_for(first), Some(Msg::B));
5996 let second = ui.focus_next(Some(first), true).expect("suivant");
5997 assert_eq!(ui.msg_for(second), Some(Msg::C));
5998 let wrapped = ui.focus_next(Some(second), true).expect("boucle");
5999 assert_eq!(
6000 ui.msg_for(wrapped),
6001 Some(Msg::B),
6002 "Tab cycles inside the open menu"
6003 );
6004 // The background (top left) is out of scope while the menu is open.
6005 assert_eq!(
6006 ui.focus_hit(Point::new(10.0, 10.0)),
6007 None,
6008 "fond hors scope"
6009 );
6010
6011 // The menu **closed**: no trap, Tab starts at the background.
6012 let closed: Flex<Msg> = Flex::column()
6013 .child(Button::new("bg").on_press(Msg::A))
6014 .child(PopupMenuButton::new(
6015 Button::new("open").on_press(Msg::B),
6016 false,
6017 Msg::D,
6018 ));
6019 let ui2 = build_ui(
6020 &closed,
6021 Size::new(400.0, 300.0),
6022 &Runtime::default(),
6023 &Theme::default(),
6024 );
6025 let f = ui2.focus_next(None, true).expect("premier");
6026 assert_eq!(ui2.msg_for(f), Some(Msg::A), "with no open menu, no trap");
6027 }
6028
6029 #[test]
6030 fn escape_infrastructure_finds_path_and_topmost_dismiss() {
6031 use crate::portal::{OverlayPortal, Placement};
6032 // An open portal (a modal with a dismissal) around a clickable anchor.
6033 let anchor = Container::<Msg>::new()
6034 .width(50.0)
6035 .height(30.0)
6036 .on_click(Msg::A);
6037 let content = Container::<Msg>::new()
6038 .width(80.0)
6039 .height(40.0)
6040 .on_click(Msg::B);
6041 let portal = OverlayPortal::new(anchor)
6042 .overlay(content, Placement::Center)
6043 .dismiss(Msg::C);
6044 let tree: Flex<Msg> = Flex::column().child(portal);
6045
6046 let ui = build_ui(
6047 &tree,
6048 Size::new(300.0, 200.0),
6049 &Runtime::default(),
6050 &Theme::default(),
6051 );
6052 // The topmost dismissal is the portal's.
6053 assert_eq!(ui.top_dismiss(), Some(Msg::C));
6054
6055 // Focus "inside the dialog": the root→content path goes through the portal, which
6056 // consumes Escape while bubbling. (The Center content, 80×40, is centred in 300×200 →
6057 // its centre is at (150, 100).)
6058 let inner = ui
6059 .hit(Point::new(150.0, 100.0))
6060 .expect("contenu de la modale");
6061 let path = find_path(&tree, inner);
6062 assert!(path.len() >= 3, "root, portal, content: {}", path.len());
6063 assert_eq!(
6064 path.last().unwrap().on_click(),
6065 Some(Msg::B),
6066 "the target closes the path"
6067 );
6068 let consumed = path
6069 .iter()
6070 .rev()
6071 .find_map(|w| match w.on_key(&crate::Key::Escape) {
6072 crate::KeyResponse::Handled(msg) => Some(msg),
6073 _ => None,
6074 });
6075 assert_eq!(
6076 consumed,
6077 Some(Some(Msg::C)),
6078 "the portal consumes Escape while bubbling"
6079 );
6080
6081 // Chemin introuvable → vide ; pas d'overlay → pas de fermeture.
6082 assert!(find_path(&tree, WidgetId::ROOT.child(99)).is_empty());
6083 let closed: Flex<Msg> = Flex::column().child(Container::new().on_click(Msg::A));
6084 let ui2 = build_ui(
6085 &closed,
6086 Size::new(300.0, 200.0),
6087 &Runtime::default(),
6088 &Theme::default(),
6089 );
6090 assert_eq!(ui2.top_dismiss(), None);
6091 }
6092
6093 #[test]
6094 fn long_press_targets_are_collected_topmost_first() {
6095 // A long-press container holding a long-press child: a point inside the child returns
6096 // the child's message (the topmost one).
6097 let tree: Container<Msg> = Container::new()
6098 .width(200.0)
6099 .height(100.0)
6100 .on_long_press(Msg::A)
6101 .child(
6102 Container::new()
6103 .width(50.0)
6104 .height(50.0)
6105 .on_long_press(Msg::B),
6106 );
6107 let ui = build_ui(
6108 &tree,
6109 Size::new(200.0, 100.0),
6110 &Runtime::default(),
6111 &Theme::default(),
6112 );
6113 assert_eq!(ui.long_press_at(Point::new(25.0, 25.0)), Some(Msg::B));
6114 assert_eq!(ui.long_press_at(Point::new(150.0, 80.0)), Some(Msg::A));
6115 assert_eq!(ui.long_press_at(Point::new(500.0, 500.0)), None);
6116 }
6117
6118 #[test]
6119 fn hit_and_msg_for_route_correctly() {
6120 let rt = Runtime::default();
6121 let ui = build_ui(
6122 &clickable_sample(),
6123 Size::new(400.0, 100.0),
6124 &rt,
6125 &Theme::default(),
6126 );
6127 let id_a = ui.hit(Point::new(50.0, 50.0)).expect("A");
6128 let id_b = ui.hit(Point::new(300.0, 50.0)).expect("B");
6129 assert_ne!(id_a, id_b);
6130 assert_eq!(ui.msg_for(id_a), Some(Msg::A));
6131 assert_eq!(ui.msg_for(id_b), Some(Msg::B));
6132 assert_eq!(ui.hit(Point::new(3.0, 3.0)), None);
6133 }
6134
6135 /// Every rectangle a frame paints down the right-hand edge of a 200-wide viewport.
6136 /// The bar is drawn last, over the content, and it is the only thing over there.
6137 fn painted(ui: &Ui<()>) -> Vec<(Rect, Color)> {
6138 ui.scene()
6139 .primitives()
6140 .iter()
6141 .filter_map(|p| match p {
6142 Primitive::Rect { rect, color, .. }
6143 if rect.x >= 200.0 - BAR_SIZE - BAR_MARGIN * 2.0 =>
6144 {
6145 Some((*rect, *color))
6146 }
6147 _ => None,
6148 })
6149 .collect()
6150 }
6151
6152 /// A tall page in a short window, its bar **pinned**: whether a bar fades is the
6153 /// business of the tests below, and these are about whether there is one to fade.
6154 fn bars(area: SingleChildScrollView<()>, how: Scrollbars) -> Vec<(Rect, Color)> {
6155 let runtime = Runtime::with_scrollbars(how);
6156 let ui = build_ui(
6157 &area.thumb_visibility(true),
6158 Size::new(200.0, 100.0),
6159 &runtime,
6160 &Theme::dark(),
6161 );
6162 painted(&ui)
6163 }
6164
6165 /// A tall column in a short viewport: something to scroll.
6166 fn tall() -> SingleChildScrollView<()> {
6167 SingleChildScrollView::new()
6168 .width(200.0)
6169 .height(100.0)
6170 .child(Container::<()>::new().width(200.0).height(1000.0))
6171 }
6172
6173 /// **A touch screen gets no scrollbar** (`app.dart:870`).
6174 ///
6175 /// This drew one on every platform, on both axes, always — a permanent bar over the
6176 /// content of every scrolling page on a phone, where the reference draws nothing at
6177 /// all. A finger already knows where it is on the page.
6178 #[test]
6179 fn a_touch_screen_gets_no_scrollbar() {
6180 assert_eq!(bars(tall(), Scrollbars::Never), vec![]);
6181 assert_eq!(
6182 bars(tall(), Scrollbars::Always).len(),
6183 1,
6184 "and a desktop gets one"
6185 );
6186
6187 // Nothing to drag, either: a bar that is not drawn is not a target.
6188 let runtime = Runtime::with_scrollbars(Scrollbars::Never);
6189 let ui = build_ui(&tall(), Size::new(200.0, 100.0), &runtime, &Theme::dark());
6190 assert!(ui.scrollbars().is_empty());
6191 }
6192
6193 /// **A bar takes the scheme's word for the brightness, not the surface's luminance.**
6194 ///
6195 /// The opacities either side of this are three times apart at rest (`scrollbar.dart:242`,
6196 /// `:248`), so getting the question wrong is not a shade — it is a bar that reads as
6197 /// a smudge, or one that reads as a stripe.
6198 ///
6199 /// A **dimmed light** scheme is where the two answers part. The measurement it used
6200 /// to make says dark; the scheme says light, and the scheme is the one that knows.
6201 #[test]
6202 fn a_bar_reads_the_scheme_s_brightness_and_not_its_luminance() {
6203 use crate::media::Brightness;
6204 let dimmed = |brightness| {
6205 let mut theme = Theme::light();
6206 theme.scheme.surface = Color::rgb8(110, 110, 110);
6207 theme.scheme.brightness = brightness;
6208 theme
6209 };
6210 let thumb = |theme: &Theme| {
6211 let runtime = Runtime::with_scrollbars(Scrollbars::Always);
6212 let ui = build_ui(
6213 &tall().thumb_visibility(true),
6214 Size::new(200.0, 100.0),
6215 &runtime,
6216 theme,
6217 );
6218 painted(&ui)[0].1
6219 };
6220
6221 let light = dimmed(Brightness::Light);
6222 assert!(
6223 light.scheme.surface.compute_luminance() < 0.5,
6224 "the luminance the old line measured says dark"
6225 );
6226 assert_eq!(
6227 thumb(&light),
6228 light.scheme.on_surface.fade(THUMB_ON_LIGHT),
6229 "and the bar takes the scheme's word instead"
6230 );
6231
6232 let dark = dimmed(Brightness::Dark);
6233 assert_eq!(thumb(&dark), dark.scheme.on_surface.fade(THUMB_ON_DARK));
6234 }
6235
6236 /// **A theme reaches every number the bar is made of**, not only its colour. A
6237 /// scrollbar themed in its colour and sized from a constant is themed in the half
6238 /// that shows and unthemed in the half a pointer has to hit.
6239 ///
6240 /// There is no per-call override to outrank here — nobody configures a scrollbar at a
6241 /// call site, the walk draws it — so the chain is `theme ?? framework`, and the second
6242 /// half of this test is that the framework still answers where the theme says nothing.
6243 #[test]
6244 fn a_theme_reaches_every_number_the_scrollbar_is_made_of() {
6245 const THUMB: Color = Color::rgb(0.2, 0.7, 0.9);
6246 let mut theme = Theme::dark();
6247 theme.widgets.scrollbar.thickness = Some(14.0);
6248 theme.widgets.scrollbar.margin = Some(5.0);
6249 theme.widgets.scrollbar.min_thumb_length = Some(90.0);
6250 theme.widgets.scrollbar.radius = Some(2.0);
6251 theme.widgets.scrollbar.thumb_color = Some(THUMB);
6252 theme.widgets.scrollbar.opacity = Some(0.8);
6253
6254 let runtime = Runtime::with_scrollbars(Scrollbars::Always);
6255 let ui = build_ui(
6256 &tall().thumb_visibility(true),
6257 Size::new(200.0, 100.0),
6258 &runtime,
6259 &theme,
6260 );
6261 // `painted` filters on the strip a bar of the **default** thickness occupies, so
6262 // it cannot see a themed one — which is the point of the theme. This asks for the
6263 // strip the themed bar is in.
6264 let over_there: Vec<(Rect, Color)> = ui
6265 .scene()
6266 .primitives()
6267 .iter()
6268 .filter_map(|p| match p {
6269 Primitive::Rect { rect, color, .. } if rect.x >= 200.0 - 14.0 - 5.0 * 2.0 => {
6270 Some((*rect, *color))
6271 }
6272 _ => None,
6273 })
6274 .collect();
6275 assert_eq!(over_there.len(), 1, "still one rectangle: the thumb");
6276 let (thumb, color) = over_there[0];
6277 assert!(
6278 (thumb.width - 14.0).abs() < 0.01,
6279 "the theme's thickness: {thumb:?}"
6280 );
6281 assert!(
6282 ((thumb.x + thumb.width) - (200.0 - 5.0)).abs() < 0.01,
6283 "held clear by the theme's margin, not shrunk to fit: {thumb:?}"
6284 );
6285 assert!(
6286 thumb.height >= 90.0,
6287 "and never shorter than the theme's floor: {thumb:?}"
6288 );
6289 assert_eq!(
6290 color,
6291 THUMB.fade(0.8),
6292 "the theme's colour, faded to the theme's resting level"
6293 );
6294
6295 // The radius is not in `painted`, which reads the rectangles' colours; take it
6296 // from the primitive itself. A pill is what an unthemed bar is, and this one is
6297 // not a pill.
6298 let radius = ui.scene().primitives().iter().find_map(|p| match p {
6299 Primitive::Rect { radius, .. } if radius.top_left > 0.0 => Some(radius.top_left),
6300 _ => None,
6301 });
6302 assert_eq!(radius, Some(2.0), "the theme's corner radius");
6303
6304 // And with a theme that says nothing, the framework still answers.
6305 let plain = bars(tall(), Scrollbars::Always);
6306 let (thumb, color) = plain[0];
6307 assert!((thumb.width - BAR_SIZE).abs() < 0.01);
6308 assert_eq!(color, Theme::dark().scheme.on_surface.fade(THUMB_ON_DARK));
6309 }
6310
6311 /// **And it is a thumb and nothing else** (`scrollbar.dart:281`): the reference's
6312 /// track is transparent unless a caller asks for one, and even then it is 3 to 5 %.
6313 /// This painted an 18 % stripe down every scrolling page, under a 55 % thumb.
6314 #[test]
6315 fn a_bar_is_a_thumb_and_no_track() {
6316 let painted = bars(tall(), Scrollbars::Always);
6317 assert_eq!(painted.len(), 1, "one rectangle: the thumb");
6318 let (thumb, color) = painted[0];
6319 assert!(
6320 (thumb.width - BAR_SIZE).abs() < 0.01,
6321 "at the reference's thickness: {thumb:?}"
6322 );
6323 assert!(
6324 ((thumb.x + thumb.width) - (200.0 - BAR_MARGIN)).abs() < 0.01,
6325 "and clear of the edge by `crossAxisMargin`, not shrunk to fit: {thumb:?}"
6326 );
6327 assert!(
6328 thumb.height >= MIN_THUMB,
6329 "and never shorter than a pointer can catch: {thumb:?}"
6330 );
6331 let theme = Theme::dark();
6332 assert_eq!(color, theme.scheme.on_surface.fade(THUMB_ON_DARK));
6333 }
6334
6335 /// **Along the horizontal axis, never** (`app.dart:861`) — on any platform, under any
6336 /// setting. A sideways strip is scrolled by whatever made it sideways, and a bar
6337 /// under it takes room from content that is short of it already.
6338 #[test]
6339 fn a_sideways_strip_never_gets_one() {
6340 let strip = SingleChildScrollView::new()
6341 .axis(crate::scroll::Axis::Horizontal)
6342 .width(200.0)
6343 .height(100.0)
6344 .child(Container::<()>::new().width(1000.0).height(100.0));
6345 let runtime = Runtime::with_scrollbars(Scrollbars::Always);
6346 let ui = build_ui(&strip, Size::new(200.0, 100.0), &runtime, &Theme::dark());
6347 assert!(
6348 ui.scrollbars().is_empty(),
6349 "a desktop draws none across, either"
6350 );
6351 }
6352
6353 /// And **one area may answer for itself**, over the application's answer — the
6354 /// per-area say the physics beside it already had.
6355 #[test]
6356 fn an_area_may_ask_for_its_own_answer() {
6357 assert_eq!(
6358 bars(tall().scrollbars(Scrollbars::Always), Scrollbars::Never).len(),
6359 1,
6360 "asked for one where the platform gives none"
6361 );
6362 assert_eq!(
6363 bars(tall().scrollbars(Scrollbars::Never), Scrollbars::Always),
6364 vec![],
6365 "and refused one where it does"
6366 );
6367 }
6368
6369 /// **A bar nobody has scrolled is not there at all.** The reference's fade starts
6370 /// closed and is opened only by movement (`scrollbar.dart:1960`); a list that has
6371 /// just appeared shows its content, not its furniture.
6372 ///
6373 /// It is **registered** even so, which a bar the platform does not draw is not. The
6374 /// difference is real: that one is not there, and this one is here and invisible —
6375 /// and a mouse coming near it is the only thing that can bring it back.
6376 #[test]
6377 fn an_untouched_area_shows_no_bar_but_can_still_be_reached_for() {
6378 let runtime = Runtime::with_scrollbars(Scrollbars::Always);
6379 let ui = build_ui(&tall(), Size::new(200.0, 100.0), &runtime, &Theme::dark());
6380 assert_eq!(painted(&ui), vec![], "nothing is painted for it");
6381
6382 let bar = *ui.scrollbars().first().expect("registered all the same");
6383 assert_eq!(bar.opacity, 0.0);
6384 let on_it = Point::new(194.0, 20.0);
6385 assert!(
6386 ui.scrollbar_at(on_it).is_none(),
6387 "and a bar at zero cannot be grabbed"
6388 );
6389 assert!(
6390 ui.scrollbar_near(on_it).is_some(),
6391 "but it can be reached for, which is what brings it back"
6392 );
6393 // The reach is wider than the bar: 8 pixels at a tenth of an opacity is not
6394 // something a hand aims at precisely.
6395 assert!(
6396 ui.scrollbar_near(Point::new(178.0, 20.0)).is_some(),
6397 "the reach takes in a tap target's room around the thumb: {:?}",
6398 bar.reach
6399 );
6400 }
6401
6402 /// **A pinned bar does not fade** (`scrollbar.dart:214`) — for an area whose content
6403 /// does not look scrollable, or one a reader should be able to aim at without
6404 /// scrolling first to make the bar appear.
6405 #[test]
6406 fn a_pinned_bar_does_not_fade() {
6407 let runtime = Runtime::with_scrollbars(Scrollbars::Always);
6408 let ui = build_ui(
6409 &tall().thumb_visibility(true),
6410 Size::new(200.0, 100.0),
6411 &runtime,
6412 &Theme::dark(),
6413 );
6414 assert_eq!(painted(&ui).len(), 1, "there without being scrolled to");
6415 assert_eq!(ui.scrollbars()[0].opacity, 1.0);
6416 }
6417
6418 /// **A thumb under a pointer, and a thumb in a hand**, are two different colours
6419 /// (`scrollbar.dart:245`, `:244`) — and neither is the resting one. The dragged
6420 /// value arrives at once: the hand is already on it.
6421 #[test]
6422 fn a_thumb_answers_the_pointer() {
6423 use crate::runtime::ScrollbarFade;
6424 let build = |prime: &dyn Fn(&mut Runtime)| {
6425 let mut runtime = Runtime::with_scrollbars(Scrollbars::Always);
6426 prime(&mut runtime);
6427 let ui = build_ui(
6428 &tall().thumb_visibility(true),
6429 Size::new(200.0, 100.0),
6430 &runtime,
6431 &Theme::dark(),
6432 );
6433 let id = ui.scrollbars()[0].id;
6434 (id, painted(&ui)[0].1)
6435 };
6436
6437 let theme = Theme::dark();
6438 let (id, resting) = build(&|_| {});
6439 assert_eq!(resting, theme.scheme.on_surface.fade(THUMB_ON_DARK));
6440
6441 let (_, warmed) = build(&|rt| {
6442 rt.scrollbar_fade.insert(
6443 id,
6444 ScrollbarFade {
6445 opacity: 1.0,
6446 hover: 1.0,
6447 ..ScrollbarFade::default()
6448 },
6449 );
6450 });
6451 assert_eq!(warmed, theme.scheme.on_surface.fade(THUMB_HOVER_ON_DARK));
6452
6453 let (_, grabbed) = build(&|rt| rt.scrollbar_dragged = Some(id));
6454 assert_eq!(grabbed, theme.scheme.on_surface.fade(THUMB_DRAG_ON_DARK));
6455 }
6456
6457 #[test]
6458 fn hover_progress_interpolates_color() {
6459 let rt = Runtime::default();
6460 let base = build_ui(
6461 &clickable_sample(),
6462 Size::new(400.0, 100.0),
6463 &rt,
6464 &Theme::default(),
6465 );
6466 let id_a = base.hit(Point::new(50.0, 50.0)).unwrap();
6467
6468 // Without any progress: the base colour (red).
6469 if let Primitive::Rect { color, .. } = base.scene().primitives()[0] {
6470 assert_eq!(color, Color::rgb(1.0, 0.0, 0.0));
6471 } else {
6472 panic!("expected a rect");
6473 }
6474
6475 // Full progress: the hover colour (green).
6476 let mut rt = Runtime::default();
6477 rt.input.hovered = Some(id_a);
6478 rt.anims.insert(
6479 id_a,
6480 crate::Anim {
6481 hover: 1.0,
6482 ..Default::default()
6483 },
6484 );
6485 let ui = build_ui(
6486 &clickable_sample(),
6487 Size::new(400.0, 100.0),
6488 &rt,
6489 &Theme::default(),
6490 );
6491 if let Primitive::Rect { color, .. } = ui.scene().primitives()[0] {
6492 assert_eq!(color, Color::rgb(0.0, 1.0, 0.0));
6493 } else {
6494 panic!("expected a rect");
6495 }
6496 }
6497
6498 #[test]
6499 fn multiline_field_registers_as_scrollable_when_overflowing() {
6500 // A multi-line field whose content exceeds `rows` registers itself as a scrollable
6501 // area (with `max_y > 0`) — which is what the wheel and the scrollbar target. A short
6502 // field does not register.
6503 let tall = TextField::<Msg>::new("a\nb\nc\nd\ne\nf")
6504 .on_input(Msg::Edited)
6505 .rows(2)
6506 .width(200.0);
6507 let tree: Flex<Msg> = Flex::column().child(tall);
6508 let rt = Runtime::default();
6509 let ui = build_ui(&tree, Size::new(220.0, 240.0), &rt, &Theme::default());
6510 let maxes = ui.scrollable_maxes();
6511 assert_eq!(maxes.len(), 1, "the overflowing field registers");
6512 assert!(maxes[0].2 > 0.0, "max_y > 0 (overflowing content)");
6513
6514 let short = TextField::<Msg>::new("a\nb")
6515 .on_input(Msg::Edited)
6516 .rows(4)
6517 .width(200.0);
6518 let tree: Flex<Msg> = Flex::column().child(short);
6519 let ui = build_ui(&tree, Size::new(220.0, 240.0), &rt, &Theme::default());
6520 assert!(
6521 ui.scrollable_maxes().is_empty(),
6522 "a short field does not scroll"
6523 );
6524 }
6525
6526 #[test]
6527 fn only_text_inputs_place_a_cursor() {
6528 // The invariant of the click fix (milestone 39): a focusable button returns NO cursor
6529 // (`cursor_at` = None), so the shell does not start a text selection on it and does not
6530 // capture the click. Only text fields place one.
6531 let button = Button::<Msg>::new("x").on_press(Msg::A);
6532 assert_eq!(Widget::<Msg>::cursor_at(&button, 10.0, 5.0, 200.0, 0), None);
6533 let input = TextField::<Msg>::new("hi").on_input(Msg::Edited);
6534 assert!(Widget::<Msg>::cursor_at(&input, 10.0, 5.0, 200.0, 0).is_some());
6535 }
6536
6537 /// A form taller than its window: six buttons of 60 px in a 150 px scroll.
6538 fn long_form() -> SingleChildScrollView<Msg> {
6539 let mut column = Flex::<Msg>::column();
6540 for i in 0..6 {
6541 column = column.child(
6542 Button::new(format!("field {i}"))
6543 .on_press(Msg::A)
6544 .height(60.0),
6545 );
6546 }
6547 SingleChildScrollView::new()
6548 .width(200.0)
6549 .height(150.0)
6550 .child(column)
6551 }
6552
6553 fn form_ui(runtime: &Runtime) -> Ui<Msg> {
6554 build_ui(
6555 &long_form(),
6556 Size::new(200.0, 150.0),
6557 runtime,
6558 &Theme::default(),
6559 )
6560 }
6561
6562 /// **Every** field is a focus stop, not only the two and a half on screen.
6563 ///
6564 /// Registering what the eye can see is how a long form came to have most of its
6565 /// fields unreachable from the keyboard: Tab walked the visible ones and wrapped.
6566 #[test]
6567 fn tab_reaches_the_fields_below_the_fold() {
6568 let ui = form_ui(&Runtime::default());
6569 assert_eq!(ui.focusables.len(), 6, "all six, not the visible ones");
6570 // And the traversal is in tree order, so Tab walks down the form.
6571 let ids: Vec<_> = ui.focusables.iter().map(|f| f.id).collect();
6572 assert_eq!(ui.focus_next(Some(ids[2]), true), Some(ids[3]));
6573 }
6574
6575 /// A stop clipped away by a scroll keeps its **real** box, and an empty visible one.
6576 /// The two are different questions and the registry answers both.
6577 #[test]
6578 fn a_stop_out_of_sight_keeps_its_geometry() {
6579 let ui = form_ui(&Runtime::default());
6580 let fifth = ui.focusables[5];
6581 assert!((fifth.bounds.y - 300.0).abs() < 0.5, "{:?}", fifth.bounds);
6582 assert_eq!(fifth.rect.height, 0.0, "none of it is on screen");
6583 assert!(fifth.scroll.is_some(), "something around it scrolls");
6584 }
6585
6586 /// A click still lands only on what can be seen. The unclipped box of a field
6587 /// scrolled out of sight overlaps the window, and a tap there must not focus it.
6588 #[test]
6589 fn a_click_still_only_reaches_what_is_visible() {
6590 let ui = form_ui(&Runtime::default());
6591 // The second field, in the middle of its own visible box.
6592 let second = ui.focusables[1];
6593 let inside = Point::new(
6594 second.rect.x + second.rect.width / 2.0,
6595 second.rect.y + second.rect.height / 2.0,
6596 );
6597 assert_eq!(ui.focus_hit(inside).map(|(id, _)| id), Some(second.id));
6598 // The sixth field's **real** box overlaps nothing on screen, and a tap where it
6599 // would be if the form were not scrolled hits nothing at all.
6600 let sixth = ui.focusables[5];
6601 let past = Point::new(
6602 sixth.bounds.x + sixth.bounds.width / 2.0,
6603 sixth.bounds.y + sixth.bounds.height / 2.0,
6604 );
6605 assert!(past.y > 150.0, "past the fold: {past:?}");
6606 assert_eq!(ui.focus_hit(past), None);
6607 }
6608
6609 /// Focus lands below the fold and the view follows: the least movement that brings
6610 /// the field's foot to the window's.
6611 #[test]
6612 fn revealing_a_stop_below_the_fold_scrolls_to_it() {
6613 let runtime = Runtime::default();
6614 let ui = form_ui(&runtime);
6615 let area = ui.scroll_regions()[0];
6616 let fifth = ui.focusables[5].id;
6617 let moves = ui.reveal(fifth, &runtime);
6618 // Field 5 sits at 300..360 and the window is 150 tall: its foot reaches the
6619 // window's foot at 360 - 150 = 210.
6620 assert_eq!(moves, vec![(area.id, (0.0, 210.0))]);
6621 }
6622
6623 /// Already in view, nothing moves — the answer is the empty list rather than a
6624 /// zero, so no caller has to ask twice.
6625 #[test]
6626 fn revealing_something_already_in_view_moves_nothing() {
6627 let runtime = Runtime::default();
6628 let ui = form_ui(&runtime);
6629 assert!(ui.reveal(ui.focusables[0].id, &runtime).is_empty());
6630 assert!(ui.reveal(ui.focusables[1].id, &runtime).is_empty());
6631 // And a stop the frame does not contain at all is not an error.
6632 assert!(ui.reveal(WidgetId::from_u64(7), &runtime).is_empty());
6633 }
6634
6635 /// Coming back **up** the form scrolls the other way, and only as far as it must.
6636 #[test]
6637 fn revealing_a_stop_above_the_fold_scrolls_back() {
6638 let mut runtime = Runtime::default();
6639 let area_id = form_ui(&runtime).scroll_regions()[0].id;
6640 runtime.scroll.insert(area_id, (0.0, 210.0));
6641 let ui = form_ui(&runtime);
6642 let first = ui.focusables[0].id;
6643 // Field 0 sits at 0..60: its head reaches the window's head at offset 0.
6644 assert_eq!(ui.reveal(first, &runtime), vec![(area_id, (0.0, 0.0))]);
6645 // At offset 210 the window shows 210..360, so field 4 (240..300) is wholly in
6646 // it and does not move. Field 3 (180..240) hangs over the top edge and would.
6647 assert!(ui.reveal(ui.focusables[4].id, &runtime).is_empty());
6648 assert!(!ui.reveal(ui.focusables[3].id, &runtime).is_empty());
6649 }
6650
6651 /// The arithmetic on its own: the least movement, the leading edge for anything too
6652 /// big to fit, and the clamp at the ends of the travel.
6653 #[test]
6654 fn the_reveal_moves_as_little_as_it_can() {
6655 let area = Scrollable {
6656 id: WidgetId::ROOT,
6657 viewport: Rect::new(0.0, 0.0, 100.0, 100.0),
6658 max_x: 0.0,
6659 max_y: 400.0,
6660 physics: None,
6661 refresh: None,
6662 page: None,
6663 reverse_x: false,
6664 reverse_y: false,
6665 host: None,
6666 keep_visible: None,
6667 };
6668 // Inside: nothing.
6669 assert_eq!(
6670 area.reveal(Rect::new(0.0, 10.0, 50.0, 20.0), (0.0, 0.0)).1,
6671 (0.0, 0.0)
6672 );
6673 // Below: its foot comes to the window's foot, and no further.
6674 assert_eq!(
6675 area.reveal(Rect::new(0.0, 140.0, 50.0, 20.0), (0.0, 0.0)).0,
6676 (0.0, 60.0)
6677 );
6678 // Above: its head comes to the window's head.
6679 assert_eq!(
6680 area.reveal(Rect::new(0.0, -30.0, 50.0, 20.0), (0.0, 50.0))
6681 .0,
6682 (0.0, 20.0)
6683 );
6684 // Taller than the window: the **start** edge wins, because that is what is read.
6685 assert_eq!(
6686 area.reveal(Rect::new(0.0, -10.0, 50.0, 300.0), (0.0, 40.0))
6687 .0,
6688 (0.0, 30.0)
6689 );
6690 // And the travel is a wall: an offset past the end is not asked for.
6691 assert_eq!(
6692 area.reveal(Rect::new(0.0, 900.0, 50.0, 20.0), (0.0, 0.0)).0,
6693 (0.0, 400.0)
6694 );
6695 }
6696
6697 /// A reversed axis numbers its offsets from the far end, so the same movement of the
6698 /// content is the opposite movement of the offset. One function knows that, and the
6699 /// reveal asks it rather than keeping a second opinion.
6700 #[test]
6701 fn the_reveal_follows_a_reversed_axis() {
6702 let area = Scrollable {
6703 id: WidgetId::ROOT,
6704 viewport: Rect::new(0.0, 0.0, 100.0, 100.0),
6705 max_x: 0.0,
6706 max_y: 400.0,
6707 physics: None,
6708 refresh: None,
6709 page: None,
6710 reverse_x: false,
6711 reverse_y: true,
6712 host: None,
6713 keep_visible: None,
6714 };
6715 // A box below the window: the content must move **up** by 60 either way. On a
6716 // reversed axis a rising offset pushes the content down (`content_origin` is
6717 // `viewport - content + offset` there), so the offset comes **down** by 60 to do
6718 // what a plain axis would need it to go up by.
6719 let (offset, shift) = area.reveal(Rect::new(0.0, 140.0, 50.0, 20.0), (0.0, 100.0));
6720 assert_eq!(shift, (0.0, -60.0));
6721 assert_eq!(offset, (0.0, 40.0));
6722 }
6723
6724 /// Centring puts the box in the middle of the window, and the clamp is what keeps
6725 /// the first and last items of a row where they belong rather than half off it.
6726 #[test]
6727 fn centring_is_clamped_to_the_travel() {
6728 let area = Scrollable {
6729 id: WidgetId::ROOT,
6730 viewport: Rect::new(0.0, 0.0, 300.0, 50.0),
6731 max_x: 700.0,
6732 max_y: 0.0,
6733 physics: None,
6734 refresh: None,
6735 page: None,
6736 reverse_x: false,
6737 reverse_y: false,
6738 host: None,
6739 keep_visible: None,
6740 };
6741 // A box at 400..500 has its middle at 450; the window's is at 150.
6742 assert_eq!(
6743 area.centre(Rect::new(400.0, 0.0, 100.0, 50.0), (0.0, 0.0))
6744 .0,
6745 (300.0, 0.0)
6746 );
6747 // The first item cannot be centred without scrolling backwards, so it stays.
6748 assert_eq!(
6749 area.centre(Rect::new(0.0, 0.0, 100.0, 50.0), (0.0, 0.0)).0,
6750 (0.0, 0.0)
6751 );
6752 // Nor the last past the end of the travel.
6753 assert_eq!(
6754 area.centre(Rect::new(900.0, 0.0, 100.0, 50.0), (0.0, 0.0))
6755 .0,
6756 (700.0, 0.0)
6757 );
6758 }
6759
6760 /// **A lone child could widen the box it was given, and take the screen with it.**
6761 ///
6762 /// A flex item's automatic minimum is its **min-content** size, so agreeing to shrink
6763 /// buys nothing on its own: the item shrinks willingly and then refuses to go below
6764 /// the widest thing inside it. A column holding one over-wide row came out at that
6765 /// row's width; the padded box around it came out at the same; and every sibling,
6766 /// stretched across the column, came out at the same again — a whole screen laid out
6767 /// to the width of its widest line and then clipped by the window.
6768 ///
6769 /// Reported from a real application: a shop whose banner, search field, filter row
6770 /// and product grid were all cut off at the same edge, because one segmented control
6771 /// four segments wide had decided how wide the screen was.
6772 ///
6773 /// The over-wide child still overflows — that is the reference's behaviour and the
6774 /// honest one — but it no longer drags its parent and its siblings out with it.
6775 #[test]
6776 fn a_lone_child_cannot_widen_the_box_it_was_given() {
6777 let ordinary = Color::rgb(0.1, 0.2, 0.3);
6778 let overwide = Color::rgb(0.9, 0.8, 0.7);
6779 let screen: SingleChildScrollView<Msg> = SingleChildScrollView::new().width(300.0).child(
6780 Container::new().padding(24.0).child(
6781 Flex::column()
6782 .align(frus_layout::Align::Stretch)
6783 .child(Container::new().height(20.0).color(ordinary))
6784 .child(Container::new().width(600.0).height(20.0).color(overwide)),
6785 ),
6786 );
6787 let ui = build_ui(
6788 &screen,
6789 Size::new(300.0, 800.0),
6790 &Runtime::default(),
6791 &Theme::default(),
6792 );
6793 let width_of = |wanted: Color| {
6794 ui.scene()
6795 .primitives()
6796 .iter()
6797 .find_map(|p| match p {
6798 Primitive::Rect { color, rect, .. } if *color == wanted => Some(rect.width),
6799 _ => None,
6800 })
6801 .expect("the box is drawn")
6802 };
6803 // 300 less the padding either side, and not 600.
6804 assert_eq!(width_of(ordinary), 252.0);
6805 // The one that asked to be too wide still is: it overflows rather than being
6806 // squeezed, and rather than resizing everything around it.
6807 assert_eq!(width_of(overwide), 600.0);
6808 }
6809
6810 #[test]
6811 fn tab_cycles_focusables_in_order() {
6812 let tree = Flex::<Msg>::column()
6813 .child(Button::new("un").on_press(Msg::A))
6814 .child(Button::new("deux").on_press(Msg::B));
6815 let ui = build_ui(
6816 &tree,
6817 Size::new(200.0, 200.0),
6818 &Runtime::default(),
6819 &Theme::default(),
6820 );
6821 assert_eq!(ui.focusables.len(), 2, "both buttons are focusable");
6822 let first = ui.focusables[0].id;
6823 let second = ui.focusables[1].id;
6824
6825 // With no focus: Tab -> first, Shift+Tab -> last.
6826 assert_eq!(ui.focus_next(None, true), Some(first));
6827 assert_eq!(ui.focus_next(None, false), Some(second));
6828 // Wrap-around.
6829 assert_eq!(ui.focus_next(Some(first), true), Some(second));
6830 assert_eq!(ui.focus_next(Some(second), true), Some(first));
6831 assert_eq!(ui.focus_next(Some(first), false), Some(second));
6832 }
6833
6834 #[test]
6835 fn focus_ring_for_button_not_for_textinput() {
6836 let theme = Theme::default();
6837 let ring = theme.focus.fade(0.4); // focus_progress = 0 → alpha 0.4
6838 let count_ring = |tree: &dyn Widget<Msg>, keyboard: bool| -> usize {
6839 let mut rt = Runtime::default();
6840 let probe = build_ui(tree, Size::new(200.0, 200.0), &rt, &theme);
6841 rt.input.focused = probe.focusables.first().map(|f| f.id);
6842 rt.focus_visible = keyboard;
6843 let ui = build_ui(tree, Size::new(200.0, 200.0), &rt, &theme);
6844 ui.scene()
6845 .primitives()
6846 .iter()
6847 .filter(
6848 |p| matches!(p, Primitive::Rect { border_color, .. } if *border_color == ring),
6849 )
6850 .count()
6851 };
6852 let with_button = Flex::<Msg>::column().child(Button::new("x").on_press(Msg::A));
6853 assert!(
6854 count_ring(&with_button, true) >= 1,
6855 "a button focused with the keyboard has a generic ring"
6856 );
6857 // Focus taken with the pointer: no ring (FocusHighlightMode).
6858 assert_eq!(
6859 count_ring(&with_button, false),
6860 0,
6861 "a click does not flash a ring"
6862 );
6863
6864 let with_input = Flex::<Msg>::column().child(TextField::new("hi").on_input(Msg::Edited));
6865 assert_eq!(
6866 count_ring(&with_input, true),
6867 0,
6868 "the field draws its own focus"
6869 );
6870 }
6871
6872 #[test]
6873 fn arrow_focus_navigates_geometrically() {
6874 // A 2×2 grid of buttons; each target is identified by its message.
6875 let grid: Flex<Msg> = Flex::column()
6876 .child(
6877 Flex::row()
6878 .child(Button::new("a").on_press(Msg::A))
6879 .child(Button::new("b").on_press(Msg::B)),
6880 )
6881 .child(
6882 Flex::row()
6883 .child(Button::new("c").on_press(Msg::C))
6884 .child(Button::new("d").on_press(Msg::D)),
6885 );
6886 let ui = build_ui(
6887 &grid,
6888 Size::new(300.0, 200.0),
6889 &Runtime::default(),
6890 &Theme::default(),
6891 );
6892 let top_left = ui.focus_next(None, true).expect("premier focusable");
6893 assert_eq!(ui.msg_for(top_left), Some(Msg::A));
6894
6895 // Right: a → b; down: a → c; and nothing to the left of a.
6896 let right = ui
6897 .focus_directional(top_left, FocusDirection::Right)
6898 .expect("to the right");
6899 assert_eq!(ui.msg_for(right), Some(Msg::B));
6900 let down = ui
6901 .focus_directional(top_left, FocusDirection::Down)
6902 .expect("downwards");
6903 assert_eq!(ui.msg_for(down), Some(Msg::C));
6904 assert_eq!(ui.focus_directional(top_left, FocusDirection::Left), None);
6905 // The diagonal is kept in check: from b, down → d (aligned), not c.
6906 let down_right = ui
6907 .focus_directional(right, FocusDirection::Down)
6908 .expect("down from b");
6909 assert_eq!(ui.msg_for(down_right), Some(Msg::D));
6910 }
6911
6912 #[test]
6913 fn keyed_identity_survives_middle_removal() {
6914 let colored = |c: Color| Container::<Msg>::new().width(50.0).height(20.0).color(c);
6915 let red = Color::rgb(1.0, 0.0, 0.0);
6916 let green = Color::rgb(0.0, 1.0, 0.0);
6917 let blue = Color::rgb(0.0, 0.0, 1.0);
6918
6919 let owner_of = |ui: &Ui<Msg>, c: Color| -> u64 {
6920 ui.scene()
6921 .primitives()
6922 .iter()
6923 .find_map(|p| match p {
6924 Primitive::Rect { color, owner, .. } if *color == c => Some(*owner),
6925 _ => None,
6926 })
6927 .expect("the primitive is there")
6928 };
6929
6930 // The list [red(key 1), green(key 2), blue(key 3)].
6931 let full = Flex::<Msg>::column()
6932 .child(Keyed::new(1u64, colored(red)))
6933 .child(Keyed::new(2u64, colored(green)))
6934 .child(Keyed::new(3u64, colored(blue)));
6935 let ui_full = build_ui(
6936 &full,
6937 Size::new(200.0, 200.0),
6938 &Runtime::default(),
6939 &Theme::default(),
6940 );
6941
6942 // The list [red(1), blue(3)]: the green in the middle is removed → blue goes from index 2 to 1.
6943 let removed = Flex::<Msg>::column()
6944 .child(Keyed::new(1u64, colored(red)))
6945 .child(Keyed::new(3u64, colored(blue)));
6946 let ui_removed = build_ui(
6947 &removed,
6948 Size::new(200.0, 200.0),
6949 &Runtime::default(),
6950 &Theme::default(),
6951 );
6952
6953 // Blue's identity (owner, key 3) is UNCHANGED despite the shift in position.
6954 assert_eq!(owner_of(&ui_full, blue), owner_of(&ui_removed, blue));
6955
6956 // Without a key, the 2nd child's positional identity DOES change (index 2 vs 1).
6957 let unkeyed_full = Flex::<Msg>::column()
6958 .child(colored(red))
6959 .child(colored(green))
6960 .child(colored(blue));
6961 let unkeyed_removed = Flex::<Msg>::column()
6962 .child(colored(red))
6963 .child(colored(blue));
6964 let u1 = build_ui(
6965 &unkeyed_full,
6966 Size::new(200.0, 200.0),
6967 &Runtime::default(),
6968 &Theme::default(),
6969 );
6970 let u2 = build_ui(
6971 &unkeyed_removed,
6972 Size::new(200.0, 200.0),
6973 &Runtime::default(),
6974 &Theme::default(),
6975 );
6976 assert_ne!(owner_of(&u1, blue), owner_of(&u2, blue));
6977 }
6978
6979 #[test]
6980 fn center_overlay_scrim_click_dismisses() {
6981 // A Center modal with `.dismiss`: clicking the scrim (outside the content) returns the
6982 // dismissal message; clicking the content does not.
6983 let modal = Container::<Msg>::new()
6984 .width(100.0)
6985 .height(60.0)
6986 .color(Color::WHITE);
6987 let portal: OverlayPortal<Msg> =
6988 OverlayPortal::new(Container::<Msg>::new().width(20.0).height(20.0))
6989 .overlay(modal, Placement::Center)
6990 .dismiss(Msg::A);
6991 let ui = build_ui(
6992 &portal,
6993 Size::new(400.0, 300.0),
6994 &Runtime::default(),
6995 &Theme::default(),
6996 );
6997
6998 // The top-left corner: on the scrim → it dismisses.
6999 let corner = ui.hit(Point::new(5.0, 5.0)).expect("a clickable scrim");
7000 assert_eq!(ui.msg_for(corner), Some(Msg::A));
7001 }
7002
7003 /// **The shell's mark reaches the field** (milestone 511): a focused field whose
7004 /// selection the runtime marks as a touch one paints its two handles; the same
7005 /// selection unmarked paints none, and so does a marked field that lost the focus.
7006 #[test]
7007 fn a_touch_selection_reaches_the_field_as_handles() {
7008 let tree = Flex::column().width(300.0).height(80.0).child(
7009 TextField::new("hello world")
7010 .width(200.0)
7011 .on_input(Msg::Edited),
7012 );
7013 let theme = Theme::default();
7014 let size = Size::new(300.0, 80.0);
7015 let (id, _) = build_ui(&tree, size, &Runtime::default(), &theme)
7016 .focus_hit(Point::new(10.0, 10.0))
7017 .expect("the field");
7018 let handles = |focused: bool, marked: bool| {
7019 let mut rt = Runtime::default();
7020 rt.edits.insert(
7021 id,
7022 Edit {
7023 cursor: 11,
7024 anchor: Some(6),
7025 composing: None,
7026 },
7027 );
7028 rt.input.focused = focused.then_some(id);
7029 rt.selection_handles = marked.then_some(id);
7030 let ui = build_ui(&tree, size, &rt, &theme);
7031 ui.scene()
7032 .primitives()
7033 .iter()
7034 .filter(|p| {
7035 matches!(p, Primitive::Path { fill: Some(c), .. } if *c == theme.scheme.primary)
7036 })
7037 .count()
7038 };
7039 assert_eq!(handles(true, true), 2);
7040 assert_eq!(handles(true, false), 0, "a selection made with a mouse");
7041 assert_eq!(handles(false, true), 0, "a field left behind");
7042 }
7043
7044 /// A wrapper that fuses with its field shares its identity, so it is the wrapper the
7045 /// shell asks for the handles — and a wrapper that forgot to pass the question on
7046 /// would leave a finger nothing to take (the three silent bugs `Responsive` has
7047 /// already cost, milestones 477 to 495).
7048 #[test]
7049 fn the_wrappers_that_fuse_with_a_field_pass_its_handles_on() {
7050 let field = || TextField::<Msg>::new("hello world").width(200.0);
7051 let edit = Edit {
7052 cursor: 11,
7053 anchor: Some(6),
7054 composing: None,
7055 };
7056 let direct = Widget::<Msg>::selection_handles(&field(), 200.0, &edit, 0.0);
7057 assert!(direct.is_some());
7058 let keyed = crate::Keyed::new(1, field());
7059 let responsive = crate::Responsive::new(crate::SizeClass::Compact).compact(field());
7060 let boxed: Box<dyn Widget<Msg>> = Box::new(field());
7061 assert_eq!(keyed.selection_handles(200.0, &edit, 0.0), direct);
7062 assert_eq!(responsive.selection_handles(200.0, &edit, 0.0), direct);
7063 assert_eq!(boxed.selection_handles(200.0, &edit, 0.0), direct);
7064 }
7065
7066 /// **A field knows the form it is part of** (milestone 512): the stops inside one
7067 /// [`crate::AutofillGroup`] are that form, in tree order, whichever of them asks; two
7068 /// groups side by side are two forms; a field in no group is a form of one; and a
7069 /// group sits in the focus registry, so a cached subtree replays it like any stop.
7070 #[test]
7071 fn a_field_knows_the_form_it_is_part_of() {
7072 use crate::{AutofillGroup, AutofillHint};
7073 let field = |value: &str, hint: AutofillHint| {
7074 TextField::new(value)
7075 .width(200.0)
7076 .on_input(Msg::Edited)
7077 .autofill([hint])
7078 };
7079 let tree = Flex::column()
7080 .width(300.0)
7081 .height(400.0)
7082 .child(AutofillGroup::new(
7083 Flex::column()
7084 .child(field("someone", AutofillHint::Username))
7085 .child(field("secret", AutofillHint::Password)),
7086 ))
7087 .child(AutofillGroup::new(field(
7088 "123456",
7089 AutofillHint::OneTimeCode,
7090 )))
7091 .child(TextField::new("search").width(200.0).on_input(Msg::Edited));
7092 let ui = build_ui(
7093 &tree,
7094 Size::new(300.0, 400.0),
7095 &Runtime::default(),
7096 &Theme::default(),
7097 );
7098 let ids: Vec<WidgetId> = ui.focusable_ids().collect();
7099 assert_eq!(ids.len(), 4);
7100 let form = |id: WidgetId| ui.form_of(id).iter().map(|(f, _)| *f).collect::<Vec<_>>();
7101 assert_eq!(form(ids[0]), vec![ids[0], ids[1]], "the sign-in pair");
7102 assert_eq!(
7103 form(ids[1]),
7104 vec![ids[0], ids[1]],
7105 "whichever of the two asks"
7106 );
7107 assert_eq!(
7108 form(ids[2]),
7109 vec![ids[2]],
7110 "a second group is a second form"
7111 );
7112 assert_eq!(
7113 form(ids[3]),
7114 vec![ids[3]],
7115 "a field in no group is a form of one"
7116 );
7117 assert!(ui.form_of(WidgetId::from_u64(7)).is_empty());
7118 // The two groups are told apart by identity, and a lone field has none.
7119 let (first, second) = (ui.form_group(ids[0]), ui.form_group(ids[2]));
7120 assert!(first.is_some() && second.is_some() && first != second);
7121 assert_eq!(ui.form_group(ids[3]), None);
7122 assert_eq!(ui.group_stops(first.unwrap()).len(), 2);
7123 // Each stop carries its box, which is what the platform anchors its offer to.
7124 assert!(ui.form_of(ids[1]).iter().all(|(_, r)| r.height > 0.0));
7125 }
7126
7127 #[test]
7128 fn find_widget_and_edit_types() {
7129 let tree = Flex::column()
7130 .width(300.0)
7131 .height(80.0)
7132 .child(TextField::new("hi").width(200.0).on_input(Msg::Edited));
7133 let rt = Runtime::default();
7134 let ui = build_ui(&tree, Size::new(300.0, 80.0), &rt, &Theme::default());
7135 let (id, _rect) = ui.focus_hit(Point::new(10.0, 10.0)).expect("the field");
7136
7137 let widget = find_widget(&tree, id).expect("the widget was found");
7138 let mut edit = Edit {
7139 cursor: 2,
7140 anchor: None,
7141 composing: None,
7142 };
7143 assert_eq!(
7144 widget.on_edit(&mut edit, &Key::Text("!".to_string())),
7145 Some(Msg::Edited("hi!".to_string()))
7146 );
7147 }
7148
7149 #[test]
7150 fn a_traversal_can_arrive_before_the_layout_pass() {
7151 // Milestone 415. A `ThemeBuilder`'s subtree does not exist until something has
7152 // called `build_themed` on it, and `children()` says so in its own doc: "a
7153 // traversal that arrives first should call `build_themed`, not this".
7154 //
7155 // The shell has such a traversal. On a **burst of keystrokes** — a software
7156 // keyboard, `adb input text`, auto-repeat — it rebuilds the tree from `view` alone
7157 // and reads it immediately, with no layout pass in between, so every deferred
7158 // subtree in it is empty and every widget inside one is unreachable.
7159 let deferred = |value: &str| {
7160 let value = value.to_string();
7161 crate::ThemeBuilder::new(move |_: &Theme| {
7162 TextField::new(value.clone())
7163 .width(200.0)
7164 .on_input(Msg::Edited)
7165 })
7166 };
7167 let laid_out = Flex::column()
7168 .width(300.0)
7169 .height(80.0)
7170 .child(deferred("hi"));
7171 let rt = Runtime::default();
7172 let ui = build_ui(&laid_out, Size::new(300.0, 80.0), &rt, &Theme::default());
7173 let (id, _) = ui.focus_hit(Point::new(10.0, 10.0)).expect("the field");
7174 assert!(
7175 find_widget(&laid_out, id).is_some(),
7176 "after a layout pass the field is where the hit-test said it was"
7177 );
7178
7179 // The same tree, freshly built and not yet laid out. `build_deferred` is what the
7180 // layout pass does on its way down, and nothing else.
7181 let fresh = Flex::column()
7182 .width(300.0)
7183 .height(80.0)
7184 .child(deferred("hi"));
7185 build_deferred(&fresh, &Theme::default(), &Runtime::default());
7186 assert!(
7187 find_widget(&fresh, id).is_some(),
7188 "prepared the same way, the same traversal reaches the same widget"
7189 );
7190 }
7191
7192 #[test]
7193 #[should_panic(expected = "before it was built")]
7194 fn reading_an_unbuilt_deferred_subtree_trips_the_wire() {
7195 // Without the preparation the traversal used to return `None` and the caller
7196 // shrugged: no field found, no caret revealed, nothing said. The failure mode was
7197 // **silence**, so the guard is a panic in debug rather than a comment.
7198 let fresh =
7199 Flex::<Msg>::column()
7200 .width(300.0)
7201 .height(80.0)
7202 .child(crate::ThemeBuilder::new(|_: &Theme| {
7203 Container::<Msg>::new().width(10.0).height(10.0)
7204 }));
7205 let _ = find_widget(&fresh, WidgetId::ROOT.child(9));
7206 }
7207
7208 #[test]
7209 fn a_deferred_subtree_is_built_under_its_own_theme() {
7210 // The layout pass swaps the theme before it builds (`theme_override`), so a
7211 // builder inside a `Themed` sees the subtree's theme and not the frame's.
7212 // `build_deferred` has to make the same swap or it would build the wrong subtree
7213 // — and being *nearly* the walk is worse than not being it at all.
7214 let seen = std::rc::Rc::new(std::cell::Cell::new(0.0f32));
7215 let recorder = {
7216 let seen = seen.clone();
7217 crate::ThemeBuilder::new(move |theme: &Theme| {
7218 seen.set(theme.radius);
7219 Container::<Msg>::new().width(10.0).height(10.0)
7220 })
7221 };
7222 let inner = Theme {
7223 radius: 99.0,
7224 ..Theme::default()
7225 };
7226 let tree = Flex::<Msg>::column().child(crate::Themed::new(inner, recorder));
7227 build_deferred(&tree, &Theme::default(), &Runtime::default());
7228 assert_eq!(seen.get(), 99.0, "the subtree's theme, not the frame's");
7229 }
7230
7231 #[test]
7232 fn find_by_key_resolves_a_keyed_field_to_its_focus_id() {
7233 // Two named fields: `find_by_key` finds each one's focus identity (the one the
7234 // hit-test would assign), and it tells the keys apart.
7235 fn hash(k: &str) -> u64 {
7236 use std::hash::{Hash, Hasher};
7237 let mut h = std::collections::hash_map::DefaultHasher::new();
7238 k.hash(&mut h);
7239 h.finish()
7240 }
7241 let tree: Flex<Msg> = Flex::column()
7242 .child(Keyed::new(
7243 "email",
7244 TextField::new("").on_input(Msg::Edited),
7245 ))
7246 .child(Keyed::new(
7247 "password",
7248 TextField::new("").on_input(Msg::Edited),
7249 ));
7250
7251 let email = find_by_key(&tree, hash("email")).expect("email was found");
7252 let password = find_by_key(&tree, hash("password")).expect("password was found");
7253 assert_ne!(email, password, "distinct keys → distinct identities");
7254 // The identity resolved is indeed the by-key identity (stable, independent of the
7255 // position).
7256 assert_eq!(email, WidgetId::ROOT.keyed(hash("email")));
7257 assert!(
7258 find_by_key(&tree, hash("absent")).is_none(),
7259 "an unknown key → None"
7260 );
7261
7262 // And above all: it is **exactly** the identity the focus hit-test would assign to
7263 // that field — so setting this focus really does route to it.
7264 let rt = Runtime::default();
7265 let ui = build_ui(&tree, Size::new(300.0, 200.0), &rt, &Theme::default());
7266 let (hit_id, _) = ui
7267 .focus_hit(Point::new(10.0, 10.0))
7268 .expect("the email field under the cursor");
7269 assert_eq!(email, hit_id, "find_by_key == the field's focus identity");
7270 }
7271
7272 #[test]
7273 fn ink_is_painted_over_the_surface_and_under_its_child() {
7274 use crate::ink::InkWell;
7275 let tree = Container::<Msg>::new().width(120.0).height(60.0).child(
7276 InkWell::<Msg>::new().radius(8.0).on_click(Msg::A).child(
7277 Container::<Msg>::new()
7278 .width(120.0)
7279 .height(60.0)
7280 .color(frus_core::Color::WHITE),
7281 ),
7282 );
7283 let size = Size::new(120.0, 60.0);
7284
7285 // Dry: the well paints nothing of its own.
7286 let rt = Runtime::default();
7287 let dry = build_ui(&tree, size, &rt, &Theme::default());
7288 let plain = dry.scene.len();
7289 let id = dry.inks.first().expect("the well registered its box").0;
7290 assert_eq!(
7291 dry.ink_box(id).map(|r| (r.width, r.height)),
7292 Some((120.0, 60.0)),
7293 "the registry carries the **whole** box, which is what sizes the splash"
7294 );
7295
7296 // A finger lands in the top-left corner, and is still down.
7297 let mut rt = Runtime::default();
7298 rt.input.pressed = Some(id);
7299 rt.ink_press(id, Point::new(10.0, 10.0), Size::new(120.0, 60.0));
7300 rt.advance_ink(1.0 / 60.0);
7301 let ui = build_ui(&tree, size, &rt, &Theme::default());
7302 assert_eq!(ui.scene.len(), plain + 1, "the splash adds one layer");
7303
7304 // It is a shape-clipped layer, so the ink cannot escape the rounded corners…
7305 let (index, clip_shape) = ui
7306 .scene
7307 .primitives()
7308 .iter()
7309 .enumerate()
7310 .find_map(|(i, p)| match p {
7311 frus_core::Primitive::Layer { clip_shape, .. } => Some((i, clip_shape.clone())),
7312 _ => None,
7313 })
7314 .expect("the ink is a composited layer");
7315 assert_eq!(clip_shape, frus_core::ClipShape::RRect(8.0.into()));
7316
7317 // …and it is painted **before** the child that sits on top of it: the ink is
7318 // under the content, the way a material surface holds it.
7319 let child_index = ui
7320 .scene
7321 .primitives()
7322 .iter()
7323 .rposition(|p| matches!(p, frus_core::Primitive::Rect { .. }))
7324 .expect("the child painted a rectangle");
7325 assert!(
7326 index < child_index,
7327 "ink at {index} must come before the content at {child_index}"
7328 );
7329 }
7330
7331 #[test]
7332 fn a_splash_left_unconfirmed_does_not_wait_for_ever() {
7333 // The finger came down and never came back up on this widget — it slid off, or
7334 // the widget went away mid-press. Nothing will ever confirm the splash, and
7335 // without the sweep in `advance_ink` the ink would sit there for good.
7336 let mut rt = Runtime::default();
7337 let id = WidgetId::from_u64(42);
7338 rt.input.pressed = Some(id);
7339 rt.ink_press(id, Point::new(5.0, 5.0), Size::new(100.0, 40.0));
7340 // Held for a fifth of a second: the ink is up, and it stays up.
7341 for _ in 0..12 {
7342 rt.advance_ink(1.0 / 60.0);
7343 }
7344 assert!(rt.ink.contains_key(&id), "the splash waits for the finger");
7345
7346 // The finger is gone and nothing confirmed the tap.
7347 rt.input.pressed = None;
7348 let mut frames = 0;
7349 while rt.advance_ink(1.0 / 60.0) && frames < 200 {
7350 frames += 1;
7351 }
7352 assert!(rt.ink.is_empty(), "the ink is gone after {frames} frames");
7353 assert!(frames <= 6, "and quickly: a cancel is 75 ms, took {frames}");
7354 }
7355
7356 #[test]
7357 fn an_overscroll_glow_is_painted_over_its_scroll_area() {
7358 let tree = SingleChildScrollView::<Msg>::new()
7359 .width(200.0)
7360 .height(100.0)
7361 .child(Container::<Msg>::new().width(100.0).height(400.0));
7362 let size = Size::new(200.0, 100.0);
7363
7364 // Quiet: nothing extra is drawn.
7365 let rt = Runtime::default();
7366 let plain = build_ui(&tree, size, &rt, &Theme::default()).scene.len();
7367
7368 // A fling has just landed on the bottom edge.
7369 let mut rt = Runtime::default();
7370 let id = build_ui(&tree, size, &rt, &Theme::default()).scroll_regions()[0].id;
7371 rt.glow_absorb(id, crate::overscroll::GlowEdge::Bottom, 4000.0);
7372 rt.advance_glow(0.02);
7373 let ui = build_ui(&tree, size, &rt, &Theme::default());
7374 assert_eq!(
7375 ui.scene.len(),
7376 plain + 1,
7377 "the glow adds exactly one shape to the frame"
7378 );
7379 // The glow sits under the scrollbars, so it is not the last primitive: it is
7380 // the only filled path in the frame.
7381 let clip = ui
7382 .scene
7383 .primitives()
7384 .iter()
7385 .find_map(|p| match p {
7386 frus_core::Primitive::Path { clip, .. } => Some(*clip),
7387 _ => None,
7388 })
7389 .expect("the glow is a filled path");
7390 // Drawn against the bottom of the viewport, and inside it.
7391 assert!(
7392 (clip.y + clip.height - 100.0).abs() < 1.0,
7393 "clip = {clip:?}"
7394 );
7395 assert!(clip.width <= 200.0 + 1e-3);
7396 }
7397
7398 #[test]
7399 fn a_scroll_area_carries_its_physics_into_the_registry() {
7400 let content = Container::<Msg>::new().width(100.0).height(400.0);
7401 // Unset: the region says nothing and the application decides.
7402 let plain = SingleChildScrollView::new()
7403 .width(200.0)
7404 .height(100.0)
7405 .child(content);
7406 let rt = Runtime::default();
7407 let ui = build_ui(&plain, Size::new(200.0, 100.0), &rt, &Theme::default());
7408 assert_eq!(ui.scroll_regions()[0].physics, None);
7409 assert_eq!(
7410 ui.scroll_regions()[0].physics_or(ScrollPhysics::Clamping),
7411 ScrollPhysics::Clamping,
7412 "an area with no opinion follows the application"
7413 );
7414
7415 // Set: the area's own choice wins over the application's.
7416 let bouncy = SingleChildScrollView::new()
7417 .width(200.0)
7418 .height(100.0)
7419 .physics(ScrollPhysics::BOUNCING)
7420 .child(Container::<Msg>::new().width(100.0).height(400.0));
7421 let ui = build_ui(&bouncy, Size::new(200.0, 100.0), &rt, &Theme::default());
7422 let area = ui.scroll_regions()[0];
7423 assert_eq!(area.physics, Some(ScrollPhysics::BOUNCING));
7424 assert_eq!(
7425 area.physics_or(ScrollPhysics::Clamping),
7426 ScrollPhysics::BOUNCING
7427 );
7428 // And the metrics it hands the physics describe the right axis.
7429 let metrics = area.metrics_y(10.0);
7430 assert_eq!(metrics.pixels, 10.0);
7431 assert_eq!(metrics.max, 300.0); // 400 of content in a 100 viewport
7432 assert_eq!(metrics.viewport, 100.0);
7433 }
7434
7435 #[test]
7436 fn scroll_translates_and_clips_content() {
7437 let content = Flex::<Msg>::column()
7438 .gap(0.0)
7439 .child(
7440 Container::new()
7441 .height(60.0)
7442 .color(Color::rgb(1.0, 0.0, 0.0)),
7443 )
7444 .child(
7445 Container::new()
7446 .height(60.0)
7447 .color(Color::rgb(0.0, 1.0, 0.0)),
7448 )
7449 .child(
7450 Container::new()
7451 .height(60.0)
7452 .color(Color::rgb(0.0, 0.0, 1.0)),
7453 );
7454 let tree = SingleChildScrollView::new()
7455 .width(200.0)
7456 .height(100.0)
7457 .child(content);
7458
7459 let rt = Runtime::default();
7460 let ui = build_ui(&tree, Size::new(200.0, 100.0), &rt, &Theme::default());
7461 let area = ui.scrollables[0];
7462 let sid = area.id;
7463 assert_eq!(area.max_y, 80.0); // 180 - 100
7464 assert_eq!(area.max_x, 0.0);
7465 assert_eq!(ui.first_rect().0.y, 0.0);
7466 assert_eq!(ui.first_rect().1, Rect::new(0.0, 0.0, 200.0, 100.0));
7467
7468 let mut rt = Runtime::default();
7469 rt.scroll.insert(sid, (0.0, 50.0));
7470 let ui2 = build_ui(&tree, Size::new(200.0, 100.0), &rt, &Theme::default());
7471 assert_eq!(ui2.first_rect().0.y, -50.0);
7472 }
7473
7474 #[test]
7475 fn kanban_cards_are_reorderable_without_being_clickable() {
7476 use crate::Kanban;
7477 let board = Kanban::new(|_, _, _, _| Msg::A).column("To do", ["Card A"]);
7478 let rt = Runtime::default();
7479 let ui = build_ui(&board, Size::new(400.0, 300.0), &rt, &Theme::default());
7480 // The card **and** the drop zone are registered as reorderables.
7481 assert!(
7482 ui.reorderables.len() >= 2,
7483 "the card + the drop zone are in the reorderables registry"
7484 );
7485 // A point on the card can be **picked up** (it is reorderable) but is **not clickable**
7486 // — which is the whole point of the registry: without it, `ui.hit` alone would never
7487 // find the card.
7488 let (card_id, card) = ui.reorderables[0];
7489 let p = Point::new(card.x + 5.0, card.y + 5.0);
7490 assert!(
7491 ui.reorderable_at(p).is_some(),
7492 "the card can be picked up at that point"
7493 );
7494 assert!(
7495 ui.hit(p).is_none(),
7496 "…but it is not clickable (absent from the hit registry)"
7497 );
7498 // `widget_rect` must find the card **through the reorderable fallback** (it is not
7499 // focusable): otherwise the shell's vertical drag preview never starts.
7500 assert_eq!(
7501 ui.widget_rect(card_id),
7502 Some(card),
7503 "widget_rect falls back to the reorderables registry"
7504 );
7505 }
7506
7507 #[test]
7508 fn reorderables_inside_a_scroll_are_still_registered() {
7509 // The milestone 258/260 scenario: the board (with its reorderable cards) is **wrapped
7510 // in a `SingleChildScrollView`**. The reorderables must stay registered — otherwise dragging a card
7511 // stops engaging as soon as the board scrolls.
7512 use crate::{Axis, Kanban, SingleChildScrollView};
7513 let board = Kanban::new(|_, _, _, _| Msg::A).column("To do", ["Card A"]);
7514 let scrolled = SingleChildScrollView::new()
7515 .axis(Axis::Horizontal)
7516 .width(400.0)
7517 .height(300.0)
7518 .child(board);
7519 let rt = Runtime::default();
7520 let ui = build_ui(&scrolled, Size::new(400.0, 300.0), &rt, &Theme::default());
7521 assert!(
7522 ui.reorderables.len() >= 2,
7523 "the card + the drop zone stay reorderable even inside a SingleChildScrollView (got {})",
7524 ui.reorderables.len()
7525 );
7526 }
7527
7528 #[test]
7529 fn reorderables_inside_a_per_column_card_scroll_are_still_registered() {
7530 // Milestone 264: `card_area_height` puts each column's cards inside a **vertical
7531 // `SingleChildScrollView` with an explicit height**. This is exactly the case that *collapsed* at
7532 // milestone 263 (a flex scroll with no defined ancestor height → cards clipped to zero
7533 // and no longer reorderable). With a **defined** height the visible cards must stay
7534 // registered as reorderables — a guard against a regression of per-column dragging.
7535 use crate::Kanban;
7536 let board = Kanban::new(|_, _, _, _| Msg::A)
7537 .card_area_height(220.0)
7538 .column("To do", ["Card A", "Card B"]);
7539 let rt = Runtime::default();
7540 let ui = build_ui(&board, Size::new(400.0, 300.0), &rt, &Theme::default());
7541 assert!(
7542 ui.reorderables.len() >= 3,
7543 "2 cards + the drop zone stay reorderable inside a vertical scroll with a defined height \
7544 (got {})",
7545 ui.reorderables.len()
7546 );
7547 }
7548
7549 #[test]
7550 fn scrollable_columns_fill_the_board_height_then_scroll() {
7551 // Milestone 266: `Kanban::scrollable_columns()` makes the columns **fill** the board's
7552 // height (laid out in an ancestor with a defined height), and each column scrolls its
7553 // cards vertically **with no explicit height** (the flex does the arithmetic, so the
7554 // milestone 264 stopgap is gone). This checks that at least one column has a vertical
7555 // `SingleChildScrollView` whose viewport **fills** the height (far more than the default 200) and
7556 // **scrolls** (max_y > 0), which is the proof of fill-then-scroll.
7557 use crate::{Axis, Container, Flex, Kanban, SingleChildScrollView};
7558 // Enough cards to **overflow** the filled viewport (otherwise max_y = 0: nothing to scroll).
7559 let long: Vec<String> = (0..24).map(|i| format!("card {i}")).collect();
7560 let board = Kanban::new(|_, _, _, _| Msg::A)
7561 .scrollable_columns()
7562 .column("To do", long);
7563 // `board_screen`'s nesting: the board inside a **plain `Container` with padding** (the
7564 // visual margin), itself inside a horizontal `flex(1)` SingleChildScrollView, inside a screen (a Flex
7565 // column) of bounded height. That `Auto` `Container` **collapsed** at milestone 266
7566 // (hence the `Flex` `flex(1)` workaround); since `compute_scroll` **fills the
7567 // constrained axis**, it fills the viewport's height and the board follows — no filler
7568 // container needed any more.
7569 let padded = Container::<Msg>::new().padding(24.0).child(board);
7570 let scroll_h = SingleChildScrollView::new()
7571 .axis(Axis::Horizontal)
7572 .width(360.0)
7573 .flex(1.0)
7574 .child(padded);
7575 let root: Flex<Msg> = Flex::column().width(400.0).height(600.0).child(scroll_h);
7576 let ui = build_ui(
7577 &root,
7578 Size::new(400.0, 600.0),
7579 &Runtime::default(),
7580 &Theme::default(),
7581 );
7582 // A column's **vertical** scroll: a tall viewport (it fills) and it scrolls.
7583 let filled = ui
7584 .scrollables
7585 .iter()
7586 .any(|area| area.viewport.height > 300.0 && area.max_y > 0.0);
7587 assert!(
7588 filled,
7589 "a column fills the board's height then scrolls (scrollables: {:?})",
7590 ui.scrollables
7591 );
7592 }
7593
7594 #[test]
7595 fn subtree_ids_covers_a_widget_and_its_descendants() {
7596 use crate::Text;
7597 // Root (Container) > Flex(column) > [Text, Text].
7598 let tree: Container<Msg> = Container::new().child(
7599 Flex::<Msg>::column()
7600 .child(Text::new("a"))
7601 .child(Text::new("b")),
7602 );
7603 let all = collect_ids(&tree);
7604 // From the root: identical to `collect_ids` (the same positional walk).
7605 assert_eq!(subtree_ids(&tree, WidgetId::ROOT), all);
7606 // A child's subtree starts with its own identity and stays a **strict** subset of the
7607 // tree (which is how a rich card's ghost captures all of its content).
7608 let flex_id = all[1];
7609 let sub = subtree_ids(Widget::children(&tree)[0].as_ref(), flex_id);
7610 assert_eq!(
7611 sub[0], flex_id,
7612 "the subtree starts with the identity supplied"
7613 );
7614 assert!(
7615 sub.len() < all.len() && sub.iter().all(|i| all.contains(i)),
7616 "a subset of the tree including the descendants"
7617 );
7618 }
7619
7620 impl<Msg: Clone> Ui<Msg> {
7621 fn first_rect(&self) -> (Rect, Rect) {
7622 for primitive in self.scene.primitives() {
7623 if let Primitive::Rect { rect, clip, .. } = primitive {
7624 return (*rect, *clip);
7625 }
7626 }
7627 panic!("no rect at all");
7628 }
7629 }
7630
7631 /// A static subtree under `RepaintBoundary` (mixed content: text + boxes) — enough to
7632 /// produce several primitives.
7633 fn boundary_tree() -> Container<Msg> {
7634 use crate::Text;
7635 Container::new().repaint_boundary().child(
7636 Flex::<Msg>::column()
7637 .child(Text::new("Statique"))
7638 .child(Container::new().width(30.0).height(20.0).on_click(Msg::A))
7639 .child(Button::new("ok").on_press(Msg::B)),
7640 )
7641 }
7642
7643 #[test]
7644 fn repaint_boundary_reuses_a_static_subtree_bit_identical() {
7645 let tree = boundary_tree();
7646 let size = Size::new(200.0, 200.0);
7647 let theme = Theme::default();
7648 let rt = Runtime::default();
7649
7650 // Frame 1: nothing cached → 1 miss (a full paint), the subtree is captured.
7651 let ui1 = build_ui(&tree, size, &rt, &theme);
7652 // The **primitives** are what is compared (the scene's transient ambient state — the
7653 // current clip/owner — is not rendered).
7654 let dbg1 = format!("{:?}", ui1.scene().primitives());
7655 assert_eq!(rt.paint_cache.borrow().last_frame_stats(), (0, 1));
7656
7657 // Frame 2: the same generation + the same state → the boundary is reused.
7658 let ui2 = build_ui(&tree, size, &rt, &theme);
7659 let dbg2 = format!("{:?}", ui2.scene().primitives());
7660 assert_eq!(
7661 rt.paint_cache.borrow().last_frame_stats(),
7662 (1, 0),
7663 "the boundary is reused"
7664 );
7665 assert_eq!(
7666 dbg1, dbg2,
7667 "the replayed scene is bit-for-bit identical to the full repaint"
7668 );
7669 // The interaction maps are replayed too (a click stays routable).
7670 assert_eq!(ui1.hits.len(), ui2.hits.len());
7671 assert!(!ui2.hits.is_empty(), "the subtree's hit is indeed replayed");
7672 }
7673
7674 #[test]
7675 fn repaint_boundary_invalidated_by_generation_bump() {
7676 let tree = boundary_tree();
7677 let size = Size::new(200.0, 200.0);
7678 let theme = Theme::default();
7679 let rt = Runtime::default();
7680
7681 build_ui(&tree, size, &rt, &theme); // frame 1 : miss + capture
7682
7683 // The `view` is rebuilt (the config may have changed).
7684 rt.paint_cache.borrow_mut().bump_generation();
7685 build_ui(&tree, size, &rt, &theme); // frame 2
7686 assert_eq!(
7687 rt.paint_cache.borrow().last_frame_stats(),
7688 (0, 1),
7689 "a stale generation → a full repaint"
7690 );
7691 }
7692
7693 #[test]
7694 fn repaint_boundary_invalidated_by_interaction_change() {
7695 let tree = boundary_tree();
7696 let size = Size::new(200.0, 200.0);
7697 let theme = Theme::default();
7698 let mut rt = Runtime::default();
7699
7700 build_ui(&tree, size, &rt, &theme); // frame 1 : miss + capture
7701
7702 // No rebuild, but a descendant's interaction state changes (the boundary's animated
7703 // hover) → the fingerprint differs → a repaint.
7704 rt.anims.insert(
7705 WidgetId::ROOT,
7706 crate::Anim {
7707 hover: 0.5,
7708 ..Default::default()
7709 },
7710 );
7711 build_ui(&tree, size, &rt, &theme); // frame 2
7712 assert_eq!(
7713 rt.paint_cache.borrow().last_frame_stats(),
7714 (0, 1),
7715 "the interaction state changed → a full repaint"
7716 );
7717
7718 // Once the state has settled, the boundary is reused again.
7719 build_ui(&tree, size, &rt, &theme); // frame 3
7720 assert_eq!(rt.paint_cache.borrow().last_frame_stats(), (1, 0));
7721 }
7722}