use std::{fmt, mem, sync::Arc, time::Duration};
use atomic::{Atomic, Ordering};
use bitflags::bitflags;
use parking_lot::Mutex;
use zng_ext_input::touch::TouchPhase;
use zng_var::{
animation::{
easing::{self, EasingStep, EasingTime},
AnimationHandle, ChaseAnimation, Transition,
},
ReadOnlyContextVar, VARS,
};
use zng_wgt::prelude::*;
use super::{cmd, SMOOTH_SCROLLING_VAR};
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
pub struct ScrollMode: u8 {
const NONE = 0;
const VERTICAL = 0b01;
const HORIZONTAL = 0b10;
const PAN = 0b11;
const ZOOM = 0b111;
}
}
impl_from_and_into_var! {
fn from(zoom: bool) -> ScrollMode {
if zoom {
ScrollMode::ZOOM
} else {
ScrollMode::NONE
}
}
}
context_var! {
pub(super) static SCROLL_VERTICAL_OFFSET_VAR: Factor = 0.fct();
pub(super) static SCROLL_HORIZONTAL_OFFSET_VAR: Factor = 0.fct();
pub(super) static OVERSCROLL_VERTICAL_OFFSET_VAR: Factor = 0.fct();
pub(super) static OVERSCROLL_HORIZONTAL_OFFSET_VAR: Factor = 0.fct();
pub(super) static SCROLL_VERTICAL_RATIO_VAR: Factor = 0.fct();
pub(super) static SCROLL_HORIZONTAL_RATIO_VAR: Factor = 0.fct();
pub(super) static SCROLL_VERTICAL_CONTENT_OVERFLOWS_VAR: bool = false;
pub(super) static SCROLL_HORIZONTAL_CONTENT_OVERFLOWS_VAR: bool = false;
pub(super) static SCROLL_VIEWPORT_SIZE_VAR: PxSize = PxSize::zero();
pub(super) static SCROLL_CONTENT_SIZE_VAR: PxSize = PxSize::zero();
pub(super) static SCROLL_SCALE_VAR: Factor = 1.fct();
pub(super) static SCROLL_MODE_VAR: ScrollMode = ScrollMode::empty();
}
context_local! {
static SCROLL_CONFIG: ScrollConfig = ScrollConfig::default();
}
#[derive(Debug, Clone, Copy, bytemuck::NoUninit)]
#[repr(C)]
struct RenderedOffsets {
h: Factor,
v: Factor,
z: Factor,
}
#[derive(Default, Debug)]
enum ZoomState {
#[default]
None,
Chasing(ChaseAnimation<Factor>),
TouchStart {
start_factor: Factor,
start_center: euclid::Point2D<f32, Px>,
applied_offset: euclid::Vector2D<f32, Px>,
},
}
#[derive(Debug)]
struct ScrollConfig {
id: Option<WidgetId>,
chase: [Mutex<Option<ChaseAnimation<Factor>>>; 2], zoom: Mutex<ZoomState>,
rendered: Atomic<RenderedOffsets>,
overscroll: [Mutex<AnimationHandle>; 2],
inertia: [Mutex<AnimationHandle>; 2],
auto: [Mutex<AnimationHandle>; 2],
}
impl Default for ScrollConfig {
fn default() -> Self {
Self {
id: Default::default(),
chase: Default::default(),
zoom: Default::default(),
rendered: Atomic::new(RenderedOffsets {
h: 0.fct(),
v: 0.fct(),
z: 0.fct(),
}),
overscroll: Default::default(),
inertia: Default::default(),
auto: Default::default(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
pub enum ScrollFrom {
Var(Px),
VarTarget(Px),
Rendered(Px),
}
pub struct SCROLL;
impl SCROLL {
pub fn try_id(&self) -> Option<WidgetId> {
SCROLL_CONFIG.get().id
}
pub fn id(&self) -> WidgetId {
self.try_id().expect("not inside scroll")
}
pub fn config_node(&self, child: impl UiNode) -> impl UiNode {
let child = match_node(child, move |_, op| {
if let UiNodeOp::Render { .. } | UiNodeOp::RenderUpdate { .. } = op {
let h = SCROLL_HORIZONTAL_OFFSET_VAR.get();
let v = SCROLL_VERTICAL_OFFSET_VAR.get();
let z = SCROLL_SCALE_VAR.get();
SCROLL_CONFIG.get().rendered.store(RenderedOffsets { h, v, z }, Ordering::Relaxed);
}
});
with_context_local_init(child, &SCROLL_CONFIG, || ScrollConfig {
id: WIDGET.try_id(),
..Default::default()
})
}
pub fn mode(&self) -> ReadOnlyContextVar<ScrollMode> {
SCROLL_MODE_VAR.read_only()
}
pub fn vertical_offset(&self) -> ContextVar<Factor> {
SCROLL_VERTICAL_OFFSET_VAR
}
pub fn horizontal_offset(&self) -> ContextVar<Factor> {
SCROLL_HORIZONTAL_OFFSET_VAR
}
pub fn zoom_scale(&self) -> ContextVar<Factor> {
SCROLL_SCALE_VAR
}
pub fn rendered_offset(&self) -> Factor2d {
let cfg = SCROLL_CONFIG.get().rendered.load(Ordering::Relaxed);
Factor2d::new(cfg.h, cfg.v)
}
pub fn rendered_zoom_scale(&self) -> Factor {
SCROLL_CONFIG.get().rendered.load(Ordering::Relaxed).z
}
pub fn vertical_overscroll(&self) -> ReadOnlyContextVar<Factor> {
OVERSCROLL_VERTICAL_OFFSET_VAR.read_only()
}
pub fn horizontal_overscroll(&self) -> ReadOnlyContextVar<Factor> {
OVERSCROLL_HORIZONTAL_OFFSET_VAR.read_only()
}
pub fn vertical_ratio(&self) -> ReadOnlyContextVar<Factor> {
SCROLL_VERTICAL_RATIO_VAR.read_only()
}
pub fn horizontal_ratio(&self) -> ReadOnlyContextVar<Factor> {
SCROLL_HORIZONTAL_RATIO_VAR.read_only()
}
pub fn vertical_content_overflows(&self) -> ReadOnlyContextVar<bool> {
SCROLL_VERTICAL_CONTENT_OVERFLOWS_VAR.read_only()
}
pub fn horizontal_content_overflows(&self) -> ReadOnlyContextVar<bool> {
SCROLL_HORIZONTAL_CONTENT_OVERFLOWS_VAR.read_only()
}
pub fn viewport_size(&self) -> ReadOnlyContextVar<PxSize> {
SCROLL_VIEWPORT_SIZE_VAR.read_only()
}
pub fn content_size(&self) -> ReadOnlyContextVar<PxSize> {
SCROLL_CONTENT_SIZE_VAR.read_only()
}
pub fn scroll_vertical(&self, delta: ScrollFrom) {
self.scroll_vertical_clamp(delta, f32::MIN, f32::MAX);
}
pub fn scroll_horizontal(&self, delta: ScrollFrom) {
self.scroll_horizontal_clamp(delta, f32::MIN, f32::MAX)
}
pub fn scroll_vertical_clamp(&self, delta: ScrollFrom, min: f32, max: f32) {
self.scroll_clamp(true, SCROLL_VERTICAL_OFFSET_VAR, delta, min, max)
}
pub fn scroll_horizontal_clamp(&self, delta: ScrollFrom, min: f32, max: f32) {
self.scroll_clamp(false, SCROLL_HORIZONTAL_OFFSET_VAR, delta, min, max)
}
fn scroll_clamp(&self, vertical: bool, scroll_offset_var: ContextVar<Factor>, delta: ScrollFrom, min: f32, max: f32) {
let viewport = SCROLL_VIEWPORT_SIZE_VAR.get().to_array()[vertical as usize];
let content = SCROLL_CONTENT_SIZE_VAR.get().to_array()[vertical as usize];
let max_scroll = content - viewport;
if max_scroll <= 0 {
return;
}
match delta {
ScrollFrom::Var(a) => {
let amount = a.0 as f32 / max_scroll.0 as f32;
let f = scroll_offset_var.get();
SCROLL.chase(vertical, scroll_offset_var, |_| (f.0 + amount).clamp(min, max).fct());
}
ScrollFrom::VarTarget(a) => {
let amount = a.0 as f32 / max_scroll.0 as f32;
SCROLL.chase(vertical, scroll_offset_var, |f| (f.0 + amount).clamp(min, max).fct());
}
ScrollFrom::Rendered(a) => {
let amount = a.0 as f32 / max_scroll.0 as f32;
let f = SCROLL_CONFIG.get().rendered.load(Ordering::Relaxed).h;
SCROLL.chase(vertical, scroll_offset_var, |_| (f.0 + amount).clamp(min, max).fct());
}
}
}
pub fn auto_scroll(&self, velocity: DipVector) {
let viewport = SCROLL_VIEWPORT_SIZE_VAR.get();
let content = SCROLL_CONTENT_SIZE_VAR.get();
let max_scroll = content - viewport;
let velocity = velocity.to_px(WINDOW.info().scale_factor());
fn scroll(dimension: usize, velocity: Px, max_scroll: Px, offset_var: &ContextVar<Factor>) {
if velocity == 0 {
SCROLL_CONFIG.get().auto[dimension].lock().clone().stop();
} else {
let mut travel = max_scroll * offset_var.get();
let mut target = 0.0;
if velocity > Px(0) {
travel = max_scroll - travel;
target = 1.0;
}
let time = (travel.0 as f32 / velocity.0.abs() as f32).secs();
VARS.with_animation_controller(zng_var::animation::ForceAnimationController, || {
let handle = offset_var.ease(target, time, easing::linear);
mem::replace(&mut *SCROLL_CONFIG.get().auto[dimension].lock(), handle).stop();
});
}
}
scroll(0, velocity.x, max_scroll.width, &SCROLL_HORIZONTAL_OFFSET_VAR);
scroll(1, velocity.y, max_scroll.height, &SCROLL_VERTICAL_OFFSET_VAR);
}
pub fn scroll_vertical_touch(&self, delta: Px) {
self.scroll_touch(true, SCROLL_VERTICAL_OFFSET_VAR, OVERSCROLL_VERTICAL_OFFSET_VAR, delta)
}
pub fn scroll_horizontal_touch(&self, delta: Px) {
self.scroll_touch(false, SCROLL_HORIZONTAL_OFFSET_VAR, OVERSCROLL_HORIZONTAL_OFFSET_VAR, delta)
}
fn scroll_touch(&self, vertical: bool, scroll_offset_var: ContextVar<Factor>, overscroll_offset_var: ContextVar<Factor>, delta: Px) {
let viewport = SCROLL_VIEWPORT_SIZE_VAR.get().to_array()[vertical as usize];
let content = SCROLL_CONTENT_SIZE_VAR.get().to_array()[vertical as usize];
let max_scroll = content - viewport;
if max_scroll <= 0 {
return;
}
let delta = delta.0 as f32 / max_scroll.0 as f32;
let current = scroll_offset_var.get();
let mut next = current + delta.fct();
let mut overscroll = 0.fct();
if next > 1.fct() {
overscroll = next - 1.fct();
next = 1.fct();
let overscroll_px = overscroll * content.0.fct();
let overscroll_max = viewport.0.fct();
overscroll = overscroll_px.min(overscroll_max) / overscroll_max;
} else if next < 0.fct() {
overscroll = next;
next = 0.fct();
let overscroll_px = -overscroll * content.0.fct();
let overscroll_max = viewport.0.fct();
overscroll = -(overscroll_px.min(overscroll_max) / overscroll_max);
}
let _ = scroll_offset_var.set(next);
if overscroll != 0.fct() {
let new_handle = self.increment_overscroll(overscroll_offset_var, overscroll);
let config = SCROLL_CONFIG.get();
let mut handle = config.overscroll[vertical as usize].lock();
mem::replace(&mut *handle, new_handle).stop();
} else {
self.clear_horizontal_overscroll();
}
}
fn increment_overscroll(&self, overscroll: ContextVar<Factor>, delta: Factor) -> AnimationHandle {
enum State {
Increment,
ClearDelay,
Clear(Transition<Factor>),
}
let mut state = State::Increment;
overscroll.animate(move |a, o| match &mut state {
State::Increment => {
*o.to_mut() += delta;
*o.to_mut() = (*o).clamp((-1).fct(), 1.fct());
a.sleep(300.ms());
state = State::ClearDelay;
}
State::ClearDelay => {
a.restart();
let t = Transition::new(**o, 0.fct());
state = State::Clear(t);
}
State::Clear(t) => {
let step = easing::linear(a.elapsed_stop(300.ms()));
o.set(t.sample(step));
}
})
}
pub fn clear_vertical_overscroll(&self) {
self.clear_overscroll(true, OVERSCROLL_VERTICAL_OFFSET_VAR)
}
pub fn clear_horizontal_overscroll(&self) {
self.clear_overscroll(false, OVERSCROLL_HORIZONTAL_OFFSET_VAR)
}
fn clear_overscroll(&self, vertical: bool, overscroll_offset_var: ContextVar<Factor>) {
if overscroll_offset_var.get() != 0.fct() {
let new_handle = overscroll_offset_var.ease(0.fct(), 100.ms(), easing::linear);
let config = SCROLL_CONFIG.get();
let mut handle = config.overscroll[vertical as usize].lock();
mem::replace(&mut *handle, new_handle).stop();
}
}
pub fn scroll_vertical_touch_inertia(&self, delta: Px, duration: Duration) {
self.scroll_touch_inertia(true, SCROLL_VERTICAL_OFFSET_VAR, OVERSCROLL_VERTICAL_OFFSET_VAR, delta, duration)
}
pub fn scroll_horizontal_touch_inertia(&self, delta: Px, duration: Duration) {
self.scroll_touch_inertia(
false,
SCROLL_HORIZONTAL_OFFSET_VAR,
OVERSCROLL_HORIZONTAL_OFFSET_VAR,
delta,
duration,
)
}
fn scroll_touch_inertia(
&self,
vertical: bool,
scroll_offset_var: ContextVar<Factor>,
overscroll_offset_var: ContextVar<Factor>,
delta: Px,
duration: Duration,
) {
let viewport = SCROLL_VIEWPORT_SIZE_VAR.get().to_array()[vertical as usize];
let content = SCROLL_CONTENT_SIZE_VAR.get().to_array()[vertical as usize];
let max_scroll = content - viewport;
if max_scroll <= 0 {
return;
}
let delta = delta.0 as f32 / max_scroll.0 as f32;
let current = scroll_offset_var.get();
let mut next = current + delta.fct();
let mut overscroll = 0.fct();
if next > 1.fct() {
overscroll = next - 1.fct();
next = 1.fct();
let overscroll_px = overscroll * content.0.fct();
let overscroll_max = viewport.0.fct();
overscroll = overscroll_px.min(overscroll_max) / overscroll_max;
} else if next < 0.fct() {
overscroll = next;
next = 0.fct();
let overscroll_px = -overscroll * content.0.fct();
let overscroll_max = viewport.0.fct();
overscroll = -(overscroll_px.min(overscroll_max) / overscroll_max);
}
let cfg = SCROLL_CONFIG.get();
let easing = |t| easing::ease_out(easing::quad, t);
*cfg.inertia[vertical as usize].lock() = if overscroll != 0.fct() {
let transition = Transition::new(current, next + overscroll);
let overscroll_var = overscroll_offset_var.actual_var();
let overscroll_tr = Transition::new(overscroll, 0.fct());
let mut is_inertia_anim = true;
scroll_offset_var.animate(move |animation, value| {
if is_inertia_anim {
let step = easing(animation.elapsed(duration));
let v = transition.sample(step);
if v < 0.fct() || v > 1.fct() {
value.set(v.clamp_range());
animation.restart();
is_inertia_anim = false;
let _ = overscroll_var.set(overscroll_tr.from);
} else {
value.set(v);
}
} else {
let step = easing::linear(animation.elapsed_stop(300.ms()));
let v = overscroll_tr.sample(step);
let _ = overscroll_var.set(v);
}
})
} else {
scroll_offset_var.ease(next, duration, easing)
};
}
pub fn chase_vertical(&self, modify_offset: impl FnOnce(Factor) -> Factor) {
#[cfg(feature = "dyn_closure")]
let modify_offset: Box<dyn FnOnce(Factor) -> Factor> = Box::new(modify_offset);
self.chase(true, SCROLL_VERTICAL_OFFSET_VAR, modify_offset);
}
pub fn chase_horizontal(&self, modify_offset: impl FnOnce(Factor) -> Factor) {
#[cfg(feature = "dyn_closure")]
let modify_offset: Box<dyn FnOnce(Factor) -> Factor> = Box::new(modify_offset);
self.chase(false, SCROLL_HORIZONTAL_OFFSET_VAR, modify_offset);
}
fn chase(&self, vertical: bool, scroll_offset_var: ContextVar<Factor>, modify_offset: impl FnOnce(Factor) -> Factor) {
let smooth = SMOOTH_SCROLLING_VAR.get();
let config = SCROLL_CONFIG.get();
let mut chase = config.chase[vertical as usize].lock();
match &mut *chase {
Some(t) => {
if smooth.is_disabled() {
let t = modify_offset(*t.target()).clamp_range();
let _ = scroll_offset_var.set(t);
*chase = None;
} else {
let easing = smooth.easing.clone();
t.modify(|f| *f = modify_offset(*f).clamp_range(), smooth.duration, move |t| easing(t));
}
}
None => {
let t = modify_offset(scroll_offset_var.get()).clamp_range();
if smooth.is_disabled() {
let _ = scroll_offset_var.set(t);
} else {
let easing = smooth.easing.clone();
let anim = scroll_offset_var.chase(t, smooth.duration, move |t| easing(t));
*chase = Some(anim);
}
}
}
}
pub fn chase_zoom(&self, modify_scale: impl FnOnce(Factor) -> Factor) {
#[cfg(feature = "dyn_closure")]
let modify_scale: Box<dyn FnOnce(Factor) -> Factor> = Box::new(modify_scale);
self.chase_zoom_impl(modify_scale);
}
fn chase_zoom_impl(&self, modify_scale: impl FnOnce(Factor) -> Factor) {
if !SCROLL_MODE_VAR.get().contains(ScrollMode::ZOOM) {
return;
}
let smooth = SMOOTH_SCROLLING_VAR.get();
let config = SCROLL_CONFIG.get();
let mut zoom = config.zoom.lock();
let min = super::MIN_ZOOM_VAR.get();
let max = super::MAX_ZOOM_VAR.get();
match &mut *zoom {
ZoomState::Chasing(t) => {
if smooth.is_disabled() {
let next = modify_scale(*t.target()).clamp(min, max);
let _ = SCROLL_SCALE_VAR.set(next);
*zoom = ZoomState::None;
} else {
let easing = smooth.easing.clone();
t.modify(|f| *f = modify_scale(*f).clamp(min, max), smooth.duration, move |t| easing(t));
}
}
_ => {
let t = modify_scale(SCROLL_SCALE_VAR.get()).clamp(min, max);
if smooth.is_disabled() {
let _ = SCROLL_SCALE_VAR.set(t);
} else {
let easing = smooth.easing.clone();
let anim = SCROLL_SCALE_VAR.chase(t, smooth.duration, move |t| easing(t));
*zoom = ZoomState::Chasing(anim);
}
}
}
}
pub fn zoom(&self, modify_scale: impl FnOnce(Factor) -> Factor, origin: PxPoint) {
#[cfg(feature = "dyn_closure")]
let modify_scale: Box<dyn FnOnce(Factor) -> Factor> = Box::new(modify_scale);
self.zoom_impl(modify_scale, origin);
}
fn zoom_impl(&self, modify_scale: impl FnOnce(Factor) -> Factor, center_in_viewport: PxPoint) {
if !SCROLL_MODE_VAR.get().contains(ScrollMode::ZOOM) {
return;
}
let content = WIDGET.info().scroll_info().unwrap().content();
let mut center_in_content = -content.origin + center_in_viewport.to_vector();
let mut content_size = content.size;
let rendered_scale = SCROLL.rendered_zoom_scale();
SCROLL.chase_zoom(|f| {
let s = modify_scale(f);
let f = s / rendered_scale;
center_in_content *= f;
content_size *= f;
s
});
let viewport_size = SCROLL_VIEWPORT_SIZE_VAR.get();
let max_scroll = content_size - viewport_size;
let offset = center_in_content - center_in_viewport;
if offset.y != Px(0) && max_scroll.height > Px(0) {
let offset_y = offset.y.0 as f32 / max_scroll.height.0 as f32;
SCROLL.chase_vertical(|_| offset_y.fct());
}
if offset.x != Px(0) && max_scroll.width > Px(0) {
let offset_x = offset.x.0 as f32 / max_scroll.width.0 as f32;
SCROLL.chase_horizontal(|_| offset_x.fct());
}
}
pub fn zoom_touch(&self, phase: TouchPhase, scale: Factor, center_in_viewport: euclid::Point2D<f32, Px>) {
if !SCROLL_MODE_VAR.get().contains(ScrollMode::ZOOM) {
return;
}
let cfg = SCROLL_CONFIG.get();
let rendered_scale = SCROLL.rendered_zoom_scale();
let start_scale;
let start_center;
let mut cfg = cfg.zoom.lock();
if let TouchPhase::Start = phase {
start_scale = rendered_scale;
start_center = center_in_viewport;
*cfg = ZoomState::TouchStart {
start_factor: start_scale,
start_center: center_in_viewport,
applied_offset: euclid::vec2(0.0, 0.0),
};
} else if let ZoomState::TouchStart {
start_factor: scale,
start_center: center_in_viewport,
..
} = &*cfg
{
start_scale = *scale;
start_center = *center_in_viewport;
} else {
return;
}
let applied_offset = if let ZoomState::TouchStart { applied_offset, .. } = &mut *cfg {
applied_offset
} else {
unreachable!()
};
let scale = start_scale + (scale - 1.0.fct());
let min = super::MIN_ZOOM_VAR.get();
let max = super::MAX_ZOOM_VAR.get();
let scale = scale.clamp(min, max);
let translate_offset = start_center - center_in_viewport;
let translate_delta = translate_offset - *applied_offset;
*applied_offset = translate_offset;
let content = WIDGET.info().scroll_info().unwrap().content();
let mut center_in_content = -content.origin.cast::<f32>() + center_in_viewport.to_vector();
let mut content_size = content.size.cast::<f32>();
let scale_transform = scale / rendered_scale;
center_in_content *= scale_transform;
content_size *= scale_transform;
let viewport_size = SCROLL_VIEWPORT_SIZE_VAR.get().cast::<f32>();
let max_scroll = content_size - viewport_size;
let zoom_offset = center_in_content - center_in_viewport;
let offset = zoom_offset + translate_delta;
let _ = SCROLL_SCALE_VAR.set(scale);
if offset.y != 0.0 && max_scroll.height > 0.0 {
let offset_y = offset.y / max_scroll.height;
let _ = SCROLL_VERTICAL_OFFSET_VAR.set(offset_y.clamp(0.0, 1.0));
}
if offset.x != 0.0 && max_scroll.width > 0.0 {
let offset_x = offset.x / max_scroll.width;
let _ = SCROLL_HORIZONTAL_OFFSET_VAR.set(offset_x.clamp(0.0, 1.0));
}
}
fn can_scroll(&self, predicate: impl Fn(PxSize, PxSize) -> bool + Send + Sync + 'static) -> impl Var<bool> {
merge_var!(SCROLL_VIEWPORT_SIZE_VAR, SCROLL_CONTENT_SIZE_VAR, move |&vp, &ct| predicate(vp, ct))
}
pub fn can_scroll_vertical(&self) -> impl Var<bool> {
self.can_scroll(|vp, ct| ct.height > vp.height)
}
pub fn can_scroll_horizontal(&self) -> impl Var<bool> {
self.can_scroll(|vp, ct| ct.width > vp.width)
}
fn can_scroll_v(&self, predicate: impl Fn(PxSize, PxSize, Factor) -> bool + Send + Sync + 'static) -> impl Var<bool> {
merge_var!(
SCROLL_VIEWPORT_SIZE_VAR,
SCROLL_CONTENT_SIZE_VAR,
SCROLL_VERTICAL_OFFSET_VAR,
move |&vp, &ct, &vo| predicate(vp, ct, vo)
)
}
pub fn can_scroll_down(&self) -> impl Var<bool> {
self.can_scroll_v(|vp, ct, vo| ct.height > vp.height && 1.fct() > vo)
}
pub fn can_scroll_up(&self) -> impl Var<bool> {
self.can_scroll_v(|vp, ct, vo| ct.height > vp.height && 0.fct() < vo)
}
fn can_scroll_h(&self, predicate: impl Fn(PxSize, PxSize, Factor) -> bool + Send + Sync + 'static) -> impl Var<bool> {
merge_var!(
SCROLL_VIEWPORT_SIZE_VAR,
SCROLL_CONTENT_SIZE_VAR,
SCROLL_HORIZONTAL_OFFSET_VAR,
move |&vp, &ct, &ho| predicate(vp, ct, ho)
)
}
pub fn can_scroll_left(&self) -> impl Var<bool> {
self.can_scroll_h(|vp, ct, ho| ct.width > vp.width && 0.fct() < ho)
}
pub fn can_scroll_right(&self) -> impl Var<bool> {
self.can_scroll_h(|vp, ct, ho| ct.width > vp.width && 1.fct() > ho)
}
pub fn scroll_to(&self, mode: impl Into<super::cmd::ScrollToMode>) {
cmd::scroll_to(WIDGET.info(), mode.into())
}
pub fn scroll_to_zoom(&self, mode: impl Into<super::cmd::ScrollToMode>, zoom: impl Into<Factor>) {
cmd::scroll_to_zoom(WIDGET.info(), mode.into(), zoom.into())
}
pub fn can_zoom_in(&self) -> bool {
SCROLL_MODE_VAR.get().contains(ScrollMode::ZOOM) && SCROLL_SCALE_VAR.get() < super::MAX_ZOOM_VAR.get()
}
pub fn can_zoom_out(&self) -> bool {
SCROLL_MODE_VAR.get().contains(ScrollMode::ZOOM) && SCROLL_SCALE_VAR.get() > super::MIN_ZOOM_VAR.get()
}
}
impl SCROLL {
pub fn context_values_set(&self, set: &mut ContextValueSet) {
set.insert(&SCROLL_CONFIG);
}
}
pub trait WidgetInfoExt {
fn is_scroll(&self) -> bool;
fn scroll_info(&self) -> Option<ScrollInfo>;
fn viewport(&self) -> Option<PxRect>;
}
impl WidgetInfoExt for WidgetInfo {
fn is_scroll(&self) -> bool {
self.meta().get(*SCROLL_INFO_ID).is_some()
}
fn scroll_info(&self) -> Option<ScrollInfo> {
self.meta().get(*SCROLL_INFO_ID).cloned()
}
fn viewport(&self) -> Option<PxRect> {
self.meta().get(*SCROLL_INFO_ID).map(|r| r.viewport())
}
}
#[derive(Debug)]
struct ScrollData {
viewport_transform: PxTransform,
viewport_size: PxSize,
joiner_size: PxSize,
content: PxRect,
zoom_scale: Factor,
}
impl Default for ScrollData {
fn default() -> Self {
Self {
viewport_transform: Default::default(),
viewport_size: Default::default(),
joiner_size: Default::default(),
content: Default::default(),
zoom_scale: 1.fct(),
}
}
}
#[derive(Clone, Default, Debug)]
pub struct ScrollInfo(Arc<Mutex<ScrollData>>);
impl ScrollInfo {
pub fn viewport(&self) -> PxRect {
self.viewport_transform()
.outer_transformed(PxBox::from_size(self.viewport_size()))
.unwrap_or_default()
.to_rect()
}
pub fn viewport_size(&self) -> PxSize {
self.0.lock().viewport_size
}
pub fn viewport_transform(&self) -> PxTransform {
self.0.lock().viewport_transform
}
pub fn joiner_size(&self) -> PxSize {
self.0.lock().joiner_size
}
pub fn content(&self) -> PxRect {
self.0.lock().content
}
pub fn zoom_scale(&self) -> Factor {
self.0.lock().zoom_scale
}
pub(super) fn set_viewport_size(&self, size: PxSize) {
self.0.lock().viewport_size = size;
}
pub(super) fn set_viewport_transform(&self, transform: PxTransform) {
self.0.lock().viewport_transform = transform;
}
pub(super) fn set_joiner_size(&self, size: PxSize) {
self.0.lock().joiner_size = size;
}
pub(super) fn set_content(&self, content: PxRect, scale: Factor) {
let mut m = self.0.lock();
m.content = content;
m.zoom_scale = scale;
}
}
static_id! {
pub(super) static ref SCROLL_INFO_ID: StateId<ScrollInfo>;
}
#[derive(Clone)]
pub struct SmoothScrolling {
pub duration: Duration,
pub easing: Arc<dyn Fn(EasingTime) -> EasingStep + Send + Sync>,
}
impl fmt::Debug for SmoothScrolling {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SmoothScrolling")
.field("duration", &self.duration)
.finish_non_exhaustive()
}
}
impl PartialEq for SmoothScrolling {
fn eq(&self, other: &Self) -> bool {
self.duration == other.duration && Arc::ptr_eq(&self.easing, &other.easing)
}
}
impl Default for SmoothScrolling {
fn default() -> Self {
Self::new(150.ms(), easing::linear)
}
}
impl SmoothScrolling {
pub fn new(duration: Duration, easing: impl Fn(EasingTime) -> EasingStep + Send + Sync + 'static) -> Self {
Self {
duration,
easing: Arc::new(easing),
}
}
pub fn disabled() -> Self {
Self::new(Duration::ZERO, easing::none)
}
pub fn is_disabled(&self) -> bool {
self.duration == Duration::ZERO
}
}
impl_from_and_into_var! {
fn from(duration: Duration) -> SmoothScrolling {
SmoothScrolling {
duration,
..Default::default()
}
}
fn from(enabled: bool) -> SmoothScrolling {
if enabled {
SmoothScrolling::default()
} else {
SmoothScrolling::disabled()
}
}
fn from<F: Fn(EasingTime) -> EasingStep + Send + Sync + 'static>((duration, easing): (Duration, F)) -> SmoothScrolling {
SmoothScrolling::new(duration, easing)
}
fn from((duration, easing): (Duration, easing::EasingFn)) -> SmoothScrolling {
SmoothScrolling::new(duration, easing.ease_fn())
}
}
#[derive(Debug, Default, Clone, PartialEq)]
pub struct AutoScrollArgs {}