use std::rc::Rc;
use std::time::Duration;
use inset_animation::{
Animatable, AnimationController, AnimationStatus, AnyAnimation, Curve, CurveTween, Curves,
Tween,
};
use inset_embedder::{Matrix4, Offset, Rect, clamp_double};
use inset_foundation::{App, AsyncCallback, Handle, HandleId, ListenableObject, Listener};
use inset_painting::{Axis, AxisDirection, transform_rect};
use inset_scheduler::{Ticker, TickerCallback, TickerProvider, TickerProviderObject};
use crate::box_::{AnyRenderBox, BoxHitTestResult};
use crate::object::{
AnyRenderObject, RenderHandle, RenderObjectBase, RenderObjectWithChildMixin, resolve,
};
use crate::painting_context::PaintingContext;
use crate::sliver::{
RenderSliver, RenderSliverHelpers, SliverGeometry, SliverHitTestResult,
apply_growth_direction_to_axis_direction, calculate_cache_offset,
};
use crate::viewport_offset::ScrollDirection;
fn trim(original: Option<Rect>, top: f64, right: f64, bottom: f64, left: f64) -> Option<Rect> {
original.map(|rect| rect.intersect(Rect::from_ltrb(left, top, right, bottom)))
}
#[derive(Clone)]
pub struct TickerProviderRef(Rc<dyn TickerProvider>);
impl TickerProviderRef {
pub fn new<T: TickerProviderObject>(provider: Handle<T>) -> TickerProviderRef {
TickerProviderRef(Rc::new(provider))
}
}
impl TickerProvider for TickerProviderRef {
fn create_ticker(&self, app: &mut App, on_tick: TickerCallback) -> Handle<Ticker> {
self.0.create_ticker(app, on_tick)
}
}
impl PartialEq for TickerProviderRef {
fn eq(&self, other: &TickerProviderRef) -> bool {
Rc::ptr_eq(&self.0, &other.0)
}
}
impl std::fmt::Debug for TickerProviderRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("TickerProviderRef")
}
}
#[derive(Clone)]
pub struct OverScrollHeaderStretchConfiguration {
pub stretch_trigger_offset: f64,
pub on_stretch_trigger: Option<AsyncCallback>,
}
impl OverScrollHeaderStretchConfiguration {
pub fn new() -> OverScrollHeaderStretchConfiguration {
OverScrollHeaderStretchConfiguration {
stretch_trigger_offset: 100.0,
on_stretch_trigger: None,
}
}
pub fn stretch_trigger_offset(mut self, value: f64) -> OverScrollHeaderStretchConfiguration {
self.stretch_trigger_offset = value;
self
}
pub fn on_stretch_trigger(
mut self,
value: AsyncCallback,
) -> OverScrollHeaderStretchConfiguration {
self.on_stretch_trigger = Some(value);
self
}
}
impl Default for OverScrollHeaderStretchConfiguration {
fn default() -> OverScrollHeaderStretchConfiguration {
OverScrollHeaderStretchConfiguration::new()
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PersistentHeaderShowOnScreenConfiguration {
pub min_show_on_screen_extent: f64,
pub max_show_on_screen_extent: f64,
}
impl PersistentHeaderShowOnScreenConfiguration {
pub const fn new() -> PersistentHeaderShowOnScreenConfiguration {
PersistentHeaderShowOnScreenConfiguration {
min_show_on_screen_extent: f64::NEG_INFINITY,
max_show_on_screen_extent: f64::INFINITY,
}
}
pub fn min_show_on_screen_extent(
mut self,
value: f64,
) -> PersistentHeaderShowOnScreenConfiguration {
self.min_show_on_screen_extent = value;
debug_assert!(self.min_show_on_screen_extent <= self.max_show_on_screen_extent);
self
}
pub fn max_show_on_screen_extent(
mut self,
value: f64,
) -> PersistentHeaderShowOnScreenConfiguration {
self.max_show_on_screen_extent = value;
debug_assert!(self.min_show_on_screen_extent <= self.max_show_on_screen_extent);
self
}
}
impl Default for PersistentHeaderShowOnScreenConfiguration {
fn default() -> PersistentHeaderShowOnScreenConfiguration {
PersistentHeaderShowOnScreenConfiguration::new()
}
}
#[derive(Clone)]
pub struct FloatingHeaderSnapConfiguration {
pub curve: Rc<dyn Curve>,
pub duration: Duration,
}
impl FloatingHeaderSnapConfiguration {
pub fn new() -> FloatingHeaderSnapConfiguration {
FloatingHeaderSnapConfiguration {
curve: Curves::ease(),
duration: Duration::from_millis(300),
}
}
pub fn curve(mut self, value: Rc<dyn Curve>) -> FloatingHeaderSnapConfiguration {
self.curve = value;
self
}
pub fn duration(mut self, value: Duration) -> FloatingHeaderSnapConfiguration {
self.duration = value;
self
}
}
impl Default for FloatingHeaderSnapConfiguration {
fn default() -> FloatingHeaderSnapConfiguration {
FloatingHeaderSnapConfiguration::new()
}
}
pub struct RenderSliverPersistentHeaderData {
last_stretch_offset: f64,
needs_update_child: bool,
last_shrink_offset: f64,
last_overlaps_content: bool,
pub stretch_configuration: Option<OverScrollHeaderStretchConfiguration>,
}
impl RenderSliverPersistentHeaderData {
pub fn new(
stretch_configuration: Option<OverScrollHeaderStretchConfiguration>,
) -> RenderSliverPersistentHeaderData {
RenderSliverPersistentHeaderData {
last_stretch_offset: 0.0,
needs_update_child: true,
last_shrink_offset: 0.0,
last_overlaps_content: false,
stretch_configuration,
}
}
}
#[macro_export]
macro_rules! render_sliver_persistent_header_accessors {
() => {
fn header_data(
self: $crate::RenderHandle<Self>,
app: &::inset_foundation::App,
) -> &$crate::RenderSliverPersistentHeaderData {
&self.get(app).header
}
fn header_data_mut(
self: $crate::RenderHandle<Self>,
app: &mut ::inset_foundation::App,
) -> &mut $crate::RenderSliverPersistentHeaderData {
&mut self.get_mut(app).header
}
};
}
pub trait RenderSliverPersistentHeader:
RenderSliver + RenderObjectWithChildMixin<ChildType = AnyRenderBox> + RenderSliverHelpers
{
fn header_data(self: RenderHandle<Self>, app: &App) -> &RenderSliverPersistentHeaderData;
fn header_data_mut(
self: RenderHandle<Self>,
app: &mut App,
) -> &mut RenderSliverPersistentHeaderData;
fn max_extent(self: RenderHandle<Self>, app: &App) -> f64;
fn min_extent(self: RenderHandle<Self>, app: &App) -> f64;
fn child_extent(self: RenderHandle<Self>, app: &App) -> f64 {
let Some(child) = self.child(app) else {
return 0.0;
};
debug_assert!(child.has_size(app));
match self.constraints(app).axis() {
Axis::Vertical => child.size(app).height(),
Axis::Horizontal => child.size(app).width(),
}
}
fn last_shrink_offset(self: RenderHandle<Self>, app: &App) -> f64 {
self.header_data(app).last_shrink_offset
}
fn last_overlaps_content(self: RenderHandle<Self>, app: &App) -> bool {
self.header_data(app).last_overlaps_content
}
fn stretch_configuration(
self: RenderHandle<Self>,
app: &App,
) -> Option<OverScrollHeaderStretchConfiguration> {
self.header_data(app).stretch_configuration.clone()
}
fn set_stretch_configuration(
self: RenderHandle<Self>,
app: &mut App,
value: Option<OverScrollHeaderStretchConfiguration>,
) {
self.header_data_mut(app).stretch_configuration = value;
}
fn update_child(
self: RenderHandle<Self>,
app: &mut App,
shrink_offset: f64,
overlaps_content: bool,
) {
let _ = (self, app, shrink_offset, overlaps_content);
}
fn mark_needs_layout(self: RenderHandle<Self>, app: &mut App) {
self.header_data_mut(app).needs_update_child = true;
RenderSliver::mark_needs_layout(self, app);
}
fn layout_child(
self: RenderHandle<Self>,
app: &mut App,
scroll_offset: f64,
max_extent: f64,
overlaps_content: bool,
) {
let shrink_offset = scroll_offset.min(max_extent);
let header = self.header_data(app);
if header.needs_update_child
|| header.last_shrink_offset != shrink_offset
|| header.last_overlaps_content != overlaps_content
{
self.as_object().invoke_layout_callback(app, |app| {
self.update_child(app, shrink_offset, overlaps_content);
});
let header = self.header_data_mut(app);
header.last_shrink_offset = shrink_offset;
header.last_overlaps_content = overlaps_content;
header.needs_update_child = false;
}
debug_assert!(
self.min_extent(app) <= max_extent,
"the max_extent for this persistent header is less than its min_extent"
);
let constraints = self.constraints(app);
let stretch_configuration = self.stretch_configuration(app);
let mut stretch_offset = 0.0;
if stretch_configuration.is_some() && constraints.scroll_offset == 0.0 {
stretch_offset += constraints.overlap.abs();
}
if let Some(child) = self.child(app) {
let min_extent = self.min_extent(app);
let child_constraints = constraints.as_box_constraints(
0.0,
min_extent.max(max_extent - shrink_offset) + stretch_offset,
None,
);
child.layout(app, child_constraints, true);
}
if let Some(configuration) = &stretch_configuration
&& let Some(on_stretch_trigger) = configuration.on_stretch_trigger.clone()
&& stretch_offset >= configuration.stretch_trigger_offset
&& self.header_data(app).last_stretch_offset <= configuration.stretch_trigger_offset
{
drop(on_stretch_trigger(app));
}
self.header_data_mut(app).last_stretch_offset = stretch_offset;
}
fn hit_test_children(
self: RenderHandle<Self>,
app: &mut App,
result: &mut SliverHitTestResult<'_>,
main_axis_position: f64,
cross_axis_position: f64,
) -> bool {
debug_assert!(self.geometry(app).hit_test_extent > 0.0);
let Some(child) = self.child(app) else {
return false;
};
self.hit_test_box_child(
app,
&mut BoxHitTestResult::wrap(result),
child,
main_axis_position,
cross_axis_position,
)
}
fn apply_paint_transform(
self: RenderHandle<Self>,
app: &App,
child: AnyRenderObject,
transform: &mut Matrix4,
) {
debug_assert_eq!(Some(child), self.child(app).map(AnyRenderBox::as_object));
let child = child.as_box().expect("the child is a box");
self.apply_paint_transform_for_box_child(app, child, transform);
}
fn paint(
self: RenderHandle<Self>,
app: &mut App,
context: &mut PaintingContext,
offset: Offset,
) {
let Some(child) = self.child(app) else {
return;
};
if !self.geometry(app).visible {
return;
}
let constraints = self.constraints(app);
let paint_extent = self.geometry(app).paint_extent;
let child_position = RenderSliver::child_main_axis_position(self, app, child.as_object());
let child_extent = self.child_extent(app);
let offset = offset
+ match apply_growth_direction_to_axis_direction(
constraints.axis_direction,
constraints.growth_direction,
) {
AxisDirection::Up => Offset::new(0.0, paint_extent - child_position - child_extent),
AxisDirection::Left => {
Offset::new(paint_extent - child_position - child_extent, 0.0)
}
AxisDirection::Right => Offset::new(child_position, 0.0),
AxisDirection::Down => Offset::new(0.0, child_position),
};
context.paint_child(app, child.as_object(), offset);
}
}
#[derive(Debug, Default)]
pub struct RenderSliverScrollingPersistentHeaderData {
child_position: Option<f64>,
}
impl RenderSliverScrollingPersistentHeaderData {
pub const fn new() -> RenderSliverScrollingPersistentHeaderData {
RenderSliverScrollingPersistentHeaderData {
child_position: None,
}
}
}
pub trait RenderSliverScrollingPersistentHeader: RenderSliverPersistentHeader {
fn scrolling_data(
self: RenderHandle<Self>,
app: &App,
) -> &RenderSliverScrollingPersistentHeaderData;
fn scrolling_data_mut(
self: RenderHandle<Self>,
app: &mut App,
) -> &mut RenderSliverScrollingPersistentHeaderData;
fn update_geometry(self: RenderHandle<Self>, app: &mut App) -> f64 {
let constraints = self.constraints(app);
let mut stretch_offset = 0.0;
if self.stretch_configuration(app).is_some() {
stretch_offset += constraints.overlap.abs();
}
let max_extent = self.max_extent(app);
let paint_extent = max_extent - constraints.scroll_offset;
let cache_extent = calculate_cache_offset(constraints, 0.0, max_extent);
self.set_geometry(
app,
SliverGeometry::builder()
.scroll_extent(max_extent)
.paint_origin(constraints.overlap.min(0.0))
.paint_extent(clamp_double(
paint_extent,
0.0,
constraints.remaining_paint_extent,
))
.cache_extent(cache_extent)
.max_paint_extent(max_extent + stretch_offset)
.has_visual_overflow(true)
.build(),
);
if stretch_offset > 0.0 {
0.0
} else {
(paint_extent - self.child_extent(app)).min(0.0)
}
}
fn perform_layout(self: RenderHandle<Self>, app: &mut App) {
let scroll_offset = self.constraints(app).scroll_offset;
let max_extent = self.max_extent(app);
self.layout_child(app, scroll_offset, max_extent, false);
let child_position = self.update_geometry(app);
self.scrolling_data_mut(app).child_position = Some(child_position);
}
fn child_main_axis_position(
self: RenderHandle<Self>,
app: &App,
child: AnyRenderObject,
) -> f64 {
debug_assert_eq!(Some(child), self.child(app).map(AnyRenderBox::as_object));
self.scrolling_data(app)
.child_position
.expect("the header has been laid out")
}
}
#[derive(Clone, Copy, Debug)]
pub struct RenderSliverPinnedPersistentHeaderData {
pub show_on_screen_configuration: Option<PersistentHeaderShowOnScreenConfiguration>,
}
impl RenderSliverPinnedPersistentHeaderData {
pub const fn new() -> RenderSliverPinnedPersistentHeaderData {
RenderSliverPinnedPersistentHeaderData {
show_on_screen_configuration: Some(PersistentHeaderShowOnScreenConfiguration::new()),
}
}
}
impl Default for RenderSliverPinnedPersistentHeaderData {
fn default() -> RenderSliverPinnedPersistentHeaderData {
RenderSliverPinnedPersistentHeaderData::new()
}
}
pub trait RenderSliverPinnedPersistentHeader: RenderSliverPersistentHeader {
fn pinned_data(self: RenderHandle<Self>, app: &App) -> &RenderSliverPinnedPersistentHeaderData;
fn pinned_data_mut(
self: RenderHandle<Self>,
app: &mut App,
) -> &mut RenderSliverPinnedPersistentHeaderData;
fn perform_layout(self: RenderHandle<Self>, app: &mut App) {
let constraints = self.constraints(app);
let max_extent = self.max_extent(app);
let overlaps_content = constraints.overlap > 0.0;
self.layout_child(app, constraints.scroll_offset, max_extent, overlaps_content);
let effective_remaining_paint_extent =
(constraints.remaining_paint_extent - constraints.overlap).max(0.0);
let layout_extent = clamp_double(
max_extent - constraints.scroll_offset,
0.0,
effective_remaining_paint_extent,
);
let stretch_offset = if self.stretch_configuration(app).is_some() {
constraints.overlap.abs()
} else {
0.0
};
let child_extent = self.child_extent(app);
let min_extent = self.min_extent(app);
self.set_geometry(
app,
SliverGeometry::builder()
.scroll_extent(max_extent)
.paint_origin(constraints.overlap)
.paint_extent(child_extent.min(effective_remaining_paint_extent))
.layout_extent(layout_extent)
.max_paint_extent(max_extent + stretch_offset)
.max_scroll_obstruction_extent(min_extent)
.cache_extent(if layout_extent > 0.0 {
-constraints.cache_origin + layout_extent
} else {
layout_extent
})
.has_visual_overflow(true)
.build(),
);
}
fn child_main_axis_position(
self: RenderHandle<Self>,
_app: &App,
_child: AnyRenderObject,
) -> f64 {
let _ = self;
0.0
}
fn show_on_screen(
self: RenderHandle<Self>,
app: &mut App,
descendant: Option<AnyRenderObject>,
rect: Option<Rect>,
duration: Duration,
curve: Rc<dyn Curve>,
) {
let local_bounds = match descendant {
Some(descendant) => Some(transform_rect(
&descendant.get_transform_to(app, Some(self.as_object())),
rect.unwrap_or_else(|| descendant.paint_bounds(app)),
)),
None => rect,
};
let constraints = self.constraints(app);
let child_extent = self.child_extent(app);
let new_rect = match apply_growth_direction_to_axis_direction(
constraints.axis_direction,
constraints.growth_direction,
) {
AxisDirection::Up => trim(
local_bounds,
f64::NEG_INFINITY,
f64::INFINITY,
child_extent,
f64::NEG_INFINITY,
),
AxisDirection::Left => trim(
local_bounds,
f64::NEG_INFINITY,
child_extent,
f64::INFINITY,
f64::NEG_INFINITY,
),
AxisDirection::Right => trim(
local_bounds,
f64::NEG_INFINITY,
f64::INFINITY,
f64::INFINITY,
0.0,
),
AxisDirection::Down => trim(
local_bounds,
0.0,
f64::INFINITY,
f64::INFINITY,
f64::NEG_INFINITY,
),
};
RenderObjectBase::show_on_screen(
self,
app,
Some(self.as_object()),
new_rect,
duration,
curve,
);
}
}
pub struct RenderSliverFloatingPersistentHeaderData {
controller: Option<Handle<AnimationController>>,
animation: Option<AnyAnimation<f64>>,
last_actual_scroll_offset: Option<f64>,
effective_scroll_offset: Option<f64>,
last_started_scroll_direction: Option<ScrollDirection>,
child_position: Option<f64>,
vsync: Option<TickerProviderRef>,
pub snap_configuration: Option<FloatingHeaderSnapConfiguration>,
pub show_on_screen_configuration: Option<PersistentHeaderShowOnScreenConfiguration>,
}
impl RenderSliverFloatingPersistentHeaderData {
pub fn new(
vsync: Option<TickerProviderRef>,
snap_configuration: Option<FloatingHeaderSnapConfiguration>,
show_on_screen_configuration: Option<PersistentHeaderShowOnScreenConfiguration>,
) -> RenderSliverFloatingPersistentHeaderData {
RenderSliverFloatingPersistentHeaderData {
controller: None,
animation: None,
last_actual_scroll_offset: None,
effective_scroll_offset: None,
last_started_scroll_direction: None,
child_position: None,
vsync,
snap_configuration,
show_on_screen_configuration,
}
}
}
pub trait RenderSliverFloatingPersistentHeader: RenderSliverPersistentHeader {
fn floating_data(
self: RenderHandle<Self>,
app: &App,
) -> &RenderSliverFloatingPersistentHeaderData;
fn floating_data_mut(
self: RenderHandle<Self>,
app: &mut App,
) -> &mut RenderSliverFloatingPersistentHeaderData;
fn did_detach(self: RenderHandle<Self>, app: &mut App) {
if let Some(controller) = self.floating_data(app).controller {
controller.dispose(app);
}
self.floating_data_mut(app).controller = None;
}
fn vsync(self: RenderHandle<Self>, app: &App) -> Option<TickerProviderRef> {
self.floating_data(app).vsync.clone()
}
fn set_vsync(self: RenderHandle<Self>, app: &mut App, value: Option<TickerProviderRef>) {
if value == self.floating_data(app).vsync {
return;
}
self.floating_data_mut(app).vsync = value.clone();
match value {
None => {
if let Some(controller) = self.floating_data(app).controller {
controller.dispose(app);
}
self.floating_data_mut(app).controller = None;
}
Some(vsync) => {
if let Some(controller) = self.floating_data(app).controller {
controller.resync(app, vsync);
}
}
}
}
fn snap_configuration(
self: RenderHandle<Self>,
app: &App,
) -> Option<FloatingHeaderSnapConfiguration> {
self.floating_data(app).snap_configuration.clone()
}
fn set_snap_configuration(
self: RenderHandle<Self>,
app: &mut App,
value: Option<FloatingHeaderSnapConfiguration>,
) {
self.floating_data_mut(app).snap_configuration = value;
}
fn show_on_screen_configuration(
self: RenderHandle<Self>,
app: &App,
) -> Option<PersistentHeaderShowOnScreenConfiguration> {
self.floating_data(app).show_on_screen_configuration
}
fn set_show_on_screen_configuration(
self: RenderHandle<Self>,
app: &mut App,
value: Option<PersistentHeaderShowOnScreenConfiguration>,
) {
self.floating_data_mut(app).show_on_screen_configuration = value;
}
fn effective_scroll_offset(self: RenderHandle<Self>, app: &App) -> Option<f64> {
self.floating_data(app).effective_scroll_offset
}
fn update_geometry(self: RenderHandle<Self>, app: &mut App) -> f64 {
let constraints = self.constraints(app);
let mut stretch_offset = 0.0;
if self.stretch_configuration(app).is_some() {
stretch_offset += constraints.overlap.abs();
}
let max_extent = self.max_extent(app);
let paint_extent = max_extent
- self
.effective_scroll_offset(app)
.expect("perform_layout sets the effective scroll offset");
let layout_extent = max_extent - constraints.scroll_offset;
self.set_geometry(
app,
SliverGeometry::builder()
.scroll_extent(max_extent)
.paint_origin(constraints.overlap.min(0.0))
.paint_extent(clamp_double(
paint_extent,
0.0,
constraints.remaining_paint_extent,
))
.layout_extent(clamp_double(
layout_extent,
0.0,
constraints.remaining_paint_extent,
))
.max_paint_extent(max_extent + stretch_offset)
.has_visual_overflow(true)
.build(),
);
if stretch_offset > 0.0 {
0.0
} else {
(paint_extent - self.child_extent(app)).min(0.0)
}
}
fn update_animation(
self: RenderHandle<Self>,
app: &mut App,
duration: Duration,
end_value: f64,
curve: Rc<dyn Curve>,
) {
let vsync = self
.vsync(app)
.expect("vsync must be set if the floating header changes size animatedly");
let controller = match self.floating_data(app).controller {
Some(controller) => controller,
None => {
let controller = AnimationController::create(
app,
None,
Some(duration),
None,
0.0,
1.0,
inset_animation::AnimationBehavior::Normal,
vsync,
);
self.floating_data_mut(app).controller = Some(controller);
controller.add_listener(
app,
Listener::handle_method(self.handle(), snap_tick::<Self>),
);
controller
}
};
let begin = self.effective_scroll_offset(app);
let tween = Tween::new(app, begin, Some(end_value));
let curve_tween = CurveTween::new(app, curve);
let animation = controller.drive(app, tween.chain(curve_tween));
self.floating_data_mut(app).animation = Some(animation);
}
fn update_scroll_start_direction(
self: RenderHandle<Self>,
app: &mut App,
direction: ScrollDirection,
) {
self.floating_data_mut(app).last_started_scroll_direction = Some(direction);
}
fn maybe_start_snap_animation(
self: RenderHandle<Self>,
app: &mut App,
direction: ScrollDirection,
) {
let Some(snap) = self.snap_configuration(app) else {
return;
};
let effective_scroll_offset = self
.effective_scroll_offset(app)
.expect("the header has been laid out");
let max_extent = self.max_extent(app);
if direction == ScrollDirection::Forward && effective_scroll_offset <= 0.0 {
return;
}
if direction == ScrollDirection::Reverse && effective_scroll_offset >= max_extent {
return;
}
let end_value = if direction == ScrollDirection::Forward {
0.0
} else {
max_extent
};
self.update_animation(app, snap.duration, end_value, Rc::clone(&snap.curve));
if let Some(controller) = self.floating_data(app).controller {
controller.forward(app, Some(0.0));
}
}
fn maybe_stop_snap_animation(
self: RenderHandle<Self>,
app: &mut App,
_direction: ScrollDirection,
) {
if let Some(controller) = self.floating_data(app).controller {
controller.stop(app, false);
}
}
fn perform_layout(self: RenderHandle<Self>, app: &mut App) {
let constraints = self.constraints(app);
let max_extent = self.max_extent(app);
let data = self.floating_data(app);
let last_actual_scroll_offset = data.last_actual_scroll_offset;
let effective_scroll_offset = data.effective_scroll_offset;
let last_started_scroll_direction = data.last_started_scroll_direction;
let new_effective_scroll_offset = match last_actual_scroll_offset {
Some(last_actual_scroll_offset)
if constraints.scroll_offset < last_actual_scroll_offset
|| effective_scroll_offset.is_some_and(|offset| offset < max_extent) =>
{
let mut delta = last_actual_scroll_offset - constraints.scroll_offset;
let mut effective_scroll_offset =
effective_scroll_offset.expect("laid out at least once");
let allow_floating_expansion = constraints.user_scroll_direction
== ScrollDirection::Forward
|| last_started_scroll_direction == Some(ScrollDirection::Forward);
if allow_floating_expansion {
if effective_scroll_offset > max_extent {
effective_scroll_offset = max_extent;
}
} else if delta > 0.0 {
delta = 0.0;
}
clamp_double(
effective_scroll_offset - delta,
0.0,
constraints.scroll_offset,
)
}
_ => constraints.scroll_offset,
};
self.floating_data_mut(app).effective_scroll_offset = Some(new_effective_scroll_offset);
let overlaps_content = new_effective_scroll_offset < constraints.scroll_offset;
self.layout_child(
app,
new_effective_scroll_offset,
max_extent,
overlaps_content,
);
let child_position = self.update_geometry(app);
let data = self.floating_data_mut(app);
data.child_position = Some(child_position);
data.last_actual_scroll_offset = Some(constraints.scroll_offset);
}
fn show_on_screen(
self: RenderHandle<Self>,
app: &mut App,
descendant: Option<AnyRenderObject>,
rect: Option<Rect>,
duration: Duration,
curve: Rc<dyn Curve>,
) {
let Some(show_on_screen) = self.show_on_screen_configuration(app) else {
return RenderObjectBase::show_on_screen(self, app, descendant, rect, duration, curve);
};
let child = self.child(app);
debug_assert!(child.is_some() || descendant.is_none());
let child_bounds = match descendant {
Some(descendant) => Some(transform_rect(
&descendant.get_transform_to(app, child.map(AnyRenderBox::as_object)),
rect.unwrap_or_else(|| descendant.paint_bounds(app)),
)),
None => rect,
};
let constraints = self.constraints(app);
let child_extent = self.child_extent(app);
let (mut target_extent, target_rect) = match apply_growth_direction_to_axis_direction(
constraints.axis_direction,
constraints.growth_direction,
) {
AxisDirection::Up => (
child_extent - child_bounds.map_or(0.0, |bounds| bounds.top),
trim(
child_bounds,
f64::NEG_INFINITY,
f64::INFINITY,
child_extent,
f64::NEG_INFINITY,
),
),
AxisDirection::Right => (
child_bounds.map_or(child_extent, |bounds| bounds.right),
trim(
child_bounds,
f64::NEG_INFINITY,
f64::INFINITY,
f64::INFINITY,
0.0,
),
),
AxisDirection::Down => (
child_bounds.map_or(child_extent, |bounds| bounds.bottom),
trim(
child_bounds,
0.0,
f64::INFINITY,
f64::INFINITY,
f64::NEG_INFINITY,
),
),
AxisDirection::Left => (
child_extent - child_bounds.map_or(0.0, |bounds| bounds.left),
trim(
child_bounds,
f64::NEG_INFINITY,
child_extent,
f64::INFINITY,
f64::NEG_INFINITY,
),
),
};
let max_extent = self.max_extent(app);
let effective_max_extent = child_extent.max(max_extent);
target_extent = clamp_double(
clamp_double(
target_extent,
show_on_screen.min_show_on_screen_extent,
show_on_screen.max_show_on_screen_extent,
),
child_extent,
effective_max_extent,
);
let controller_status = self
.floating_data(app)
.controller
.map(|controller| controller.status(app));
if target_extent > child_extent && controller_status != Some(AnimationStatus::Forward) {
let target_scroll_offset = max_extent - target_extent;
debug_assert!(
self.vsync(app).is_some(),
"vsync must be set if the floating header changes size animatedly"
);
self.update_animation(app, duration, target_scroll_offset, Rc::clone(&curve));
if let Some(controller) = self.floating_data(app).controller {
controller.forward(app, Some(0.0));
}
}
let descendant = match descendant {
None => Some(self.as_object()),
Some(_) => child.map(AnyRenderBox::as_object),
};
RenderObjectBase::show_on_screen(self, app, descendant, target_rect, duration, curve);
}
fn child_main_axis_position(
self: RenderHandle<Self>,
app: &App,
child: AnyRenderObject,
) -> f64 {
debug_assert_eq!(Some(child), self.child(app).map(AnyRenderBox::as_object));
self.floating_data(app).child_position.unwrap_or(0.0)
}
fn as_floating_persistent_header(
self: RenderHandle<Self>,
) -> AnyRenderSliverFloatingPersistentHeader {
AnyRenderSliverFloatingPersistentHeader {
id: self.id(),
vtable: const { &RenderSliverFloatingPersistentHeaderVTable::of::<Self>() },
}
}
}
struct RenderSliverFloatingPersistentHeaderVTable {
update_scroll_start_direction: fn(&mut App, HandleId, ScrollDirection),
maybe_start_snap_animation: fn(&mut App, HandleId, ScrollDirection),
maybe_stop_snap_animation: fn(&mut App, HandleId, ScrollDirection),
}
impl RenderSliverFloatingPersistentHeaderVTable {
const fn of<T: RenderSliverFloatingPersistentHeader>()
-> RenderSliverFloatingPersistentHeaderVTable {
RenderSliverFloatingPersistentHeaderVTable {
update_scroll_start_direction: |app, id, direction| {
T::update_scroll_start_direction(resolve(id), app, direction)
},
maybe_start_snap_animation: |app, id, direction| {
T::maybe_start_snap_animation(resolve(id), app, direction)
},
maybe_stop_snap_animation: |app, id, direction| {
T::maybe_stop_snap_animation(resolve(id), app, direction)
},
}
}
}
#[derive(Clone, Copy)]
pub struct AnyRenderSliverFloatingPersistentHeader {
id: HandleId,
vtable: &'static RenderSliverFloatingPersistentHeaderVTable,
}
impl AnyRenderSliverFloatingPersistentHeader {
pub fn update_scroll_start_direction(self, app: &mut App, direction: ScrollDirection) {
(self.vtable.update_scroll_start_direction)(app, self.id, direction)
}
pub fn maybe_start_snap_animation(self, app: &mut App, direction: ScrollDirection) {
(self.vtable.maybe_start_snap_animation)(app, self.id, direction)
}
pub fn maybe_stop_snap_animation(self, app: &mut App, direction: ScrollDirection) {
(self.vtable.maybe_stop_snap_animation)(app, self.id, direction)
}
}
impl PartialEq for AnyRenderSliverFloatingPersistentHeader {
fn eq(&self, other: &AnyRenderSliverFloatingPersistentHeader) -> bool {
self.id == other.id
}
}
impl Eq for AnyRenderSliverFloatingPersistentHeader {}
fn snap_tick<T: RenderSliverFloatingPersistentHeader>(this: Handle<T>, app: &mut App) {
let this = RenderHandle::from_handle(this);
let Some(animation) = this.floating_data(app).animation else {
return;
};
let value = animation.value(app);
if this.effective_scroll_offset(app) == Some(value) {
return;
}
this.floating_data_mut(app).effective_scroll_offset = Some(value);
RenderSliverPersistentHeader::mark_needs_layout(this, app);
}
pub trait RenderSliverFloatingPinnedPersistentHeader: RenderSliverFloatingPersistentHeader {
fn update_geometry(self: RenderHandle<Self>, app: &mut App) -> f64 {
let constraints = self.constraints(app);
let min_extent = self.min_extent(app);
let min_allowed_extent = if constraints.remaining_paint_extent > min_extent {
min_extent
} else {
constraints.remaining_paint_extent
};
let max_extent = self.max_extent(app);
let paint_extent = max_extent
- self
.effective_scroll_offset(app)
.expect("perform_layout sets the effective scroll offset");
let clamped_paint_extent = clamp_double(
paint_extent,
min_allowed_extent,
constraints.remaining_paint_extent,
);
let layout_extent = max_extent - constraints.scroll_offset;
let stretch_offset = if self.stretch_configuration(app).is_some() {
constraints.overlap.abs()
} else {
0.0
};
self.set_geometry(
app,
SliverGeometry::builder()
.scroll_extent(max_extent)
.paint_origin(constraints.overlap.min(0.0))
.paint_extent(clamped_paint_extent)
.layout_extent(clamp_double(layout_extent, 0.0, clamped_paint_extent))
.max_paint_extent(max_extent + stretch_offset)
.max_scroll_obstruction_extent(min_extent)
.has_visual_overflow(true)
.build(),
);
0.0
}
}
#[cfg(test)]
mod tests {
use std::cell::Cell;
use inset_embedder::Size;
use inset_foundation::{AppCell, Task};
use super::*;
use crate::object::{RenderObject, RenderObjectData, RenderObjectWithChildData};
use crate::sliver::{AnyRenderSliver, RenderSliverData};
use crate::sliver_multi_box_adaptor::test_support::hit_testable_box;
use crate::sliver_multi_box_adaptor::viewport_test_support::ScrollHarness;
macro_rules! persistent_header_leaf {
($leaf:ident, $extra:ident, $perform_layout:path, $child_position:path) => {
struct $leaf {
render_object: RenderObjectData,
render_sliver: RenderSliverData,
child: RenderObjectWithChildData<AnyRenderBox>,
header: RenderSliverPersistentHeaderData,
extra: $extra,
min_extent: f64,
max_extent: f64,
}
impl RenderObjectWithChildMixin for $leaf {
type ChildType = AnyRenderBox;
fn child_data(
self: RenderHandle<Self>,
app: &App,
) -> &RenderObjectWithChildData<AnyRenderBox> {
&self.get(app).child
}
fn child_data_mut(
self: RenderHandle<Self>,
app: &mut App,
) -> &mut RenderObjectWithChildData<AnyRenderBox> {
&mut self.get_mut(app).child
}
}
impl RenderSliverHelpers for $leaf {}
impl RenderSliverPersistentHeader for $leaf {
crate::render_sliver_persistent_header_accessors!();
fn max_extent(self: RenderHandle<Self>, app: &App) -> f64 {
self.get(app).max_extent
}
fn min_extent(self: RenderHandle<Self>, app: &App) -> f64 {
self.get(app).min_extent
}
}
impl RenderObject for $leaf {
crate::render_object_accessors!();
fn visit_children(
self: RenderHandle<Self>,
app: &App,
visitor: &mut dyn FnMut(AnyRenderObject),
) {
if let Some(child) = self.child(app) {
visitor(child.as_object());
}
}
fn apply_paint_transform(
self: RenderHandle<Self>,
app: &App,
child: AnyRenderObject,
transform: &mut Matrix4,
) {
RenderSliverPersistentHeader::apply_paint_transform(self, app, child, transform)
}
fn perform_layout(self: RenderHandle<Self>, app: &mut App) {
$perform_layout(self, app)
}
fn paint(
self: RenderHandle<Self>,
app: &mut App,
context: &mut PaintingContext,
offset: Offset,
) {
RenderSliverPersistentHeader::paint(self, app, context, offset)
}
}
impl RenderSliver for $leaf {
crate::render_sliver_accessors!();
fn hit_test_children(
self: RenderHandle<Self>,
app: &mut App,
result: &mut SliverHitTestResult<'_>,
main_axis_position: f64,
cross_axis_position: f64,
) -> bool {
RenderSliverPersistentHeader::hit_test_children(
self,
app,
result,
main_axis_position,
cross_axis_position,
)
}
fn child_main_axis_position(
self: RenderHandle<Self>,
app: &App,
child: AnyRenderObject,
) -> f64 {
$child_position(self, app, child)
}
}
};
}
type ScrollingExtra = RenderSliverScrollingPersistentHeaderData;
type PinnedExtra = RenderSliverPinnedPersistentHeaderData;
persistent_header_leaf!(
TestScrollingHeader,
ScrollingExtra,
RenderSliverScrollingPersistentHeader::perform_layout,
RenderSliverScrollingPersistentHeader::child_main_axis_position
);
impl RenderSliverScrollingPersistentHeader for TestScrollingHeader {
fn scrolling_data(
self: RenderHandle<Self>,
app: &App,
) -> &RenderSliverScrollingPersistentHeaderData {
&self.get(app).extra
}
fn scrolling_data_mut(
self: RenderHandle<Self>,
app: &mut App,
) -> &mut RenderSliverScrollingPersistentHeaderData {
&mut self.get_mut(app).extra
}
}
persistent_header_leaf!(
TestPinnedHeader,
PinnedExtra,
RenderSliverPinnedPersistentHeader::perform_layout,
RenderSliverPinnedPersistentHeader::child_main_axis_position
);
impl RenderSliverPinnedPersistentHeader for TestPinnedHeader {
fn pinned_data(
self: RenderHandle<Self>,
app: &App,
) -> &RenderSliverPinnedPersistentHeaderData {
&self.get(app).extra
}
fn pinned_data_mut(
self: RenderHandle<Self>,
app: &mut App,
) -> &mut RenderSliverPinnedPersistentHeaderData {
&mut self.get_mut(app).extra
}
}
fn scrolling_header(
app: &mut App,
min_extent: f64,
max_extent: f64,
) -> RenderHandle<TestScrollingHeader> {
let child = hit_testable_box(app, max_extent);
let header = RenderHandle::new_sliver(
app,
TestScrollingHeader {
render_object: RenderObjectData::new(),
render_sliver: RenderSliverData::new(),
child: RenderObjectWithChildData::new(),
header: RenderSliverPersistentHeaderData::new(None),
extra: RenderSliverScrollingPersistentHeaderData::new(),
min_extent,
max_extent,
},
);
header.set_child(app, Some(child));
header
}
fn pinned_header(
app: &mut App,
min_extent: f64,
max_extent: f64,
) -> RenderHandle<TestPinnedHeader> {
let child = hit_testable_box(app, max_extent);
let header = RenderHandle::new_sliver(
app,
TestPinnedHeader {
render_object: RenderObjectData::new(),
render_sliver: RenderSliverData::new(),
child: RenderObjectWithChildData::new(),
header: RenderSliverPersistentHeaderData::new(None),
extra: RenderSliverPinnedPersistentHeaderData::new(),
min_extent,
max_extent,
},
);
header.set_child(app, Some(child));
header
}
fn lay_out(app: &mut App, sliver: AnyRenderSliver, scroll_offset: f64) -> ScrollHarness {
let harness = ScrollHarness::new(app, sliver, Size::new(200.0, 400.0));
harness.scroll_to(app, scroll_offset);
harness
}
#[test]
fn a_scrolling_header_shrinks_as_it_scrolls_off() {
let cell = AppCell::new();
let mut app = cell.borrow_mut();
let header = scrolling_header(&mut app, 40.0, 120.0);
lay_out(&mut app, header.as_sliver(), 50.0);
let geometry = header.geometry(&app);
assert_eq!(geometry.scroll_extent, 120.0);
assert_eq!(geometry.paint_extent, 70.0);
assert_eq!(geometry.max_scroll_obstruction_extent, 0.0);
assert_eq!(header.child_extent(&app), 70.0);
assert_eq!(header.last_shrink_offset(&app), 50.0);
let child = header.child(&app).expect("has a child");
assert_eq!(
RenderSliver::child_main_axis_position(header, &app, child.as_object()),
0.0
);
}
#[test]
fn a_pinned_header_stays_at_the_leading_edge() {
let cell = AppCell::new();
let mut app = cell.borrow_mut();
let header = pinned_header(&mut app, 40.0, 120.0);
lay_out(&mut app, header.as_sliver(), 100.0);
let geometry = header.geometry(&app);
assert_eq!(geometry.scroll_extent, 120.0);
assert_eq!(geometry.max_scroll_obstruction_extent, 40.0);
assert_eq!(header.child_extent(&app), 40.0);
assert_eq!(geometry.paint_extent, 40.0);
assert_eq!(geometry.layout_extent, 20.0);
let child = header.child(&app).expect("has a child");
assert_eq!(
RenderSliver::child_main_axis_position(header, &app, child.as_object()),
0.0
);
}
#[test]
fn an_unscrolled_pinned_header_is_at_its_maximum_extent() {
let cell = AppCell::new();
let mut app = cell.borrow_mut();
let header = pinned_header(&mut app, 40.0, 120.0);
lay_out(&mut app, header.as_sliver(), 0.0);
let geometry = header.geometry(&app);
assert_eq!(geometry.paint_extent, 120.0);
assert_eq!(geometry.layout_extent, 120.0);
assert_eq!(header.last_shrink_offset(&app), 0.0);
assert!(!header.last_overlaps_content(&app));
}
#[test]
fn layout_child_reports_the_shrink_offset() {
let cell = AppCell::new();
let mut app = cell.borrow_mut();
let header = scrolling_header(&mut app, 40.0, 120.0);
lay_out(&mut app, header.as_sliver(), 80.0);
assert_eq!(header.last_shrink_offset(&app), 80.0);
assert_eq!(header.child_extent(&app), 40.0);
}
#[test]
fn the_stretch_trigger_fires_once_per_crossing() {
let cell = AppCell::new();
let mut app = cell.borrow_mut();
let triggered = Rc::new(Cell::new(0));
let counter = Rc::clone(&triggered);
let configuration =
OverScrollHeaderStretchConfiguration::new().on_stretch_trigger(Rc::new(move |_app| {
counter.set(counter.get() + 1);
Task::ready(())
}));
let header = scrolling_header(&mut app, 40.0, 120.0);
header.set_stretch_configuration(&mut app, Some(configuration));
let harness = lay_out(&mut app, header.as_sliver(), 0.0);
assert_eq!(triggered.get(), 0);
harness.scroll_to(&mut app, -120.0);
assert_eq!(triggered.get(), 1);
harness.scroll_to(&mut app, -130.0);
assert_eq!(triggered.get(), 1, "already past the trigger offset");
harness.scroll_to(&mut app, 0.0);
harness.scroll_to(&mut app, -100.0);
assert_eq!(triggered.get(), 2);
}
#[test]
fn the_show_on_screen_configuration_defaults_to_unbounded() {
let configuration = PersistentHeaderShowOnScreenConfiguration::new();
assert_eq!(configuration.min_show_on_screen_extent, f64::NEG_INFINITY);
assert_eq!(configuration.max_show_on_screen_extent, f64::INFINITY);
let snap = FloatingHeaderSnapConfiguration::new();
assert_eq!(snap.duration, Duration::from_millis(300));
}
}