use alloc::collections::BTreeMap;
use azul_core::{
callbacks::{TimerCallbackReturn, Update},
dom::{DomId, DomNodeId},
geom::LogicalPosition,
refany::RefAny,
styled_dom::NodeHierarchyItemId,
task::TerminateTimer,
};
use crate::{
managers::scroll_state::{ScrollInput, ScrollInputQueue, ScrollInputSource, ScrollNodeInfo},
timer::TimerCallbackInfo,
};
use azul_css::props::style::scrollbar::{ScrollPhysics, OverflowScrolling, OverscrollBehavior};
const MAX_SCROLL_EVENTS_PER_TICK: usize = 100;
#[derive(Debug)]
pub struct ScrollPhysicsState {
pub input_queue: ScrollInputQueue,
pub node_velocities: BTreeMap<(DomId, NodeId), NodeScrollPhysics>,
pub pending_positions: BTreeMap<(DomId, NodeId), LogicalPosition>,
pub scroll_physics: ScrollPhysics,
}
use azul_core::id::NodeId;
#[derive(Debug, Clone, Default)]
pub struct NodeScrollPhysics {
pub velocity: LogicalPosition,
pub is_rubber_banding: bool,
}
impl ScrollPhysicsState {
pub fn new(input_queue: ScrollInputQueue, scroll_physics: ScrollPhysics) -> Self {
Self {
input_queue,
node_velocities: BTreeMap::new(),
pending_positions: BTreeMap::new(),
scroll_physics,
}
}
pub fn is_active(&self) -> bool {
let threshold = self.scroll_physics.min_velocity_threshold;
self.input_queue.has_pending()
|| self.node_velocities.values().any(|v| {
v.velocity.x.abs() > threshold
|| v.velocity.y.abs() > threshold
|| v.is_rubber_banding
})
|| !self.pending_positions.is_empty()
}
}
fn scroll_physics_state_destructor(data: &mut RefAny) {
let _ = data;
}
pub extern "C" fn scroll_physics_timer_callback(
mut data: RefAny,
mut timer_info: TimerCallbackInfo,
) -> TimerCallbackReturn {
let mut physics = match data.downcast_mut::<ScrollPhysicsState>() {
Some(p) => p,
None => return TimerCallbackReturn::terminate_unchanged(),
};
let sp = &physics.scroll_physics;
let dt = sp.timer_interval_ms.max(1) as f32 / 1000.0;
let friction_rate = friction_from_deceleration(sp.deceleration_rate);
let velocity_threshold = sp.min_velocity_threshold;
let wheel_multiplier = sp.wheel_multiplier;
let max_velocity = sp.max_velocity;
let overscroll_elasticity = sp.overscroll_elasticity;
let max_overscroll_distance = sp.max_overscroll_distance;
let bounce_back_duration_ms = sp.bounce_back_duration_ms;
let inputs = physics.input_queue.take_recent(MAX_SCROLL_EVENTS_PER_TICK);
for input in inputs {
let key = (input.dom_id, input.node_id);
match input.source {
ScrollInputSource::TrackpadContinuous => {
let current = timer_info
.get_scroll_node_info(input.dom_id, input.node_id)
.map(|info| info.current_offset)
.unwrap_or_default();
let new_pos = LogicalPosition {
x: current.x + input.delta.x,
y: current.y + input.delta.y,
};
physics.pending_positions.insert(key, new_pos);
physics.node_velocities.remove(&key);
}
ScrollInputSource::WheelDiscrete => {
let node_physics = physics
.node_velocities
.entry(key)
.or_insert_with(NodeScrollPhysics::default);
node_physics.velocity.x += input.delta.x * wheel_multiplier * 60.0;
node_physics.velocity.y += input.delta.y * wheel_multiplier * 60.0;
node_physics.velocity.x = node_physics.velocity.x.clamp(-max_velocity, max_velocity);
node_physics.velocity.y = node_physics.velocity.y.clamp(-max_velocity, max_velocity);
}
ScrollInputSource::Programmatic => {
let current = timer_info
.get_scroll_node_info(input.dom_id, input.node_id)
.map(|info| info.current_offset)
.unwrap_or_default();
let new_pos = LogicalPosition {
x: current.x + input.delta.x,
y: current.y + input.delta.y,
};
physics.pending_positions.insert(key, new_pos);
}
}
}
let mut velocity_updates: Vec<((DomId, NodeId), LogicalPosition)> = Vec::new();
for ((dom_id, node_id), node_physics) in physics.node_velocities.iter_mut() {
let info = match timer_info.get_scroll_node_info(*dom_id, *node_id) {
Some(i) => i,
None => continue,
};
let rubber_band_x = node_allows_rubber_band_x(&info, overscroll_elasticity);
let rubber_band_y = node_allows_rubber_band_y(&info, overscroll_elasticity);
let overshoot_x = calculate_overshoot(info.current_offset.x, 0.0, info.max_scroll_x);
let overshoot_y = calculate_overshoot(info.current_offset.y, 0.0, info.max_scroll_y);
let is_overshooting_x = overshoot_x.abs() > 0.01;
let is_overshooting_y = overshoot_y.abs() > 0.01;
if is_overshooting_x && rubber_band_x {
let spring_k = spring_constant_from_bounce_duration(bounce_back_duration_ms);
let spring_force_x = -spring_k * overshoot_x;
node_physics.velocity.x += spring_force_x * dt;
node_physics.is_rubber_banding = true;
}
if is_overshooting_y && rubber_band_y {
let spring_k = spring_constant_from_bounce_duration(bounce_back_duration_ms);
let spring_force_y = -spring_k * overshoot_y;
node_physics.velocity.y += spring_force_y * dt;
node_physics.is_rubber_banding = true;
}
if !node_physics.is_rubber_banding
&& node_physics.velocity.x.abs() < velocity_threshold
&& node_physics.velocity.y.abs() < velocity_threshold
{
node_physics.velocity = LogicalPosition::zero();
continue;
}
let displacement = LogicalPosition {
x: node_physics.velocity.x * dt,
y: node_physics.velocity.y * dt,
};
let raw_new_x = info.current_offset.x + displacement.x;
let raw_new_y = info.current_offset.y + displacement.y;
let new_x = if rubber_band_x && max_overscroll_distance > 0.0 {
rubber_band_clamp(raw_new_x, 0.0, info.max_scroll_x, max_overscroll_distance, overscroll_elasticity)
} else {
raw_new_x.max(0.0).min(info.max_scroll_x)
};
let new_y = if rubber_band_y && max_overscroll_distance > 0.0 {
rubber_band_clamp(raw_new_y, 0.0, info.max_scroll_y, max_overscroll_distance, overscroll_elasticity)
} else {
raw_new_y.max(0.0).min(info.max_scroll_y)
};
let new_pos = LogicalPosition { x: new_x, y: new_y };
let decay = (-friction_rate * dt * 60.0).exp();
node_physics.velocity.x *= decay;
node_physics.velocity.y *= decay;
if !rubber_band_x {
if new_pos.x <= 0.0 || new_pos.x >= info.max_scroll_x {
node_physics.velocity.x = 0.0;
}
}
if !rubber_band_y {
if new_pos.y <= 0.0 || new_pos.y >= info.max_scroll_y {
node_physics.velocity.y = 0.0;
}
}
let new_overshoot_x = calculate_overshoot(new_pos.x, 0.0, info.max_scroll_x);
let new_overshoot_y = calculate_overshoot(new_pos.y, 0.0, info.max_scroll_y);
if new_overshoot_x.abs() < 0.5 && new_overshoot_y.abs() < 0.5 {
node_physics.is_rubber_banding = false;
}
if node_physics.velocity.x.abs() < velocity_threshold {
node_physics.velocity.x = 0.0;
}
if node_physics.velocity.y.abs() < velocity_threshold {
node_physics.velocity.y = 0.0;
}
velocity_updates.push(((*dom_id, *node_id), new_pos));
}
physics
.node_velocities
.retain(|_, v| v.velocity.x.abs() > 0.0 || v.velocity.y.abs() > 0.0 || v.is_rubber_banding);
let mut any_changes = false;
let direct_positions: Vec<_> = physics.pending_positions.iter().map(|(k, v)| (*k, *v)).collect();
physics.pending_positions.clear();
for ((dom_id, node_id), position) in direct_positions {
let clamped = match timer_info.get_scroll_node_info(dom_id, node_id) {
Some(info) => LogicalPosition {
x: position.x.max(0.0).min(info.max_scroll_x),
y: position.y.max(0.0).min(info.max_scroll_y),
},
None => position,
};
let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
timer_info.scroll_to(dom_id, hierarchy_id, clamped);
any_changes = true;
}
for ((dom_id, node_id), position) in velocity_updates {
let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
timer_info.scroll_to(dom_id, hierarchy_id, position);
any_changes = true;
}
if physics.is_active() || any_changes {
TimerCallbackReturn {
should_update: if any_changes {
Update::RefreshDom
} else {
Update::DoNothing
},
should_terminate: TerminateTimer::Continue,
}
} else {
TimerCallbackReturn::terminate_unchanged()
}
}
fn node_allows_rubber_band_x(info: &ScrollNodeInfo, global_elasticity: f32) -> bool {
if info.overscroll_behavior_x == OverscrollBehavior::None {
return false;
}
if info.overflow_scrolling == OverflowScrolling::Touch {
return true;
}
global_elasticity > 0.0
}
fn node_allows_rubber_band_y(info: &ScrollNodeInfo, global_elasticity: f32) -> bool {
if info.overscroll_behavior_y == OverscrollBehavior::None {
return false;
}
if info.overflow_scrolling == OverflowScrolling::Touch {
return true;
}
global_elasticity > 0.0
}
fn calculate_overshoot(pos: f32, min: f32, max: f32) -> f32 {
if pos < min {
pos - min } else if pos > max {
pos - max } else {
0.0
}
}
fn rubber_band_clamp(
raw_pos: f32,
min: f32,
max: f32,
max_overscroll: f32,
elasticity: f32,
) -> f32 {
if raw_pos >= min && raw_pos <= max {
return raw_pos;
}
let (boundary, overshoot) = if raw_pos < min {
(min, min - raw_pos) } else {
(max, raw_pos - max)
};
let clamped_overscroll = if max_overscroll > 0.0 {
max_overscroll * (1.0 - (-elasticity * overshoot / max_overscroll).exp())
} else {
0.0
};
if raw_pos < min {
boundary - clamped_overscroll
} else {
boundary + clamped_overscroll
}
}
fn friction_from_deceleration(deceleration_rate: f32) -> f32 {
(1.0 - deceleration_rate.clamp(0.0, 0.999)).max(0.001)
}
fn spring_constant_from_bounce_duration(duration_ms: u32) -> f32 {
let duration_s = duration_ms.max(50) as f32 / 1000.0;
let omega = core::f32::consts::TAU / duration_s;
omega * omega
}