frust_widgets/viewport.rs
1//! `CustomScrollView`: a scroll surface whose children are slivers — Flutter's
2//! `CustomScrollView` over `RenderViewport` (`rendering/viewport.dart`).
3//!
4//! A [`ScrollView`](crate::ScrollView) lays its one child out at unbounded
5//! height and scrolls the result. A [`CustomScrollView`] instead lays each of
6//! its children out with the sliver protocol ([`SliverConstraints`] in,
7//! [`SliverGeometry`] out; see [`frust_core::SliverWidget`]): every child is
8//! told where the viewport has scrolled to and how much paint space is left,
9//! so a sliver can lay out only what is visible or inside the cache area. The
10//! drag, fling, overscroll, nested-claim and controller machinery is the
11//! shared [`crate::scroll_core`] that `ScrollView` and `ListView` run on, so all
12//! three behave the same under the finger, the wheel and a
13//! [`ScrollController`].
14//!
15//! # The layout loop
16//!
17//! [`ViewportWidget`]'s layout is `RenderViewport.performLayout` for a viewport
18//! anchored at its leading edge:
19//!
20//! 1. The main-axis extent is the incoming constraint along the scroll axis;
21//! the cross-axis extent the constraint across it. The cache area reaches
22//! [`CustomScrollView::cache_extent`] past both ends of the visible region.
23//! 2. Children are laid out in order. Each gets a [`SliverConstraints`] built
24//! from the running state: `scroll_offset = max(0, offset − preceding)`,
25//! the preceding scroll extent, the overlap left by earlier slivers' paint
26//! (`max_paint_offset − layout_offset`), the paint extent still unclaimed,
27//! the cache window `[offset − cache, offset + extent + cache)` expressed
28//! from the child's own scroll offset (`cache_origin` never reaching before
29//! the child's start, `remaining_cache_extent` measured from there), and
30//! the user's scroll direction. Every value is sanitised
31//! first (NaN dropped, negatives clamped to zero, the cache origin kept
32//! non-positive), so a transient zero or NaN window size can never trip
33//! [`SliverConstraints::debug_assert_valid`].
34//! 3. A child answers through [`ChildPod::layout_sliver_child`], which applies
35//! the per-pod layout skip. A child that is not a sliver answers `None` and
36//! is laid out as a box through the same geometry as [`crate::sliver_box`]
37//! (tight across, unbounded along, painted at `−scroll_offset` inside its
38//! paint extent and clipped when cut).
39//! 4. A child reporting a
40//! [`scroll_offset_correction`](SliverGeometry::scroll_offset_correction)
41//! ends the attempt: the correction is applied to the scroll position (and
42//! to the drag accumulator, so a live drag continues from the corrected
43//! position; a running release simulation restarts from it at its current
44//! velocity, while a controller tween keeps its absolute target, as
45//! Flutter's does) and the loop starts again. The restarts are bounded at
46//! ten cycles: a sliver still correcting on the last one trips a
47//! `debug_assert!`, and a release build keeps that cycle's layout,
48//! correction ignored.
49//! 5. Each child's pod is placed at the running paint position (plus its
50//! `paint_origin`), and the maximum scroll extent — the sum of every
51//! child's scroll extent less the viewport extent, never negative — is
52//! published to the core, the attached controller and [`ScrollInfo`].
53//!
54//! After the loop, a position the new extent leaves out of range is clamped
55//! (outside a live drag, settle or simulation, exactly as `ScrollView` does)
56//! and any queued controller command is applied; either one moving the
57//! position runs the loop once more.
58//!
59//! Under an unbounded main axis the viewport shrink-wraps: every child is
60//! given infinite remaining paint extent and the viewport sizes itself to the
61//! sum of the children's scroll extents, so nothing scrolls. An unbounded
62//! cross axis lays children out at zero cross extent, as `ScrollView` treats
63//! an unbounded width.
64//!
65//! # Placement is a layout product
66//!
67//! A child's origin is decided by the loop alone, never adjusted at paint:
68//! moving a sliver changes its constraints, which only a layout can answer.
69//! Motion the paint pump drives — a fling, a release settle, a controller
70//! tween — therefore shows up on the frame after the pump moved the position:
71//! the paint that moved it requests a layout
72//! ([`PaintCtx::request_layout`]), and that frame lays out at the new
73//! position. Input-driven motion (a drag, the wheel) and a jump drained at
74//! layout have no such lag.
75//!
76//! # Paint, hit testing and semantics
77//!
78//! Paint clips to the viewport, publishes it as the visible rect
79//! ([`PaintCtx::constrain_visible_rect`]) so a `Flex` or a list inside a sliver
80//! can cull, and skips every child whose geometry is not
81//! [`visible`](SliverGeometry::visible) — a child laid out only because it
82//! sits in the cache area is never painted. Children paint last to first, so
83//! an earlier sliver paints over a later one it overlaps (Flutter's paint
84//! order), and are hit-tested first to last, each only within its
85//! [`hit_test_extent`](SliverGeometry::hit_test_extent) and the viewport's
86//! bounds. Semantics is a [`Role::ScrollView`] node carrying the scroll
87//! position and range along the axis, over the children that are visible or
88//! cached.
89//!
90//! # Which children are slivers
91//!
92//! A child is a sliver when its widget answers [`Widget::as_sliver`]. The
93//! transparent wrappers forward that answer to what they wrap, so a sliver
94//! stays a sliver inside them: [`Either`](crate::Either) and
95//! [`EitherOf3`](crate::EitherOf3) through [`EitherOf8`](crate::EitherOf8),
96//! [`hero`](crate::hero), [`shield`](crate::shield),
97//! [`auto_scroll_zone`](crate::auto_scroll_zone) and
98//! [`GestureDetector`](crate::GestureDetector) — and an [`AnyView`] or a
99//! component boundary, which forward through the boxed widget. Any other
100//! single-child wrapper (`Padding`, `Align`, `SizedBox`, a container, …)
101//! answers `None`, so the viewport lays it out as a box, exactly as
102//! [`crate::sliver_box`] would, whatever it wraps.
103//!
104//! # Axis
105//!
106//! [`CustomScrollView::axis`] picks vertical (the default) or horizontal
107//! scrolling. The shared core is built with the viewport's axis and reads
108//! pointer travel and wheel deltas along it, so every event reaches the core
109//! untouched. The one transposition left is the stretch overscroll effect: the
110//! core's stretch transform is vertical-only, so a horizontal viewport mirrors
111//! its origin and size through a swap around it.
112//!
113//! **Nested claims are axis-aware.** The ambient inner-scroll claim
114//! (`scroll.rs`'s *Nested scrolling*) carries no axis, and every other scroll
115//! surface is vertical. A viewport therefore registers into its nearest
116//! enclosing surface's claim only when the two scroll along the same axis, and
117//! discards a claim registered into its own cell by a surface on the other
118//! axis. A horizontal carousel inside a vertical list thus never makes the
119//! list defer a vertical drag to it, and vice versa.
120//!
121//! # Limits
122//!
123//! Growth is forward only: slivers are laid out from the leading edge, in the
124//! axis' natural direction ([`frust_core::GrowthDirection::Forward`]). There is
125//! no center anchor (Flutter's `center`/`anchor`), so nothing can grow toward
126//! the leading edge, and no overlap absorber (Flutter's
127//! `SliverOverlapAbsorber`/`NestedScrollView`).
128
129use std::cell::{Cell, RefCell};
130use std::rc::Rc;
131
132use frust_core::accesskit::Role;
133use frust_core::{
134 AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent,
135 LayoutCtx, PaintCtx, PaintScene, PointerButton, PointerEvent, PointerPhase, ScrollAxis,
136 ScrollDirection, SemanticsCtx, SliverConstraints, SliverGeometry, View, ViewSeq, Widget,
137};
138use frust_theme::Theme;
139use kurbo::{Affine, Point, Rect, Size};
140
141use crate::authoring::presses;
142use crate::physics::effect::OverscrollEffect;
143use crate::physics::{ScrollPhysics, default_overscroll_effect};
144use crate::scroll::{InnerScrollState, ScrollInfo, ambient_scroll_claim};
145use crate::scroll_controller::ScrollController;
146use crate::scroll_core::{BallisticState, ScrollSurface, ScrollSurfaceCore};
147use crate::slivers::adapter::{
148 axis_point, box_child_origin, box_geometry, finite_extent, non_negative,
149};
150
151/// How far past each end of the visible region a [`CustomScrollView`] lays
152/// children out by default, in logical px — Flutter's
153/// `RenderAbstractViewport.defaultCacheExtent`.
154pub const DEFAULT_CACHE_EXTENT: f64 = 250.0;
155
156/// How many times one layout may restart for a
157/// [`scroll_offset_correction`](SliverGeometry::scroll_offset_correction)
158/// before the viewport gives up on converging (the module docs' *The layout
159/// loop*).
160const MAX_LAYOUT_CYCLES: usize = 10;
161
162/// A view-held scroll-observation callback (erased on build).
163type OnScroll<State> = Rc<dyn Fn(&mut State, ScrollInfo)>;
164
165/// A view-held pull-to-refresh release callback (erased on build).
166type OnRefresh<State> = Rc<dyn Fn(&mut State)>;
167
168/// A declarative scroll surface over a sequence of slivers. See the
169/// [module docs](self).
170pub struct CustomScrollView<State: 'static> {
171 slivers: Vec<AnyView<State>>,
172 axis: ScrollAxis,
173 cache_extent: f64,
174 on_scroll: Option<OnScroll<State>>,
175 on_refresh_release: Option<OnRefresh<State>>,
176 physics: Option<Rc<dyn ScrollPhysics>>,
177 effect: OverscrollEffect,
178 controller: Option<ScrollController>,
179}
180
181/// A vertical [`CustomScrollView`] over `slivers`.
182///
183/// `slivers` is any [`ViewSeq`]: a `Vec`/array of views, a tuple of mixed
184/// view types, an `Option` (`None` drops the slot), or `views(iter)`. A child
185/// that is not a sliver is laid out as a box through the
186/// [`sliver_box`](crate::sliver_box) geometry, so plain views mix freely with
187/// slivers.
188///
189/// ```
190/// use frust_widgets::{CustomScrollView, custom_scroll_view, sliver_box, text};
191/// let view: CustomScrollView<()> = custom_scroll_view((text("a"), sliver_box(text("b"))));
192/// # let _ = view;
193/// ```
194pub fn custom_scroll_view<State: 'static, M>(
195 slivers: impl ViewSeq<State, M>,
196) -> CustomScrollView<State> {
197 let mut list = Vec::new();
198 slivers.extend_views(&mut list);
199 CustomScrollView {
200 slivers: list,
201 axis: ScrollAxis::Vertical,
202 cache_extent: DEFAULT_CACHE_EXTENT,
203 on_scroll: None,
204 on_refresh_release: None,
205 physics: None,
206 effect: default_overscroll_effect(),
207 controller: None,
208 }
209}
210
211impl<State: 'static> CustomScrollView<State> {
212 /// Scroll along `axis` — [`ScrollAxis::Vertical`] by default. See the
213 /// module docs' *Axis*.
214 ///
215 /// ```
216 /// use frust_core::ScrollAxis;
217 /// use frust_widgets::{CustomScrollView, custom_scroll_view, text};
218 /// let view: CustomScrollView<()> =
219 /// custom_scroll_view((text("a"), text("b"))).axis(ScrollAxis::Horizontal);
220 /// # let _ = view;
221 /// ```
222 pub fn axis(mut self, axis: ScrollAxis) -> Self {
223 self.axis = axis;
224 self
225 }
226
227 /// Lay out children up to `extent` logical px past each end of the visible
228 /// region ([`DEFAULT_CACHE_EXTENT`] unless set), so content about to
229 /// scroll in is already laid out. A cached child is laid out but not
230 /// painted. A negative, NaN or infinite value is treated as `0.0`.
231 pub fn cache_extent(mut self, extent: f64) -> Self {
232 self.cache_extent = extent;
233 self
234 }
235
236 /// Observe scroll position changes — the same contract as
237 /// [`ScrollView::on_scroll`](crate::ScrollView::on_scroll): fired on each
238 /// input-driven change, one event late for paint-driven motion, and
239 /// flushed on the next frame for a controller jump or tween.
240 pub fn on_scroll<F: Fn(&mut State, ScrollInfo) + 'static>(mut self, callback: F) -> Self {
241 self.on_scroll = Some(Rc::new(callback));
242 self
243 }
244
245 /// The pull-to-refresh trigger — the same contract as
246 /// [`ScrollView::on_refresh_release`](crate::ScrollView::on_refresh_release):
247 /// fires on pointer `Up` when the surface was pulled past its leading edge
248 /// by more than the shared trigger distance, never on a `Cancel`.
249 pub fn on_refresh_release<F: Fn(&mut State) + 'static>(mut self, callback: F) -> Self {
250 self.on_refresh_release = Some(Rc::new(callback));
251 self
252 }
253
254 /// Install a custom [`ScrollPhysics`] — the same build/rebuild contract as
255 /// [`ScrollView::physics`](crate::ScrollView::physics): a view carrying one
256 /// reinstalls it every rebuild, a view with none leaves the installed
257 /// physics alone, and a fresh build starts at the platform default.
258 ///
259 /// ```
260 /// use frust_widgets::{Clamping, CustomScrollView, custom_scroll_view, text};
261 /// let view: CustomScrollView<()> = custom_scroll_view(text("a")).physics(Clamping::new());
262 /// # let _ = view;
263 /// ```
264 pub fn physics(mut self, physics: impl ScrollPhysics + 'static) -> Self {
265 self.physics = Some(Rc::new(physics));
266 self
267 }
268
269 /// Select how past-edge pull is visualized — the same contract as
270 /// [`ScrollView::overscroll_effect`](crate::ScrollView::overscroll_effect).
271 /// Under [`OverscrollEffect::Translate`] the pull moves the slivers (they
272 /// are laid out at the out-of-range position); the other two lay them out
273 /// at the in-range one.
274 pub fn overscroll_effect(mut self, effect: OverscrollEffect) -> Self {
275 self.effect = effect;
276 self
277 }
278
279 /// Attach a [`ScrollController`] — the same build/rebuild contract as
280 /// [`ScrollView::controller`](crate::ScrollView::controller). Item commands
281 /// ([`ScrollController::scroll_to_item`]) are list-only and ignored here.
282 pub fn controller(mut self, controller: ScrollController) -> Self {
283 self.controller = Some(controller);
284 self
285 }
286}
287
288/// What the last layout decided for one child.
289#[derive(Clone, Copy, Debug)]
290struct Slot {
291 /// The geometry the child reported (or the box adapter computed).
292 geometry: SliverGeometry,
293 /// Where the child's paint starts along the scroll axis, in viewport space.
294 paint_offset: f64,
295 /// Whether the child was laid out as a box through the adapter geometry.
296 is_box: bool,
297}
298
299impl Default for Slot {
300 fn default() -> Self {
301 Self {
302 geometry: SliverGeometry::ZERO,
303 paint_offset: 0.0,
304 is_box: false,
305 }
306 }
307}
308
309/// The retained widget for a [`CustomScrollView`]: the children's pods and
310/// what the last layout decided for each, the scroll position, and the shared
311/// scroll core (the module docs).
312pub struct ViewportWidget {
313 children: Vec<ChildPod>,
314 slots: Vec<Slot>,
315 axis: ScrollAxis,
316 /// The view's cache extent, sanitised to a finite non-negative value.
317 cache_extent: f64,
318 /// The effective scroll position, past-edge displacement included —
319 /// `ScrollWidget`'s storage shape: a drag past an edge lets it run out of
320 /// range, and the clamped paths go through [`ViewportWidget::set_offset`].
321 offset: f64,
322 /// This widget's own size from the last layout.
323 viewport: Size,
324 /// The main-axis extent children were laid out against: the incoming
325 /// constraint, or `+∞` when shrink-wrapping.
326 main_extent: f64,
327 /// The cross-axis extent children were laid out against.
328 cross_extent: f64,
329 /// The finite main-axis extent the viewport occupies — what the physics
330 /// reads as the viewport dimension.
331 viewport_main: f64,
332 /// The largest in-range position from the last completed layout.
333 max_extent: f64,
334 /// The position the last layout laid the children out at
335 /// ([`ViewportWidget::layout_pixels`] at the time).
336 laid_out_pixels: f64,
337 /// The direction the position last moved in between two layouts.
338 direction: ScrollDirection,
339 /// Set for exactly the primary `Down` forward, so the forward installs this
340 /// viewport's axis frame around it (the module docs' *Axis*).
341 arming_down: bool,
342 /// The axis a nested viewport reported when it registered into this
343 /// viewport's claim cell on the last `Down` — `None` when nothing did, or
344 /// when the registrant was a (vertical) `ScrollView`/`ListView`.
345 down_inner_axis: Option<ScrollAxis>,
346 core: ScrollSurfaceCore,
347}
348
349impl ViewportWidget {
350 fn new(children: Vec<ChildPod>, axis: ScrollAxis, cache_extent: f64) -> Self {
351 let slots = vec![Slot::default(); children.len()];
352 Self {
353 children,
354 slots,
355 axis,
356 cache_extent: finite_extent(cache_extent),
357 offset: 0.0,
358 viewport: Size::ZERO,
359 main_extent: 0.0,
360 cross_extent: 0.0,
361 viewport_main: 0.0,
362 max_extent: 0.0,
363 laid_out_pixels: 0.0,
364 direction: ScrollDirection::Idle,
365 arming_down: false,
366 down_inner_axis: None,
367 core: ScrollSurfaceCore::with_axis(axis),
368 }
369 }
370
371 /// The current scroll position (in range unless a drag, settle or
372 /// simulation holds it past an edge).
373 pub fn offset(&self) -> f64 {
374 self.offset
375 }
376
377 /// The largest in-range scroll position: the children's summed scroll
378 /// extent less the viewport extent, never negative.
379 pub fn max_offset(&self) -> f64 {
380 self.max_extent
381 }
382
383 /// The axis this viewport scrolls along.
384 pub fn axis(&self) -> ScrollAxis {
385 self.core.axis()
386 }
387
388 /// How many children the viewport holds.
389 pub fn sliver_count(&self) -> usize {
390 self.children.len()
391 }
392
393 /// The geometry child `index` reported from the last layout — the box
394 /// adapter's geometry for a child laid out as a box — or `None` past the
395 /// last child.
396 pub fn sliver_geometry(&self, index: usize) -> Option<SliverGeometry> {
397 self.slots.get(index).map(|slot| slot.geometry)
398 }
399
400 /// Whether post-release motion is in flight (a fling or a simulation).
401 pub fn is_flinging(&self) -> bool {
402 self.core.is_flinging()
403 }
404
405 fn set_offset(&mut self, value: f64) {
406 self.offset = value.clamp(0.0, self.max_extent);
407 }
408
409 /// The position children are laid out at: the live position under
410 /// [`OverscrollEffect::Translate`], the in-range one under the other two
411 /// effects (which paint the pull differently or not at all). Never NaN.
412 fn layout_pixels(&self) -> f64 {
413 let pixels = match self.core.effect {
414 OverscrollEffect::Translate => self.offset,
415 OverscrollEffect::Stretch | OverscrollEffect::None => self.offset - self.displacement(),
416 };
417 if pixels.is_finite() { pixels } else { 0.0 }
418 }
419
420 /// Whether user input owns the current motion — a drag, a release fling,
421 /// settle or simulation — rather than a controller tween or nothing.
422 fn user_motion(&self) -> bool {
423 let core = &self.core;
424 core.scrolling
425 || core.settling
426 || core.fling.is_some()
427 || (core.ballistic.is_some() && !core.controller_animating)
428 }
429
430 /// The `user_scroll_direction` children are laid out with.
431 fn user_scroll_direction(&self) -> ScrollDirection {
432 if self.user_motion() {
433 self.direction
434 } else {
435 ScrollDirection::Idle
436 }
437 }
438
439 /// Record which way the position moved since the last layout.
440 fn observe_direction(&mut self) {
441 let pixels = self.layout_pixels();
442 if pixels < self.laid_out_pixels {
443 self.direction = ScrollDirection::Forward;
444 } else if pixels > self.laid_out_pixels {
445 self.direction = ScrollDirection::Reverse;
446 }
447 }
448
449 /// Run the layout loop, restarting it for each correction a child
450 /// reports, at most [`MAX_LAYOUT_CYCLES`] times.
451 fn layout_slivers(&mut self, ctx: &mut LayoutCtx) {
452 for cycle in 0..MAX_LAYOUT_CYCLES {
453 let last = cycle + 1 == MAX_LAYOUT_CYCLES;
454 match self.attempt_layout(ctx, !last) {
455 Some(correction) => self.apply_correction(correction),
456 None => return,
457 }
458 }
459 }
460
461 /// Shift the position by a child's `correction` before the loop restarts.
462 /// A live drag's accumulator moves with it; a release simulation restarts
463 /// from the corrected position at its current velocity (its curve is in
464 /// absolute positions and would otherwise drag the content straight back);
465 /// a controller tween keeps its absolute target.
466 fn apply_correction(&mut self, correction: f64) {
467 if !correction.is_finite() {
468 return;
469 }
470 self.offset += correction;
471 self.core.drag_position += correction;
472 if self.core.controller_animating {
473 return;
474 }
475 let Some(velocity) = self
476 .core
477 .ballistic
478 .as_ref()
479 .map(|state| state.sim.dx(state.elapsed_secs(self.core.last_frame_time)))
480 else {
481 return;
482 };
483 let metrics = self.metrics();
484 match self
485 .core
486 .physics
487 .create_ballistic_simulation(&metrics, velocity)
488 {
489 Some(sim) => self.core.ballistic = Some(BallisticState { sim, start: None }),
490 None => self.core.stop_ballistic(),
491 }
492 }
493
494 /// One pass of the layout loop (the module docs' *The layout loop*).
495 /// Returns the correction that ended it early, if any — never on the
496 /// final cycle (`allow_correction` false), which lays every child out and
497 /// keeps the result.
498 fn attempt_layout(&mut self, ctx: &mut LayoutCtx, allow_correction: bool) -> Option<f64> {
499 let axis = self.axis;
500 let main = self.main_extent;
501 let cross = self.cross_extent;
502 let cache = self.cache_extent;
503 let pixels = self.layout_pixels();
504 let direction = self.user_scroll_direction();
505
506 // The viewport's leading edge is its anchor: content starts `pixels`
507 // before it.
508 let center_offset = -pixels;
509 let reverse_remaining_paint = center_offset.max(0.0).min(main);
510 let remaining_paint = (main - center_offset).max(0.0).min(main);
511 let mut scroll_offset = pixels.max(0.0);
512 let mut layout_offset = if center_offset >= main {
513 center_offset
514 } else {
515 reverse_remaining_paint
516 };
517 let initial_layout_offset = layout_offset;
518 let mut max_paint_offset = layout_offset + pixels.min(0.0);
519 let mut preceding = 0.0;
520
521 for (pod, slot) in self.children.iter_mut().zip(self.slots.iter_mut()) {
522 let sliver_scroll_offset = scroll_offset.max(0.0);
523 // The cache window is `[pixels − cache, pixels + main + cache)` in
524 // scroll terms; in this child's own terms it starts `cache` before
525 // its scroll offset (never before its own start) and runs from
526 // there to the window's end.
527 let cache_origin = (-cache).max(-sliver_scroll_offset);
528 let remaining_cache =
529 scroll_offset + main + cache - sliver_scroll_offset - cache_origin;
530 let sc = sanitized(SliverConstraints {
531 axis,
532 scroll_offset: sliver_scroll_offset,
533 preceding_scroll_extent: preceding,
534 overlap: max_paint_offset - layout_offset,
535 remaining_paint_extent: remaining_paint - layout_offset + initial_layout_offset,
536 cross_axis_extent: cross,
537 viewport_main_axis_extent: main,
538 cache_origin,
539 remaining_cache_extent: remaining_cache,
540 user_scroll_direction: direction,
541 });
542 let (geometry, is_box) = match pod.layout_sliver_child(ctx, &sc) {
543 Some(geometry) => (geometry, false),
544 None => {
545 let size = pod.layout_child(ctx, &sc.as_box_constraints());
546 (box_geometry(axis.main_extent(size), &sc), true)
547 }
548 };
549 if let Some(correction) = geometry.scroll_offset_correction {
550 if allow_correction {
551 return Some(correction);
552 }
553 debug_assert!(
554 false,
555 "CustomScrollView: a sliver still asked for a scroll-offset correction \
556 ({correction}) after {MAX_LAYOUT_CYCLES} layout cycles"
557 );
558 }
559 let effective_offset = layout_offset + geometry.paint_origin;
560 let paint_offset = if geometry.visible || scroll_offset > 0.0 {
561 effective_offset
562 } else {
563 -scroll_offset + initial_layout_offset
564 };
565 let origin = axis_point(axis, paint_offset, 0.0);
566 pod.set_origin(if is_box {
567 origin + box_child_origin(&sc).to_vec2()
568 } else {
569 origin
570 });
571 *slot = Slot {
572 geometry,
573 paint_offset,
574 is_box,
575 };
576 max_paint_offset = (effective_offset + geometry.paint_extent).max(max_paint_offset);
577 scroll_offset -= geometry.scroll_extent;
578 preceding += geometry.scroll_extent;
579 layout_offset += geometry.layout_extent;
580 }
581
582 let total = finite_extent(preceding);
583 if main.is_finite() {
584 self.viewport_main = main;
585 self.max_extent = (total - main).max(0.0);
586 } else {
587 // Shrink-wrapping: the viewport is as long as its content.
588 self.viewport_main = total;
589 self.max_extent = 0.0;
590 }
591 self.laid_out_pixels = pixels;
592 None
593 }
594
595 /// Whether `position` (viewport space) falls inside child `index`'s hit
596 /// region: from its paint start for its hit-test extent, inside the
597 /// viewport.
598 fn hits(&self, index: usize, position: Point) -> bool {
599 let Some(slot) = self.slots.get(index) else {
600 return false;
601 };
602 let main = along(self.axis, position);
603 let across = along(transpose_axis(self.axis), position);
604 let local = main - slot.paint_offset;
605 main >= 0.0
606 && main < self.viewport_main
607 && across >= 0.0
608 && across < self.cross_extent
609 && local >= 0.0
610 && local < slot.geometry.hit_test_extent
611 }
612
613 /// Route a content event to the children: a broadcast, a focus-routed
614 /// event or a captured gesture through
615 /// [`crate::authoring::route_event`]'s own paths; anything else hit-tested
616 /// first child to last against each child's hit region
617 /// ([`ViewportWidget::hits`]), with `route_event`'s blur-on-outside-`Down`
618 /// sweep.
619 fn route_to_slivers(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
620 if event.is_broadcast()
621 || event.is_focus_routed()
622 || self.children.iter().any(ChildPod::is_active)
623 {
624 return crate::authoring::route_event(&mut self.children, ctx, event);
625 }
626 let position = event.position();
627 let mut handled = EventResult::Ignored;
628 let mut kept_focus = None;
629 for index in 0..self.children.len() {
630 if self.hits(index, position)
631 && self.children[index].event_child(ctx, event) == EventResult::Handled
632 {
633 if self.children[index].holds_live_focus() {
634 kept_focus = Some(index);
635 }
636 handled = EventResult::Handled;
637 break;
638 }
639 }
640 if matches!(event, InputEvent::Pointer(p) if p.phase == PointerPhase::Down) {
641 for (index, pod) in self.children.iter_mut().enumerate() {
642 if Some(index) != kept_focus && pod.is_focused() {
643 pod.set_focused(false);
644 }
645 }
646 }
647 handled
648 }
649
650 /// Hand a pointer event to the core, with the axis-aware claim rule around
651 /// a primary `Down` (the module docs' *Axis*).
652 fn pointer_on_axis(
653 &mut self,
654 ctx: &mut EventCtx,
655 event: &InputEvent,
656 p: &PointerEvent,
657 t_ms: f64,
658 ) -> EventResult {
659 let primary_down = p.phase == PointerPhase::Down && presses(p);
660 // A registration this viewport must not leave in its host's cell: the
661 // host scrolls along the other axis.
662 let mut restore = None;
663 if primary_down && let Some(host) = ambient_scroll_claim() {
664 match enclosing_viewport(&host) {
665 Some(frame) if frame.axis == self.axis => frame.inner_axis.set(Some(self.axis)),
666 Some(_) => restore = Some((host.get(), host)),
667 // The enclosing surface is a `ScrollView` or `ListView`.
668 None if self.axis == ScrollAxis::Vertical => {}
669 None => restore = Some((host.get(), host)),
670 }
671 }
672 self.arming_down = primary_down;
673 if primary_down {
674 self.down_inner_axis = None;
675 }
676 let result = self.handle_pointer(ctx, event, p, t_ms);
677 self.arming_down = false;
678 if let Some((saved, host)) = restore {
679 host.set(saved);
680 }
681 if primary_down
682 && self.core.inner_at_down.registered
683 && self.down_inner_axis.unwrap_or(ScrollAxis::Vertical) != self.axis
684 {
685 self.core.inner_at_down = InnerScrollState::default();
686 }
687 result
688 }
689
690 /// The event entry, parameterised on an explicit timestamp so velocity
691 /// math is deterministic in tests; [`Widget::event`] supplies the real
692 /// clock. Publishes whatever position the event left behind.
693 fn event_at(&mut self, ctx: &mut EventCtx, event: &InputEvent, t_ms: f64) -> EventResult {
694 let result = self.route_event_at(ctx, event, t_ms);
695 self.publish_to_controller();
696 result
697 }
698
699 /// The event body behind [`ViewportWidget::event_at`]: content events
700 /// are routed here, the wheel and pointer gesture by the shared core.
701 fn route_event_at(&mut self, ctx: &mut EventCtx, event: &InputEvent, t_ms: f64) -> EventResult {
702 if !matches!(
703 event,
704 InputEvent::Pointer(p) if p.phase == PointerPhase::Cancel
705 ) {
706 self.deliver_pending_scroll(ctx);
707 }
708 match event {
709 InputEvent::Scroll { delta, .. } => self.handle_wheel(ctx, delta),
710 InputEvent::Pointer(p) => self.pointer_on_axis(ctx, event, p, t_ms),
711 // Broadcasts, focus-routed events and anything hit-tested (a
712 // `Scale`, a file drop) belong to the content.
713 _ => self.route_to_slivers(ctx, event),
714 }
715 }
716
717 /// The stretch overscroll transform for the viewport at `origin`/`size`
718 /// (absolute), along this viewport's axis.
719 fn stretch_transform(&self, origin: Point, size: Size) -> Option<Affine> {
720 match self.axis {
721 ScrollAxis::Vertical => self.core.stretch_transform(origin, size),
722 ScrollAxis::Horizontal => {
723 let swap = Affine::new([0.0, 1.0, 1.0, 0.0, 0.0, 0.0]);
724 self.core
725 .stretch_transform(transpose(origin), Size::new(size.height, size.width))
726 .map(|transform| swap * transform * swap)
727 }
728 }
729 }
730}
731
732/// `sc` with every caller-derived extent made safe for
733/// [`SliverConstraints::debug_assert_valid`]: NaN dropped, negatives clamped
734/// to zero (infinity kept), the cache origin kept non-positive.
735fn sanitized(sc: SliverConstraints) -> SliverConstraints {
736 let not_nan = |value: f64| if value.is_nan() { 0.0 } else { value };
737 SliverConstraints {
738 scroll_offset: non_negative(sc.scroll_offset),
739 preceding_scroll_extent: not_nan(sc.preceding_scroll_extent),
740 overlap: not_nan(sc.overlap),
741 remaining_paint_extent: non_negative(sc.remaining_paint_extent),
742 cross_axis_extent: non_negative(sc.cross_axis_extent),
743 viewport_main_axis_extent: non_negative(sc.viewport_main_axis_extent),
744 cache_origin: not_nan(sc.cache_origin).min(0.0),
745 remaining_cache_extent: non_negative(sc.remaining_cache_extent),
746 ..sc
747 }
748}
749
750/// `point`'s component along `axis`.
751fn along(axis: ScrollAxis, point: Point) -> f64 {
752 match axis {
753 ScrollAxis::Vertical => point.y,
754 ScrollAxis::Horizontal => point.x,
755 }
756}
757
758/// The other axis.
759fn transpose_axis(axis: ScrollAxis) -> ScrollAxis {
760 match axis {
761 ScrollAxis::Vertical => ScrollAxis::Horizontal,
762 ScrollAxis::Horizontal => ScrollAxis::Vertical,
763 }
764}
765
766/// `point` with its two components swapped.
767fn transpose(point: Point) -> Point {
768 Point::new(point.y, point.x)
769}
770
771// --- Axis-aware nested claims ------------------------------------------------
772
773/// A viewport currently forwarding a primary `Down`: the claim cell it pushed
774/// around that forward, its axis, and the axis of the nested viewport that
775/// registered into that cell (if one did).
776struct AxisFrame {
777 claim: Rc<Cell<InnerScrollState>>,
778 axis: ScrollAxis,
779 inner_axis: Cell<Option<ScrollAxis>>,
780}
781
782thread_local! {
783 /// The viewports currently forwarding a primary `Down`, innermost last —
784 /// what lets a nested viewport learn its enclosing surface's axis (the
785 /// module docs' *Axis*). UI-thread-affine like the claim stack it shadows.
786 static VIEWPORT_FRAMES: RefCell<Vec<Rc<AxisFrame>>> = const { RefCell::new(Vec::new()) };
787}
788
789/// Pops [`VIEWPORT_FRAMES`] on drop, so an unwinding dispatch cannot leave a
790/// stale frame behind.
791struct AxisFrameGuard;
792
793impl Drop for AxisFrameGuard {
794 fn drop(&mut self) {
795 VIEWPORT_FRAMES.with(|frames| {
796 frames.borrow_mut().pop();
797 });
798 }
799}
800
801/// Run `f` with `frame` installed as the innermost viewport frame.
802fn with_axis_frame<R>(frame: &Rc<AxisFrame>, f: impl FnOnce() -> R) -> R {
803 VIEWPORT_FRAMES.with(|frames| frames.borrow_mut().push(Rc::clone(frame)));
804 let _guard = AxisFrameGuard;
805 f()
806}
807
808/// The viewport whose claim cell is `host`, when `host` belongs to one — the
809/// innermost frame, if it pushed that very cell. `None` means the enclosing
810/// surface is a `ScrollView` or a `ListView`.
811fn enclosing_viewport(host: &Rc<Cell<InnerScrollState>>) -> Option<Rc<AxisFrame>> {
812 VIEWPORT_FRAMES.with(|frames| {
813 frames
814 .borrow()
815 .last()
816 .filter(|frame| Rc::ptr_eq(&frame.claim, host))
817 .cloned()
818 })
819}
820
821/// The viewport owner side of the core: one effective `offset`, exactly
822/// `ScrollWidget`'s storage shape, with the extent the layout loop measured.
823impl ScrollSurface for ViewportWidget {
824 fn core(&self) -> &ScrollSurfaceCore {
825 &self.core
826 }
827
828 fn core_mut(&mut self) -> &mut ScrollSurfaceCore {
829 &mut self.core
830 }
831
832 fn max_scroll_extent(&self) -> f64 {
833 self.max_extent
834 }
835
836 fn viewport_extent(&self) -> f64 {
837 self.viewport_main
838 }
839
840 fn base_offset(&self) -> f64 {
841 self.offset
842 }
843
844 fn set_base_offset(&mut self, value: f64) {
845 self.set_offset(value);
846 }
847
848 fn pixels(&self) -> f64 {
849 self.offset
850 }
851
852 fn displacement(&self) -> f64 {
853 self.offset - self.offset.clamp(0.0, self.max_extent)
854 }
855
856 fn set_allowed_position(&mut self, allowed: f64) {
857 self.offset = allowed;
858 }
859
860 fn decay_displacement(&mut self, retained: f64) {
861 let target = self.offset.clamp(0.0, self.max_extent);
862 let remaining = target - self.offset;
863 self.offset = target - remaining * retained;
864 }
865
866 fn settle_to_edge(&mut self) {
867 self.set_offset(self.offset);
868 }
869
870 /// Nothing to do here: placement is a layout product (the module docs'
871 /// *Placement is a layout product*). A position moved by input dirties
872 /// this pod through the handled event; one moved at paint requests the
873 /// layout from [`Widget::paint`].
874 fn sync_origins(&mut self) {}
875
876 fn scroll_info(&self) -> ScrollInfo {
877 ScrollInfo {
878 offset: self.offset.clamp(0.0, self.max_extent),
879 max_offset: self.max_extent,
880 overscroll: self.displacement(),
881 }
882 }
883
884 fn forward_pointer(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
885 if !std::mem::take(&mut self.arming_down) {
886 return self.route_to_slivers(ctx, event);
887 }
888 // The primary `Down`'s forward: the core has just pushed this
889 // viewport's claim cell, so it is the ambient one.
890 let Some(claim) = ambient_scroll_claim() else {
891 return self.route_to_slivers(ctx, event);
892 };
893 let frame = Rc::new(AxisFrame {
894 claim,
895 axis: self.axis,
896 inner_axis: Cell::new(None),
897 });
898 let result = with_axis_frame(&frame, || self.route_to_slivers(ctx, event));
899 self.down_inner_axis = frame.inner_axis.get();
900 result
901 }
902
903 fn take_over_from_content(&mut self, ctx: &mut EventCtx, position: Point) {
904 let cancel = InputEvent::Pointer(PointerEvent {
905 phase: PointerPhase::Cancel,
906 position,
907 button: PointerButton::Primary,
908 });
909 // A takeover, not the gesture's end: the captured child gets its
910 // `Cancel` and is then released through the context, which also ends
911 // a contact opt-in held inside it.
912 if let Some(pod) = self.children.iter_mut().find(|pod| pod.is_active()) {
913 pod.event_child(ctx, &cancel);
914 ctx.release_captured_child(pod);
915 } else {
916 crate::authoring::route_event(&mut self.children, ctx, &cancel);
917 }
918 }
919}
920
921impl<State: 'static> View<State> for CustomScrollView<State> {
922 type Element = ViewportWidget;
923
924 fn build(&self, ctx: &mut BuildCtx<'_>) -> ViewportWidget {
925 let children = self
926 .slivers
927 .iter()
928 .map(|view| crate::authoring::build_child(view, ctx))
929 .collect();
930 let mut widget = ViewportWidget::new(children, self.axis, self.cache_extent);
931 widget.core.on_scroll = self
932 .on_scroll
933 .as_ref()
934 .map(crate::authoring::erase_callback_arg);
935 widget.core.on_refresh_release = self
936 .on_refresh_release
937 .as_ref()
938 .map(crate::authoring::erase_callback);
939 widget.core.install(self.physics.as_ref(), self.effect);
940 widget.core.attach_controller(self.controller.as_ref());
941 widget
942 }
943
944 fn rebuild(
945 &self,
946 prev: &Self,
947 element: &mut ViewportWidget,
948 ctx: &mut BuildCtx<'_>,
949 ) -> ChangeFlags {
950 // Closures are not comparable — always reinstall the erased adapters.
951 element.core.on_scroll = self
952 .on_scroll
953 .as_ref()
954 .map(crate::authoring::erase_callback_arg);
955 element.core.on_refresh_release = self
956 .on_refresh_release
957 .as_ref()
958 .map(crate::authoring::erase_callback);
959 element.core.install(self.physics.as_ref(), self.effect);
960 let mut flags = crate::authoring::rebuild_children(
961 &prev.slivers,
962 &self.slivers,
963 &mut element.children,
964 ctx,
965 |view| view,
966 |_| None,
967 );
968 element
969 .slots
970 .resize(element.children.len(), Slot::default());
971 let cache_extent = finite_extent(self.cache_extent);
972 if element.axis != self.axis || element.cache_extent != cache_extent {
973 element.axis = self.axis;
974 element.core.set_axis(self.axis);
975 element.cache_extent = cache_extent;
976 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
977 }
978 // A new binding publishes, and a queued command applies, at the next
979 // layout or paint — make sure a frame-gated shell runs one.
980 if element.core.attach_controller(self.controller.as_ref()) {
981 flags |= ChangeFlags::PAINT;
982 }
983 // A queued command moves the children when it applies, and the layout
984 // drain is where it applies first: report LAYOUT so the layout skip
985 // runs that drain (`ScrollView`'s rebuild does the same).
986 if element.core.has_pending_commands() {
987 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
988 }
989 flags
990 }
991
992 fn teardown(&self, element: &mut ViewportWidget, ctx: &mut BuildCtx<'_>) {
993 for (view, pod) in self.slivers.iter().zip(element.children.iter_mut()) {
994 crate::authoring::teardown_child(view, pod, ctx);
995 }
996 }
997}
998
999impl Widget for ViewportWidget {
1000 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1001 let max = bc.max();
1002 self.cross_extent = finite_extent(self.axis.cross_extent(max));
1003 self.main_extent = non_negative(self.axis.main_extent(max));
1004 self.observe_direction();
1005 self.layout_slivers(ctx);
1006 // The content's extent may have moved under a resting position: clamp
1007 // it, as `ScrollView` does — never while a drag, settle or simulation
1008 // legitimately holds it past an edge.
1009 let core = &self.core;
1010 if !core.scrolling && !core.settling && core.ballistic.is_none() {
1011 self.set_offset(self.offset);
1012 if self.layout_pixels() != self.laid_out_pixels {
1013 self.layout_slivers(ctx);
1014 }
1015 }
1016 // A controller command clamps against the extent just measured.
1017 self.core.laid_out = true;
1018 let reduce_motion = Theme::from_layout_ctx(ctx)
1019 .map(|t| t.motion.reduce_motion)
1020 .unwrap_or(false);
1021 self.drain_controller(reduce_motion);
1022 if self.layout_pixels() != self.laid_out_pixels {
1023 self.layout_slivers(ctx);
1024 }
1025 self.publish_to_controller();
1026 self.viewport = self.axis.size(self.viewport_main, self.cross_extent);
1027 bc.constrain(self.viewport)
1028 }
1029
1030 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
1031 // Record the frame clock, apply a command recorded since layout, pump
1032 // post-release motion and publish — see `ScrollSurface::paint_motion`.
1033 let reduce_motion = Theme::from_paint_ctx(ctx)
1034 .map(|t| t.motion.reduce_motion)
1035 .unwrap_or(false);
1036 self.paint_motion(ctx, reduce_motion);
1037 // Whatever moved the position here is shown by the next layout.
1038 if self.layout_pixels() != self.laid_out_pixels {
1039 ctx.request_layout();
1040 }
1041 let origin = ctx.origin();
1042 scene.push_clip(origin, ctx.size());
1043 ctx.constrain_visible_rect(Rect::from_origin_size(origin, ctx.size()));
1044 // Paint-only, inside the viewport clip, exactly as `ScrollView`'s.
1045 let stretch = self.stretch_transform(origin, ctx.size());
1046 if let Some(transform) = stretch {
1047 scene.push_transform(transform);
1048 }
1049 let axis = self.axis;
1050 let cross = self.cross_extent;
1051 for (pod, slot) in self.children.iter_mut().zip(&self.slots).rev() {
1052 if !slot.geometry.visible {
1053 continue;
1054 }
1055 // The box adapter clips a cut child to its paint extent; a sliver
1056 // clips itself.
1057 let clip = slot.is_box && slot.geometry.has_visual_overflow;
1058 if clip {
1059 let start = origin + axis_point(axis, slot.paint_offset, 0.0).to_vec2();
1060 scene.push_clip(start, axis.size(slot.geometry.paint_extent, cross));
1061 }
1062 pod.paint_child(ctx, scene);
1063 if clip {
1064 scene.pop_clip();
1065 }
1066 }
1067 if stretch.is_some() {
1068 scene.pop_transform();
1069 }
1070 scene.pop_clip();
1071 }
1072
1073 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1074 let t = self.core.event_time_ms();
1075 self.event_at(ctx, event, t)
1076 }
1077
1078 fn semantics(&self, ctx: &mut SemanticsCtx) {
1079 let offset = self.offset;
1080 let max_offset = self.max_extent;
1081 let axis = self.axis;
1082 ctx.push_container(
1083 Role::ScrollView,
1084 move |node| match axis {
1085 ScrollAxis::Vertical => {
1086 node.set_scroll_y(offset);
1087 node.set_scroll_y_min(0.0);
1088 node.set_scroll_y_max(max_offset);
1089 }
1090 ScrollAxis::Horizontal => {
1091 node.set_scroll_x(offset);
1092 node.set_scroll_x_min(0.0);
1093 node.set_scroll_x_max(max_offset);
1094 }
1095 },
1096 |ctx| {
1097 // Visible and cached children, so assistive scrolling can
1098 // reach what is just past the edge.
1099 for (pod, slot) in self.children.iter().zip(&self.slots) {
1100 if slot.geometry.visible || slot.geometry.cache_extent > 0.0 {
1101 pod.semantics_child(ctx);
1102 }
1103 }
1104 },
1105 );
1106 }
1107
1108 crate::authoring::visit_children!(children);
1109}
1110
1111#[cfg(test)]
1112mod tests {
1113 use super::*;
1114 use frust_core::{FrameTime, RenderRoot};
1115 use std::any::Any;
1116
1117 use crate::test_support::{RecordingScene, leaf};
1118
1119 const WINDOW: Size = Size::new(400.0, 500.0);
1120
1121 fn widget(root: &RenderRoot<(), CustomScrollView<()>>) -> &ViewportWidget {
1122 let id = root.root_id().expect("root built");
1123 (root.tree().pod(id).expect("root pod").widget() as &dyn Any)
1124 .downcast_ref::<ViewportWidget>()
1125 .expect("root is a ViewportWidget")
1126 }
1127
1128 fn mount(
1129 logic: &mut impl FnMut(&mut ()) -> CustomScrollView<()>,
1130 ) -> RenderRoot<(), CustomScrollView<()>> {
1131 let mut root = RenderRoot::new();
1132 root.rebuild(logic, &mut ());
1133 root.layout(WINDOW);
1134 root.paint(&mut RecordingScene::default(), FrameTime::ZERO);
1135 root
1136 }
1137
1138 #[test]
1139 fn sanitising_drops_nan_and_negative_extents() {
1140 let sc = sanitized(SliverConstraints {
1141 axis: ScrollAxis::Vertical,
1142 scroll_offset: f64::NAN,
1143 preceding_scroll_extent: f64::NAN,
1144 overlap: f64::NAN,
1145 remaining_paint_extent: -3.0,
1146 cross_axis_extent: f64::NAN,
1147 viewport_main_axis_extent: -1.0,
1148 cache_origin: 5.0,
1149 remaining_cache_extent: f64::NAN,
1150 user_scroll_direction: ScrollDirection::Idle,
1151 });
1152 sc.debug_assert_valid();
1153 assert_eq!(sc.remaining_paint_extent, 0.0);
1154 assert_eq!(sc.cache_origin, 0.0);
1155 }
1156
1157 #[test]
1158 fn a_zero_window_lays_out_without_tripping_the_constraint_checks() {
1159 let mut logic = |_: &mut ()| custom_scroll_view((leaf(100.0, 100.0), leaf(100.0, 50.0)));
1160 let mut root = RenderRoot::new();
1161 root.rebuild(&mut logic, &mut ());
1162 root.layout(Size::ZERO);
1163 let w = widget(&root);
1164 assert_eq!(w.max_offset(), 150.0);
1165 assert!(!w.sliver_geometry(0).expect("child 0").visible);
1166 }
1167
1168 #[test]
1169 fn an_unbounded_main_axis_shrink_wraps() {
1170 let mut logic = |_: &mut ()| custom_scroll_view((leaf(100.0, 100.0), leaf(100.0, 50.0)));
1171 let mut root = RenderRoot::new();
1172 root.rebuild(&mut logic, &mut ());
1173 root.layout(Size::new(400.0, f64::INFINITY));
1174 let w = widget(&root);
1175 assert_eq!(w.viewport.height, 150.0);
1176 assert_eq!(w.max_offset(), 0.0);
1177 assert_eq!(w.sliver_geometry(1).expect("child 1").paint_extent, 50.0);
1178 }
1179
1180 #[test]
1181 fn plain_box_children_are_adapted_and_placed_in_sequence() {
1182 let mut logic = |_: &mut ()| {
1183 custom_scroll_view((leaf(400.0, 300.0), leaf(400.0, 300.0), leaf(400.0, 300.0)))
1184 .cache_extent(0.0)
1185 };
1186 let root = mount(&mut logic);
1187 let w = widget(&root);
1188 assert_eq!(w.max_offset(), 400.0);
1189 let g = |i| w.sliver_geometry(i).expect("child");
1190 assert_eq!(g(0).paint_extent, 300.0);
1191 assert_eq!(g(1).paint_extent, 200.0);
1192 assert!(g(1).has_visual_overflow);
1193 assert!(!g(2).visible);
1194 assert_eq!(w.slots[1].paint_offset, 300.0);
1195 assert!(w.slots.iter().all(|slot| slot.is_box));
1196 }
1197}