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::builder()
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                .build(),
556        );
557        if stretch_offset > 0.0 {
558            0.0
559        } else {
560            (paint_extent - self.child_extent(app)).min(0.0)
561        }
562    }
563
564    /// The body of Flutter's `performLayout` override.
565    fn perform_layout(self: RenderHandle<Self>, app: &mut App) {
566        let scroll_offset = self.constraints(app).scroll_offset;
567        let max_extent = self.max_extent(app);
568        self.layout_child(app, scroll_offset, max_extent, false);
569        let child_position = self.update_geometry(app);
570        self.scrolling_data_mut(app).child_position = Some(child_position);
571    }
572
573    /// The body of Flutter's `childMainAxisPosition` override.
574    fn child_main_axis_position(
575        self: RenderHandle<Self>,
576        app: &App,
577        child: AnyRenderObject,
578    ) -> f64 {
579        debug_assert_eq!(Some(child), self.child(app).map(AnyRenderBox::as_object));
580        self.scrolling_data(app)
581            .child_position
582            .expect("the header has been laid out")
583    }
584}
585
586/// Flutter's `RenderSliverPinnedPersistentHeader` field.
587#[derive(Clone, Copy, Debug)]
588pub struct RenderSliverPinnedPersistentHeaderData {
589    /// Specifies the persistent header's behavior when `show_on_screen` is called.
590    ///
591    /// If set to `None`, the persistent header will delegate the `show_on_screen` call to its
592    /// parent render object.
593    pub show_on_screen_configuration: Option<PersistentHeaderShowOnScreenConfiguration>,
594}
595
596impl RenderSliverPinnedPersistentHeaderData {
597    /// Creates the state of a pinned persistent header.
598    pub const fn new() -> RenderSliverPinnedPersistentHeaderData {
599        RenderSliverPinnedPersistentHeaderData {
600            show_on_screen_configuration: Some(PersistentHeaderShowOnScreenConfiguration::new()),
601        }
602    }
603}
604
605impl Default for RenderSliverPinnedPersistentHeaderData {
606    fn default() -> RenderSliverPinnedPersistentHeaderData {
607        RenderSliverPinnedPersistentHeaderData::new()
608    }
609}
610
611/// A sliver with a box child which never scrolls off the viewport in the positive scroll
612/// direction, and which first scrolls on at a full size but then shrinks as the viewport continues
613/// to scroll.
614///
615/// This sliver avoids overlapping other earlier slivers where possible.
616pub trait RenderSliverPinnedPersistentHeader: RenderSliverPersistentHeader {
617    /// Mixin field access.
618    fn pinned_data(self: RenderHandle<Self>, app: &App) -> &RenderSliverPinnedPersistentHeaderData;
619
620    /// See [`pinned_data`](Self::pinned_data).
621    fn pinned_data_mut(
622        self: RenderHandle<Self>,
623        app: &mut App,
624    ) -> &mut RenderSliverPinnedPersistentHeaderData;
625
626    /// The body of Flutter's `performLayout` override.
627    fn perform_layout(self: RenderHandle<Self>, app: &mut App) {
628        let constraints = self.constraints(app);
629        let max_extent = self.max_extent(app);
630        let overlaps_content = constraints.overlap > 0.0;
631        self.layout_child(app, constraints.scroll_offset, max_extent, overlaps_content);
632        let effective_remaining_paint_extent =
633            (constraints.remaining_paint_extent - constraints.overlap).max(0.0);
634        let layout_extent = clamp_double(
635            max_extent - constraints.scroll_offset,
636            0.0,
637            effective_remaining_paint_extent,
638        );
639        let stretch_offset = if self.stretch_configuration(app).is_some() {
640            constraints.overlap.abs()
641        } else {
642            0.0
643        };
644        let child_extent = self.child_extent(app);
645        let min_extent = self.min_extent(app);
646        self.set_geometry(
647            app,
648            SliverGeometry::builder()
649                .scroll_extent(max_extent)
650                .paint_origin(constraints.overlap)
651                .paint_extent(child_extent.min(effective_remaining_paint_extent))
652                .layout_extent(layout_extent)
653                .max_paint_extent(max_extent + stretch_offset)
654                .max_scroll_obstruction_extent(min_extent)
655                .cache_extent(if layout_extent > 0.0 {
656                    -constraints.cache_origin + layout_extent
657                } else {
658                    layout_extent
659                })
660                // Conservatively say we do have overflow to avoid complexity.
661                .has_visual_overflow(true)
662                .build(),
663        );
664    }
665
666    /// The body of Flutter's `childMainAxisPosition` override.
667    fn child_main_axis_position(
668        self: RenderHandle<Self>,
669        _app: &App,
670        _child: AnyRenderObject,
671    ) -> f64 {
672        let _ = self;
673        0.0
674    }
675
676    /// The body of Flutter's `showOnScreen` override.
677    fn show_on_screen(
678        self: RenderHandle<Self>,
679        app: &mut App,
680        descendant: Option<AnyRenderObject>,
681        rect: Option<Rect>,
682        duration: Duration,
683        curve: Rc<dyn Curve>,
684    ) {
685        let local_bounds = match descendant {
686            Some(descendant) => Some(transform_rect(
687                &descendant.get_transform_to(app, Some(self.as_object())),
688                rect.unwrap_or_else(|| descendant.paint_bounds(app)),
689            )),
690            None => rect,
691        };
692
693        let constraints = self.constraints(app);
694        let child_extent = self.child_extent(app);
695        let new_rect = match apply_growth_direction_to_axis_direction(
696            constraints.axis_direction,
697            constraints.growth_direction,
698        ) {
699            AxisDirection::Up => trim(
700                local_bounds,
701                f64::NEG_INFINITY,
702                f64::INFINITY,
703                child_extent,
704                f64::NEG_INFINITY,
705            ),
706            AxisDirection::Left => trim(
707                local_bounds,
708                f64::NEG_INFINITY,
709                child_extent,
710                f64::INFINITY,
711                f64::NEG_INFINITY,
712            ),
713            AxisDirection::Right => trim(
714                local_bounds,
715                f64::NEG_INFINITY,
716                f64::INFINITY,
717                f64::INFINITY,
718                0.0,
719            ),
720            AxisDirection::Down => trim(
721                local_bounds,
722                0.0,
723                f64::INFINITY,
724                f64::INFINITY,
725                f64::NEG_INFINITY,
726            ),
727        };
728
729        RenderObjectBase::show_on_screen(
730            self,
731            app,
732            Some(self.as_object()),
733            new_rect,
734            duration,
735            curve,
736        );
737    }
738}
739
740/// Flutter's `RenderSliverFloatingPersistentHeader` fields.
741pub struct RenderSliverFloatingPersistentHeaderData {
742    controller: Option<Handle<AnimationController>>,
743    animation: Option<AnyAnimation<f64>>,
744    last_actual_scroll_offset: Option<f64>,
745    effective_scroll_offset: Option<f64>,
746    // Important for pointer scrolling, which does not have the same concept of a hold and release
747    // scroll movement, like dragging. This keeps track of the last ScrollDirection when scrolling
748    // started.
749    last_started_scroll_direction: Option<ScrollDirection>,
750    // Distance from our leading edge to the child's leading edge, in the axis direction.
751    // Negative if we're scrolled off the top.
752    child_position: Option<f64>,
753    vsync: Option<TickerProviderRef>,
754    /// Defines the parameters used to snap (animate) the floating header in and out of view.
755    ///
756    /// If this is `None` then the floating header does not snap.
757    pub snap_configuration: Option<FloatingHeaderSnapConfiguration>,
758    /// Specifies the persistent header's behavior when `show_on_screen` is called.
759    ///
760    /// If set to `None`, the persistent header will delegate the `show_on_screen` call to its
761    /// parent render object.
762    pub show_on_screen_configuration: Option<PersistentHeaderShowOnScreenConfiguration>,
763}
764
765impl RenderSliverFloatingPersistentHeaderData {
766    /// Creates the state of a floating persistent header that has not been laid out.
767    pub fn new(
768        vsync: Option<TickerProviderRef>,
769        snap_configuration: Option<FloatingHeaderSnapConfiguration>,
770        show_on_screen_configuration: Option<PersistentHeaderShowOnScreenConfiguration>,
771    ) -> RenderSliverFloatingPersistentHeaderData {
772        RenderSliverFloatingPersistentHeaderData {
773            controller: None,
774            animation: None,
775            last_actual_scroll_offset: None,
776            effective_scroll_offset: None,
777            last_started_scroll_direction: None,
778            child_position: None,
779            vsync,
780            snap_configuration,
781            show_on_screen_configuration,
782        }
783    }
784}
785
786/// A sliver with a box child which shrinks and scrolls like a
787/// [`RenderSliverScrollingPersistentHeader`], but immediately comes back when the user scrolls in
788/// the reverse direction.
789///
790/// See also:
791///
792///  * [`RenderSliverFloatingPinnedPersistentHeader`], which is similar but sticks to the start of
793///    the viewport rather than scrolling off.
794pub trait RenderSliverFloatingPersistentHeader: RenderSliverPersistentHeader {
795    /// Mixin field access.
796    fn floating_data(
797        self: RenderHandle<Self>,
798        app: &App,
799    ) -> &RenderSliverFloatingPersistentHeaderData;
800
801    /// See [`floating_data`](Self::floating_data).
802    fn floating_data_mut(
803        self: RenderHandle<Self>,
804        app: &mut App,
805    ) -> &mut RenderSliverFloatingPersistentHeaderData;
806
807    /// The body of Flutter's `detach` override: disposes the snap animation controller, which is
808    /// lazily recreated if we are reattached.
809    fn did_detach(self: RenderHandle<Self>, app: &mut App) {
810        if let Some(controller) = self.floating_data(app).controller {
811            controller.dispose(app);
812        }
813        self.floating_data_mut(app).controller = None;
814    }
815
816    /// A [`TickerProvider`] to use when animating the scroll position.
817    fn vsync(self: RenderHandle<Self>, app: &App) -> Option<TickerProviderRef> {
818        self.floating_data(app).vsync.clone()
819    }
820
821    /// Sets [`vsync`](Self::vsync).
822    fn set_vsync(self: RenderHandle<Self>, app: &mut App, value: Option<TickerProviderRef>) {
823        if value == self.floating_data(app).vsync {
824            return;
825        }
826        self.floating_data_mut(app).vsync = value.clone();
827        match value {
828            None => {
829                if let Some(controller) = self.floating_data(app).controller {
830                    controller.dispose(app);
831                }
832                self.floating_data_mut(app).controller = None;
833            }
834            Some(vsync) => {
835                if let Some(controller) = self.floating_data(app).controller {
836                    controller.resync(app, vsync);
837                }
838            }
839        }
840    }
841
842    /// Defines the parameters used to snap (animate) the floating header in and out of view.
843    fn snap_configuration(
844        self: RenderHandle<Self>,
845        app: &App,
846    ) -> Option<FloatingHeaderSnapConfiguration> {
847        self.floating_data(app).snap_configuration.clone()
848    }
849
850    /// Sets [`snap_configuration`](Self::snap_configuration).
851    fn set_snap_configuration(
852        self: RenderHandle<Self>,
853        app: &mut App,
854        value: Option<FloatingHeaderSnapConfiguration>,
855    ) {
856        self.floating_data_mut(app).snap_configuration = value;
857    }
858
859    /// Specifies the persistent header's behavior when `show_on_screen` is called.
860    fn show_on_screen_configuration(
861        self: RenderHandle<Self>,
862        app: &App,
863    ) -> Option<PersistentHeaderShowOnScreenConfiguration> {
864        self.floating_data(app).show_on_screen_configuration
865    }
866
867    /// Sets [`show_on_screen_configuration`](Self::show_on_screen_configuration).
868    fn set_show_on_screen_configuration(
869        self: RenderHandle<Self>,
870        app: &mut App,
871        value: Option<PersistentHeaderShowOnScreenConfiguration>,
872    ) {
873        self.floating_data_mut(app).show_on_screen_configuration = value;
874    }
875
876    /// Flutter's `_effectiveScrollOffset`, the offset the header is laid out at.
877    fn effective_scroll_offset(self: RenderHandle<Self>, app: &App) -> Option<f64> {
878        self.floating_data(app).effective_scroll_offset
879    }
880
881    /// Updates the geometry, and returns the new value for
882    /// [`RenderSliver::child_main_axis_position`].
883    fn update_geometry(self: RenderHandle<Self>, app: &mut App) -> f64 {
884        let constraints = self.constraints(app);
885        let mut stretch_offset = 0.0;
886        if self.stretch_configuration(app).is_some() {
887            stretch_offset += constraints.overlap.abs();
888        }
889        let max_extent = self.max_extent(app);
890        let paint_extent = max_extent
891            - self
892                .effective_scroll_offset(app)
893                .expect("perform_layout sets the effective scroll offset");
894        let layout_extent = max_extent - constraints.scroll_offset;
895        self.set_geometry(
896            app,
897            SliverGeometry::builder()
898                .scroll_extent(max_extent)
899                .paint_origin(constraints.overlap.min(0.0))
900                .paint_extent(clamp_double(
901                    paint_extent,
902                    0.0,
903                    constraints.remaining_paint_extent,
904                ))
905                .layout_extent(clamp_double(
906                    layout_extent,
907                    0.0,
908                    constraints.remaining_paint_extent,
909                ))
910                .max_paint_extent(max_extent + stretch_offset)
911                // Conservatively say we do have overflow to avoid complexity.
912                .has_visual_overflow(true)
913                .build(),
914        );
915        if stretch_offset > 0.0 {
916            0.0
917        } else {
918            (paint_extent - self.child_extent(app)).min(0.0)
919        }
920    }
921
922    /// Flutter's `_updateAnimation`.
923    fn update_animation(
924        self: RenderHandle<Self>,
925        app: &mut App,
926        duration: Duration,
927        end_value: f64,
928        curve: Rc<dyn Curve>,
929    ) {
930        let vsync = self
931            .vsync(app)
932            .expect("vsync must be set if the floating header changes size animatedly");
933        let controller = match self.floating_data(app).controller {
934            Some(controller) => controller,
935            None => {
936                let controller = AnimationController::create(
937                    app,
938                    None,
939                    Some(duration),
940                    None,
941                    0.0,
942                    1.0,
943                    inset_animation::AnimationBehavior::Normal,
944                    vsync,
945                );
946                self.floating_data_mut(app).controller = Some(controller);
947                controller.add_listener(
948                    app,
949                    Listener::handle_method(self.handle(), snap_tick::<Self>),
950                );
951                controller
952            }
953        };
954        let begin = self.effective_scroll_offset(app);
955        let tween = Tween::new(app, begin, Some(end_value));
956        let curve_tween = CurveTween::new(app, curve);
957        let animation = controller.drive(app, tween.chain(curve_tween));
958        self.floating_data_mut(app).animation = Some(animation);
959    }
960
961    /// Update the last known scroll direction when scrolling began.
962    fn update_scroll_start_direction(
963        self: RenderHandle<Self>,
964        app: &mut App,
965        direction: ScrollDirection,
966    ) {
967        self.floating_data_mut(app).last_started_scroll_direction = Some(direction);
968    }
969
970    /// If the header isn't already fully exposed, then scroll it into view.
971    fn maybe_start_snap_animation(
972        self: RenderHandle<Self>,
973        app: &mut App,
974        direction: ScrollDirection,
975    ) {
976        let Some(snap) = self.snap_configuration(app) else {
977            return;
978        };
979        let effective_scroll_offset = self
980            .effective_scroll_offset(app)
981            .expect("the header has been laid out");
982        let max_extent = self.max_extent(app);
983        if direction == ScrollDirection::Forward && effective_scroll_offset <= 0.0 {
984            return;
985        }
986        if direction == ScrollDirection::Reverse && effective_scroll_offset >= max_extent {
987            return;
988        }
989
990        let end_value = if direction == ScrollDirection::Forward {
991            0.0
992        } else {
993            max_extent
994        };
995        self.update_animation(app, snap.duration, end_value, Rc::clone(&snap.curve));
996        if let Some(controller) = self.floating_data(app).controller {
997            controller.forward(app, Some(0.0));
998        }
999    }
1000
1001    /// If a header snap animation or a `show_on_screen` expand animation is underway then stop it.
1002    fn maybe_stop_snap_animation(
1003        self: RenderHandle<Self>,
1004        app: &mut App,
1005        _direction: ScrollDirection,
1006    ) {
1007        if let Some(controller) = self.floating_data(app).controller {
1008            controller.stop(app, false);
1009        }
1010    }
1011
1012    /// The body of Flutter's `performLayout` override.
1013    fn perform_layout(self: RenderHandle<Self>, app: &mut App) {
1014        let constraints = self.constraints(app);
1015        let max_extent = self.max_extent(app);
1016        let data = self.floating_data(app);
1017        let last_actual_scroll_offset = data.last_actual_scroll_offset;
1018        let effective_scroll_offset = data.effective_scroll_offset;
1019        let last_started_scroll_direction = data.last_started_scroll_direction;
1020        // We've laid out at least once to get an initial position, and either we are scrolling
1021        // back, so should reveal, or some part of it is visible, so should shrink or reveal as
1022        // appropriate.
1023        let new_effective_scroll_offset = match last_actual_scroll_offset {
1024            Some(last_actual_scroll_offset)
1025                if constraints.scroll_offset < last_actual_scroll_offset
1026                    || effective_scroll_offset.is_some_and(|offset| offset < max_extent) =>
1027            {
1028                let mut delta = last_actual_scroll_offset - constraints.scroll_offset;
1029                let mut effective_scroll_offset =
1030                    effective_scroll_offset.expect("laid out at least once");
1031                let allow_floating_expansion = constraints.user_scroll_direction
1032                    == ScrollDirection::Forward
1033                    || last_started_scroll_direction == Some(ScrollDirection::Forward);
1034                if allow_floating_expansion {
1035                    if effective_scroll_offset > max_extent {
1036                        // We're scrolled off-screen, but should reveal, so pretend we're just at
1037                        // the limit.
1038                        effective_scroll_offset = max_extent;
1039                    }
1040                } else if delta > 0.0 {
1041                    // Disallow the expansion. (But allow shrinking, i.e. delta < 0.0 is fine.)
1042                    delta = 0.0;
1043                }
1044                clamp_double(
1045                    effective_scroll_offset - delta,
1046                    0.0,
1047                    constraints.scroll_offset,
1048                )
1049            }
1050            _ => constraints.scroll_offset,
1051        };
1052        self.floating_data_mut(app).effective_scroll_offset = Some(new_effective_scroll_offset);
1053        let overlaps_content = new_effective_scroll_offset < constraints.scroll_offset;
1054
1055        self.layout_child(
1056            app,
1057            new_effective_scroll_offset,
1058            max_extent,
1059            overlaps_content,
1060        );
1061        let child_position = self.update_geometry(app);
1062        let data = self.floating_data_mut(app);
1063        data.child_position = Some(child_position);
1064        data.last_actual_scroll_offset = Some(constraints.scroll_offset);
1065    }
1066
1067    /// The body of Flutter's `showOnScreen` override.
1068    fn show_on_screen(
1069        self: RenderHandle<Self>,
1070        app: &mut App,
1071        descendant: Option<AnyRenderObject>,
1072        rect: Option<Rect>,
1073        duration: Duration,
1074        curve: Rc<dyn Curve>,
1075    ) {
1076        let Some(show_on_screen) = self.show_on_screen_configuration(app) else {
1077            return RenderObjectBase::show_on_screen(self, app, descendant, rect, duration, curve);
1078        };
1079
1080        let child = self.child(app);
1081        debug_assert!(child.is_some() || descendant.is_none());
1082        // We prefer the child's coordinate space (instead of the sliver's) because it's easier for
1083        // us to convert the target rect into target extents: when the sliver is sitting above the
1084        // leading edge (not possible with pinned headers), the leading edge of the sliver and the
1085        // leading edge of the child will not be aligned. The only exception is when the child is
1086        // `None` (and thus `descendant` is `None`).
1087        let child_bounds = match descendant {
1088            Some(descendant) => Some(transform_rect(
1089                &descendant.get_transform_to(app, child.map(AnyRenderBox::as_object)),
1090                rect.unwrap_or_else(|| descendant.paint_bounds(app)),
1091            )),
1092            None => rect,
1093        };
1094
1095        let constraints = self.constraints(app);
1096        let child_extent = self.child_extent(app);
1097        let (mut target_extent, target_rect) = match apply_growth_direction_to_axis_direction(
1098            constraints.axis_direction,
1099            constraints.growth_direction,
1100        ) {
1101            AxisDirection::Up => (
1102                child_extent - child_bounds.map_or(0.0, |bounds| bounds.top),
1103                trim(
1104                    child_bounds,
1105                    f64::NEG_INFINITY,
1106                    f64::INFINITY,
1107                    child_extent,
1108                    f64::NEG_INFINITY,
1109                ),
1110            ),
1111            AxisDirection::Right => (
1112                child_bounds.map_or(child_extent, |bounds| bounds.right),
1113                trim(
1114                    child_bounds,
1115                    f64::NEG_INFINITY,
1116                    f64::INFINITY,
1117                    f64::INFINITY,
1118                    0.0,
1119                ),
1120            ),
1121            AxisDirection::Down => (
1122                child_bounds.map_or(child_extent, |bounds| bounds.bottom),
1123                trim(
1124                    child_bounds,
1125                    0.0,
1126                    f64::INFINITY,
1127                    f64::INFINITY,
1128                    f64::NEG_INFINITY,
1129                ),
1130            ),
1131            AxisDirection::Left => (
1132                child_extent - child_bounds.map_or(0.0, |bounds| bounds.left),
1133                trim(
1134                    child_bounds,
1135                    f64::NEG_INFINITY,
1136                    child_extent,
1137                    f64::INFINITY,
1138                    f64::NEG_INFINITY,
1139                ),
1140            ),
1141        };
1142
1143        // A stretch header can have a bigger child extent than max extent.
1144        let max_extent = self.max_extent(app);
1145        let effective_max_extent = child_extent.max(max_extent);
1146
1147        target_extent = clamp_double(
1148            clamp_double(
1149                target_extent,
1150                show_on_screen.min_show_on_screen_extent,
1151                show_on_screen.max_show_on_screen_extent,
1152            ),
1153            // Clamp the value back to the valid range after applying additional constraints.
1154            // Contracting is not allowed.
1155            child_extent,
1156            effective_max_extent,
1157        );
1158
1159        // Expands the header if needed, with animation.
1160        let controller_status = self
1161            .floating_data(app)
1162            .controller
1163            .map(|controller| controller.status(app));
1164        if target_extent > child_extent && controller_status != Some(AnimationStatus::Forward) {
1165            let target_scroll_offset = max_extent - target_extent;
1166            debug_assert!(
1167                self.vsync(app).is_some(),
1168                "vsync must be set if the floating header changes size animatedly"
1169            );
1170            self.update_animation(app, duration, target_scroll_offset, Rc::clone(&curve));
1171            if let Some(controller) = self.floating_data(app).controller {
1172                controller.forward(app, Some(0.0));
1173            }
1174        }
1175
1176        let descendant = match descendant {
1177            None => Some(self.as_object()),
1178            Some(_) => child.map(AnyRenderBox::as_object),
1179        };
1180        RenderObjectBase::show_on_screen(self, app, descendant, target_rect, duration, curve);
1181    }
1182
1183    /// The body of Flutter's `childMainAxisPosition` override.
1184    fn child_main_axis_position(
1185        self: RenderHandle<Self>,
1186        app: &App,
1187        child: AnyRenderObject,
1188    ) -> f64 {
1189        debug_assert_eq!(Some(child), self.child(app).map(AnyRenderBox::as_object));
1190        self.floating_data(app).child_position.unwrap_or(0.0)
1191    }
1192
1193    /// The type-erased `RenderSliverFloatingPersistentHeader` handle. Free: the vtable is a
1194    /// `const`, and the id is copied.
1195    fn as_floating_persistent_header(
1196        self: RenderHandle<Self>,
1197    ) -> AnyRenderSliverFloatingPersistentHeader {
1198        AnyRenderSliverFloatingPersistentHeader {
1199            id: self.id(),
1200            vtable: const { &RenderSliverFloatingPersistentHeaderVTable::of::<Self>() },
1201        }
1202    }
1203}
1204
1205/// The vtable of an erased [`AnyRenderSliverFloatingPersistentHeader`].
1206struct RenderSliverFloatingPersistentHeaderVTable {
1207    update_scroll_start_direction: fn(&mut App, HandleId, ScrollDirection),
1208    maybe_start_snap_animation: fn(&mut App, HandleId, ScrollDirection),
1209    maybe_stop_snap_animation: fn(&mut App, HandleId, ScrollDirection),
1210}
1211
1212impl RenderSliverFloatingPersistentHeaderVTable {
1213    const fn of<T: RenderSliverFloatingPersistentHeader>()
1214    -> RenderSliverFloatingPersistentHeaderVTable {
1215        RenderSliverFloatingPersistentHeaderVTable {
1216            update_scroll_start_direction: |app, id, direction| {
1217                T::update_scroll_start_direction(resolve(id), app, direction)
1218            },
1219            maybe_start_snap_animation: |app, id, direction| {
1220                T::maybe_start_snap_animation(resolve(id), app, direction)
1221            },
1222            maybe_stop_snap_animation: |app, id, direction| {
1223                T::maybe_stop_snap_animation(resolve(id), app, direction)
1224            },
1225        }
1226    }
1227}
1228
1229/// Erased [`RenderSliverFloatingPersistentHeader`]: what Dart's
1230/// `findAncestorRenderObjectOfType<RenderSliverFloatingPersistentHeader>()` hands back.
1231#[derive(Clone, Copy)]
1232pub struct AnyRenderSliverFloatingPersistentHeader {
1233    id: HandleId,
1234    vtable: &'static RenderSliverFloatingPersistentHeaderVTable,
1235}
1236
1237impl AnyRenderSliverFloatingPersistentHeader {
1238    /// See [`RenderSliverFloatingPersistentHeader::update_scroll_start_direction`].
1239    pub fn update_scroll_start_direction(self, app: &mut App, direction: ScrollDirection) {
1240        (self.vtable.update_scroll_start_direction)(app, self.id, direction)
1241    }
1242
1243    /// See [`RenderSliverFloatingPersistentHeader::maybe_start_snap_animation`].
1244    pub fn maybe_start_snap_animation(self, app: &mut App, direction: ScrollDirection) {
1245        (self.vtable.maybe_start_snap_animation)(app, self.id, direction)
1246    }
1247
1248    /// See [`RenderSliverFloatingPersistentHeader::maybe_stop_snap_animation`].
1249    pub fn maybe_stop_snap_animation(self, app: &mut App, direction: ScrollDirection) {
1250        (self.vtable.maybe_stop_snap_animation)(app, self.id, direction)
1251    }
1252}
1253
1254impl PartialEq for AnyRenderSliverFloatingPersistentHeader {
1255    fn eq(&self, other: &AnyRenderSliverFloatingPersistentHeader) -> bool {
1256        self.id == other.id
1257    }
1258}
1259
1260impl Eq for AnyRenderSliverFloatingPersistentHeader {}
1261
1262/// The snap animation's listener: Dart's closure inside `_updateAnimation`.
1263fn snap_tick<T: RenderSliverFloatingPersistentHeader>(this: Handle<T>, app: &mut App) {
1264    let this = RenderHandle::from_handle(this);
1265    let Some(animation) = this.floating_data(app).animation else {
1266        return;
1267    };
1268    let value = animation.value(app);
1269    if this.effective_scroll_offset(app) == Some(value) {
1270        return;
1271    }
1272    this.floating_data_mut(app).effective_scroll_offset = Some(value);
1273    RenderSliverPersistentHeader::mark_needs_layout(this, app);
1274}
1275
1276/// A sliver with a box child which shrinks and then remains pinned to the start of the viewport
1277/// like a [`RenderSliverPinnedPersistentHeader`], but immediately grows when the user scrolls in
1278/// the reverse direction.
1279///
1280/// See also:
1281///
1282///  * [`RenderSliverFloatingPersistentHeader`], which is similar but scrolls off the top rather
1283///    than sticking to it.
1284pub trait RenderSliverFloatingPinnedPersistentHeader: RenderSliverFloatingPersistentHeader {
1285    /// The body of Flutter's `updateGeometry` override.
1286    fn update_geometry(self: RenderHandle<Self>, app: &mut App) -> f64 {
1287        let constraints = self.constraints(app);
1288        let min_extent = self.min_extent(app);
1289        let min_allowed_extent = if constraints.remaining_paint_extent > min_extent {
1290            min_extent
1291        } else {
1292            constraints.remaining_paint_extent
1293        };
1294        let max_extent = self.max_extent(app);
1295        let paint_extent = max_extent
1296            - self
1297                .effective_scroll_offset(app)
1298                .expect("perform_layout sets the effective scroll offset");
1299        let clamped_paint_extent = clamp_double(
1300            paint_extent,
1301            min_allowed_extent,
1302            constraints.remaining_paint_extent,
1303        );
1304        let layout_extent = max_extent - constraints.scroll_offset;
1305        let stretch_offset = if self.stretch_configuration(app).is_some() {
1306            constraints.overlap.abs()
1307        } else {
1308            0.0
1309        };
1310        self.set_geometry(
1311            app,
1312            SliverGeometry::builder()
1313                .scroll_extent(max_extent)
1314                .paint_origin(constraints.overlap.min(0.0))
1315                .paint_extent(clamped_paint_extent)
1316                .layout_extent(clamp_double(layout_extent, 0.0, clamped_paint_extent))
1317                .max_paint_extent(max_extent + stretch_offset)
1318                .max_scroll_obstruction_extent(min_extent)
1319                // Conservatively say we do have overflow to avoid complexity.
1320                .has_visual_overflow(true)
1321                .build(),
1322        );
1323        0.0
1324    }
1325}
1326
1327#[cfg(test)]
1328mod tests {
1329    use std::cell::Cell;
1330
1331    use inset_embedder::Size;
1332    use inset_foundation::{AppCell, Task};
1333
1334    use super::*;
1335    use crate::object::{RenderObject, RenderObjectData, RenderObjectWithChildData};
1336    use crate::sliver::{AnyRenderSliver, RenderSliverData};
1337    use crate::sliver_multi_box_adaptor::test_support::hit_testable_box;
1338    use crate::sliver_multi_box_adaptor::viewport_test_support::ScrollHarness;
1339
1340    /// The shape every persistent header leaf in this test shares: a min and a max extent, and a
1341    /// box child of the header's current extent.
1342    macro_rules! persistent_header_leaf {
1343        ($leaf:ident, $extra:ident, $perform_layout:path, $child_position:path) => {
1344            struct $leaf {
1345                render_object: RenderObjectData,
1346                render_sliver: RenderSliverData,
1347                child: RenderObjectWithChildData<AnyRenderBox>,
1348                header: RenderSliverPersistentHeaderData,
1349                extra: $extra,
1350                min_extent: f64,
1351                max_extent: f64,
1352            }
1353
1354            impl RenderObjectWithChildMixin for $leaf {
1355                type ChildType = AnyRenderBox;
1356
1357                fn child_data(
1358                    self: RenderHandle<Self>,
1359                    app: &App,
1360                ) -> &RenderObjectWithChildData<AnyRenderBox> {
1361                    &self.get(app).child
1362                }
1363
1364                fn child_data_mut(
1365                    self: RenderHandle<Self>,
1366                    app: &mut App,
1367                ) -> &mut RenderObjectWithChildData<AnyRenderBox> {
1368                    &mut self.get_mut(app).child
1369                }
1370            }
1371
1372            impl RenderSliverHelpers for $leaf {}
1373
1374            impl RenderSliverPersistentHeader for $leaf {
1375                crate::render_sliver_persistent_header_accessors!();
1376
1377                fn max_extent(self: RenderHandle<Self>, app: &App) -> f64 {
1378                    self.get(app).max_extent
1379                }
1380
1381                fn min_extent(self: RenderHandle<Self>, app: &App) -> f64 {
1382                    self.get(app).min_extent
1383                }
1384            }
1385
1386            impl RenderObject for $leaf {
1387                crate::render_object_accessors!();
1388
1389                fn visit_children(
1390                    self: RenderHandle<Self>,
1391                    app: &App,
1392                    visitor: &mut dyn FnMut(AnyRenderObject),
1393                ) {
1394                    if let Some(child) = self.child(app) {
1395                        visitor(child.as_object());
1396                    }
1397                }
1398
1399                fn apply_paint_transform(
1400                    self: RenderHandle<Self>,
1401                    app: &App,
1402                    child: AnyRenderObject,
1403                    transform: &mut Matrix4,
1404                ) {
1405                    RenderSliverPersistentHeader::apply_paint_transform(self, app, child, transform)
1406                }
1407
1408                fn perform_layout(self: RenderHandle<Self>, app: &mut App) {
1409                    $perform_layout(self, app)
1410                }
1411
1412                fn paint(
1413                    self: RenderHandle<Self>,
1414                    app: &mut App,
1415                    context: &mut PaintingContext,
1416                    offset: Offset,
1417                ) {
1418                    RenderSliverPersistentHeader::paint(self, app, context, offset)
1419                }
1420            }
1421
1422            impl RenderSliver for $leaf {
1423                crate::render_sliver_accessors!();
1424
1425                fn hit_test_children(
1426                    self: RenderHandle<Self>,
1427                    app: &mut App,
1428                    result: &mut SliverHitTestResult<'_>,
1429                    main_axis_position: f64,
1430                    cross_axis_position: f64,
1431                ) -> bool {
1432                    RenderSliverPersistentHeader::hit_test_children(
1433                        self,
1434                        app,
1435                        result,
1436                        main_axis_position,
1437                        cross_axis_position,
1438                    )
1439                }
1440
1441                fn child_main_axis_position(
1442                    self: RenderHandle<Self>,
1443                    app: &App,
1444                    child: AnyRenderObject,
1445                ) -> f64 {
1446                    $child_position(self, app, child)
1447                }
1448            }
1449        };
1450    }
1451
1452    type ScrollingExtra = RenderSliverScrollingPersistentHeaderData;
1453    type PinnedExtra = RenderSliverPinnedPersistentHeaderData;
1454
1455    persistent_header_leaf!(
1456        TestScrollingHeader,
1457        ScrollingExtra,
1458        RenderSliverScrollingPersistentHeader::perform_layout,
1459        RenderSliverScrollingPersistentHeader::child_main_axis_position
1460    );
1461
1462    impl RenderSliverScrollingPersistentHeader for TestScrollingHeader {
1463        fn scrolling_data(
1464            self: RenderHandle<Self>,
1465            app: &App,
1466        ) -> &RenderSliverScrollingPersistentHeaderData {
1467            &self.get(app).extra
1468        }
1469
1470        fn scrolling_data_mut(
1471            self: RenderHandle<Self>,
1472            app: &mut App,
1473        ) -> &mut RenderSliverScrollingPersistentHeaderData {
1474            &mut self.get_mut(app).extra
1475        }
1476    }
1477
1478    persistent_header_leaf!(
1479        TestPinnedHeader,
1480        PinnedExtra,
1481        RenderSliverPinnedPersistentHeader::perform_layout,
1482        RenderSliverPinnedPersistentHeader::child_main_axis_position
1483    );
1484
1485    impl RenderSliverPinnedPersistentHeader for TestPinnedHeader {
1486        fn pinned_data(
1487            self: RenderHandle<Self>,
1488            app: &App,
1489        ) -> &RenderSliverPinnedPersistentHeaderData {
1490            &self.get(app).extra
1491        }
1492
1493        fn pinned_data_mut(
1494            self: RenderHandle<Self>,
1495            app: &mut App,
1496        ) -> &mut RenderSliverPinnedPersistentHeaderData {
1497            &mut self.get_mut(app).extra
1498        }
1499    }
1500
1501    fn scrolling_header(
1502        app: &mut App,
1503        min_extent: f64,
1504        max_extent: f64,
1505    ) -> RenderHandle<TestScrollingHeader> {
1506        let child = hit_testable_box(app, max_extent);
1507        let header = RenderHandle::new_sliver(
1508            app,
1509            TestScrollingHeader {
1510                render_object: RenderObjectData::new(),
1511                render_sliver: RenderSliverData::new(),
1512                child: RenderObjectWithChildData::new(),
1513                header: RenderSliverPersistentHeaderData::new(None),
1514                extra: RenderSliverScrollingPersistentHeaderData::new(),
1515                min_extent,
1516                max_extent,
1517            },
1518        );
1519        header.set_child(app, Some(child));
1520        header
1521    }
1522
1523    fn pinned_header(
1524        app: &mut App,
1525        min_extent: f64,
1526        max_extent: f64,
1527    ) -> RenderHandle<TestPinnedHeader> {
1528        let child = hit_testable_box(app, max_extent);
1529        let header = RenderHandle::new_sliver(
1530            app,
1531            TestPinnedHeader {
1532                render_object: RenderObjectData::new(),
1533                render_sliver: RenderSliverData::new(),
1534                child: RenderObjectWithChildData::new(),
1535                header: RenderSliverPersistentHeaderData::new(None),
1536                extra: RenderSliverPinnedPersistentHeaderData::new(),
1537                min_extent,
1538                max_extent,
1539            },
1540        );
1541        header.set_child(app, Some(child));
1542        header
1543    }
1544
1545    /// A header's `layout_child` runs inside a layout callback, so layout has to go through the
1546    /// owner's layout phase.
1547    fn lay_out(app: &mut App, sliver: AnyRenderSliver, scroll_offset: f64) -> ScrollHarness {
1548        let harness = ScrollHarness::new(app, sliver, Size::new(200.0, 400.0));
1549        harness.scroll_to(app, scroll_offset);
1550        harness
1551    }
1552
1553    /// `sliver_persistent_header_test.dart`: a scrolling header shrinks its paint extent as it
1554    /// scrolls off, keeping its full scroll extent.
1555    #[test]
1556    fn a_scrolling_header_shrinks_as_it_scrolls_off() {
1557        let cell = AppCell::new();
1558        let mut app = cell.borrow_mut();
1559        let header = scrolling_header(&mut app, 40.0, 120.0);
1560        lay_out(&mut app, header.as_sliver(), 50.0);
1561
1562        let geometry = header.geometry(&app);
1563        assert_eq!(geometry.scroll_extent, 120.0);
1564        assert_eq!(geometry.paint_extent, 70.0);
1565        assert_eq!(geometry.max_scroll_obstruction_extent, 0.0);
1566        // The child shrinks to what is left of the maximum extent, floored at the minimum.
1567        assert_eq!(header.child_extent(&app), 70.0);
1568        assert_eq!(header.last_shrink_offset(&app), 50.0);
1569        let child = header.child(&app).expect("has a child");
1570        assert_eq!(
1571            RenderSliver::child_main_axis_position(header, &app, child.as_object()),
1572            0.0
1573        );
1574    }
1575
1576    /// A pinned header stays at the leading edge and reports its minimum extent as the scroll
1577    /// obstruction.
1578    #[test]
1579    fn a_pinned_header_stays_at_the_leading_edge() {
1580        let cell = AppCell::new();
1581        let mut app = cell.borrow_mut();
1582        let header = pinned_header(&mut app, 40.0, 120.0);
1583        lay_out(&mut app, header.as_sliver(), 100.0);
1584
1585        let geometry = header.geometry(&app);
1586        assert_eq!(geometry.scroll_extent, 120.0);
1587        assert_eq!(geometry.max_scroll_obstruction_extent, 40.0);
1588        // Shrunk to the minimum extent, still painting at the leading edge.
1589        assert_eq!(header.child_extent(&app), 40.0);
1590        assert_eq!(geometry.paint_extent, 40.0);
1591        assert_eq!(geometry.layout_extent, 20.0);
1592        let child = header.child(&app).expect("has a child");
1593        assert_eq!(
1594            RenderSliver::child_main_axis_position(header, &app, child.as_object()),
1595            0.0
1596        );
1597    }
1598
1599    /// An unscrolled pinned header is at its maximum extent.
1600    #[test]
1601    fn an_unscrolled_pinned_header_is_at_its_maximum_extent() {
1602        let cell = AppCell::new();
1603        let mut app = cell.borrow_mut();
1604        let header = pinned_header(&mut app, 40.0, 120.0);
1605        lay_out(&mut app, header.as_sliver(), 0.0);
1606
1607        let geometry = header.geometry(&app);
1608        assert_eq!(geometry.paint_extent, 120.0);
1609        assert_eq!(geometry.layout_extent, 120.0);
1610        assert_eq!(header.last_shrink_offset(&app), 0.0);
1611        assert!(!header.last_overlaps_content(&app));
1612    }
1613
1614    /// `layout_child` re-runs `update_child` only when the shrink offset or overlap changes.
1615    #[test]
1616    fn layout_child_reports_the_shrink_offset() {
1617        let cell = AppCell::new();
1618        let mut app = cell.borrow_mut();
1619        let header = scrolling_header(&mut app, 40.0, 120.0);
1620        lay_out(&mut app, header.as_sliver(), 80.0);
1621
1622        assert_eq!(header.last_shrink_offset(&app), 80.0);
1623        // The child is laid out with the remaining extent, floored at the minimum extent.
1624        assert_eq!(header.child_extent(&app), 40.0);
1625    }
1626
1627    /// The stretch trigger fires once as the overscroll crosses the trigger offset, and again
1628    /// only after the header has come back under it.
1629    #[test]
1630    fn the_stretch_trigger_fires_once_per_crossing() {
1631        let cell = AppCell::new();
1632        let mut app = cell.borrow_mut();
1633        let triggered = Rc::new(Cell::new(0));
1634        let counter = Rc::clone(&triggered);
1635        let configuration =
1636            OverScrollHeaderStretchConfiguration::new().on_stretch_trigger(Rc::new(move |_app| {
1637                counter.set(counter.get() + 1);
1638                Task::ready(())
1639            }));
1640        let header = scrolling_header(&mut app, 40.0, 120.0);
1641        header.set_stretch_configuration(&mut app, Some(configuration));
1642        let harness = lay_out(&mut app, header.as_sliver(), 0.0);
1643        assert_eq!(triggered.get(), 0);
1644
1645        harness.scroll_to(&mut app, -120.0);
1646        assert_eq!(triggered.get(), 1);
1647        harness.scroll_to(&mut app, -130.0);
1648        assert_eq!(triggered.get(), 1, "already past the trigger offset");
1649
1650        harness.scroll_to(&mut app, 0.0);
1651        harness.scroll_to(&mut app, -100.0);
1652        assert_eq!(triggered.get(), 2);
1653    }
1654
1655    /// The show-on-screen configuration defaults to the full unbounded range.
1656    #[test]
1657    fn the_show_on_screen_configuration_defaults_to_unbounded() {
1658        let configuration = PersistentHeaderShowOnScreenConfiguration::new();
1659        assert_eq!(configuration.min_show_on_screen_extent, f64::NEG_INFINITY);
1660        assert_eq!(configuration.max_show_on_screen_extent, f64::INFINITY);
1661        let snap = FloatingHeaderSnapConfiguration::new();
1662        assert_eq!(snap.duration, Duration::from_millis(300));
1663    }
1664}