Skip to main content

inset_rendering/
sliver_persistent_header.rs

1//! Flutter counterpart: `rendering/sliver_persistent_header.dart`.
2//!
3//! Semantics wait; see `PORTING.md`.
4
5use std::rc::Rc;
6use std::time::Duration;
7
8use inset_animation::{
9    Animatable, AnimationController, AnimationStatus, AnyAnimation, Curve, CurveTween, Curves,
10    Tween,
11};
12use inset_embedder::{Matrix4, Offset, Rect, clamp_double};
13use inset_foundation::{App, AsyncCallback, Handle, HandleId, ListenableObject, Listener};
14use inset_painting::{Axis, AxisDirection, transform_rect};
15use inset_scheduler::{Ticker, TickerCallback, TickerProvider, TickerProviderObject};
16
17use crate::box_::{AnyRenderBox, BoxHitTestResult};
18use crate::object::{
19    AnyRenderObject, RenderHandle, RenderObjectBase, RenderObjectWithChildMixin, resolve,
20};
21use crate::painting_context::PaintingContext;
22use crate::sliver::{
23    RenderSliver, RenderSliverHelpers, SliverGeometry, SliverHitTestResult,
24    apply_growth_direction_to_axis_direction, calculate_cache_offset,
25};
26use crate::viewport_offset::ScrollDirection;
27
28/// Trims the specified edges of the given `Rect`, so that they do not exceed the given values.
29///
30/// Flutter's private `_trim`; the omitted Dart defaults are the unbounded edges.
31fn trim(original: Option<Rect>, top: f64, right: f64, bottom: f64, left: f64) -> Option<Rect> {
32    original.map(|rect| rect.intersect(Rect::from_ltrb(left, top, right, bottom)))
33}
34
35/// An erased [`TickerProvider`] a render object can hold as a field, compared by identity.
36///
37/// Dart's `TickerProvider? vsync` field.
38#[derive(Clone)]
39pub struct TickerProviderRef(Rc<dyn TickerProvider>);
40
41impl TickerProviderRef {
42    /// Erases an arena object that vends tickers.
43    pub fn new<T: TickerProviderObject>(provider: Handle<T>) -> TickerProviderRef {
44        TickerProviderRef(Rc::new(provider))
45    }
46}
47
48impl TickerProvider for TickerProviderRef {
49    fn create_ticker(&self, app: &mut App, on_tick: TickerCallback) -> Handle<Ticker> {
50        self.0.create_ticker(app, on_tick)
51    }
52}
53
54impl PartialEq for TickerProviderRef {
55    fn eq(&self, other: &TickerProviderRef) -> bool {
56        Rc::ptr_eq(&self.0, &other.0)
57    }
58}
59
60impl std::fmt::Debug for TickerProviderRef {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        f.write_str("TickerProviderRef")
63    }
64}
65
66/// Specifies how a stretched header is to trigger a callback.
67#[derive(Clone)]
68pub struct OverScrollHeaderStretchConfiguration {
69    /// The offset of overscroll required to trigger the
70    /// [`on_stretch_trigger`](Self::on_stretch_trigger).
71    pub stretch_trigger_offset: f64,
72
73    /// The callback function to be executed when a user over-scrolls to the offset specified
74    /// by [`stretch_trigger_offset`](Self::stretch_trigger_offset).
75    pub on_stretch_trigger: Option<AsyncCallback>,
76}
77
78impl OverScrollHeaderStretchConfiguration {
79    /// Creates an object that specifies how a stretched header may activate an
80    /// [`AsyncCallback`].
81    ///
82    /// Dart's default `stretch_trigger_offset` is 100.0.
83    pub fn new() -> OverScrollHeaderStretchConfiguration {
84        OverScrollHeaderStretchConfiguration {
85            stretch_trigger_offset: 100.0,
86            on_stretch_trigger: None,
87        }
88    }
89
90    /// Sets [`stretch_trigger_offset`](Self::stretch_trigger_offset).
91    pub fn stretch_trigger_offset(mut self, value: f64) -> OverScrollHeaderStretchConfiguration {
92        self.stretch_trigger_offset = value;
93        self
94    }
95
96    /// Sets [`on_stretch_trigger`](Self::on_stretch_trigger).
97    pub fn on_stretch_trigger(
98        mut self,
99        value: AsyncCallback,
100    ) -> OverScrollHeaderStretchConfiguration {
101        self.on_stretch_trigger = Some(value);
102        self
103    }
104}
105
106impl Default for OverScrollHeaderStretchConfiguration {
107    fn default() -> OverScrollHeaderStretchConfiguration {
108        OverScrollHeaderStretchConfiguration::new()
109    }
110}
111
112/// Specifies how a pinned header or a floating header should react to
113/// [`crate::RenderObject::show_on_screen`] calls.
114#[derive(Clone, Copy, Debug, PartialEq)]
115pub struct PersistentHeaderShowOnScreenConfiguration {
116    /// The smallest the floating header can expand to in the main axis direction, in response to a
117    /// [`crate::RenderObject::show_on_screen`] call, in addition to its
118    /// [`RenderSliverPersistentHeader::min_extent`].
119    ///
120    /// When a floating persistent header is told to show a [`Rect`] on screen, it may expand
121    /// itself to accommodate the rect. The minimum extent that is allowed for such expansion is
122    /// either the header's `min_extent` or this value, whichever is larger.
123    ///
124    /// Defaults to negative infinity, and must be less than or equal to
125    /// [`max_show_on_screen_extent`](Self::max_show_on_screen_extent). Has no effect unless the
126    /// persistent header is a floating header.
127    pub min_show_on_screen_extent: f64,
128
129    /// The biggest the floating header can expand to in the main axis direction, in response to a
130    /// [`crate::RenderObject::show_on_screen`] call, in addition to its
131    /// [`RenderSliverPersistentHeader::max_extent`].
132    ///
133    /// Defaults to infinity, and must be greater than or equal to
134    /// [`min_show_on_screen_extent`](Self::min_show_on_screen_extent). Has no effect unless the
135    /// persistent header is a floating header.
136    pub max_show_on_screen_extent: f64,
137}
138
139impl PersistentHeaderShowOnScreenConfiguration {
140    /// Creates an object that specifies how a pinned or floating persistent header should behave
141    /// in response to [`crate::RenderObject::show_on_screen`] calls.
142    pub const fn new() -> PersistentHeaderShowOnScreenConfiguration {
143        PersistentHeaderShowOnScreenConfiguration {
144            min_show_on_screen_extent: f64::NEG_INFINITY,
145            max_show_on_screen_extent: f64::INFINITY,
146        }
147    }
148
149    /// Sets [`min_show_on_screen_extent`](Self::min_show_on_screen_extent).
150    pub fn min_show_on_screen_extent(
151        mut self,
152        value: f64,
153    ) -> PersistentHeaderShowOnScreenConfiguration {
154        self.min_show_on_screen_extent = value;
155        debug_assert!(self.min_show_on_screen_extent <= self.max_show_on_screen_extent);
156        self
157    }
158
159    /// Sets [`max_show_on_screen_extent`](Self::max_show_on_screen_extent).
160    pub fn max_show_on_screen_extent(
161        mut self,
162        value: f64,
163    ) -> PersistentHeaderShowOnScreenConfiguration {
164        self.max_show_on_screen_extent = value;
165        debug_assert!(self.min_show_on_screen_extent <= self.max_show_on_screen_extent);
166        self
167    }
168}
169
170impl Default for PersistentHeaderShowOnScreenConfiguration {
171    fn default() -> PersistentHeaderShowOnScreenConfiguration {
172        PersistentHeaderShowOnScreenConfiguration::new()
173    }
174}
175
176/// Specifies how a floating header is to be "snapped" (animated) into or out of view.
177#[derive(Clone)]
178pub struct FloatingHeaderSnapConfiguration {
179    /// The snap animation curve.
180    pub curve: Rc<dyn Curve>,
181
182    /// The snap animation's duration.
183    pub duration: Duration,
184}
185
186impl FloatingHeaderSnapConfiguration {
187    /// Creates an object that specifies how a floating header is to be snapped into or out of
188    /// view.
189    ///
190    /// Dart's defaults: [`Curves::ease`] and 300 milliseconds.
191    pub fn new() -> FloatingHeaderSnapConfiguration {
192        FloatingHeaderSnapConfiguration {
193            curve: Curves::ease(),
194            duration: Duration::from_millis(300),
195        }
196    }
197
198    /// Sets [`curve`](Self::curve).
199    pub fn curve(mut self, value: Rc<dyn Curve>) -> FloatingHeaderSnapConfiguration {
200        self.curve = value;
201        self
202    }
203
204    /// Sets [`duration`](Self::duration).
205    pub fn duration(mut self, value: Duration) -> FloatingHeaderSnapConfiguration {
206        self.duration = value;
207        self
208    }
209}
210
211impl Default for FloatingHeaderSnapConfiguration {
212    fn default() -> FloatingHeaderSnapConfiguration {
213        FloatingHeaderSnapConfiguration::new()
214    }
215}
216
217/// Flutter's `RenderSliverPersistentHeader` fields.
218pub struct RenderSliverPersistentHeaderData {
219    last_stretch_offset: f64,
220    needs_update_child: bool,
221    last_shrink_offset: f64,
222    last_overlaps_content: bool,
223    /// Defines the parameters used to execute a callback when a stretching header over-scrolls.
224    ///
225    /// If this is `None` then the callback is not triggered.
226    pub stretch_configuration: Option<OverScrollHeaderStretchConfiguration>,
227}
228
229impl RenderSliverPersistentHeaderData {
230    /// Creates the state of a persistent header that has not been laid out.
231    pub fn new(
232        stretch_configuration: Option<OverScrollHeaderStretchConfiguration>,
233    ) -> RenderSliverPersistentHeaderData {
234        RenderSliverPersistentHeaderData {
235            last_stretch_offset: 0.0,
236            needs_update_child: true,
237            last_shrink_offset: 0.0,
238            last_overlaps_content: false,
239            stretch_configuration,
240        }
241    }
242}
243
244/// Implements [`RenderSliverPersistentHeader`] field accessors for a `header` field.
245#[macro_export]
246macro_rules! render_sliver_persistent_header_accessors {
247    () => {
248        fn header_data(
249            self: $crate::RenderHandle<Self>,
250            app: &::inset_foundation::App,
251        ) -> &$crate::RenderSliverPersistentHeaderData {
252            &self.get(app).header
253        }
254        fn header_data_mut(
255            self: $crate::RenderHandle<Self>,
256            app: &mut ::inset_foundation::App,
257        ) -> &mut $crate::RenderSliverPersistentHeaderData {
258            &mut self.get_mut(app).header
259        }
260    };
261}
262
263/// A sliver that has a box child which scrolls normally, except that when it hits the leading
264/// edge (typically the top) of the viewport, it shrinks to a minimum size
265/// ([`min_extent`](Self::min_extent)).
266///
267/// This trait primarily provides helpers for managing the child, in particular:
268///
269///  * [`layout_child`](Self::layout_child), which applies min and max extents and a scroll offset
270///    to lay out the child. This is normally called from `perform_layout`.
271///  * [`child_extent`](Self::child_extent), to convert the child's box layout dimensions to the
272///    sliver geometry model.
273///  * hit testing, painting, and other details of the sliver protocol.
274///
275/// Implementors must implement `perform_layout`, [`min_extent`](Self::min_extent) and
276/// [`max_extent`](Self::max_extent), and typically also will implement
277/// [`update_child`](Self::update_child).
278pub trait RenderSliverPersistentHeader:
279    RenderSliver + RenderObjectWithChildMixin<ChildType = AnyRenderBox> + RenderSliverHelpers
280{
281    /// Mixin field access.
282    fn header_data(self: RenderHandle<Self>, app: &App) -> &RenderSliverPersistentHeaderData;
283
284    /// See [`header_data`](Self::header_data).
285    fn header_data_mut(
286        self: RenderHandle<Self>,
287        app: &mut App,
288    ) -> &mut RenderSliverPersistentHeaderData;
289
290    /// The biggest that this render object can become, in the main axis direction.
291    ///
292    /// This value should not be based on the child. If it changes, call `mark_needs_layout`.
293    fn max_extent(self: RenderHandle<Self>, app: &App) -> f64;
294
295    /// The smallest that this render object can become, in the main axis direction.
296    ///
297    /// If this is based on the intrinsic dimensions of the child, the child should be measured
298    /// during [`update_child`](Self::update_child) and the value cached and returned here.
299    fn min_extent(self: RenderHandle<Self>, app: &App) -> f64;
300
301    /// The dimension of the child in the main axis.
302    fn child_extent(self: RenderHandle<Self>, app: &App) -> f64 {
303        let Some(child) = self.child(app) else {
304            return 0.0;
305        };
306        debug_assert!(child.has_size(app));
307        match self.constraints(app).axis() {
308            Axis::Vertical => child.size(app).height(),
309            Axis::Horizontal => child.size(app).width(),
310        }
311    }
312
313    /// The most recent `shrink_offset` passed to [`update_child`](Self::update_child).
314    fn last_shrink_offset(self: RenderHandle<Self>, app: &App) -> f64 {
315        self.header_data(app).last_shrink_offset
316    }
317
318    /// The most recent `overlaps_content` passed to [`update_child`](Self::update_child).
319    fn last_overlaps_content(self: RenderHandle<Self>, app: &App) -> bool {
320        self.header_data(app).last_overlaps_content
321    }
322
323    /// Defines the parameters used to execute a callback when a stretching header over-scrolls.
324    fn stretch_configuration(
325        self: RenderHandle<Self>,
326        app: &App,
327    ) -> Option<OverScrollHeaderStretchConfiguration> {
328        self.header_data(app).stretch_configuration.clone()
329    }
330
331    /// Sets [`stretch_configuration`](Self::stretch_configuration).
332    fn set_stretch_configuration(
333        self: RenderHandle<Self>,
334        app: &mut App,
335        value: Option<OverScrollHeaderStretchConfiguration>,
336    ) {
337        self.header_data_mut(app).stretch_configuration = value;
338    }
339
340    /// Update the child render object if necessary.
341    ///
342    /// Called before the first layout, any time `mark_needs_layout` is called, and any time the
343    /// scroll offset changes. The `shrink_offset` is the difference between the
344    /// [`max_extent`](Self::max_extent) and the current size. Zero means the header is fully
345    /// expanded, any greater number up to `max_extent` means that the header has been scrolled by
346    /// that much. The `overlaps_content` argument is true if the sliver's leading edge is beyond
347    /// its normal place in the viewport contents.
348    ///
349    /// When this method is called by [`layout_child`](Self::layout_child), the child can be set,
350    /// mutated, or replaced. Any time this method would mutate the child, call `mark_needs_layout`.
351    fn update_child(
352        self: RenderHandle<Self>,
353        app: &mut App,
354        shrink_offset: f64,
355        overlaps_content: bool,
356    ) {
357        let _ = (self, app, shrink_offset, overlaps_content);
358    }
359
360    /// The body of Flutter's `markNeedsLayout` override, run before the inherited body.
361    ///
362    /// This is automatically called whenever the child's intrinsic dimensions change, at which
363    /// point we should remeasure them during the next layout.
364    fn mark_needs_layout(self: RenderHandle<Self>, app: &mut App) {
365        self.header_data_mut(app).needs_update_child = true;
366        RenderSliver::mark_needs_layout(self, app);
367    }
368
369    /// Lays out the child.
370    ///
371    /// This is called by `perform_layout`. It applies the given `scroll_offset` (which need not
372    /// match the offset given by the constraints) and the `max_extent` (which need not match the
373    /// value returned by [`max_extent`](Self::max_extent)).
374    ///
375    /// The `overlaps_content` argument is passed to [`update_child`](Self::update_child).
376    fn layout_child(
377        self: RenderHandle<Self>,
378        app: &mut App,
379        scroll_offset: f64,
380        max_extent: f64,
381        overlaps_content: bool,
382    ) {
383        let shrink_offset = scroll_offset.min(max_extent);
384        let header = self.header_data(app);
385        if header.needs_update_child
386            || header.last_shrink_offset != shrink_offset
387            || header.last_overlaps_content != overlaps_content
388        {
389            self.as_object().invoke_layout_callback(app, |app| {
390                self.update_child(app, shrink_offset, overlaps_content);
391            });
392            let header = self.header_data_mut(app);
393            header.last_shrink_offset = shrink_offset;
394            header.last_overlaps_content = overlaps_content;
395            header.needs_update_child = false;
396        }
397        debug_assert!(
398            self.min_extent(app) <= max_extent,
399            "the max_extent for this persistent header is less than its min_extent"
400        );
401        let constraints = self.constraints(app);
402        let stretch_configuration = self.stretch_configuration(app);
403        let mut stretch_offset = 0.0;
404        if stretch_configuration.is_some() && constraints.scroll_offset == 0.0 {
405            stretch_offset += constraints.overlap.abs();
406        }
407
408        if let Some(child) = self.child(app) {
409            let min_extent = self.min_extent(app);
410            let child_constraints = constraints.as_box_constraints(
411                0.0,
412                min_extent.max(max_extent - shrink_offset) + stretch_offset,
413                None,
414            );
415            child.layout(app, child_constraints, true);
416        }
417
418        if let Some(configuration) = &stretch_configuration
419            && let Some(on_stretch_trigger) = configuration.on_stretch_trigger.clone()
420            && stretch_offset >= configuration.stretch_trigger_offset
421            && self.header_data(app).last_stretch_offset <= configuration.stretch_trigger_offset
422        {
423            // The future the trigger hands back is not awaited, as Dart's is not.
424            drop(on_stretch_trigger(app));
425        }
426        self.header_data_mut(app).last_stretch_offset = stretch_offset;
427    }
428
429    /// The body of Flutter's `hitTestChildren` override.
430    fn hit_test_children(
431        self: RenderHandle<Self>,
432        app: &mut App,
433        result: &mut SliverHitTestResult<'_>,
434        main_axis_position: f64,
435        cross_axis_position: f64,
436    ) -> bool {
437        debug_assert!(self.geometry(app).hit_test_extent > 0.0);
438        let Some(child) = self.child(app) else {
439            return false;
440        };
441        self.hit_test_box_child(
442            app,
443            &mut BoxHitTestResult::wrap(result),
444            child,
445            main_axis_position,
446            cross_axis_position,
447        )
448    }
449
450    /// The body of Flutter's `applyPaintTransform` override.
451    fn apply_paint_transform(
452        self: RenderHandle<Self>,
453        app: &App,
454        child: AnyRenderObject,
455        transform: &mut Matrix4,
456    ) {
457        debug_assert_eq!(Some(child), self.child(app).map(AnyRenderBox::as_object));
458        let child = child.as_box().expect("the child is a box");
459        self.apply_paint_transform_for_box_child(app, child, transform);
460    }
461
462    /// The body of Flutter's `paint` override.
463    fn paint(
464        self: RenderHandle<Self>,
465        app: &mut App,
466        context: &mut PaintingContext,
467        offset: Offset,
468    ) {
469        let Some(child) = self.child(app) else {
470            return;
471        };
472        if !self.geometry(app).visible {
473            return;
474        }
475        let constraints = self.constraints(app);
476        let paint_extent = self.geometry(app).paint_extent;
477        let child_position = RenderSliver::child_main_axis_position(self, app, child.as_object());
478        let child_extent = self.child_extent(app);
479        let offset = offset
480            + match apply_growth_direction_to_axis_direction(
481                constraints.axis_direction,
482                constraints.growth_direction,
483            ) {
484                AxisDirection::Up => Offset::new(0.0, paint_extent - child_position - child_extent),
485                AxisDirection::Left => {
486                    Offset::new(paint_extent - child_position - child_extent, 0.0)
487                }
488                AxisDirection::Right => Offset::new(child_position, 0.0),
489                AxisDirection::Down => Offset::new(0.0, child_position),
490            };
491        context.paint_child(app, child.as_object(), offset);
492    }
493}
494
495/// Flutter's `RenderSliverScrollingPersistentHeader` field.
496#[derive(Debug, Default)]
497pub struct RenderSliverScrollingPersistentHeaderData {
498    // Distance from our leading edge to the child's leading edge, in the axis direction.
499    // Negative if we're scrolled off the top.
500    child_position: Option<f64>,
501}
502
503impl RenderSliverScrollingPersistentHeaderData {
504    /// Creates the state of a scrolling persistent header that has not been laid out.
505    pub const fn new() -> RenderSliverScrollingPersistentHeaderData {
506        RenderSliverScrollingPersistentHeaderData {
507            child_position: None,
508        }
509    }
510}
511
512/// A sliver with a box child which scrolls normally, except that when it hits the leading edge
513/// (typically the top) of the viewport, it shrinks to a minimum size before continuing to scroll.
514///
515/// This sliver makes no effort to avoid overlapping other content.
516pub trait RenderSliverScrollingPersistentHeader: RenderSliverPersistentHeader {
517    /// Mixin field access.
518    fn scrolling_data(
519        self: RenderHandle<Self>,
520        app: &App,
521    ) -> &RenderSliverScrollingPersistentHeaderData;
522
523    /// See [`scrolling_data`](Self::scrolling_data).
524    fn scrolling_data_mut(
525        self: RenderHandle<Self>,
526        app: &mut App,
527    ) -> &mut RenderSliverScrollingPersistentHeaderData;
528
529    /// Updates the geometry, and returns the new value for
530    /// [`RenderSliver::child_main_axis_position`].
531    fn update_geometry(self: RenderHandle<Self>, app: &mut App) -> f64 {
532        let constraints = self.constraints(app);
533        let mut stretch_offset = 0.0;
534        if self.stretch_configuration(app).is_some() {
535            stretch_offset += constraints.overlap.abs();
536        }
537        let max_extent = self.max_extent(app);
538        let paint_extent = max_extent - constraints.scroll_offset;
539        let cache_extent = calculate_cache_offset(constraints, 0.0, max_extent);
540
541        self.set_geometry(
542            app,
543            SliverGeometry::new()
544                .scroll_extent(max_extent)
545                .paint_origin(constraints.overlap.min(0.0))
546                .paint_extent(clamp_double(
547                    paint_extent,
548                    0.0,
549                    constraints.remaining_paint_extent,
550                ))
551                .cache_extent(cache_extent)
552                .max_paint_extent(max_extent + stretch_offset)
553                // Conservatively say we do have overflow to avoid complexity.
554                .has_visual_overflow(true),
555        );
556        if stretch_offset > 0.0 {
557            0.0
558        } else {
559            (paint_extent - self.child_extent(app)).min(0.0)
560        }
561    }
562
563    /// The body of Flutter's `performLayout` override.
564    fn perform_layout(self: RenderHandle<Self>, app: &mut App) {
565        let scroll_offset = self.constraints(app).scroll_offset;
566        let max_extent = self.max_extent(app);
567        self.layout_child(app, scroll_offset, max_extent, false);
568        let child_position = self.update_geometry(app);
569        self.scrolling_data_mut(app).child_position = Some(child_position);
570    }
571
572    /// The body of Flutter's `childMainAxisPosition` override.
573    fn child_main_axis_position(
574        self: RenderHandle<Self>,
575        app: &App,
576        child: AnyRenderObject,
577    ) -> f64 {
578        debug_assert_eq!(Some(child), self.child(app).map(AnyRenderBox::as_object));
579        self.scrolling_data(app)
580            .child_position
581            .expect("the header has been laid out")
582    }
583}
584
585/// Flutter's `RenderSliverPinnedPersistentHeader` field.
586#[derive(Clone, Copy, Debug)]
587pub struct RenderSliverPinnedPersistentHeaderData {
588    /// Specifies the persistent header's behavior when `show_on_screen` is called.
589    ///
590    /// If set to `None`, the persistent header will delegate the `show_on_screen` call to its
591    /// parent render object.
592    pub show_on_screen_configuration: Option<PersistentHeaderShowOnScreenConfiguration>,
593}
594
595impl RenderSliverPinnedPersistentHeaderData {
596    /// Creates the state of a pinned persistent header.
597    pub const fn new() -> RenderSliverPinnedPersistentHeaderData {
598        RenderSliverPinnedPersistentHeaderData {
599            show_on_screen_configuration: Some(PersistentHeaderShowOnScreenConfiguration::new()),
600        }
601    }
602}
603
604impl Default for RenderSliverPinnedPersistentHeaderData {
605    fn default() -> RenderSliverPinnedPersistentHeaderData {
606        RenderSliverPinnedPersistentHeaderData::new()
607    }
608}
609
610/// A sliver with a box child which never scrolls off the viewport in the positive scroll
611/// direction, and which first scrolls on at a full size but then shrinks as the viewport continues
612/// to scroll.
613///
614/// This sliver avoids overlapping other earlier slivers where possible.
615pub trait RenderSliverPinnedPersistentHeader: RenderSliverPersistentHeader {
616    /// Mixin field access.
617    fn pinned_data(self: RenderHandle<Self>, app: &App) -> &RenderSliverPinnedPersistentHeaderData;
618
619    /// See [`pinned_data`](Self::pinned_data).
620    fn pinned_data_mut(
621        self: RenderHandle<Self>,
622        app: &mut App,
623    ) -> &mut RenderSliverPinnedPersistentHeaderData;
624
625    /// The body of Flutter's `performLayout` override.
626    fn perform_layout(self: RenderHandle<Self>, app: &mut App) {
627        let constraints = self.constraints(app);
628        let max_extent = self.max_extent(app);
629        let overlaps_content = constraints.overlap > 0.0;
630        self.layout_child(app, constraints.scroll_offset, max_extent, overlaps_content);
631        let effective_remaining_paint_extent =
632            (constraints.remaining_paint_extent - constraints.overlap).max(0.0);
633        let layout_extent = clamp_double(
634            max_extent - constraints.scroll_offset,
635            0.0,
636            effective_remaining_paint_extent,
637        );
638        let stretch_offset = if self.stretch_configuration(app).is_some() {
639            constraints.overlap.abs()
640        } else {
641            0.0
642        };
643        let child_extent = self.child_extent(app);
644        let min_extent = self.min_extent(app);
645        self.set_geometry(
646            app,
647            SliverGeometry::new()
648                .scroll_extent(max_extent)
649                .paint_origin(constraints.overlap)
650                .paint_extent(child_extent.min(effective_remaining_paint_extent))
651                .layout_extent(layout_extent)
652                .max_paint_extent(max_extent + stretch_offset)
653                .max_scroll_obstruction_extent(min_extent)
654                .cache_extent(if layout_extent > 0.0 {
655                    -constraints.cache_origin + layout_extent
656                } else {
657                    layout_extent
658                })
659                // Conservatively say we do have overflow to avoid complexity.
660                .has_visual_overflow(true),
661        );
662    }
663
664    /// The body of Flutter's `childMainAxisPosition` override.
665    fn child_main_axis_position(
666        self: RenderHandle<Self>,
667        _app: &App,
668        _child: AnyRenderObject,
669    ) -> f64 {
670        let _ = self;
671        0.0
672    }
673
674    /// The body of Flutter's `showOnScreen` override.
675    fn show_on_screen(
676        self: RenderHandle<Self>,
677        app: &mut App,
678        descendant: Option<AnyRenderObject>,
679        rect: Option<Rect>,
680        duration: Duration,
681        curve: Rc<dyn Curve>,
682    ) {
683        let local_bounds = match descendant {
684            Some(descendant) => Some(transform_rect(
685                &descendant.get_transform_to(app, Some(self.as_object())),
686                rect.unwrap_or_else(|| descendant.paint_bounds(app)),
687            )),
688            None => rect,
689        };
690
691        let constraints = self.constraints(app);
692        let child_extent = self.child_extent(app);
693        let new_rect = match apply_growth_direction_to_axis_direction(
694            constraints.axis_direction,
695            constraints.growth_direction,
696        ) {
697            AxisDirection::Up => trim(
698                local_bounds,
699                f64::NEG_INFINITY,
700                f64::INFINITY,
701                child_extent,
702                f64::NEG_INFINITY,
703            ),
704            AxisDirection::Left => trim(
705                local_bounds,
706                f64::NEG_INFINITY,
707                child_extent,
708                f64::INFINITY,
709                f64::NEG_INFINITY,
710            ),
711            AxisDirection::Right => trim(
712                local_bounds,
713                f64::NEG_INFINITY,
714                f64::INFINITY,
715                f64::INFINITY,
716                0.0,
717            ),
718            AxisDirection::Down => trim(
719                local_bounds,
720                0.0,
721                f64::INFINITY,
722                f64::INFINITY,
723                f64::NEG_INFINITY,
724            ),
725        };
726
727        RenderObjectBase::show_on_screen(
728            self,
729            app,
730            Some(self.as_object()),
731            new_rect,
732            duration,
733            curve,
734        );
735    }
736}
737
738/// Flutter's `RenderSliverFloatingPersistentHeader` fields.
739pub struct RenderSliverFloatingPersistentHeaderData {
740    controller: Option<Handle<AnimationController>>,
741    animation: Option<AnyAnimation<f64>>,
742    last_actual_scroll_offset: Option<f64>,
743    effective_scroll_offset: Option<f64>,
744    // Important for pointer scrolling, which does not have the same concept of a hold and release
745    // scroll movement, like dragging. This keeps track of the last ScrollDirection when scrolling
746    // started.
747    last_started_scroll_direction: Option<ScrollDirection>,
748    // Distance from our leading edge to the child's leading edge, in the axis direction.
749    // Negative if we're scrolled off the top.
750    child_position: Option<f64>,
751    vsync: Option<TickerProviderRef>,
752    /// Defines the parameters used to snap (animate) the floating header in and out of view.
753    ///
754    /// If this is `None` then the floating header does not snap.
755    pub snap_configuration: Option<FloatingHeaderSnapConfiguration>,
756    /// Specifies the persistent header's behavior when `show_on_screen` is called.
757    ///
758    /// If set to `None`, the persistent header will delegate the `show_on_screen` call to its
759    /// parent render object.
760    pub show_on_screen_configuration: Option<PersistentHeaderShowOnScreenConfiguration>,
761}
762
763impl RenderSliverFloatingPersistentHeaderData {
764    /// Creates the state of a floating persistent header that has not been laid out.
765    pub fn new(
766        vsync: Option<TickerProviderRef>,
767        snap_configuration: Option<FloatingHeaderSnapConfiguration>,
768        show_on_screen_configuration: Option<PersistentHeaderShowOnScreenConfiguration>,
769    ) -> RenderSliverFloatingPersistentHeaderData {
770        RenderSliverFloatingPersistentHeaderData {
771            controller: None,
772            animation: None,
773            last_actual_scroll_offset: None,
774            effective_scroll_offset: None,
775            last_started_scroll_direction: None,
776            child_position: None,
777            vsync,
778            snap_configuration,
779            show_on_screen_configuration,
780        }
781    }
782}
783
784/// A sliver with a box child which shrinks and scrolls like a
785/// [`RenderSliverScrollingPersistentHeader`], but immediately comes back when the user scrolls in
786/// the reverse direction.
787///
788/// See also:
789///
790///  * [`RenderSliverFloatingPinnedPersistentHeader`], which is similar but sticks to the start of
791///    the viewport rather than scrolling off.
792pub trait RenderSliverFloatingPersistentHeader: RenderSliverPersistentHeader {
793    /// Mixin field access.
794    fn floating_data(
795        self: RenderHandle<Self>,
796        app: &App,
797    ) -> &RenderSliverFloatingPersistentHeaderData;
798
799    /// See [`floating_data`](Self::floating_data).
800    fn floating_data_mut(
801        self: RenderHandle<Self>,
802        app: &mut App,
803    ) -> &mut RenderSliverFloatingPersistentHeaderData;
804
805    /// The body of Flutter's `detach` override: disposes the snap animation controller, which is
806    /// lazily recreated if we are reattached.
807    fn did_detach(self: RenderHandle<Self>, app: &mut App) {
808        if let Some(controller) = self.floating_data(app).controller {
809            controller.dispose(app);
810        }
811        self.floating_data_mut(app).controller = None;
812    }
813
814    /// A [`TickerProvider`] to use when animating the scroll position.
815    fn vsync(self: RenderHandle<Self>, app: &App) -> Option<TickerProviderRef> {
816        self.floating_data(app).vsync.clone()
817    }
818
819    /// Sets [`vsync`](Self::vsync).
820    fn set_vsync(self: RenderHandle<Self>, app: &mut App, value: Option<TickerProviderRef>) {
821        if value == self.floating_data(app).vsync {
822            return;
823        }
824        self.floating_data_mut(app).vsync = value.clone();
825        match value {
826            None => {
827                if let Some(controller) = self.floating_data(app).controller {
828                    controller.dispose(app);
829                }
830                self.floating_data_mut(app).controller = None;
831            }
832            Some(vsync) => {
833                if let Some(controller) = self.floating_data(app).controller {
834                    controller.resync(app, vsync);
835                }
836            }
837        }
838    }
839
840    /// Defines the parameters used to snap (animate) the floating header in and out of view.
841    fn snap_configuration(
842        self: RenderHandle<Self>,
843        app: &App,
844    ) -> Option<FloatingHeaderSnapConfiguration> {
845        self.floating_data(app).snap_configuration.clone()
846    }
847
848    /// Sets [`snap_configuration`](Self::snap_configuration).
849    fn set_snap_configuration(
850        self: RenderHandle<Self>,
851        app: &mut App,
852        value: Option<FloatingHeaderSnapConfiguration>,
853    ) {
854        self.floating_data_mut(app).snap_configuration = value;
855    }
856
857    /// Specifies the persistent header's behavior when `show_on_screen` is called.
858    fn show_on_screen_configuration(
859        self: RenderHandle<Self>,
860        app: &App,
861    ) -> Option<PersistentHeaderShowOnScreenConfiguration> {
862        self.floating_data(app).show_on_screen_configuration
863    }
864
865    /// Sets [`show_on_screen_configuration`](Self::show_on_screen_configuration).
866    fn set_show_on_screen_configuration(
867        self: RenderHandle<Self>,
868        app: &mut App,
869        value: Option<PersistentHeaderShowOnScreenConfiguration>,
870    ) {
871        self.floating_data_mut(app).show_on_screen_configuration = value;
872    }
873
874    /// Flutter's `_effectiveScrollOffset`, the offset the header is laid out at.
875    fn effective_scroll_offset(self: RenderHandle<Self>, app: &App) -> Option<f64> {
876        self.floating_data(app).effective_scroll_offset
877    }
878
879    /// Updates the geometry, and returns the new value for
880    /// [`RenderSliver::child_main_axis_position`].
881    fn update_geometry(self: RenderHandle<Self>, app: &mut App) -> f64 {
882        let constraints = self.constraints(app);
883        let mut stretch_offset = 0.0;
884        if self.stretch_configuration(app).is_some() {
885            stretch_offset += constraints.overlap.abs();
886        }
887        let max_extent = self.max_extent(app);
888        let paint_extent = max_extent
889            - self
890                .effective_scroll_offset(app)
891                .expect("perform_layout sets the effective scroll offset");
892        let layout_extent = max_extent - constraints.scroll_offset;
893        self.set_geometry(
894            app,
895            SliverGeometry::new()
896                .scroll_extent(max_extent)
897                .paint_origin(constraints.overlap.min(0.0))
898                .paint_extent(clamp_double(
899                    paint_extent,
900                    0.0,
901                    constraints.remaining_paint_extent,
902                ))
903                .layout_extent(clamp_double(
904                    layout_extent,
905                    0.0,
906                    constraints.remaining_paint_extent,
907                ))
908                .max_paint_extent(max_extent + stretch_offset)
909                // Conservatively say we do have overflow to avoid complexity.
910                .has_visual_overflow(true),
911        );
912        if stretch_offset > 0.0 {
913            0.0
914        } else {
915            (paint_extent - self.child_extent(app)).min(0.0)
916        }
917    }
918
919    /// Flutter's `_updateAnimation`.
920    fn update_animation(
921        self: RenderHandle<Self>,
922        app: &mut App,
923        duration: Duration,
924        end_value: f64,
925        curve: Rc<dyn Curve>,
926    ) {
927        let vsync = self
928            .vsync(app)
929            .expect("vsync must be set if the floating header changes size animatedly");
930        let controller = match self.floating_data(app).controller {
931            Some(controller) => controller,
932            None => {
933                let controller = AnimationController::create(
934                    app,
935                    None,
936                    Some(duration),
937                    None,
938                    0.0,
939                    1.0,
940                    inset_animation::AnimationBehavior::Normal,
941                    vsync,
942                );
943                self.floating_data_mut(app).controller = Some(controller);
944                controller.add_listener(
945                    app,
946                    Listener::handle_method(self.handle(), snap_tick::<Self>),
947                );
948                controller
949            }
950        };
951        let begin = self.effective_scroll_offset(app);
952        let tween = Tween::new(app, begin, Some(end_value));
953        let curve_tween = CurveTween::new(app, curve);
954        let animation = controller.drive(app, tween.chain(curve_tween));
955        self.floating_data_mut(app).animation = Some(animation);
956    }
957
958    /// Update the last known scroll direction when scrolling began.
959    fn update_scroll_start_direction(
960        self: RenderHandle<Self>,
961        app: &mut App,
962        direction: ScrollDirection,
963    ) {
964        self.floating_data_mut(app).last_started_scroll_direction = Some(direction);
965    }
966
967    /// If the header isn't already fully exposed, then scroll it into view.
968    fn maybe_start_snap_animation(
969        self: RenderHandle<Self>,
970        app: &mut App,
971        direction: ScrollDirection,
972    ) {
973        let Some(snap) = self.snap_configuration(app) else {
974            return;
975        };
976        let effective_scroll_offset = self
977            .effective_scroll_offset(app)
978            .expect("the header has been laid out");
979        let max_extent = self.max_extent(app);
980        if direction == ScrollDirection::Forward && effective_scroll_offset <= 0.0 {
981            return;
982        }
983        if direction == ScrollDirection::Reverse && effective_scroll_offset >= max_extent {
984            return;
985        }
986
987        let end_value = if direction == ScrollDirection::Forward {
988            0.0
989        } else {
990            max_extent
991        };
992        self.update_animation(app, snap.duration, end_value, Rc::clone(&snap.curve));
993        if let Some(controller) = self.floating_data(app).controller {
994            controller.forward(app, Some(0.0));
995        }
996    }
997
998    /// If a header snap animation or a `show_on_screen` expand animation is underway then stop it.
999    fn maybe_stop_snap_animation(
1000        self: RenderHandle<Self>,
1001        app: &mut App,
1002        _direction: ScrollDirection,
1003    ) {
1004        if let Some(controller) = self.floating_data(app).controller {
1005            controller.stop(app, false);
1006        }
1007    }
1008
1009    /// The body of Flutter's `performLayout` override.
1010    fn perform_layout(self: RenderHandle<Self>, app: &mut App) {
1011        let constraints = self.constraints(app);
1012        let max_extent = self.max_extent(app);
1013        let data = self.floating_data(app);
1014        let last_actual_scroll_offset = data.last_actual_scroll_offset;
1015        let effective_scroll_offset = data.effective_scroll_offset;
1016        let last_started_scroll_direction = data.last_started_scroll_direction;
1017        // We've laid out at least once to get an initial position, and either we are scrolling
1018        // back, so should reveal, or some part of it is visible, so should shrink or reveal as
1019        // appropriate.
1020        let new_effective_scroll_offset = match last_actual_scroll_offset {
1021            Some(last_actual_scroll_offset)
1022                if constraints.scroll_offset < last_actual_scroll_offset
1023                    || effective_scroll_offset.is_some_and(|offset| offset < max_extent) =>
1024            {
1025                let mut delta = last_actual_scroll_offset - constraints.scroll_offset;
1026                let mut effective_scroll_offset =
1027                    effective_scroll_offset.expect("laid out at least once");
1028                let allow_floating_expansion = constraints.user_scroll_direction
1029                    == ScrollDirection::Forward
1030                    || last_started_scroll_direction == Some(ScrollDirection::Forward);
1031                if allow_floating_expansion {
1032                    if effective_scroll_offset > max_extent {
1033                        // We're scrolled off-screen, but should reveal, so pretend we're just at
1034                        // the limit.
1035                        effective_scroll_offset = max_extent;
1036                    }
1037                } else if delta > 0.0 {
1038                    // Disallow the expansion. (But allow shrinking, i.e. delta < 0.0 is fine.)
1039                    delta = 0.0;
1040                }
1041                clamp_double(
1042                    effective_scroll_offset - delta,
1043                    0.0,
1044                    constraints.scroll_offset,
1045                )
1046            }
1047            _ => constraints.scroll_offset,
1048        };
1049        self.floating_data_mut(app).effective_scroll_offset = Some(new_effective_scroll_offset);
1050        let overlaps_content = new_effective_scroll_offset < constraints.scroll_offset;
1051
1052        self.layout_child(
1053            app,
1054            new_effective_scroll_offset,
1055            max_extent,
1056            overlaps_content,
1057        );
1058        let child_position = self.update_geometry(app);
1059        let data = self.floating_data_mut(app);
1060        data.child_position = Some(child_position);
1061        data.last_actual_scroll_offset = Some(constraints.scroll_offset);
1062    }
1063
1064    /// The body of Flutter's `showOnScreen` override.
1065    fn show_on_screen(
1066        self: RenderHandle<Self>,
1067        app: &mut App,
1068        descendant: Option<AnyRenderObject>,
1069        rect: Option<Rect>,
1070        duration: Duration,
1071        curve: Rc<dyn Curve>,
1072    ) {
1073        let Some(show_on_screen) = self.show_on_screen_configuration(app) else {
1074            return RenderObjectBase::show_on_screen(self, app, descendant, rect, duration, curve);
1075        };
1076
1077        let child = self.child(app);
1078        debug_assert!(child.is_some() || descendant.is_none());
1079        // We prefer the child's coordinate space (instead of the sliver's) because it's easier for
1080        // us to convert the target rect into target extents: when the sliver is sitting above the
1081        // leading edge (not possible with pinned headers), the leading edge of the sliver and the
1082        // leading edge of the child will not be aligned. The only exception is when the child is
1083        // `None` (and thus `descendant` is `None`).
1084        let child_bounds = match descendant {
1085            Some(descendant) => Some(transform_rect(
1086                &descendant.get_transform_to(app, child.map(AnyRenderBox::as_object)),
1087                rect.unwrap_or_else(|| descendant.paint_bounds(app)),
1088            )),
1089            None => rect,
1090        };
1091
1092        let constraints = self.constraints(app);
1093        let child_extent = self.child_extent(app);
1094        let (mut target_extent, target_rect) = match apply_growth_direction_to_axis_direction(
1095            constraints.axis_direction,
1096            constraints.growth_direction,
1097        ) {
1098            AxisDirection::Up => (
1099                child_extent - child_bounds.map_or(0.0, |bounds| bounds.top),
1100                trim(
1101                    child_bounds,
1102                    f64::NEG_INFINITY,
1103                    f64::INFINITY,
1104                    child_extent,
1105                    f64::NEG_INFINITY,
1106                ),
1107            ),
1108            AxisDirection::Right => (
1109                child_bounds.map_or(child_extent, |bounds| bounds.right),
1110                trim(
1111                    child_bounds,
1112                    f64::NEG_INFINITY,
1113                    f64::INFINITY,
1114                    f64::INFINITY,
1115                    0.0,
1116                ),
1117            ),
1118            AxisDirection::Down => (
1119                child_bounds.map_or(child_extent, |bounds| bounds.bottom),
1120                trim(
1121                    child_bounds,
1122                    0.0,
1123                    f64::INFINITY,
1124                    f64::INFINITY,
1125                    f64::NEG_INFINITY,
1126                ),
1127            ),
1128            AxisDirection::Left => (
1129                child_extent - child_bounds.map_or(0.0, |bounds| bounds.left),
1130                trim(
1131                    child_bounds,
1132                    f64::NEG_INFINITY,
1133                    child_extent,
1134                    f64::INFINITY,
1135                    f64::NEG_INFINITY,
1136                ),
1137            ),
1138        };
1139
1140        // A stretch header can have a bigger child extent than max extent.
1141        let max_extent = self.max_extent(app);
1142        let effective_max_extent = child_extent.max(max_extent);
1143
1144        target_extent = clamp_double(
1145            clamp_double(
1146                target_extent,
1147                show_on_screen.min_show_on_screen_extent,
1148                show_on_screen.max_show_on_screen_extent,
1149            ),
1150            // Clamp the value back to the valid range after applying additional constraints.
1151            // Contracting is not allowed.
1152            child_extent,
1153            effective_max_extent,
1154        );
1155
1156        // Expands the header if needed, with animation.
1157        let controller_status = self
1158            .floating_data(app)
1159            .controller
1160            .map(|controller| controller.status(app));
1161        if target_extent > child_extent && controller_status != Some(AnimationStatus::Forward) {
1162            let target_scroll_offset = max_extent - target_extent;
1163            debug_assert!(
1164                self.vsync(app).is_some(),
1165                "vsync must be set if the floating header changes size animatedly"
1166            );
1167            self.update_animation(app, duration, target_scroll_offset, Rc::clone(&curve));
1168            if let Some(controller) = self.floating_data(app).controller {
1169                controller.forward(app, Some(0.0));
1170            }
1171        }
1172
1173        let descendant = match descendant {
1174            None => Some(self.as_object()),
1175            Some(_) => child.map(AnyRenderBox::as_object),
1176        };
1177        RenderObjectBase::show_on_screen(self, app, descendant, target_rect, duration, curve);
1178    }
1179
1180    /// The body of Flutter's `childMainAxisPosition` override.
1181    fn child_main_axis_position(
1182        self: RenderHandle<Self>,
1183        app: &App,
1184        child: AnyRenderObject,
1185    ) -> f64 {
1186        debug_assert_eq!(Some(child), self.child(app).map(AnyRenderBox::as_object));
1187        self.floating_data(app).child_position.unwrap_or(0.0)
1188    }
1189
1190    /// The type-erased `RenderSliverFloatingPersistentHeader` handle. Free: the vtable is a
1191    /// `const`, and the id is copied.
1192    fn as_floating_persistent_header(
1193        self: RenderHandle<Self>,
1194    ) -> AnyRenderSliverFloatingPersistentHeader {
1195        AnyRenderSliverFloatingPersistentHeader {
1196            id: self.id(),
1197            vtable: const { &RenderSliverFloatingPersistentHeaderVTable::of::<Self>() },
1198        }
1199    }
1200}
1201
1202/// The vtable of an erased [`AnyRenderSliverFloatingPersistentHeader`].
1203struct RenderSliverFloatingPersistentHeaderVTable {
1204    update_scroll_start_direction: fn(&mut App, HandleId, ScrollDirection),
1205    maybe_start_snap_animation: fn(&mut App, HandleId, ScrollDirection),
1206    maybe_stop_snap_animation: fn(&mut App, HandleId, ScrollDirection),
1207}
1208
1209impl RenderSliverFloatingPersistentHeaderVTable {
1210    const fn of<T: RenderSliverFloatingPersistentHeader>()
1211    -> RenderSliverFloatingPersistentHeaderVTable {
1212        RenderSliverFloatingPersistentHeaderVTable {
1213            update_scroll_start_direction: |app, id, direction| {
1214                T::update_scroll_start_direction(resolve(id), app, direction)
1215            },
1216            maybe_start_snap_animation: |app, id, direction| {
1217                T::maybe_start_snap_animation(resolve(id), app, direction)
1218            },
1219            maybe_stop_snap_animation: |app, id, direction| {
1220                T::maybe_stop_snap_animation(resolve(id), app, direction)
1221            },
1222        }
1223    }
1224}
1225
1226/// Erased [`RenderSliverFloatingPersistentHeader`]: what Dart's
1227/// `findAncestorRenderObjectOfType<RenderSliverFloatingPersistentHeader>()` hands back.
1228#[derive(Clone, Copy)]
1229pub struct AnyRenderSliverFloatingPersistentHeader {
1230    id: HandleId,
1231    vtable: &'static RenderSliverFloatingPersistentHeaderVTable,
1232}
1233
1234impl AnyRenderSliverFloatingPersistentHeader {
1235    /// See [`RenderSliverFloatingPersistentHeader::update_scroll_start_direction`].
1236    pub fn update_scroll_start_direction(self, app: &mut App, direction: ScrollDirection) {
1237        (self.vtable.update_scroll_start_direction)(app, self.id, direction)
1238    }
1239
1240    /// See [`RenderSliverFloatingPersistentHeader::maybe_start_snap_animation`].
1241    pub fn maybe_start_snap_animation(self, app: &mut App, direction: ScrollDirection) {
1242        (self.vtable.maybe_start_snap_animation)(app, self.id, direction)
1243    }
1244
1245    /// See [`RenderSliverFloatingPersistentHeader::maybe_stop_snap_animation`].
1246    pub fn maybe_stop_snap_animation(self, app: &mut App, direction: ScrollDirection) {
1247        (self.vtable.maybe_stop_snap_animation)(app, self.id, direction)
1248    }
1249}
1250
1251impl PartialEq for AnyRenderSliverFloatingPersistentHeader {
1252    fn eq(&self, other: &AnyRenderSliverFloatingPersistentHeader) -> bool {
1253        self.id == other.id
1254    }
1255}
1256
1257impl Eq for AnyRenderSliverFloatingPersistentHeader {}
1258
1259/// The snap animation's listener: Dart's closure inside `_updateAnimation`.
1260fn snap_tick<T: RenderSliverFloatingPersistentHeader>(this: Handle<T>, app: &mut App) {
1261    let this = RenderHandle::from_handle(this);
1262    let Some(animation) = this.floating_data(app).animation else {
1263        return;
1264    };
1265    let value = animation.value(app);
1266    if this.effective_scroll_offset(app) == Some(value) {
1267        return;
1268    }
1269    this.floating_data_mut(app).effective_scroll_offset = Some(value);
1270    RenderSliverPersistentHeader::mark_needs_layout(this, app);
1271}
1272
1273/// A sliver with a box child which shrinks and then remains pinned to the start of the viewport
1274/// like a [`RenderSliverPinnedPersistentHeader`], but immediately grows when the user scrolls in
1275/// the reverse direction.
1276///
1277/// See also:
1278///
1279///  * [`RenderSliverFloatingPersistentHeader`], which is similar but scrolls off the top rather
1280///    than sticking to it.
1281pub trait RenderSliverFloatingPinnedPersistentHeader: RenderSliverFloatingPersistentHeader {
1282    /// The body of Flutter's `updateGeometry` override.
1283    fn update_geometry(self: RenderHandle<Self>, app: &mut App) -> f64 {
1284        let constraints = self.constraints(app);
1285        let min_extent = self.min_extent(app);
1286        let min_allowed_extent = if constraints.remaining_paint_extent > min_extent {
1287            min_extent
1288        } else {
1289            constraints.remaining_paint_extent
1290        };
1291        let max_extent = self.max_extent(app);
1292        let paint_extent = max_extent
1293            - self
1294                .effective_scroll_offset(app)
1295                .expect("perform_layout sets the effective scroll offset");
1296        let clamped_paint_extent = clamp_double(
1297            paint_extent,
1298            min_allowed_extent,
1299            constraints.remaining_paint_extent,
1300        );
1301        let layout_extent = max_extent - constraints.scroll_offset;
1302        let stretch_offset = if self.stretch_configuration(app).is_some() {
1303            constraints.overlap.abs()
1304        } else {
1305            0.0
1306        };
1307        self.set_geometry(
1308            app,
1309            SliverGeometry::new()
1310                .scroll_extent(max_extent)
1311                .paint_origin(constraints.overlap.min(0.0))
1312                .paint_extent(clamped_paint_extent)
1313                .layout_extent(clamp_double(layout_extent, 0.0, clamped_paint_extent))
1314                .max_paint_extent(max_extent + stretch_offset)
1315                .max_scroll_obstruction_extent(min_extent)
1316                // Conservatively say we do have overflow to avoid complexity.
1317                .has_visual_overflow(true),
1318        );
1319        0.0
1320    }
1321}
1322
1323#[cfg(test)]
1324mod tests {
1325    use std::cell::Cell;
1326
1327    use inset_embedder::Size;
1328    use inset_foundation::{AppCell, Task};
1329
1330    use super::*;
1331    use crate::object::{RenderObject, RenderObjectData, RenderObjectWithChildData};
1332    use crate::sliver::{AnyRenderSliver, RenderSliverData};
1333    use crate::sliver_multi_box_adaptor::test_support::hit_testable_box;
1334    use crate::sliver_multi_box_adaptor::viewport_test_support::ScrollHarness;
1335
1336    /// The shape every persistent header leaf in this test shares: a min and a max extent, and a
1337    /// box child of the header's current extent.
1338    macro_rules! persistent_header_leaf {
1339        ($leaf:ident, $extra:ident, $perform_layout:path, $child_position:path) => {
1340            struct $leaf {
1341                render_object: RenderObjectData,
1342                render_sliver: RenderSliverData,
1343                child: RenderObjectWithChildData<AnyRenderBox>,
1344                header: RenderSliverPersistentHeaderData,
1345                extra: $extra,
1346                min_extent: f64,
1347                max_extent: f64,
1348            }
1349
1350            impl RenderObjectWithChildMixin for $leaf {
1351                type ChildType = AnyRenderBox;
1352
1353                fn child_data(
1354                    self: RenderHandle<Self>,
1355                    app: &App,
1356                ) -> &RenderObjectWithChildData<AnyRenderBox> {
1357                    &self.get(app).child
1358                }
1359
1360                fn child_data_mut(
1361                    self: RenderHandle<Self>,
1362                    app: &mut App,
1363                ) -> &mut RenderObjectWithChildData<AnyRenderBox> {
1364                    &mut self.get_mut(app).child
1365                }
1366            }
1367
1368            impl RenderSliverHelpers for $leaf {}
1369
1370            impl RenderSliverPersistentHeader for $leaf {
1371                crate::render_sliver_persistent_header_accessors!();
1372
1373                fn max_extent(self: RenderHandle<Self>, app: &App) -> f64 {
1374                    self.get(app).max_extent
1375                }
1376
1377                fn min_extent(self: RenderHandle<Self>, app: &App) -> f64 {
1378                    self.get(app).min_extent
1379                }
1380            }
1381
1382            impl RenderObject for $leaf {
1383                crate::render_object_accessors!();
1384
1385                fn visit_children(
1386                    self: RenderHandle<Self>,
1387                    app: &App,
1388                    visitor: &mut dyn FnMut(AnyRenderObject),
1389                ) {
1390                    if let Some(child) = self.child(app) {
1391                        visitor(child.as_object());
1392                    }
1393                }
1394
1395                fn apply_paint_transform(
1396                    self: RenderHandle<Self>,
1397                    app: &App,
1398                    child: AnyRenderObject,
1399                    transform: &mut Matrix4,
1400                ) {
1401                    RenderSliverPersistentHeader::apply_paint_transform(self, app, child, transform)
1402                }
1403
1404                fn perform_layout(self: RenderHandle<Self>, app: &mut App) {
1405                    $perform_layout(self, app)
1406                }
1407
1408                fn paint(
1409                    self: RenderHandle<Self>,
1410                    app: &mut App,
1411                    context: &mut PaintingContext,
1412                    offset: Offset,
1413                ) {
1414                    RenderSliverPersistentHeader::paint(self, app, context, offset)
1415                }
1416            }
1417
1418            impl RenderSliver for $leaf {
1419                crate::render_sliver_accessors!();
1420
1421                fn hit_test_children(
1422                    self: RenderHandle<Self>,
1423                    app: &mut App,
1424                    result: &mut SliverHitTestResult<'_>,
1425                    main_axis_position: f64,
1426                    cross_axis_position: f64,
1427                ) -> bool {
1428                    RenderSliverPersistentHeader::hit_test_children(
1429                        self,
1430                        app,
1431                        result,
1432                        main_axis_position,
1433                        cross_axis_position,
1434                    )
1435                }
1436
1437                fn child_main_axis_position(
1438                    self: RenderHandle<Self>,
1439                    app: &App,
1440                    child: AnyRenderObject,
1441                ) -> f64 {
1442                    $child_position(self, app, child)
1443                }
1444            }
1445        };
1446    }
1447
1448    type ScrollingExtra = RenderSliverScrollingPersistentHeaderData;
1449    type PinnedExtra = RenderSliverPinnedPersistentHeaderData;
1450
1451    persistent_header_leaf!(
1452        TestScrollingHeader,
1453        ScrollingExtra,
1454        RenderSliverScrollingPersistentHeader::perform_layout,
1455        RenderSliverScrollingPersistentHeader::child_main_axis_position
1456    );
1457
1458    impl RenderSliverScrollingPersistentHeader for TestScrollingHeader {
1459        fn scrolling_data(
1460            self: RenderHandle<Self>,
1461            app: &App,
1462        ) -> &RenderSliverScrollingPersistentHeaderData {
1463            &self.get(app).extra
1464        }
1465
1466        fn scrolling_data_mut(
1467            self: RenderHandle<Self>,
1468            app: &mut App,
1469        ) -> &mut RenderSliverScrollingPersistentHeaderData {
1470            &mut self.get_mut(app).extra
1471        }
1472    }
1473
1474    persistent_header_leaf!(
1475        TestPinnedHeader,
1476        PinnedExtra,
1477        RenderSliverPinnedPersistentHeader::perform_layout,
1478        RenderSliverPinnedPersistentHeader::child_main_axis_position
1479    );
1480
1481    impl RenderSliverPinnedPersistentHeader for TestPinnedHeader {
1482        fn pinned_data(
1483            self: RenderHandle<Self>,
1484            app: &App,
1485        ) -> &RenderSliverPinnedPersistentHeaderData {
1486            &self.get(app).extra
1487        }
1488
1489        fn pinned_data_mut(
1490            self: RenderHandle<Self>,
1491            app: &mut App,
1492        ) -> &mut RenderSliverPinnedPersistentHeaderData {
1493            &mut self.get_mut(app).extra
1494        }
1495    }
1496
1497    fn scrolling_header(
1498        app: &mut App,
1499        min_extent: f64,
1500        max_extent: f64,
1501    ) -> RenderHandle<TestScrollingHeader> {
1502        let child = hit_testable_box(app, max_extent);
1503        let header = RenderHandle::new_sliver(
1504            app,
1505            TestScrollingHeader {
1506                render_object: RenderObjectData::new(),
1507                render_sliver: RenderSliverData::new(),
1508                child: RenderObjectWithChildData::new(),
1509                header: RenderSliverPersistentHeaderData::new(None),
1510                extra: RenderSliverScrollingPersistentHeaderData::new(),
1511                min_extent,
1512                max_extent,
1513            },
1514        );
1515        header.set_child(app, Some(child));
1516        header
1517    }
1518
1519    fn pinned_header(
1520        app: &mut App,
1521        min_extent: f64,
1522        max_extent: f64,
1523    ) -> RenderHandle<TestPinnedHeader> {
1524        let child = hit_testable_box(app, max_extent);
1525        let header = RenderHandle::new_sliver(
1526            app,
1527            TestPinnedHeader {
1528                render_object: RenderObjectData::new(),
1529                render_sliver: RenderSliverData::new(),
1530                child: RenderObjectWithChildData::new(),
1531                header: RenderSliverPersistentHeaderData::new(None),
1532                extra: RenderSliverPinnedPersistentHeaderData::new(),
1533                min_extent,
1534                max_extent,
1535            },
1536        );
1537        header.set_child(app, Some(child));
1538        header
1539    }
1540
1541    /// A header's `layout_child` runs inside a layout callback, so layout has to go through the
1542    /// owner's layout phase.
1543    fn lay_out(app: &mut App, sliver: AnyRenderSliver, scroll_offset: f64) -> ScrollHarness {
1544        let harness = ScrollHarness::new(app, sliver, Size::new(200.0, 400.0));
1545        harness.scroll_to(app, scroll_offset);
1546        harness
1547    }
1548
1549    /// `sliver_persistent_header_test.dart`: a scrolling header shrinks its paint extent as it
1550    /// scrolls off, keeping its full scroll extent.
1551    #[test]
1552    fn a_scrolling_header_shrinks_as_it_scrolls_off() {
1553        let cell = AppCell::new();
1554        let mut app = cell.borrow_mut();
1555        let header = scrolling_header(&mut app, 40.0, 120.0);
1556        lay_out(&mut app, header.as_sliver(), 50.0);
1557
1558        let geometry = header.geometry(&app);
1559        assert_eq!(geometry.scroll_extent, 120.0);
1560        assert_eq!(geometry.paint_extent, 70.0);
1561        assert_eq!(geometry.max_scroll_obstruction_extent, 0.0);
1562        // The child shrinks to what is left of the maximum extent, floored at the minimum.
1563        assert_eq!(header.child_extent(&app), 70.0);
1564        assert_eq!(header.last_shrink_offset(&app), 50.0);
1565        let child = header.child(&app).expect("has a child");
1566        assert_eq!(
1567            RenderSliver::child_main_axis_position(header, &app, child.as_object()),
1568            0.0
1569        );
1570    }
1571
1572    /// A pinned header stays at the leading edge and reports its minimum extent as the scroll
1573    /// obstruction.
1574    #[test]
1575    fn a_pinned_header_stays_at_the_leading_edge() {
1576        let cell = AppCell::new();
1577        let mut app = cell.borrow_mut();
1578        let header = pinned_header(&mut app, 40.0, 120.0);
1579        lay_out(&mut app, header.as_sliver(), 100.0);
1580
1581        let geometry = header.geometry(&app);
1582        assert_eq!(geometry.scroll_extent, 120.0);
1583        assert_eq!(geometry.max_scroll_obstruction_extent, 40.0);
1584        // Shrunk to the minimum extent, still painting at the leading edge.
1585        assert_eq!(header.child_extent(&app), 40.0);
1586        assert_eq!(geometry.paint_extent, 40.0);
1587        assert_eq!(geometry.layout_extent, 20.0);
1588        let child = header.child(&app).expect("has a child");
1589        assert_eq!(
1590            RenderSliver::child_main_axis_position(header, &app, child.as_object()),
1591            0.0
1592        );
1593    }
1594
1595    /// An unscrolled pinned header is at its maximum extent.
1596    #[test]
1597    fn an_unscrolled_pinned_header_is_at_its_maximum_extent() {
1598        let cell = AppCell::new();
1599        let mut app = cell.borrow_mut();
1600        let header = pinned_header(&mut app, 40.0, 120.0);
1601        lay_out(&mut app, header.as_sliver(), 0.0);
1602
1603        let geometry = header.geometry(&app);
1604        assert_eq!(geometry.paint_extent, 120.0);
1605        assert_eq!(geometry.layout_extent, 120.0);
1606        assert_eq!(header.last_shrink_offset(&app), 0.0);
1607        assert!(!header.last_overlaps_content(&app));
1608    }
1609
1610    /// `layout_child` re-runs `update_child` only when the shrink offset or overlap changes.
1611    #[test]
1612    fn layout_child_reports_the_shrink_offset() {
1613        let cell = AppCell::new();
1614        let mut app = cell.borrow_mut();
1615        let header = scrolling_header(&mut app, 40.0, 120.0);
1616        lay_out(&mut app, header.as_sliver(), 80.0);
1617
1618        assert_eq!(header.last_shrink_offset(&app), 80.0);
1619        // The child is laid out with the remaining extent, floored at the minimum extent.
1620        assert_eq!(header.child_extent(&app), 40.0);
1621    }
1622
1623    /// The stretch trigger fires once as the overscroll crosses the trigger offset, and again
1624    /// only after the header has come back under it.
1625    #[test]
1626    fn the_stretch_trigger_fires_once_per_crossing() {
1627        let cell = AppCell::new();
1628        let mut app = cell.borrow_mut();
1629        let triggered = Rc::new(Cell::new(0));
1630        let counter = Rc::clone(&triggered);
1631        let configuration =
1632            OverScrollHeaderStretchConfiguration::new().on_stretch_trigger(Rc::new(move |_app| {
1633                counter.set(counter.get() + 1);
1634                Task::ready(())
1635            }));
1636        let header = scrolling_header(&mut app, 40.0, 120.0);
1637        header.set_stretch_configuration(&mut app, Some(configuration));
1638        let harness = lay_out(&mut app, header.as_sliver(), 0.0);
1639        assert_eq!(triggered.get(), 0);
1640
1641        harness.scroll_to(&mut app, -120.0);
1642        assert_eq!(triggered.get(), 1);
1643        harness.scroll_to(&mut app, -130.0);
1644        assert_eq!(triggered.get(), 1, "already past the trigger offset");
1645
1646        harness.scroll_to(&mut app, 0.0);
1647        harness.scroll_to(&mut app, -100.0);
1648        assert_eq!(triggered.get(), 2);
1649    }
1650
1651    /// The show-on-screen configuration defaults to the full unbounded range.
1652    #[test]
1653    fn the_show_on_screen_configuration_defaults_to_unbounded() {
1654        let configuration = PersistentHeaderShowOnScreenConfiguration::new();
1655        assert_eq!(configuration.min_show_on_screen_extent, f64::NEG_INFINITY);
1656        assert_eq!(configuration.max_show_on_screen_extent, f64::INFINITY);
1657        let snap = FloatingHeaderSnapConfiguration::new();
1658        assert_eq!(snap.duration, Duration::from_millis(300));
1659    }
1660}