1use alloc::collections::BTreeMap;
37
38use azul_core::{
39 callbacks::{TimerCallbackReturn, Update},
40 dom::{DomId, DomNodeId},
41 geom::LogicalPosition,
42 refany::RefAny,
43 styled_dom::NodeHierarchyItemId,
44 task::TerminateTimer,
45};
46
47use crate::{
48 managers::scroll_state::{ScrollInput, ScrollInputQueue, ScrollInputSource, ScrollNodeInfo},
49 timer::TimerCallbackInfo,
50};
51
52use azul_css::props::style::scrollbar::{ScrollPhysics, OverflowScrolling, OverscrollBehavior};
53
54const MAX_SCROLL_EVENTS_PER_TICK: usize = 100;
58
59#[derive(Debug)]
64pub struct ScrollPhysicsState {
65 pub input_queue: ScrollInputQueue,
67 pub node_velocities: BTreeMap<(DomId, NodeId), NodeScrollPhysics>,
69 pub pending_positions: BTreeMap<(DomId, NodeId), LogicalPosition>,
71 pub scroll_physics: ScrollPhysics,
73}
74
75use azul_core::id::NodeId;
77
78#[derive(Debug, Clone, Default)]
80pub struct NodeScrollPhysics {
81 pub velocity: LogicalPosition,
83 pub is_rubber_banding: bool,
85}
86
87impl ScrollPhysicsState {
88 pub fn new(input_queue: ScrollInputQueue, scroll_physics: ScrollPhysics) -> Self {
90 Self {
91 input_queue,
92 node_velocities: BTreeMap::new(),
93 pending_positions: BTreeMap::new(),
94 scroll_physics,
95 }
96 }
97
98 pub fn is_active(&self) -> bool {
100 let threshold = self.scroll_physics.min_velocity_threshold;
101 self.input_queue.has_pending()
102 || self.node_velocities.values().any(|v| {
103 v.velocity.x.abs() > threshold
104 || v.velocity.y.abs() > threshold
105 || v.is_rubber_banding
106 })
107 || !self.pending_positions.is_empty()
108 }
109}
110
111fn scroll_physics_state_destructor(data: &mut RefAny) {
113 let _ = data;
115}
116
117pub extern "C" fn scroll_physics_timer_callback(
131 mut data: RefAny,
132 mut timer_info: TimerCallbackInfo,
133) -> TimerCallbackReturn {
134 let mut physics = match data.downcast_mut::<ScrollPhysicsState>() {
136 Some(p) => p,
137 None => return TimerCallbackReturn::terminate_unchanged(),
138 };
139
140 let sp = &physics.scroll_physics;
142 let dt = sp.timer_interval_ms.max(1) as f32 / 1000.0;
143 let friction_rate = friction_from_deceleration(sp.deceleration_rate);
144 let velocity_threshold = sp.min_velocity_threshold;
145 let wheel_multiplier = sp.wheel_multiplier;
146 let max_velocity = sp.max_velocity;
147 let overscroll_elasticity = sp.overscroll_elasticity;
148 let max_overscroll_distance = sp.max_overscroll_distance;
149 let bounce_back_duration_ms = sp.bounce_back_duration_ms;
150
151 let inputs = physics.input_queue.take_recent(MAX_SCROLL_EVENTS_PER_TICK);
153
154 for input in inputs {
155 let key = (input.dom_id, input.node_id);
156 match input.source {
157 ScrollInputSource::TrackpadContinuous => {
158 let current = timer_info
160 .get_scroll_node_info(input.dom_id, input.node_id)
161 .map(|info| info.current_offset)
162 .unwrap_or_default();
163
164 let new_pos = LogicalPosition {
165 x: current.x + input.delta.x,
166 y: current.y + input.delta.y,
167 };
168 physics.pending_positions.insert(key, new_pos);
169
170 physics.node_velocities.remove(&key);
172 }
173 ScrollInputSource::WheelDiscrete => {
174 let node_physics = physics
176 .node_velocities
177 .entry(key)
178 .or_insert_with(NodeScrollPhysics::default);
179
180 node_physics.velocity.x += input.delta.x * wheel_multiplier * 60.0;
182 node_physics.velocity.y += input.delta.y * wheel_multiplier * 60.0;
183
184 node_physics.velocity.x = node_physics.velocity.x.clamp(-max_velocity, max_velocity);
186 node_physics.velocity.y = node_physics.velocity.y.clamp(-max_velocity, max_velocity);
187 }
188 ScrollInputSource::Programmatic => {
189 let current = timer_info
191 .get_scroll_node_info(input.dom_id, input.node_id)
192 .map(|info| info.current_offset)
193 .unwrap_or_default();
194
195 let new_pos = LogicalPosition {
196 x: current.x + input.delta.x,
197 y: current.y + input.delta.y,
198 };
199 physics.pending_positions.insert(key, new_pos);
200 }
201 }
202 }
203
204 let mut velocity_updates: Vec<((DomId, NodeId), LogicalPosition)> = Vec::new();
206
207 for ((dom_id, node_id), node_physics) in physics.node_velocities.iter_mut() {
208 let info = match timer_info.get_scroll_node_info(*dom_id, *node_id) {
210 Some(i) => i,
211 None => continue,
212 };
213
214 let rubber_band_x = node_allows_rubber_band_x(&info, overscroll_elasticity);
216 let rubber_band_y = node_allows_rubber_band_y(&info, overscroll_elasticity);
217
218 let overshoot_x = calculate_overshoot(info.current_offset.x, 0.0, info.max_scroll_x);
220 let overshoot_y = calculate_overshoot(info.current_offset.y, 0.0, info.max_scroll_y);
221
222 let is_overshooting_x = overshoot_x.abs() > 0.01;
223 let is_overshooting_y = overshoot_y.abs() > 0.01;
224
225 if is_overshooting_x && rubber_band_x {
227 let spring_k = spring_constant_from_bounce_duration(bounce_back_duration_ms);
229 let spring_force_x = -spring_k * overshoot_x;
230 node_physics.velocity.x += spring_force_x * dt;
231 node_physics.is_rubber_banding = true;
232 }
233 if is_overshooting_y && rubber_band_y {
234 let spring_k = spring_constant_from_bounce_duration(bounce_back_duration_ms);
235 let spring_force_y = -spring_k * overshoot_y;
236 node_physics.velocity.y += spring_force_y * dt;
237 node_physics.is_rubber_banding = true;
238 }
239
240 if !node_physics.is_rubber_banding
242 && node_physics.velocity.x.abs() < velocity_threshold
243 && node_physics.velocity.y.abs() < velocity_threshold
244 {
245 node_physics.velocity = LogicalPosition::zero();
246 continue;
247 }
248
249 let displacement = LogicalPosition {
251 x: node_physics.velocity.x * dt,
252 y: node_physics.velocity.y * dt,
253 };
254
255 let raw_new_x = info.current_offset.x + displacement.x;
256 let raw_new_y = info.current_offset.y + displacement.y;
257
258 let new_x = if rubber_band_x && max_overscroll_distance > 0.0 {
260 rubber_band_clamp(raw_new_x, 0.0, info.max_scroll_x, max_overscroll_distance, overscroll_elasticity)
262 } else {
263 raw_new_x.max(0.0).min(info.max_scroll_x)
264 };
265
266 let new_y = if rubber_band_y && max_overscroll_distance > 0.0 {
267 rubber_band_clamp(raw_new_y, 0.0, info.max_scroll_y, max_overscroll_distance, overscroll_elasticity)
268 } else {
269 raw_new_y.max(0.0).min(info.max_scroll_y)
270 };
271
272 let new_pos = LogicalPosition { x: new_x, y: new_y };
273
274 let decay = (-friction_rate * dt * 60.0).exp();
276 node_physics.velocity.x *= decay;
277 node_physics.velocity.y *= decay;
278
279 if !rubber_band_x {
281 if new_pos.x <= 0.0 || new_pos.x >= info.max_scroll_x {
282 node_physics.velocity.x = 0.0;
283 }
284 }
285 if !rubber_band_y {
286 if new_pos.y <= 0.0 || new_pos.y >= info.max_scroll_y {
287 node_physics.velocity.y = 0.0;
288 }
289 }
290
291 let new_overshoot_x = calculate_overshoot(new_pos.x, 0.0, info.max_scroll_x);
293 let new_overshoot_y = calculate_overshoot(new_pos.y, 0.0, info.max_scroll_y);
294 if new_overshoot_x.abs() < 0.5 && new_overshoot_y.abs() < 0.5 {
295 node_physics.is_rubber_banding = false;
296 }
297
298 if node_physics.velocity.x.abs() < velocity_threshold {
300 node_physics.velocity.x = 0.0;
301 }
302 if node_physics.velocity.y.abs() < velocity_threshold {
303 node_physics.velocity.y = 0.0;
304 }
305
306 velocity_updates.push(((*dom_id, *node_id), new_pos));
307 }
308
309 physics
311 .node_velocities
312 .retain(|_, v| v.velocity.x.abs() > 0.0 || v.velocity.y.abs() > 0.0 || v.is_rubber_banding);
313
314 let mut any_changes = false;
316
317 let direct_positions: Vec<_> = physics.pending_positions.iter().map(|(k, v)| (*k, *v)).collect();
319 physics.pending_positions.clear();
320 for ((dom_id, node_id), position) in direct_positions {
321 let clamped = match timer_info.get_scroll_node_info(dom_id, node_id) {
323 Some(info) => LogicalPosition {
324 x: position.x.max(0.0).min(info.max_scroll_x),
325 y: position.y.max(0.0).min(info.max_scroll_y),
326 },
327 None => position,
328 };
329 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
330 timer_info.scroll_to(dom_id, hierarchy_id, clamped);
331 any_changes = true;
332 }
333
334 for ((dom_id, node_id), position) in velocity_updates {
336 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
337 timer_info.scroll_to(dom_id, hierarchy_id, position);
338 any_changes = true;
339 }
340
341 if physics.is_active() || any_changes {
343 TimerCallbackReturn {
344 should_update: if any_changes {
345 Update::RefreshDom
346 } else {
347 Update::DoNothing
348 },
349 should_terminate: TerminateTimer::Continue,
350 }
351 } else {
352 TimerCallbackReturn::terminate_unchanged()
354 }
355}
356
357fn node_allows_rubber_band_x(info: &ScrollNodeInfo, global_elasticity: f32) -> bool {
366 if info.overscroll_behavior_x == OverscrollBehavior::None {
368 return false;
369 }
370 if info.overflow_scrolling == OverflowScrolling::Touch {
372 return true;
373 }
374 global_elasticity > 0.0
376}
377
378fn node_allows_rubber_band_y(info: &ScrollNodeInfo, global_elasticity: f32) -> bool {
380 if info.overscroll_behavior_y == OverscrollBehavior::None {
381 return false;
382 }
383 if info.overflow_scrolling == OverflowScrolling::Touch {
384 return true;
385 }
386 global_elasticity > 0.0
387}
388
389fn calculate_overshoot(pos: f32, min: f32, max: f32) -> f32 {
392 if pos < min {
393 pos - min } else if pos > max {
395 pos - max } else {
397 0.0
398 }
399}
400
401fn rubber_band_clamp(
406 raw_pos: f32,
407 min: f32,
408 max: f32,
409 max_overscroll: f32,
410 elasticity: f32,
411) -> f32 {
412 if raw_pos >= min && raw_pos <= max {
413 return raw_pos;
414 }
415
416 let (boundary, overshoot) = if raw_pos < min {
417 (min, min - raw_pos) } else {
419 (max, raw_pos - max)
420 };
421
422 let clamped_overscroll = if max_overscroll > 0.0 {
425 max_overscroll * (1.0 - (-elasticity * overshoot / max_overscroll).exp())
426 } else {
427 0.0
428 };
429
430 if raw_pos < min {
431 boundary - clamped_overscroll
432 } else {
433 boundary + clamped_overscroll
434 }
435}
436
437fn friction_from_deceleration(deceleration_rate: f32) -> f32 {
440 (1.0 - deceleration_rate.clamp(0.0, 0.999)).max(0.001)
443}
444
445fn spring_constant_from_bounce_duration(duration_ms: u32) -> f32 {
448 let duration_s = duration_ms.max(50) as f32 / 1000.0;
449 let omega = core::f32::consts::TAU / duration_s;
450 omega * omega
451}