1use std::rc::Rc;
73
74use frust_core::{
75 AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent,
76 LayoutCtx, PaintCtx, PaintScene, ScrollAxis, ScrollDirection, SemanticsCtx, SliverConstraints,
77 SliverGeometry, SliverWidget, View, Widget, any,
78};
79use kurbo::{Point, Size};
80
81use super::adapter::{axis_point, finite_extent};
82
83pub const SHRINK_REBUILD_TOLERANCE: f64 = 0.5;
88
89type HeaderBuilder<State> = Rc<dyn Fn(f64, bool) -> AnyView<State>>;
92
93pub struct SliverPersistentHeaderDelegate<F> {
103 pub min_extent: f64,
105 pub max_extent: f64,
108 pub build: F,
110}
111
112impl<F> SliverPersistentHeaderDelegate<F> {
113 pub fn new(min_extent: f64, max_extent: f64, build: F) -> Self {
115 Self {
116 min_extent,
117 max_extent,
118 build,
119 }
120 }
121}
122
123pub struct SliverPersistentHeaderView<State: 'static> {
125 min_extent: f64,
126 max_extent: f64,
127 builder: HeaderBuilder<State>,
128 pinned: bool,
129 floating: bool,
130}
131
132pub fn sliver_persistent_header<State, V, F>(
157 delegate: SliverPersistentHeaderDelegate<F>,
158) -> SliverPersistentHeaderView<State>
159where
160 State: 'static,
161 V: View<State>,
162 F: Fn(f64, bool) -> V + 'static,
163{
164 let min_extent = finite_extent(delegate.min_extent);
165 let max_extent = finite_extent(delegate.max_extent).max(min_extent);
166 let build = delegate.build;
167 SliverPersistentHeaderView {
168 min_extent,
169 max_extent,
170 builder: Rc::new(move |shrink, overlaps| any(build(shrink, overlaps))),
171 pinned: false,
172 floating: false,
173 }
174}
175
176impl<State: 'static> SliverPersistentHeaderView<State> {
177 pub fn pinned(mut self, pinned: bool) -> Self {
181 self.pinned = pinned;
182 self
183 }
184
185 pub fn floating(mut self, floating: bool) -> Self {
189 self.floating = floating;
190 self
191 }
192
193 fn child_view(&self, inputs: BuildInputs) -> AnyView<State> {
194 (self.builder)(inputs.shrink_offset, inputs.overlaps_content)
195 }
196}
197
198#[derive(Clone, Copy, Debug, PartialEq)]
200struct BuildInputs {
201 shrink_offset: f64,
202 overlaps_content: bool,
203}
204
205impl BuildInputs {
206 const EXPANDED: BuildInputs = BuildInputs {
207 shrink_offset: 0.0,
208 overlaps_content: false,
209 };
210}
211
212pub struct SliverPersistentHeaderWidget {
216 child: ChildPod,
217 min_extent: f64,
218 max_extent: f64,
219 pinned: bool,
220 floating: bool,
221 axis: ScrollAxis,
223 built: BuildInputs,
225 wanted: BuildInputs,
228 effective_scroll_offset: f64,
232 last_scroll_offset: Option<f64>,
235}
236
237impl SliverPersistentHeaderWidget {
238 fn new<State: 'static>(view: &SliverPersistentHeaderView<State>, child: ChildPod) -> Self {
239 Self {
240 child,
241 min_extent: view.min_extent,
242 max_extent: view.max_extent,
243 pinned: view.pinned,
244 floating: view.floating,
245 axis: ScrollAxis::Vertical,
246 built: BuildInputs::EXPANDED,
247 wanted: BuildInputs::EXPANDED,
248 effective_scroll_offset: 0.0,
249 last_scroll_offset: None,
250 }
251 }
252
253 pub fn shrink_offset(&self) -> f64 {
256 self.wanted.shrink_offset
257 }
258
259 pub fn overlaps_content(&self) -> bool {
262 self.wanted.overlaps_content
263 }
264
265 fn needs_rebuild(&self) -> bool {
269 let (built, wanted) = (self.built, self.wanted);
270 if built.overlaps_content != wanted.overlaps_content {
271 return true;
272 }
273 if built.shrink_offset == wanted.shrink_offset {
274 return false;
275 }
276 (built.shrink_offset - wanted.shrink_offset).abs() >= SHRINK_REBUILD_TOLERANCE
277 || wanted.shrink_offset <= 0.0
278 || wanted.shrink_offset >= self.max_extent
279 }
280
281 fn layout_child_at(
285 &mut self,
286 ctx: &mut LayoutCtx,
287 sc: &SliverConstraints,
288 shrink_offset: f64,
289 overlaps_content: bool,
290 ) -> f64 {
291 self.wanted = BuildInputs {
292 shrink_offset,
293 overlaps_content,
294 };
295 let extent = self.min_extent.max(self.max_extent - shrink_offset);
296 let tight = BoxConstraints::tight(sc.axis.size(extent, sc.cross_axis_extent));
297 sc.axis.main_extent(self.child.layout_child(ctx, &tight))
298 }
299
300 fn advance_floating(&mut self, sc: &SliverConstraints) {
310 let max = self.max_extent;
311 let scroll_offset = sc.scroll_offset;
312 match self.last_scroll_offset {
313 Some(last) if scroll_offset < last || self.effective_scroll_offset < max => {
314 let mut delta = last - scroll_offset;
315 if sc.user_scroll_direction == ScrollDirection::Forward {
316 self.effective_scroll_offset = self.effective_scroll_offset.min(max);
319 } else if delta > 0.0 {
320 delta = 0.0;
323 }
324 self.effective_scroll_offset =
325 (self.effective_scroll_offset - delta).clamp(0.0, scroll_offset);
326 }
327 _ => self.effective_scroll_offset = scroll_offset,
328 }
329 if self.effective_scroll_offset >= max {
330 self.effective_scroll_offset = scroll_offset;
331 }
332 self.last_scroll_offset = Some(scroll_offset);
333 }
334
335 fn layout_scrolling(&mut self, ctx: &mut LayoutCtx, sc: &SliverConstraints) -> SliverGeometry {
337 let max = self.max_extent;
338 let child_extent = self.layout_child_at(ctx, sc, sc.scroll_offset.min(max), false);
339 let paint = max - sc.scroll_offset;
340 self.place_child((paint.max(0.0) - child_extent).min(0.0), sc.axis);
341 SliverGeometry {
342 paint_origin: sc.overlap.min(0.0),
343 has_visual_overflow: true,
344 ..SliverGeometry::new(max, paint.clamp(0.0, sc.remaining_paint_extent), max)
345 }
346 }
347
348 fn layout_pinned(&mut self, ctx: &mut LayoutCtx, sc: &SliverConstraints) -> SliverGeometry {
350 let max = self.max_extent;
351 let child_extent =
352 self.layout_child_at(ctx, sc, sc.scroll_offset.min(max), sc.overlap > 0.0);
353 self.place_child(0.0, sc.axis);
354 let remaining = (sc.remaining_paint_extent - sc.overlap).max(0.0);
358 let paint = child_extent.min(remaining).min(sc.remaining_paint_extent);
359 let layout = (max - sc.scroll_offset).clamp(0.0, remaining).min(paint);
360 SliverGeometry {
361 paint_origin: sc.overlap,
362 layout_extent: layout,
363 has_visual_overflow: false,
364 cache_extent: if layout > 0.0 {
365 layout - sc.cache_origin
366 } else {
367 layout
368 },
369 ..SliverGeometry::new(max, paint, max)
370 }
371 }
372
373 fn layout_floating(&mut self, ctx: &mut LayoutCtx, sc: &SliverConstraints) -> SliverGeometry {
376 self.advance_floating(sc);
377 let max = self.max_extent;
378 let effective = self.effective_scroll_offset;
379 let child_extent =
380 self.layout_child_at(ctx, sc, effective.min(max), effective < sc.scroll_offset);
381 let paint_unclamped = max - effective;
382 let layout_unclamped = max - sc.scroll_offset;
383 let (paint, layout, child_position) = if self.pinned {
384 let min_allowed = self.min_extent.min(sc.remaining_paint_extent);
385 let paint = paint_unclamped.clamp(min_allowed, sc.remaining_paint_extent);
386 (paint, layout_unclamped.clamp(0.0, paint), 0.0)
387 } else {
388 (
389 paint_unclamped.clamp(0.0, sc.remaining_paint_extent),
390 layout_unclamped.clamp(0.0, sc.remaining_paint_extent),
391 (paint_unclamped.max(0.0) - child_extent).min(0.0),
392 )
393 };
394 self.place_child(child_position, sc.axis);
395 SliverGeometry {
396 paint_origin: sc.overlap.min(0.0),
397 layout_extent: layout,
398 has_visual_overflow: true,
399 cache_extent: layout,
400 ..SliverGeometry::new(max, paint, max)
401 }
402 }
403
404 fn place_child(&mut self, main: f64, axis: ScrollAxis) {
406 self.child.set_origin(axis_point(axis, main, 0.0));
407 }
408}
409
410impl<State: 'static> View<State> for SliverPersistentHeaderView<State> {
411 type Element = SliverPersistentHeaderWidget;
412
413 fn build(&self, ctx: &mut BuildCtx<'_>) -> SliverPersistentHeaderWidget {
415 let child = crate::authoring::build_child(&self.child_view(BuildInputs::EXPANDED), ctx);
416 SliverPersistentHeaderWidget::new(self, child)
417 }
418
419 fn rebuild(
420 &self,
421 prev: &Self,
422 element: &mut SliverPersistentHeaderWidget,
423 ctx: &mut BuildCtx<'_>,
424 ) -> ChangeFlags {
425 let mut flags = ChangeFlags::NONE;
426 if element.floating != self.floating {
427 element.last_scroll_offset = None;
430 }
431 if element.min_extent != self.min_extent
432 || element.max_extent != self.max_extent
433 || element.pinned != self.pinned
434 || element.floating != self.floating
435 {
436 element.min_extent = self.min_extent;
437 element.max_extent = self.max_extent;
438 element.pinned = self.pinned;
439 element.floating = self.floating;
440 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
441 }
442 let built = element.built;
443 let next = if element.needs_rebuild() {
444 element.wanted
445 } else {
446 built
447 };
448 let prev_child = prev.child_view(built);
449 let next_child = self.child_view(next);
450 flags |= crate::authoring::rebuild_child(&prev_child, &next_child, &mut element.child, ctx);
451 element.built = next;
452 flags
453 }
454
455 fn teardown(&self, element: &mut SliverPersistentHeaderWidget, ctx: &mut BuildCtx<'_>) {
456 crate::authoring::teardown_child(&self.child_view(element.built), &mut element.child, ctx);
457 }
458}
459
460impl Widget for SliverPersistentHeaderWidget {
461 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
465 let axis = self.axis;
466 let cross = finite_extent(axis.cross_extent(bc.max())).max(axis.cross_extent(bc.min()));
467 let tight = bc.tighten(axis.size(self.max_extent, cross));
468 let size = self.child.layout_child(ctx, &tight);
469 self.child.set_origin(Point::ZERO);
470 self.wanted = BuildInputs::EXPANDED;
471 size
472 }
473
474 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
477 self.child.paint_child(ctx, scene);
478 if self.needs_rebuild() {
479 ctx.request_frame();
480 }
481 }
482
483 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
487 crate::authoring::route_event_single(&mut self.child, ctx, event)
488 }
489
490 fn semantics(&self, ctx: &mut SemanticsCtx) {
491 self.child.semantics_child(ctx);
492 }
493
494 fn as_sliver(&mut self) -> Option<&mut dyn SliverWidget> {
495 Some(self)
496 }
497
498 crate::authoring::visit_children!(child);
499}
500
501impl SliverWidget for SliverPersistentHeaderWidget {
502 fn layout_sliver(&mut self, ctx: &mut LayoutCtx, sc: &SliverConstraints) -> SliverGeometry {
503 self.axis = sc.axis;
504 if self.floating {
505 self.layout_floating(ctx, sc)
506 } else if self.pinned {
507 self.layout_pinned(ctx, sc)
508 } else {
509 self.layout_scrolling(ctx, sc)
510 }
511 }
512}
513
514#[cfg(test)]
515mod tests {
516 use super::*;
517 use crate::test_support::leaf;
518
519 fn constraints(scroll_offset: f64, overlap: f64) -> SliverConstraints {
520 SliverConstraints {
521 axis: ScrollAxis::Vertical,
522 scroll_offset,
523 preceding_scroll_extent: 0.0,
524 overlap,
525 remaining_paint_extent: 500.0,
526 cross_axis_extent: 400.0,
527 viewport_main_axis_extent: 500.0,
528 cache_origin: -scroll_offset.min(250.0),
529 remaining_cache_extent: 750.0 + scroll_offset.min(250.0),
530 user_scroll_direction: ScrollDirection::Idle,
531 }
532 }
533
534 fn header(pinned: bool, floating: bool) -> SliverPersistentHeaderWidget {
535 let view: SliverPersistentHeaderView<()> = sliver_persistent_header(
536 SliverPersistentHeaderDelegate::new(60.0, 200.0, |_: f64, _: bool| leaf(10.0, 10.0)),
537 )
538 .pinned(pinned)
539 .floating(floating);
540 let mut counter = 0;
541 view.build(&mut BuildCtx::new(&mut counter))
542 }
543
544 fn layout(widget: &mut SliverPersistentHeaderWidget, sc: &SliverConstraints) -> SliverGeometry {
545 let mut ctx = LayoutCtx::new();
546 let geometry = widget.layout_sliver(&mut ctx, sc);
547 geometry.debug_assert_valid(sc);
548 geometry
549 }
550
551 #[test]
552 fn extents_are_sanitised() {
553 let view: SliverPersistentHeaderView<()> = sliver_persistent_header(
554 SliverPersistentHeaderDelegate::new(f64::NAN, -5.0, |_: f64, _: bool| leaf(1.0, 1.0)),
555 );
556 assert_eq!((view.min_extent, view.max_extent), (0.0, 0.0));
557 let view: SliverPersistentHeaderView<()> = sliver_persistent_header(
558 SliverPersistentHeaderDelegate::new(80.0, 40.0, |_: f64, _: bool| leaf(1.0, 1.0)),
559 );
560 assert_eq!((view.min_extent, view.max_extent), (80.0, 80.0));
561 }
562
563 #[test]
564 fn a_scrolling_header_shrinks_then_scrolls_away() {
565 let mut w = header(false, false);
566 let g = layout(&mut w, &constraints(100.0, 0.0));
567 assert_eq!(
568 (g.scroll_extent, g.paint_extent, g.layout_extent),
569 (200.0, 100.0, 100.0)
570 );
571 assert_eq!(w.shrink_offset(), 100.0);
572 assert_eq!(w.child.origin(), Point::ZERO);
573 let g = layout(&mut w, &constraints(170.0, 0.0));
574 assert_eq!(g.paint_extent, 30.0);
575 assert_eq!(w.child.origin(), Point::new(0.0, -30.0));
576 let g = layout(&mut w, &constraints(400.0, 0.0));
577 assert!(!g.visible);
578 assert_eq!(w.shrink_offset(), 200.0);
579 }
580
581 #[test]
582 fn a_pinned_header_keeps_its_minimum_and_reports_the_rest_as_overlap() {
583 let mut w = header(true, false);
584 for scroll_offset in [0.0, 50.0, 140.0, 199.0, 200.0, 1000.0] {
585 let g = layout(&mut w, &constraints(scroll_offset, 0.0));
586 assert!(g.paint_extent >= 60.0, "at {scroll_offset}");
587 assert_eq!(g.paint_origin, 0.0);
588 assert_eq!(g.layout_extent, (200.0 - scroll_offset).max(0.0));
589 }
590 let g = layout(&mut w, &constraints(300.0, 50.0));
592 assert_eq!(
593 (g.paint_origin, g.paint_extent, g.layout_extent),
594 (50.0, 60.0, 0.0)
595 );
596 assert!(w.overlaps_content());
597 }
598
599 #[test]
600 fn a_floating_header_only_expands_under_a_forward_user_scroll() {
601 let mut w = header(false, true);
602 assert_eq!(layout(&mut w, &constraints(1000.0, 0.0)).paint_extent, 0.0);
603 assert_eq!(layout(&mut w, &constraints(960.0, 0.0)).paint_extent, 0.0);
605 let forward = SliverConstraints {
607 user_scroll_direction: ScrollDirection::Forward,
608 ..constraints(920.0, 0.0)
609 };
610 let g = layout(&mut w, &forward);
611 assert_eq!((g.paint_extent, g.layout_extent), (40.0, 0.0));
612 assert_eq!(w.shrink_offset(), 160.0);
613 assert!(w.overlaps_content());
614 assert_eq!(layout(&mut w, &forward), g);
616 let g = layout(&mut w, &constraints(940.0, 0.0));
618 assert_eq!(g.paint_extent, 20.0);
619 }
620
621 #[test]
622 fn a_floating_pinned_header_never_paints_below_its_minimum() {
623 let mut w = header(true, true);
624 let g = layout(&mut w, &constraints(1000.0, 0.0));
625 assert_eq!((g.paint_extent, g.layout_extent), (60.0, 0.0));
626 let g = layout(
627 &mut w,
628 &SliverConstraints {
629 user_scroll_direction: ScrollDirection::Forward,
630 ..constraints(900.0, 0.0)
631 },
632 );
633 assert_eq!(g.paint_extent, 100.0);
634 }
635
636 #[test]
637 fn a_sub_tolerance_shrink_change_keeps_the_previous_build() {
638 let mut w = header(true, false);
639 w.built = BuildInputs {
640 shrink_offset: 100.0,
641 overlaps_content: false,
642 };
643 layout(&mut w, &constraints(100.4, 0.0));
644 assert!(!w.needs_rebuild());
645 layout(&mut w, &constraints(100.5, 0.0));
646 assert!(w.needs_rebuild());
647 w.built.shrink_offset = 199.8;
649 layout(&mut w, &constraints(400.0, 0.0));
650 assert!(w.needs_rebuild());
651 w.built.shrink_offset = 0.2;
652 layout(&mut w, &constraints(0.0, 0.0));
653 assert!(w.needs_rebuild());
654 }
655}