use std::any::Any;
use skia_safe::{ClipOp, Color, Contains, Point, Rect, Size};
use crate::animators::{self, FrameTick, easing};
use crate::control::{Control, GestureCx, Handled, Has, LayoutCx, PaintCx, part_mut};
use crate::controls::layout::SkiaLayout;
use crate::controls::scroll_bar::{self, ScrollBarSet, SkiaScrollBar};
use crate::gestures::{Gesture, GestureKind, VelocityAccumulator};
use crate::{layout, paint, props};
use crate::tree::{Build, ControlId, Cx, Detached, Mut, Raw, Tree, wrong_state};
use crate::types::IntoProp;
pub const WHEEL_LINE_SIZE: f32 = 150.0;
const SCROLL_VELOCITY_THRESHOLD: f32 = 5.0;
const THRESHOLD_SWIPE_ON_UP: f32 = 20.0;
const MIN_VELOCITY: f32 = 1.5;
const MAX_VELOCITY: f32 = 3000.0;
const MAX_BOUNCE_VELOCITY: f32 = 500.0;
const PAN_SMOOTHING: f32 = 0.85;
const RUBBER_EFFECT: f32 = 0.55;
const RUBBER_ON_EMPTY: f32 = 40.0;
const RUBBER_DAMPING: f32 = 0.55;
const AUTO_SCROLLING_SPEED_MS: f32 = 600.0;
const SCROLLING_SPEED_MS: f32 = 400.0;
const FLING_VALUE_THRESHOLD: f64 = 1.85;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum ScrollOrientation {
#[default]
Vertical,
Horizontal,
Both,
Neither,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum RelativePositionType {
None,
#[default]
Start,
Center,
End,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum ScrollBarVisibility {
#[default]
None,
Vertical,
Horizontal,
Both,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum SnapToChildrenType {
#[default]
Disabled,
Side,
Center,
}
impl ScrollOrientation {
fn along_x(self) -> bool {
matches!(self, Self::Horizontal | Self::Both)
}
fn along_y(self) -> bool {
matches!(self, Self::Vertical | Self::Both)
}
}
props!(ScrollProps, ScrollBuild, ScrollSet {
orientation / set_orientation: ScrollOrientation = ScrollOrientation::Vertical, MEASURE;
bounces / set_bounces: bool = true, NONE;
friction_scrolled / set_friction_scrolled: f32 = 0.3, NONE;
change_velocity_scrolled / set_change_velocity_scrolled: f32 = 1.33, NONE;
ignore_wrong_direction / set_ignore_wrong_direction: bool = false, NONE;
responds_to_gestures / set_responds_to_gestures: bool = true, NONE;
load_more_offset / set_load_more_offset: f32 = 0.0, NONE;
load_more_top_offset / set_load_more_top_offset: f32 = 0.0, NONE;
header_sticky / set_header_sticky: bool = false, MEASURE;
header_behind / set_header_behind: bool = false, MEASURE;
header_parallax_ratio / set_header_parallax_ratio: f32 = 1.0, MEASURE;
parallax_overscroll_enabled / set_parallax_overscroll_enabled: bool = true, MEASURE;
content_offset / set_content_offset: f32 = 0.0, MEASURE;
scroll_bar_thumb_color / set_scroll_bar_thumb_color: Option<Color> = None, APPLY;
scroll_bar_track_color / set_scroll_bar_track_color: Option<Color> = None, APPLY;
keep_scroll_bars_visible / set_keep_scroll_bars_visible: bool = false, APPLY;
track_index_position / set_track_index_position: RelativePositionType = RelativePositionType::None, APPLY;
track_index_position_offset / set_track_index_position_offset: f32 = 8.0, APPLY;
snap_to_children / set_snap_to_children: SnapToChildrenType = SnapToChildrenType::Disabled, NONE;
refresh_enabled / set_refresh_enabled: bool = false, NONE;
is_refreshing / set_is_refreshing: bool = false, APPLY;
refresh_distance_limit / set_refresh_distance_limit: f32 = 150.0, NONE;
refresh_show_distance / set_refresh_show_distance: f32 = 50.0, NONE;
});
#[derive(Clone, Copy)]
struct Deceleration {
value: f64,
velocity: f64,
rate: f64,
k: f64,
}
impl Deceleration {
const THRESHOLD: f64 = 0.001;
fn new(value: f32, velocity: f32, rate: f64) -> Self {
Self { value: value as f64, velocity: velocity as f64, rate, k: 1000.0 * rate.ln() }
}
fn destination(&self) -> f32 {
(if self.velocity == 0.0 { self.value } else { self.value - self.velocity / self.k }) as f32
}
fn duration_secs(&self) -> f32 {
if self.velocity == 0.0 {
return 0.0;
}
let divisor = -self.k * Self::THRESHOLD / self.velocity.abs();
(if divisor <= 0.0 { 0.0 } else { divisor.ln() / self.k }) as f32
}
fn decay(&self, secs: f32) -> f64 {
self.rate.powf(1000.0 * secs as f64)
}
fn value_at(&self, secs: f32) -> f32 {
self.value_at64(secs) as f32
}
fn value_at64(&self, secs: f32) -> f64 {
self.value + self.velocity * (self.decay(secs) - 1.0) / self.k
}
fn velocity_at(&self, secs: f32) -> f32 {
(self.velocity * self.decay(secs)) as f32
}
fn duration_to_value(&self, value: f32) -> f32 {
let distance = (value as f64 - self.value).abs();
if self.velocity == 0.0 || distance == 0.0 {
return 0.0;
}
((1.0 + self.k * distance / self.velocity.abs()).ln() / self.k) as f32
}
}
fn rate(friction: f32) -> f64 {
1.0 - friction.max(0.1) as f64 / 100.0
}
#[derive(Clone, Copy)]
struct Fling {
curve: Deceleration,
secs: f32,
edge: Option<f32>,
velocity: f32,
last_value: f64,
last_secs: f32,
slow_frames: u8,
}
impl Fling {
fn update(&mut self, dt: f32, secs: f32) -> (f32, bool) {
if secs > self.secs {
self.velocity = self.curve.velocity_at(self.secs);
return (self.curve.value_at(self.secs), true);
}
let value = self.curve.value_at64(secs);
self.velocity = self.curve.velocity_at(secs);
if self.last_secs > 0.0 {
let rate = if dt > 0.0 { (value - self.last_value).abs() / dt as f64 } else { 0.0 };
if rate < FLING_VALUE_THRESHOLD {
self.slow_frames += 1;
if self.slow_frames >= 3 {
return (value as f32, true);
}
} else {
self.slow_frames = 0;
}
}
(self.last_value, self.last_secs) = (value, secs);
(value as f32, false)
}
}
#[derive(Clone, Copy)]
pub(crate) struct Bounce {
origin: f32,
beta: f32,
wd: f32,
c1: f32,
c2: f32,
secs: f32,
}
impl Bounce {
fn new(origin: f32, displacement: f32, velocity: f32) -> Self {
Self::with_spring(origin, displacement, velocity, 1.0 + RUBBER_DAMPING, 200.0, 0.5 * (1.0 + RUBBER_DAMPING))
}
pub(crate) fn with_spring(origin: f32, displacement: f32, velocity: f32, mass: f32, stiffness: f32, damping_ratio: f32) -> Self {
let w0 = (stiffness / mass).sqrt();
let beta = damping_ratio * w0 * 2.0;
let wd = w0 * (1.0 - damping_ratio * damping_ratio).sqrt();
let c2 = (velocity + beta * displacement) / wd;
let secs = ((displacement.abs() + c2.abs()) / 0.5).ln() / beta;
Self { origin, beta, wd, c1: displacement, c2, secs }
}
pub(crate) fn update(&self, secs: f32) -> (f32, bool) {
if secs > self.secs {
return (self.origin, true);
}
let swing = self.c1 * (self.wd * secs).cos() + self.c2 * (self.wd * secs).sin();
(self.origin + (-self.beta * secs).exp() * swing, false)
}
}
#[derive(Clone, Copy)]
struct Range {
curve: Deceleration,
to: f32,
secs: f32,
last_value: f64,
last_secs: f32,
slow_frames: u8,
}
impl Range {
const VALUE_THRESHOLD: f64 = 0.1;
fn new(from: f32, to: f32, ms: f32, rate: f64) -> Self {
let mut curve = Deceleration::new(from, 0.0, rate);
let (distance, secs) = ((to - from) as f64, ms as f64 / 1000.0);
let denominator = rate.powf(1000.0 * secs) - 1.0;
curve.velocity = if denominator.abs() < 1e-5 { distance / secs } else { distance * curve.k / denominator };
Self { curve, to, secs: secs as f32, last_value: from as f64, last_secs: 0.0, slow_frames: 0 }
}
fn update(&mut self, dt: f32, secs: f32) -> (f32, bool) {
if secs > self.secs {
return (self.to, true);
}
let value = self.curve.value_at64(secs);
if self.last_secs > 0.0 {
let rate = if dt > 0.0 { (value - self.last_value).abs() / dt as f64 } else { 0.0 };
if rate < Self::VALUE_THRESHOLD {
self.slow_frames += 1;
if self.slow_frames >= 3 {
return (self.to, true);
}
} else {
self.slow_frames = 0;
}
}
(self.last_value, self.last_secs) = (value, secs);
(value as f32, false)
}
}
#[derive(Clone, Copy)]
struct Step {
from: f32,
to: f32,
ms: f32,
}
impl Step {
fn update(&self, secs: f32) -> (f32, bool) {
let progress = secs * 1000.0 / self.ms;
let value = self.from + easing::spring_out(progress) * (self.to - self.from);
(value.clamp(self.from.min(self.to), self.from.max(self.to)), progress >= 1.0)
}
}
#[derive(Clone, Copy)]
enum Motion {
Fling(Fling),
Bounce(Bounce),
Range(Range),
Step(Step),
}
#[derive(Default)]
struct Axis {
motion: Option<Motion>,
start_ms: Option<f64>,
last_secs: f32,
}
impl Axis {
fn start(&mut self, motion: Motion) {
*self = Axis { motion: Some(motion), start_ms: None, last_secs: 0.0 };
}
}
type Scrolled = Box<dyn FnMut(Raw<'_>, &mut dyn Any, &mut Cx<'_>, Point)>;
type LoadMore = Box<dyn FnMut(Raw<'_>, &mut dyn Any, &mut Cx<'_>)>;
type IndexChanged = Box<dyn FnMut(Raw<'_>, &mut dyn Any, &mut Cx<'_>, Option<usize>)>;
#[derive(Clone, Copy, Default)]
struct Slots {
count: usize,
content: Option<usize>,
header: Option<usize>,
footer: Option<usize>,
indicator: Option<usize>,
}
fn slot(tree: &Tree, id: ControlId, at: Option<usize>) -> Option<ControlId> {
tree.children(id).get(at?).copied()
}
#[derive(Clone, Copy)]
struct Placement {
pixels: Point,
header: Option<usize>,
header_shift: Point,
indicator: Option<usize>,
indicator_top: f32,
indicator_opacity: f32,
}
impl Placement {
fn write(self, tree: &mut Tree, id: ControlId) {
tree.set_content_offset(id, self.pixels);
if let Some(mut header) = slot(tree, id, self.header).and_then(|header| tree.any_mut(header)) {
header.set_left(self.header_shift.x);
header.set_top(self.header_shift.y);
}
if let Some(mut indicator) = slot(tree, id, self.indicator).and_then(|indicator| tree.any_mut(indicator)) {
indicator.set_translation_y(self.indicator_top);
indicator.set_opacity(self.indicator_opacity);
}
}
}
pub(crate) struct BarView {
pub horizontal: bool,
pub progress: f32,
pub ratio: f32,
pub overscroll: f32,
}
#[derive(Clone, Copy)]
struct Order {
index: usize,
position: RelativePositionType,
ms: f32,
stalled: u8,
}
#[derive(Default)]
pub struct SkiaScroll {
pub p: ScrollProps,
id: Option<ControlId>,
offset: Point,
min: Point,
slots: Slots,
bars: [Option<ControlId>; 2],
bars_seen: Option<(Point, Size, Size, bool)>,
drag: Option<(bool, f32)>,
bar_owns_gesture: bool,
content: Size,
extent: Size,
viewport: Size,
viewport_origin: Point,
scale: f32,
x: Axis,
y: Axis,
tick_ms: f64,
had_down: bool,
is_user_focused: bool,
is_user_panning: bool,
velocity: Point,
accumulator: VelocityAccumulator,
panning_last_delta: Point,
panning_offset: Point,
order: Option<Order>,
current: Option<(usize, f32, f32)>,
index_at: Option<Point>,
snap_due: bool,
snapped: bool,
refreshing: bool,
was_refreshing: bool,
refresh_due: bool,
indicator_height: f32,
ticking: bool,
scrolled: bool,
check_edges: bool,
load_more_at: Option<(f32, f64)>,
load_more_top_armed: bool,
child_was_panning: bool,
on_scrolled: Option<Scrolled>,
on_load_more: Option<LoadMore>,
on_load_more_top: Option<LoadMore>,
on_index_changed: Option<IndexChanged>,
on_refresh: Option<LoadMore>,
}
impl SkiaScroll {
#[allow(clippy::new_ret_no_self)]
pub fn new() -> Build<SkiaScroll> {
Build::new(SkiaScroll::default())
}
pub fn viewport_offset_x(&self) -> f32 {
self.offset.x
}
pub fn viewport_offset_y(&self) -> f32 {
self.offset.y
}
pub fn scroll_axes(&self) -> (bool, bool) {
let (h, v) = match self.p.orientation {
ScrollOrientation::Vertical => (false, true),
ScrollOrientation::Horizontal => (true, false),
ScrollOrientation::Both => (true, true),
ScrollOrientation::Neither => (false, false),
};
(h && self.min.x < 0.0, v && self.min.y < 0.0)
}
pub fn content_size(&self) -> Size {
self.content
}
pub fn is_user_panning(&self) -> bool {
self.is_user_panning
}
pub fn is_animating(&self) -> bool {
self.x.motion.is_some() || self.y.motion.is_some()
}
pub fn overscroll_distance(&self) -> Point {
self.offset - self.inside(self.offset)
}
pub fn has_pending_scroll_order(&self) -> bool {
self.order.is_some()
}
pub fn current_index(&self) -> Option<usize> {
self.current.map(|(index, ..)| index)
}
fn viewport_point(&self, position: RelativePositionType) -> Option<f32> {
let viewport = match self.p.orientation {
ScrollOrientation::Vertical => self.viewport.height,
ScrollOrientation::Horizontal => self.viewport.width,
_ => return None,
};
let offset = self.p.track_index_position_offset * self.scale;
match position {
RelativePositionType::None => None,
RelativePositionType::Start => Some(offset),
RelativePositionType::Center => Some(viewport / 2.0),
RelativePositionType::End => Some(viewport - offset),
}
}
fn snap_position(&self) -> RelativePositionType {
match (self.p.snap_to_children, self.p.track_index_position) {
(SnapToChildrenType::Disabled, _) => RelativePositionType::None,
(SnapToChildrenType::Center, _) => RelativePositionType::Center,
(_, RelativePositionType::End) => RelativePositionType::End,
_ => RelativePositionType::Start,
}
}
fn may_snap(&self) -> bool {
self.p.snap_to_children != SnapToChildrenType::Disabled && !self.snapped && !self.is_user_focused && !self.is_animating()
}
fn snap(&mut self, hit: Option<(usize, f32, f32)>) {
let (Some((_, start, end)), Some(point)) = (hit, self.viewport_point(self.snap_position())) else { return };
let anchor = match self.snap_position() {
RelativePositionType::Center => (start + end) / 2.0,
RelativePositionType::End => end,
_ => start,
};
let (horizontal, mut to) = (self.horizontal(), self.offset);
let viewport_start = if horizontal { self.viewport_origin.x } else { self.viewport_origin.y };
let target = (point - (anchor - viewport_start)) / self.scale;
if (target - *self.along(horizontal)).abs() <= 2.0 {
return;
}
*(if horizontal { &mut to.x } else { &mut to.y }) = target;
self.snapped = true;
self.scroll_to(to, AUTO_SCROLLING_SPEED_MS);
}
fn inside(&self, offset: Point) -> Point {
Point::new(offset.x.clamp(self.min.x, 0.0), offset.y.clamp(self.min.y, 0.0))
}
fn axis(&mut self, horizontal: bool) -> &mut Axis {
if horizontal { &mut self.x } else { &mut self.y }
}
fn along(&mut self, horizontal: bool) -> &mut f32 {
if horizontal { &mut self.offset.x } else { &mut self.offset.y }
}
fn pixel_offset(&self) -> Point {
let p = self.offset * self.scale;
Point::new((p.x + 0.5).floor(), (p.y + 0.5).floor())
}
fn horizontal(&self) -> bool {
self.p.orientation == ScrollOrientation::Horizontal
}
fn placement(&self) -> Placement {
let pixels = self.pixel_offset();
let follows = if self.p.header_sticky { 0.0 } else { self.p.header_parallax_ratio };
let over = self.overscroll_distance();
let pulled = !self.p.header_sticky && !self.p.parallax_overscroll_enabled && (over.x > 0.0 || over.y > 0.0);
let back = if pulled || self.scale == 0.0 { 0.0 } else { (follows - 1.0) / self.scale };
let header_shift = if self.horizontal() { Point::new(pixels.x * back, 0.0) } else { Point::new(0.0, pixels.y * back) };
let (indicator_top, indicator_opacity) = self.indicator();
Placement { pixels, header: self.slots.header, header_shift, indicator: self.slots.indicator, indicator_top, indicator_opacity }
}
fn indicator(&self) -> (f32, f32) {
let (over, show, height) = (self.overscroll_distance().y, self.p.refresh_show_distance, self.indicator_height);
let ratio = if self.refreshing { 1.0 } else if show > 0.0 { (over / show).clamp(0.0, 1.0) } else { 0.0 };
if self.slots.indicator.is_none() || !self.p.refresh_enabled || ratio <= 0.0 {
return (-height, 0.0);
}
let shown = if self.refreshing { show } else { self.offset.y.max(0.0).min(show) };
(shown - height + ((show - height) / 2.0).max(0.0), ratio)
}
fn sync_refresh(&mut self, time_ms: f64) {
if self.p.is_refreshing == self.refreshing {
return;
}
self.refreshing = self.p.is_refreshing;
self.stop_scrolling();
if self.refreshing {
(self.was_refreshing, self.refresh_due, self.order) = (true, true, None);
if self.overscroll_distance().y <= self.p.refresh_show_distance {
self.offset.y = self.p.refresh_show_distance;
}
} else if !self.offset.is_zero() {
self.was_refreshing = false;
self.scroll_to(Point::default(), AUTO_SCROLLING_SPEED_MS);
(self.x.start_ms, self.y.start_ms) = (Some(time_ms), Some(time_ms));
}
self.check_edges = true;
}
pub(crate) fn bar_view(&self, bar: ControlId) -> Option<BarView> {
let horizontal = self.bars[1] == Some(bar);
if !horizontal && self.bars[0] != Some(bar) {
return None;
}
let (offset, min, over) = (self.offset, self.min, self.overscroll_distance());
let (offset, min, overscroll, viewport, extent) = match horizontal {
true => (offset.x, min.x, over.x, self.viewport.width, self.extent.width),
false => (offset.y, min.y, over.y, self.viewport.height, self.extent.height),
};
let (progress, ratio) = if min < 0.0 { (offset / min, viewport / extent) } else { (0.0, 1.0) };
Some(BarView { horizontal, progress, ratio, overscroll })
}
fn bar_track<'a>(&self, tree: &'a Tree, horizontal: bool) -> Option<(&'a SkiaScrollBar, Rect, f32, f32, (f32, f32))> {
let bar = self.bars[horizontal as usize]?;
let (control, base) = (tree.find::<SkiaScrollBar>(bar)?, tree.base(bar)?);
let view = BarView { overscroll: 0.0, ..self.bar_view(bar)? };
let track = control.track(base.rect, self.scale, horizontal);
let (start, length) = if horizontal { (track.left, track.width()) } else { (track.top, track.height()) };
let thumb = control.thumb(length, self.scale, &view).filter(|thumb| base.p.is_visible && length > thumb.1)?;
Some((control, track, start, length, thumb))
}
fn grab(&self, tree: &Tree, horizontal: bool, point: Point) -> Option<(bool, f32)> {
let (bar, track, start, _, (offset, thumb)) = self.bar_track(tree, horizontal)?;
let pad = bar.p.grab_padding * self.scale;
let hit = match horizontal {
true => Rect::new(track.left, track.top - pad, track.right, track.bottom + pad),
false => Rect::new(track.left - pad, track.top, track.right + pad, track.bottom),
};
if !bar.p.is_draggable || !hit.contains(point) {
return None;
}
let at = if horizontal { point.x } else { point.y } - start;
Some((horizontal, if at >= offset && at <= offset + thumb { at - offset } else { thumb / 2.0 }))
}
fn drag_to(&mut self, cx: &mut GestureCx) {
let Some((horizontal, held)) = self.drag else { return };
let Some((_, _, start, length, (_, thumb))) = self.bar_track(cx.tree, horizontal) else { return };
let at = if horizontal { cx.point.x } else { cx.point.y } - start;
let progress = ((at - held) / (length - thumb)).clamp(0.0, 1.0);
let (before, min) = (self.offset, if horizontal { self.min.x } else { self.min.y });
*self.along(horizontal) = progress * min;
self.scrolled |= self.offset != before;
self.apply(cx.tree, cx.id);
}
fn bar_gesture(&mut self, cx: &mut GestureCx, gesture: &Gesture) -> bool {
if gesture.kind == GestureKind::Down {
self.drag = [false, true].into_iter().find_map(|horizontal| self.grab(cx.tree, horizontal, cx.point));
self.bar_owns_gesture = self.drag.is_some();
if self.drag.is_some() {
(self.order, self.snap_due) = (None, false);
self.stop_scrolling();
}
}
if !self.bar_owns_gesture {
return false;
}
if self.drag.is_none() {
return gesture.kind == GestureKind::Tapped;
}
match gesture.kind {
GestureKind::Down | GestureKind::Panning => self.drag_to(cx),
GestureKind::Up => {
self.drag = None;
self.apply(cx.tree, cx.id);
}
_ => {}
}
true
}
fn bars_state(&mut self) -> Option<(bool, bool, [bool; 2])> {
if self.bars == [None; 2] {
return None;
}
let scrolling = self.is_animating() || self.is_user_panning || self.drag.is_some();
let seen = (self.offset, self.extent, self.viewport, scrolling);
let changed = self.bars_seen.replace(seen).is_some_and(|before| before != seen);
Some((changed, scrolling, [self.min.y < 0.0, self.min.x < 0.0]))
}
fn clamp(&self, horizontal: bool, value: f32, strict: bool) -> f32 {
let (min, scrolls) = match horizontal {
true => (self.min.x, self.p.orientation.along_x()),
false => (self.min.y, self.p.orientation.along_y()),
};
let hard = value.clamp(min, 0.0);
if strict || !self.p.bounces || !scrolls || value == hard {
return hard;
}
let over = value - hard;
let mut dim = if horizontal { self.viewport.width } else { self.viewport.height } / self.scale;
if dim == 0.0 {
dim = RUBBER_ON_EMPTY;
}
hard + over.signum() * (1.0 - 1.0 / (over.abs() * RUBBER_EFFECT / dim + 1.0)) * dim
}
fn stop_scrolling(&mut self) {
self.x.motion = None;
self.y.motion = None;
}
fn reset_pan(&mut self) {
self.is_user_focused = true;
self.is_user_panning = false;
self.snap_due = false;
self.child_was_panning = false;
self.stop_scrolling();
self.accumulator.clear();
self.panning_last_delta = Point::default();
self.panning_offset = self.offset;
}
fn start_ticking(&mut self, tree: &mut Tree, id: ControlId) {
if !std::mem::replace(&mut self.ticking, true) {
animators::start_frame(tree, id, tick);
} else {
animators::sleep(tree, id, 0.0);
}
}
fn apply(&mut self, tree: &mut Tree, id: ControlId) {
self.check_edges = true;
self.placement().write(tree, id);
self.start_ticking(tree, id);
}
fn scroll_to(&mut self, to: Point, ms: f32) {
self.stop_scrolling();
let to = self.inside(to);
if ms <= 0.0 {
self.offset = to;
return;
}
let rate = rate(self.p.friction_scrolled);
for (horizontal, from, to) in [(true, self.offset.x, to.x), (false, self.offset.y, to.y)] {
if from != to {
self.axis(horizontal).start(Motion::Range(Range::new(from, to, ms, rate)));
}
}
}
fn start_fling(&mut self, horizontal: bool, velocity: f32) -> bool {
let (from, min) = if horizontal { (self.offset.x, self.min.x) } else { (self.offset.y, self.min.y) };
let curve = Deceleration::new(from, velocity, rate(self.p.friction_scrolled));
let destination = curve.destination();
let edge = (destination < min || destination > 0.0).then(|| destination.clamp(min, 0.0));
let secs = edge.map_or(curve.duration_secs(), |edge| curve.duration_to_value(edge));
if secs <= 0.0 {
return false;
}
let (last_value, last_secs, slow_frames) = (from as f64, 0.0, 0);
let fling = Fling { curve, secs, edge, velocity, last_value, last_secs, slow_frames };
self.axis(horizontal).start(Motion::Fling(fling));
true
}
fn bounce(&mut self, horizontal: bool, to: f32, velocity: f32) {
let displacement = *self.along(horizontal) - to;
if displacement != 0.0 || velocity != 0.0 {
let velocity = velocity.clamp(-MAX_BOUNCE_VELOCITY, MAX_BOUNCE_VELOCITY);
self.axis(horizontal).start(Motion::Bounce(Bounce::new(to, displacement, velocity)));
}
}
fn animate(&mut self, horizontal: bool, time_ms: f64) {
let axis = self.axis(horizontal);
let Some(mut motion) = axis.motion.take() else { return };
let start = *axis.start_ms.get_or_insert(time_ms);
let secs = ((time_ms - start) / 1000.0) as f32;
let dt = secs - std::mem::replace(&mut axis.last_secs, secs);
let (value, done) = match &mut motion {
Motion::Fling(fling) => fling.update(dt, secs),
Motion::Bounce(bounce) => bounce.update(secs),
Motion::Range(range) => range.update(dt, secs),
Motion::Step(step) => step.update(secs),
};
let value = if matches!(motion, Motion::Fling(_)) { self.clamp(horizontal, value, false) } else { value };
*self.along(horizontal) = value;
if !done {
self.axis(horizontal).motion = Some(motion);
} else if let Motion::Fling(Fling { edge: Some(edge), velocity, .. }) = motion
&& self.p.bounces
{
let velocity = velocity.clamp(-MAX_BOUNCE_VELOCITY, MAX_BOUNCE_VELOCITY);
if velocity.abs() > THRESHOLD_SWIPE_ON_UP * self.scale {
self.bounce(horizontal, edge, velocity);
}
}
}
fn shift_anchor(&mut self, points: f32) {
self.offset.y -= points;
self.panning_offset.y -= points;
match &mut self.y.motion {
Some(Motion::Fling(fling)) => {
fling.curve.value -= points as f64;
fling.last_value -= points as f64;
}
Some(Motion::Bounce(bounce)) => bounce.origin -= points,
Some(Motion::Range(range)) => {
range.curve.value -= points as f64;
range.last_value -= points as f64;
range.to -= points;
}
Some(Motion::Step(step)) => {
step.from -= points;
step.to -= points;
}
None => {}
}
}
fn replan_fling(&mut self) {
if let Some(Motion::Fling(Fling { edge: Some(_), velocity, .. })) = self.y.motion {
self.y.motion = None;
if velocity.abs() > MIN_VELOCITY && self.start_fling(false, velocity) {
self.y.start_ms = Some(self.tick_ms);
}
}
}
fn set_bounds(&mut self, content: Size, extent: Size, viewport: Size) -> bool {
if content == self.content && extent == self.extent && viewport == self.viewport {
return false;
}
(self.content, self.extent, self.viewport) = (content, extent, viewport);
let (orientation, scale) = (self.p.orientation, self.scale);
let travel = |extent: f32, viewport: f32| -(extent - viewport).max(0.0) / scale;
self.min.x = if orientation.along_x() { travel(extent.width, viewport.width) } else { 0.0 };
self.min.y = if orientation.along_y() { travel(extent.height, viewport.height) } else { 0.0 };
self.check_edges = true;
let inside = self.inside(self.offset);
let held = self.is_user_panning || self.is_animating() || self.refreshing;
let moves = !held && inside != self.offset;
if moves {
self.offset = inside;
}
moves
}
fn pursue(&mut self, aim: Option<(f32, bool)>) {
let Some(mut order) = self.order else { return };
let (before, mut done) = (self.offset.y, false);
if let Some((y, measured)) = aim {
let target = self.inside(Point::new(self.offset.x, y));
if let Some(Motion::Range(range)) = self.y.motion {
if range.to != target.y {
let (left, rate) = ((range.secs - self.y.last_secs).max(0.016) * 1000.0, range.curve.rate);
self.y.start(Motion::Range(Range::new(self.offset.y, target.y, left, rate)));
self.y.start_ms = Some(self.tick_ms);
}
} else if (self.offset.y - target.y).abs() <= 0.5 {
(self.offset.y, done) = (target.y, measured);
} else {
self.scroll_to(target, order.ms);
order.ms = 0.0;
}
}
order.stalled = if self.offset.y == before && !self.is_animating() { order.stalled + 1 } else { 0 };
self.order = (!done && order.stalled < 10).then_some(order);
}
fn edges_due(&mut self, time_ms: f64, items: Option<usize>) -> (bool, bool) {
let mut due = (false, false);
if !self.check_edges || self.order.is_some() {
return due;
}
self.check_edges = false;
let (offset, min, extent) = match self.p.orientation {
ScrollOrientation::Vertical => (self.offset.y, self.min.y, self.extent.height / self.scale),
ScrollOrientation::Horizontal => (self.offset.x, self.min.x, self.extent.width / self.scale),
_ => return due,
};
let (zone, top_zone) = (self.p.load_more_offset, self.p.load_more_top_offset);
if self.on_load_more.is_some() {
let (underfills, content) = (min >= 0.0, items.map_or(extent, |items| items as f32));
if let Some((at, ms)) = self.load_more_at
&& (at != content || (!underfills && offset - min > zone + 100.0 && time_ms - ms > 2000.0))
{
self.load_more_at = None;
}
if self.load_more_at.is_none() && (underfills || offset <= min + zone) {
(self.load_more_at, due.0) = (Some((content, time_ms)), true);
}
}
if self.on_load_more_top.is_some() {
if -offset > top_zone + 100.0 {
self.load_more_top_armed = true;
} else if self.load_more_top_armed && min < 0.0 && offset >= -top_zone {
(self.load_more_top_armed, due.1) = (false, true);
}
}
due
}
fn pan(&mut self, cx: &mut GestureCx, gesture: &Gesture) -> Handled {
let orientation = self.p.orientation;
if !self.p.responds_to_gestures || !self.had_down || orientation == ScrollOrientation::Neither {
return Handled::No;
}
self.velocity = gesture.velocity * (1.0 / self.scale);
if !self.is_user_panning {
if self.child_was_panning {
return Handled::No;
}
let (x, y) = (gesture.total.x.abs(), gesture.total.y.abs());
let threshold = SCROLL_VELOCITY_THRESHOLD * self.scale;
let wrong = match orientation {
ScrollOrientation::Vertical => x > y && x > threshold,
ScrollOrientation::Horizontal => y > x && y > threshold,
_ => false,
};
if self.p.ignore_wrong_direction && wrong {
self.is_user_focused = false;
return Handled::No;
}
let along = match orientation {
ScrollOrientation::Vertical => self.velocity.y.abs(),
ScrollOrientation::Horizontal => self.velocity.x.abs(),
_ => self.velocity.x.abs().max(self.velocity.y.abs()),
};
if along <= SCROLL_VELOCITY_THRESHOLD {
return Handled::No;
}
}
if !self.is_user_focused {
self.reset_pan();
}
self.is_user_panning = true;
self.was_refreshing &= self.overscroll_distance().y > 0.0;
self.snap_due = self.p.snap_to_children != SnapToChildrenType::Disabled;
self.accumulator.capture(self.velocity, gesture.time_ms);
let (before, moved) = (self.offset, gesture.delta * (1.0 / self.scale));
let step = self.panning_last_delta + (moved - self.panning_last_delta) * PAN_SMOOTHING;
self.panning_last_delta = step;
self.panning_offset += step;
if orientation != ScrollOrientation::Vertical {
self.offset.x = self.clamp(true, self.panning_offset.x, false);
}
if orientation != ScrollOrientation::Horizontal {
self.offset.y = self.clamp(false, self.panning_offset.y, false);
}
self.scrolled |= self.offset != before;
let limit = self.p.refresh_distance_limit.max(self.p.refresh_show_distance);
let can = self.p.refresh_enabled && orientation == ScrollOrientation::Vertical && !self.was_refreshing && !self.refreshing;
if can && self.slots.indicator.is_some() && self.on_refresh.is_some() && self.offset.y > limit {
self.p.is_refreshing = true;
self.sync_refresh(gesture.time_ms);
}
self.apply(cx.tree, cx.id);
Handled::Yes
}
fn release(&mut self, cx: &mut GestureCx, gesture: &Gesture) -> Handled {
self.had_down = false;
let was_panning = std::mem::take(&mut self.is_user_panning);
let orientation = self.p.orientation;
if !self.p.responds_to_gestures || orientation == ScrollOrientation::Neither {
return Handled::No;
}
self.is_user_focused = false;
let over = self.overscroll_distance();
if std::mem::take(&mut self.child_was_panning) || (!was_panning && over.is_zero()) {
self.accumulator.clear();
return Handled::No;
}
let speed = self.accumulator.final_velocity(cx.tree.time_ms, MAX_VELOCITY);
let released = if gesture.cancelled { Point::default() } else { speed };
self.accumulator.clear();
let velocity = released * self.p.change_velocity_scrolled;
if !over.is_zero() {
for (horizontal, over, velocity) in [(true, over.x, velocity.x), (false, over.y, velocity.y)] {
if over != 0.0 {
let mut edge = *self.along(horizontal) - over;
if self.refreshing && !horizontal && over > 0.0 {
edge = self.p.refresh_show_distance;
}
self.bounce(horizontal, edge, velocity);
}
}
self.apply(cx.tree, cx.id);
return Handled::Yes;
}
let threshold = THRESHOLD_SWIPE_ON_UP * self.scale;
let swipe = velocity.x.abs() > threshold || velocity.y.abs() > threshold;
let mut fling = false;
if swipe {
if orientation != ScrollOrientation::Vertical && velocity.x.abs() > MIN_VELOCITY {
fling |= self.start_fling(true, velocity.x);
}
if orientation != ScrollOrientation::Horizontal && velocity.y.abs() > MIN_VELOCITY {
fling |= self.start_fling(false, velocity.y);
}
}
if !fling && was_panning && self.may_snap() {
self.snap_due = false;
let position = self.snap_position();
self.snap(hit(self, cx.tree, cx.id, position));
}
self.apply(cx.tree, cx.id);
if fling || was_panning { Handled::Yes } else { Handled::No }
}
fn wheel(&mut self, cx: &mut GestureCx, gesture: &Gesture) -> Handled {
let orientation = self.p.orientation;
if !self.p.responds_to_gestures || orientation == ScrollOrientation::Neither {
return Handled::No;
}
let horizontal = match orientation {
ScrollOrientation::Vertical if gesture.wheel_horizontal => return Handled::No,
ScrollOrientation::Horizontal => true,
_ => gesture.wheel_horizontal,
};
let from = match self.axis(horizontal).motion {
Some(Motion::Step(step)) => step.to,
_ => *self.along(horizontal),
};
let min = if horizontal { self.min.x } else { self.min.y };
if (gesture.wheel < 0.0 && from <= min) || (gesture.wheel > 0.0 && from >= 0.0) || gesture.wheel == 0.0 {
return Handled::No;
}
self.order = None;
let to = (from + WHEEL_LINE_SIZE * gesture.wheel).clamp(min, 0.0);
self.stop_scrolling();
let current = *self.along(horizontal);
if gesture.wheel.abs() < 0.5 {
*self.along(horizontal) = to;
} else {
self.snap_due = self.p.snap_to_children != SnapToChildrenType::Disabled;
if to != current {
let axis = self.axis(horizontal);
axis.start(Motion::Step(Step { from: current, to, ms: SCROLLING_SPEED_MS }));
axis.start_ms = Some(gesture.time_ms.max(cx.tree.time_ms));
}
}
self.apply(cx.tree, cx.id);
Handled::Yes
}
}
fn find_list(tree: &Tree, content: ControlId) -> Option<ControlId> {
let is_list = |id: ControlId| tree.find::<SkiaLayout>(id).is_some_and(|layout| layout.items.is_some());
if is_list(content) {
return Some(content);
}
tree.trackers.iter().copied().find(|tracker| tree.is_ancestor(content, *tracker) && is_list(*tracker))
}
fn list_items(tree: &Tree, id: ControlId) -> Option<usize> {
let scroll = tree.find::<SkiaScroll>(id).filter(|scroll| scroll.on_load_more.is_some())?;
let list = find_list(tree, slot(tree, id, scroll.slots.content)?)?;
Some(tree.find::<SkiaLayout>(list)?.items_count())
}
fn aim(tree: &Tree, id: ControlId, order: Order) -> Option<(f32, bool)> {
let scroll = tree.find::<SkiaScroll>(id)?;
let content = slot(tree, id, scroll.slots.content)?;
let list = find_list(tree, content)?;
let layout = tree.find::<SkiaLayout>(list)?;
if order.index >= layout.items_count() {
return None;
}
let row = tree.base(list)?.rect.top - tree.base(content)?.rect.top + layout.item_offset_pixels(order.index);
let room = scroll.viewport.height - layout.item_height_pixels(order.index);
let pixels = match order.position {
RelativePositionType::End => row - room,
RelativePositionType::Center => row - room / 2.0,
_ => row,
};
Some((-pixels / scroll.scale, layout.is_item_measured(order.index)))
}
fn tracked(tree: &Tree, id: ControlId, position: RelativePositionType) -> Option<(usize, f32, f32)> {
hit(tree.find::<SkiaScroll>(id)?, tree, id, position)
}
fn hit(scroll: &SkiaScroll, tree: &Tree, id: ControlId, position: RelativePositionType) -> Option<(usize, f32, f32)> {
let (at, horizontal) = (scroll.viewport_point(position)?, scroll.horizontal());
let content = slot(tree, id, scroll.slots.content)?;
let pixels = scroll.pixel_offset();
let point = if horizontal { scroll.viewport_origin.x + at - pixels.x } else { scroll.viewport_origin.y + at - pixels.y };
for (index, child) in tree.children(content).iter().enumerate() {
let Some(child) = tree.base(*child) else { continue };
let r = child.rect;
if !child.p.is_visible || r.width() <= 0.0 || r.height() <= 0.0 {
continue;
}
let (start, end) = if horizontal { (r.left, r.right) } else { (r.top, r.bottom) };
if point >= start && point < end {
return Some((child.context_index.unwrap_or(index), start, end));
}
}
None
}
pub(crate) fn fire(cx: &mut Cx<'_>, id: ControlId, handler: impl FnOnce(Raw<'_>, &mut Cx<'_>)) {
let Some(mut node) = cx.tree.take(id) else { return };
let mut queue = Vec::new();
if let Some(control) = node.kind.as_deref_mut() {
handler(Raw { id, control, base: &mut node.base, queue: &mut queue }, &mut Cx { tree: cx.tree });
}
cx.tree.put_back(node);
cx.tree.queue.append(&mut queue);
}
fn tick(id: ControlId, time_ms: f64, state: &mut dyn Any, cx: &mut Cx<'_>) -> FrameTick {
let mut tick = FrameTick { keep: false, state_touched: false };
let Some(mut me) = cx.tree.find_mut::<SkiaScroll>(id) else { return tick };
let scroll = me.control_mut();
let before = scroll.offset;
scroll.tick_ms = time_ms;
scroll.animate(true, time_ms);
scroll.animate(false, time_ms);
scroll.scrolled |= scroll.offset != before;
let order = scroll.order;
let tracks = scroll.p.track_index_position != RelativePositionType::None || scroll.current.is_some();
let look_at = Some(scroll.offset).filter(|offset| tracks && scroll.index_at.is_some_and(|at| at != *offset));
if scroll.index_at.is_none() || !tracks {
scroll.index_at = Some(scroll.offset);
}
let (track_position, snap_position) = (scroll.p.track_index_position, scroll.snap_position());
let snap_check = scroll.snap_due && scroll.may_snap();
let aim = order.and_then(|order| aim(cx.tree, id, order));
let current = look_at.map(|offset| (offset, tracked(cx.tree, id, track_position)));
let snap_hit = if snap_check { tracked(cx.tree, id, snap_position) } else { None };
let items = list_items(cx.tree, id);
let Some(mut me) = cx.tree.find_mut::<SkiaScroll>(id) else { return tick };
let scroll = me.control_mut();
scroll.pursue(aim);
let index = scroll.current_index();
if let Some((offset, current)) = current {
(scroll.current, scroll.index_at) = (current, Some(offset));
}
let index = Some(scroll.current_index()).filter(|now| *now != index);
if snap_check && !scroll.is_animating() {
scroll.snap_due = false;
scroll.snap(snap_hit);
}
scroll.check_edges |= scroll.offset != before;
let (offset, placement) = (scroll.offset, scroll.placement());
let (bars, bars_state, keep) = (scroll.bars, scroll.bars_state(), scroll.p.keep_scroll_bars_visible);
let (more, more_top) = scroll.edges_due(time_ms, items);
let mut on_scrolled = if std::mem::take(&mut scroll.scrolled) { scroll.on_scrolled.take() } else { None };
let mut on_load_more = if more { scroll.on_load_more.take() } else { None };
let mut on_load_more_top = if more_top { scroll.on_load_more_top.take() } else { None };
let mut on_index_changed = if index.is_some() { scroll.on_index_changed.take() } else { None };
let mut on_refresh = if std::mem::take(&mut scroll.refresh_due) { scroll.on_refresh.take() } else { None };
placement.write(cx.tree, id);
let mut bars_wake: Option<f64> = None;
if let Some((changed, scrolling, travels)) = bars_state {
for (bar, travels) in bars.into_iter().zip(travels) {
if let Some(mut bar) = bar.and_then(|bar| cx.tree.find_mut::<SkiaScrollBar>(bar)) {
let wake = scroll_bar::step(&mut bar, time_ms, changed, scrolling, keep, travels);
bars_wake = [bars_wake, wake].into_iter().flatten().min_by(f64::total_cmp);
}
}
}
if let Some(on_scrolled) = &mut on_scrolled {
fire(cx, id, |me, cx| on_scrolled(me, &mut *state, cx, offset));
}
for on_load_more in [&mut on_load_more, &mut on_load_more_top, &mut on_refresh].into_iter().flatten() {
fire(cx, id, |me, cx| on_load_more(me, &mut *state, cx));
}
if let (Some(on_index_changed), Some(index)) = (&mut on_index_changed, index) {
fire(cx, id, |me, cx| on_index_changed(me, &mut *state, cx, index));
}
tick.state_touched = on_scrolled.is_some() || on_load_more.is_some() || on_load_more_top.is_some() || on_index_changed.is_some();
tick.state_touched |= on_refresh.is_some();
let Some(mut me) = cx.tree.find_mut::<SkiaScroll>(id) else { return tick };
let scroll = me.control_mut();
if on_scrolled.is_some() {
scroll.on_scrolled = on_scrolled;
}
if on_load_more.is_some() {
scroll.on_load_more = on_load_more;
}
if on_load_more_top.is_some() {
scroll.on_load_more_top = on_load_more_top;
}
if on_index_changed.is_some() {
scroll.on_index_changed = on_index_changed;
}
if on_refresh.is_some() {
scroll.on_refresh = on_refresh;
}
let snaps = scroll.snap_due && !scroll.is_user_focused;
let awake = scroll.is_animating() || scroll.check_edges || scroll.order.is_some() || snaps;
(scroll.ticking, tick.keep) = (true, true);
if !awake {
match bars_wake {
Some(wake) if wake > time_ms => animators::sleep(cx.tree, id, wake),
Some(_) => {}
None => animators::sleep(cx.tree, id, f64::INFINITY),
}
}
tick
}
impl Has<ScrollProps> for SkiaScroll {
fn part(&self) -> &ScrollProps {
&self.p
}
fn part_mut(&mut self) -> &mut ScrollProps {
&mut self.p
}
}
impl Control for SkiaScroll {
fn accessibility_role(&self) -> Option<&'static str> {
Some(crate::ui::Aria::SCROLL_VIEW)
}
fn on_props_changed(&mut self, cx: &mut Cx) {
for bar in self.bars.into_iter().flatten() {
let Some(mut bar) = cx.tree.find_mut::<SkiaScrollBar>(bar) else { continue };
if let Some(color) = self.p.scroll_bar_thumb_color {
bar.set_thumb_color(color);
}
if let Some(color) = self.p.scroll_bar_track_color {
bar.set_track_color(color);
}
}
self.sync_refresh(cx.tree.time_ms);
if let Some(id) = self.id {
self.start_ticking(cx.tree, id);
}
}
fn measure(&mut self, cx: &mut LayoutCx, width: f32, height: f32) -> Size {
let (inner, added) = layout::content_box(&cx.base().p, width, height, cx.scale);
let (along_x, along_y) = (self.p.orientation.along_x(), self.p.orientation.along_y());
let mut size = Size::default();
if let Some(content) = visible(cx, self.slots.content) {
let width = if along_x { f32::INFINITY } else { inner.width };
size = cx.measure_child(content, width, if along_y { f32::INFINITY } else { inner.height });
}
for extra in [self.slots.header, self.slots.footer] {
if let Some(extra) = visible(cx, extra) {
let extra = cx.measure_child(extra, inner.width, inner.height);
if self.horizontal() {
size.width += extra.width;
} else {
size.height += extra.height;
}
}
}
for over in self.bars.into_iter().flatten().chain(visible(cx, self.slots.indicator)) {
cx.measure_child(over, inner.width, inner.height);
}
Size::new(size.width + added.width, size.height + added.height)
}
fn arrange(&mut self, cx: &mut LayoutCx) {
(self.scale, self.id) = (cx.scale, Some(cx.id));
self.sync_refresh(cx.tree.time_ms);
let Some(child) = slot(cx.tree, cx.id, self.slots.content) else { return };
let (scale, id, horizontal) = (cx.scale, cx.id, self.horizontal());
let viewport = layout::content_rect(cx.base(), scale);
self.viewport_origin = Point::new(viewport.left, viewport.top);
let (along_x, along_y) = (self.p.orientation.along_x(), self.p.orientation.along_y());
let measured = |cx: &LayoutCx, child: ControlId| {
let base = cx.child_base(child);
if base.p.is_visible { base.measured } else { Size::default() }
};
let along = |size: Size| if horizontal { size.width } else { size.height };
let place = |from: f32, size: Size| match horizontal {
true => Rect::from_xywh(viewport.left + from, viewport.top, size.width, size.height),
false => Rect::from_xywh(viewport.left, viewport.top + from, size.width, size.height),
};
let (header, footer) = (visible(cx, self.slots.header), visible(cx, self.slots.footer));
let header_size = header.map_or(0.0, |header| along(measured(cx, header)));
let footer_size = footer.map_or(0.0, |footer| along(measured(cx, footer)));
let gap = self.p.content_offset * scale;
let covered = self.p.header_sticky || self.p.header_behind;
let lead = if header.is_some() { header_size + if covered { gap } else { 0.0 } } else { 0.0 };
let footer_lead = if header.is_some() { header_size + gap } else { 0.0 };
self.placement().write(cx.tree, id);
let mut shifted = false;
for _ in 0..2 {
let content = measured(cx, child);
let width = if along_x { content.width } else { viewport.width() };
let height = if along_y { content.height } else { viewport.height() };
cx.arrange_child(child, place(lead, Size::new(width, height)));
let shift = cx.take_viewport_shift(child);
if shift != 0.0 {
self.shift_anchor(shift / scale);
shifted = true;
}
let content = measured(cx, child);
let across = |size: Size| if horizontal { Size::new(size.width, viewport.height()) } else { Size::new(viewport.width(), size.height) };
if let Some(header) = header {
cx.arrange_child(header, place(0.0, across(measured(cx, header))));
if let Some(node) = cx.tree.node_mut(header) {
node.base.p.z_index = if self.p.header_behind { -1 } else { 1 };
}
}
if let Some(footer) = footer {
cx.arrange_child(footer, place(footer_lead + along(content), across(measured(cx, footer))));
}
let extra = header_size + footer_size + gap;
let extent = if horizontal { Size::new(content.width + extra, content.height) } else { Size::new(content.width, content.height + extra) };
if !self.set_bounds(content, extent, viewport.size()) {
break;
}
cx.tree.set_content_offset(id, self.pixel_offset());
}
if shifted {
self.replan_fling();
}
for bar in self.bars.into_iter().flatten() {
cx.arrange_child(bar, viewport);
}
let indicator = visible(cx, self.slots.indicator);
if let Some(indicator) = indicator {
let height = measured(cx, indicator).height;
cx.arrange_child(indicator, Rect::from_xywh(viewport.left, viewport.top, viewport.width(), height));
self.indicator_height = cx.child_base(indicator).rect.height() / scale;
if let Some(node) = cx.tree.node_mut(indicator) {
node.base.p.input_transparent = true;
}
}
let placement = self.placement();
cx.base_mut().content_offset = placement.pixels;
if let Some(node) = header.and_then(|header| cx.tree.node_mut(header)) {
(node.base.p.left, node.base.p.top) = (placement.header_shift.x, placement.header_shift.y);
}
if let Some(node) = indicator.and_then(|indicator| cx.tree.node_mut(indicator)) {
(node.base.p.translation_y, node.base.p.opacity) = (placement.indicator_top, placement.indicator_opacity);
}
let tracks = self.p.track_index_position != RelativePositionType::None;
if tracks || self.refresh_due || self.check_edges && (self.on_load_more.is_some() || self.on_load_more_top.is_some() || self.bars != [None; 2]) {
self.start_ticking(cx.tree, id);
}
}
fn paint(&self, cx: &mut PaintCx) {
let Some(node) = cx.node(cx.id) else { return };
let child = |at: Option<usize>| at.and_then(|at| node.children.get(at).copied());
let (content, header, footer) = (child(self.slots.content), child(self.slots.header), child(self.slots.footer));
let order = if self.p.header_behind { [header, content, footer] } else { [content, footer, header] };
cx.canvas.save();
cx.canvas.clip_rect(cx.rect, ClipOp::Intersect, true);
for child in order.into_iter().flatten() {
cx.paint_child(child);
}
for over in child(self.slots.indicator).into_iter().chain(self.bars.into_iter().flatten()) {
paint::render(cx, over);
}
cx.canvas.restore();
}
fn on_gesture(&mut self, cx: &mut GestureCx, gesture: &Gesture) -> Handled {
if self.bar_gesture(cx, gesture) {
return Handled::Yes;
}
let by = |child: Option<ControlId>| child.map_or(Handled::No, Handled::By);
match gesture.kind {
GestureKind::Down => {
(self.had_down, self.snapped) = (true, false);
if self.p.responds_to_gestures {
self.order = None;
self.reset_pan();
}
by(cx.route_children(gesture))
}
GestureKind::Panning => {
if !self.is_user_panning && self.p.responds_to_gestures && self.had_down {
if let Some(child) = cx.route_children(gesture) {
self.child_was_panning = true;
return Handled::By(child);
}
}
self.pan(cx, gesture)
}
GestureKind::Tapped => if self.is_user_panning { Handled::Yes } else { by(cx.route_children(gesture)) },
GestureKind::Up => {
let child = cx.route_children(gesture);
match self.release(cx, gesture) {
Handled::No => by(child),
handled => handled,
}
}
GestureKind::Wheel => match cx.route_children(gesture) {
Some(child) => Handled::By(child),
None => self.wheel(cx, gesture),
},
_ => Handled::No,
}
}
}
fn visible(cx: &LayoutCx, at: Option<usize>) -> Option<ControlId> {
slot(cx.tree, cx.id, at).filter(|child| cx.child_base(*child).p.is_visible)
}
fn scroll_part<T: Control>(control: &mut T) -> &mut SkiaScroll {
part_mut(control).expect("the control embeds a SkiaScroll")
}
fn load_more<T: Control, S: Any>(mut f: impl FnMut(&mut Mut<'_, T>, &mut S, &mut Cx<'_>) + 'static) -> LoadMore {
Box::new(move |me, state, cx| {
let state = state.downcast_mut::<S>().unwrap_or_else(|| wrong_state::<S>());
f(&mut me.typed(), state, cx)
})
}
impl<T: Has<ScrollProps>> Build<T> {
fn push_slot(mut self, child: impl Into<Detached>, pick: fn(&mut Slots) -> &mut Option<usize>) -> Self {
let slots = &mut scroll_part(self.control_mut()).slots;
*pick(slots) = Some(slots.count);
slots.count += 1;
self.push_child(child);
self
}
pub fn content(self, content: impl Into<Detached>) -> Self {
self.push_slot(content, |slots| &mut slots.content)
}
pub fn header(self, header: impl Into<Detached>) -> Self {
self.push_slot(header, |slots| &mut slots.header)
}
pub fn footer(self, footer: impl Into<Detached>) -> Self {
self.push_slot(footer, |slots| &mut slots.footer)
}
fn push_bar(mut self, bar: Build<SkiaScrollBar>, horizontal: bool) -> Self {
let scroll = scroll_part(self.control_mut());
scroll.bars[horizontal as usize] = Some(bar.id());
scroll.slots.count += 1;
self.push_child(bar);
self
}
pub fn scroll_bar(self, bar: Build<SkiaScrollBar>) -> Self {
self.push_bar(bar, false)
}
pub fn scroll_bar_horizontal(self, bar: Build<SkiaScrollBar>) -> Self {
self.push_bar(bar, true)
}
pub fn scroll_bars_visibility(mut self, visibility: ScrollBarVisibility) -> Self {
for (horizontal, other) in [(false, ScrollBarVisibility::Horizontal), (true, ScrollBarVisibility::Vertical)] {
let wanted = visibility != ScrollBarVisibility::None && visibility != other;
if wanted && scroll_part(self.control_mut()).bars[horizontal as usize].is_none() {
self = self.push_bar(SkiaScrollBar::new(), horizontal);
}
}
self
}
pub fn on_scrolled<S: Any>(mut self, mut f: impl FnMut(&mut Mut<'_, T>, &mut S, &mut Cx<'_>, Point) + 'static) -> Self {
scroll_part(self.control_mut()).on_scrolled = Some(Box::new(move |me, state, cx, offset| {
let state = state.downcast_mut::<S>().unwrap_or_else(|| wrong_state::<S>());
f(&mut me.typed(), state, cx, offset)
}));
self
}
pub fn refresh_indicator(self, indicator: impl Into<Detached>) -> Self {
self.push_slot(indicator, |slots| &mut slots.indicator)
}
pub fn on_refresh<S: Any>(mut self, f: impl FnMut(&mut Mut<'_, T>, &mut S, &mut Cx<'_>) + 'static) -> Self {
scroll_part(self.control_mut()).on_refresh = Some(load_more::<T, S>(f));
self
}
pub fn on_index_changed<S: Any>(mut self, mut f: impl FnMut(&mut Mut<'_, T>, &mut S, &mut Cx<'_>, Option<usize>) + 'static) -> Self {
scroll_part(self.control_mut()).on_index_changed = Some(Box::new(move |me, state, cx, index| {
let state = state.downcast_mut::<S>().unwrap_or_else(|| wrong_state::<S>());
f(&mut me.typed(), state, cx, index)
}));
self
}
pub fn on_load_more<S: Any>(mut self, f: impl FnMut(&mut Mut<'_, T>, &mut S, &mut Cx<'_>) + 'static) -> Self {
scroll_part(self.control_mut()).on_load_more = Some(load_more::<T, S>(f));
self
}
pub fn on_load_more_top<S: Any>(mut self, f: impl FnMut(&mut Mut<'_, T>, &mut S, &mut Cx<'_>) + 'static) -> Self {
scroll_part(self.control_mut()).on_load_more_top = Some(load_more::<T, S>(f));
self
}
}
impl Cx<'_> {
fn scroll_with(&mut self, id: ControlId, change: impl FnOnce(&mut SkiaScroll)) {
let Some(mut me) = self.tree.find_mut::<SkiaScroll>(id) else { return };
let scroll = me.control_mut();
let before = scroll.offset;
change(scroll);
scroll.scrolled |= scroll.offset != before;
scroll.snap_due = scroll.p.snap_to_children != SnapToChildrenType::Disabled && scroll.is_animating();
scroll.check_edges = true;
let (placement, start) = (scroll.placement(), !std::mem::replace(&mut scroll.ticking, true));
placement.write(self.tree, id);
if start {
animators::start_frame(self.tree, id, tick);
} else {
animators::sleep(self.tree, id, 0.0);
}
}
pub fn scroll_to(&mut self, scroll: impl Into<ControlId>, x: f32, y: f32, ms: impl IntoProp<f32>) {
let ms = ms.into_prop();
self.scroll_with(scroll.into(), |scroll| {
scroll.order = None;
scroll.scroll_to(Point::new(x, y), ms);
})
}
pub fn scroll_to_index(
&mut self,
scroll: impl Into<ControlId>,
index: usize,
position: RelativePositionType,
ms: impl IntoProp<f32>,
) {
let id = scroll.into();
let order = Order { index, position, ms: ms.into_prop(), stalled: 0 };
let aim = aim(self.tree, id, order);
self.scroll_with(id, |scroll| {
scroll.order = Some(order);
scroll.pursue(aim);
})
}
}