1use alloc::collections::BTreeMap;
37
38use azul_core::{
39 callbacks::{TimerCallbackReturn, Update},
40 dom::DomId,
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
59const ASSUMED_FPS: f32 = 60.0;
62
63#[derive(Debug)]
68pub struct ScrollPhysicsState {
69 pub input_queue: ScrollInputQueue,
71 pub node_velocities: BTreeMap<(DomId, NodeId), NodeScrollPhysics>,
73 pub pending_positions: BTreeMap<(DomId, NodeId), LogicalPosition>,
75 pub pending_trackpad_positions: BTreeMap<(DomId, NodeId), LogicalPosition>,
77 pub scroll_physics: ScrollPhysics,
79}
80
81use azul_core::id::NodeId;
83
84#[derive(Debug, Clone, Default)]
86pub struct NodeScrollPhysics {
87 pub velocity: LogicalPosition,
89 pub is_rubber_banding: bool,
91}
92
93impl ScrollPhysicsState {
94 pub fn new(input_queue: ScrollInputQueue, scroll_physics: ScrollPhysics) -> Self {
96 Self {
97 input_queue,
98 node_velocities: BTreeMap::new(),
99 pending_positions: BTreeMap::new(),
100 pending_trackpad_positions: BTreeMap::new(),
101 scroll_physics,
102 }
103 }
104
105 pub fn is_active(&self) -> bool {
107 let threshold = self.scroll_physics.min_velocity_threshold;
108 self.input_queue.has_pending()
109 || self.node_velocities.values().any(|v| {
110 v.velocity.x.abs() > threshold
111 || v.velocity.y.abs() > threshold
112 || v.is_rubber_banding
113 })
114 || !self.pending_positions.is_empty()
115 || !self.pending_trackpad_positions.is_empty()
116 }
117}
118
119pub extern "C" fn scroll_physics_timer_callback(
133 mut data: RefAny,
134 mut timer_info: TimerCallbackInfo,
135) -> TimerCallbackReturn {
136 let mut physics = match data.downcast_mut::<ScrollPhysicsState>() {
138 Some(p) => p,
139 None => return TimerCallbackReturn::terminate_unchanged(),
140 };
141
142 let sp = &physics.scroll_physics;
144 let dt = sp.timer_interval_ms.max(1) as f32 / 1000.0;
145 let friction_rate = friction_from_deceleration(sp.deceleration_rate);
146 let velocity_threshold = sp.min_velocity_threshold;
147 let wheel_multiplier = sp.wheel_multiplier;
148 let max_velocity = sp.max_velocity;
149 let overscroll_elasticity = sp.overscroll_elasticity;
150 let max_overscroll_distance = sp.max_overscroll_distance;
151 let bounce_back_duration_ms = sp.bounce_back_duration_ms;
152
153 let inputs = physics.input_queue.take_recent(MAX_SCROLL_EVENTS_PER_TICK);
155
156 for input in inputs {
157 let key = (input.dom_id, input.node_id);
158 match input.source {
159 ScrollInputSource::TrackpadContinuous => {
160 let current = timer_info
162 .get_scroll_node_info(input.dom_id, input.node_id)
163 .map(|info| info.current_offset)
164 .unwrap_or_default();
165
166 let new_pos = LogicalPosition {
167 x: current.x + input.delta.x,
168 y: current.y + input.delta.y,
169 };
170 physics.pending_trackpad_positions.insert(key, new_pos);
171
172 physics.node_velocities.remove(&key);
174 }
175 ScrollInputSource::WheelDiscrete => {
176 let node_physics = physics
178 .node_velocities
179 .entry(key)
180 .or_insert_with(NodeScrollPhysics::default);
181
182 node_physics.velocity.x += input.delta.x * wheel_multiplier * ASSUMED_FPS;
184 node_physics.velocity.y += input.delta.y * wheel_multiplier * ASSUMED_FPS;
185
186 node_physics.velocity.x = node_physics.velocity.x.clamp(-max_velocity, max_velocity);
188 node_physics.velocity.y = node_physics.velocity.y.clamp(-max_velocity, max_velocity);
189 }
190 ScrollInputSource::Programmatic => {
191 let current = timer_info
193 .get_scroll_node_info(input.dom_id, input.node_id)
194 .map(|info| info.current_offset)
195 .unwrap_or_default();
196
197 let new_pos = LogicalPosition {
198 x: current.x + input.delta.x,
199 y: current.y + input.delta.y,
200 };
201 physics.pending_positions.insert(key, new_pos);
202 }
203 ScrollInputSource::TrackpadEnd => {
204 let pos = physics.pending_positions.remove(&key)
208 .or_else(|| timer_info.get_scroll_node_info(input.dom_id, input.node_id)
209 .map(|info| info.current_offset));
210
211 if let Some(pos) = pos {
212 if let Some(info) = timer_info.get_scroll_node_info(input.dom_id, input.node_id) {
213 let overshoot_x = calculate_overshoot(pos.x, 0.0, info.max_scroll_x);
214 let overshoot_y = calculate_overshoot(pos.y, 0.0, info.max_scroll_y);
215
216 if overshoot_x.abs() > 0.01 || overshoot_y.abs() > 0.01 {
217 let node_phys = physics.node_velocities
218 .entry(key)
219 .or_insert_with(NodeScrollPhysics::default);
220 node_phys.velocity = LogicalPosition::zero();
224 node_phys.is_rubber_banding = true;
225 }
226
227 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(input.node_id));
230 timer_info.scroll_to_unclamped(input.dom_id, hierarchy_id, pos);
231 }
232 }
233 }
234 }
235 }
236
237 let mut velocity_updates: Vec<((DomId, NodeId), LogicalPosition)> = Vec::new();
239
240 for ((dom_id, node_id), node_physics) in physics.node_velocities.iter_mut() {
241 let info = match timer_info.get_scroll_node_info(*dom_id, *node_id) {
243 Some(i) => i,
244 None => continue,
245 };
246
247 let rubber_band_x = node_allows_rubber_band_x(&info, overscroll_elasticity);
249 let rubber_band_y = node_allows_rubber_band_y(&info, overscroll_elasticity);
250
251 let overshoot_x = calculate_overshoot(info.current_offset.x, 0.0, info.max_scroll_x);
253 let overshoot_y = calculate_overshoot(info.current_offset.y, 0.0, info.max_scroll_y);
254
255 let is_overshooting_x = overshoot_x.abs() > 0.01;
256 let is_overshooting_y = overshoot_y.abs() > 0.01;
257
258 if is_overshooting_x && rubber_band_x {
261 let spring_k = spring_constant_from_bounce_duration(bounce_back_duration_ms);
262 let damping = 2.0 * spring_k.sqrt(); let spring_force_x = -spring_k * overshoot_x - damping * node_physics.velocity.x;
264 node_physics.velocity.x += spring_force_x * dt;
265 node_physics.is_rubber_banding = true;
266 }
267 if is_overshooting_y && rubber_band_y {
268 let spring_k = spring_constant_from_bounce_duration(bounce_back_duration_ms);
269 let damping = 2.0 * spring_k.sqrt(); let spring_force_y = -spring_k * overshoot_y - damping * node_physics.velocity.y;
271 node_physics.velocity.y += spring_force_y * dt;
272 node_physics.is_rubber_banding = true;
273 }
274
275 if !node_physics.is_rubber_banding
277 && node_physics.velocity.x.abs() < velocity_threshold
278 && node_physics.velocity.y.abs() < velocity_threshold
279 {
280 node_physics.velocity = LogicalPosition::zero();
281 continue;
282 }
283
284 let displacement = LogicalPosition {
286 x: node_physics.velocity.x * dt,
287 y: node_physics.velocity.y * dt,
288 };
289
290 let raw_new_x = info.current_offset.x + displacement.x;
291 let raw_new_y = info.current_offset.y + displacement.y;
292
293 let new_x = if rubber_band_x && max_overscroll_distance > 0.0 {
295 rubber_band_clamp(raw_new_x, 0.0, info.max_scroll_x, max_overscroll_distance, overscroll_elasticity)
297 } else {
298 raw_new_x.clamp(0.0, info.max_scroll_x)
299 };
300
301 let new_y = if rubber_band_y && max_overscroll_distance > 0.0 {
302 rubber_band_clamp(raw_new_y, 0.0, info.max_scroll_y, max_overscroll_distance, overscroll_elasticity)
303 } else {
304 raw_new_y.clamp(0.0, info.max_scroll_y)
305 };
306
307 let new_pos = LogicalPosition { x: new_x, y: new_y };
308
309 let decay = (-friction_rate * dt * ASSUMED_FPS).exp();
311 node_physics.velocity.x *= decay;
312 node_physics.velocity.y *= decay;
313
314 if !rubber_band_x {
316 if new_pos.x <= 0.0 || new_pos.x >= info.max_scroll_x {
317 node_physics.velocity.x = 0.0;
318 }
319 }
320 if !rubber_band_y {
321 if new_pos.y <= 0.0 || new_pos.y >= info.max_scroll_y {
322 node_physics.velocity.y = 0.0;
323 }
324 }
325
326 let new_overshoot_x = calculate_overshoot(new_pos.x, 0.0, info.max_scroll_x);
328 let new_overshoot_y = calculate_overshoot(new_pos.y, 0.0, info.max_scroll_y);
329 if new_overshoot_x.abs() < 0.5 && new_overshoot_y.abs() < 0.5 {
330 node_physics.is_rubber_banding = false;
331 }
332
333 if node_physics.velocity.x.abs() < velocity_threshold {
335 node_physics.velocity.x = 0.0;
336 }
337 if node_physics.velocity.y.abs() < velocity_threshold {
338 node_physics.velocity.y = 0.0;
339 }
340
341 velocity_updates.push(((*dom_id, *node_id), new_pos));
342 }
343
344 physics
346 .node_velocities
347 .retain(|_, v| v.velocity.x.abs() > 0.0 || v.velocity.y.abs() > 0.0 || v.is_rubber_banding);
348
349 let mut any_changes = false;
351
352 let direct_positions: Vec<_> = physics.pending_positions.iter().map(|(k, v)| (*k, *v)).collect();
354 physics.pending_positions.clear();
355 for ((dom_id, node_id), position) in direct_positions {
356 let clamped = match timer_info.get_scroll_node_info(dom_id, node_id) {
357 Some(info) => LogicalPosition {
358 x: position.x.clamp(0.0, info.max_scroll_x),
359 y: position.y.clamp(0.0, info.max_scroll_y),
360 },
361 None => position,
362 };
363 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
364 timer_info.scroll_to(dom_id, hierarchy_id, clamped);
365 any_changes = true;
366 }
367
368 let trackpad_positions: Vec<_> = physics.pending_trackpad_positions.iter().map(|(k, v)| (*k, *v)).collect();
371 physics.pending_trackpad_positions.clear();
372 for ((dom_id, node_id), position) in trackpad_positions {
373 let clamped = match timer_info.get_scroll_node_info(dom_id, node_id) {
374 Some(info) => {
375 let rubber_x = node_allows_rubber_band_x(&info, physics.scroll_physics.overscroll_elasticity);
376 let rubber_y = node_allows_rubber_band_y(&info, physics.scroll_physics.overscroll_elasticity);
377 let max_over = physics.scroll_physics.max_overscroll_distance;
378 let elasticity = physics.scroll_physics.overscroll_elasticity;
379 LogicalPosition {
380 x: if rubber_x {
381 rubber_band_clamp(position.x, 0.0, info.max_scroll_x, max_over, elasticity)
382 } else {
383 position.x.clamp(0.0, info.max_scroll_x)
384 },
385 y: if rubber_y {
386 rubber_band_clamp(position.y, 0.0, info.max_scroll_y, max_over, elasticity)
387 } else {
388 position.y.clamp(0.0, info.max_scroll_y)
389 },
390 }
391 },
392 None => position,
393 };
394 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
395 timer_info.scroll_to_unclamped(dom_id, hierarchy_id, clamped);
396 any_changes = true;
397 }
398
399 for ((dom_id, node_id), position) in velocity_updates {
401 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
402 timer_info.scroll_to_unclamped(dom_id, hierarchy_id, position);
403 any_changes = true;
404 }
405
406 if physics.is_active() || any_changes {
408 TimerCallbackReturn {
409 should_update: Update::DoNothing, should_terminate: TerminateTimer::Continue,
411 }
412 } else {
413 TimerCallbackReturn::terminate_unchanged()
415 }
416}
417
418fn node_allows_rubber_band_x(info: &ScrollNodeInfo, global_elasticity: f32) -> bool {
428 if info.max_scroll_x <= 0.0 {
430 return false;
431 }
432 if info.overscroll_behavior_x == OverscrollBehavior::None {
433 return false;
434 }
435 if info.overflow_scrolling == OverflowScrolling::Touch {
436 return true;
437 }
438 global_elasticity > 0.0
439}
440
441fn node_allows_rubber_band_y(info: &ScrollNodeInfo, global_elasticity: f32) -> bool {
443 if info.max_scroll_y <= 0.0 {
444 return false;
445 }
446 if info.overscroll_behavior_y == OverscrollBehavior::None {
447 return false;
448 }
449 if info.overflow_scrolling == OverflowScrolling::Touch {
450 return true;
451 }
452 global_elasticity > 0.0
453}
454
455fn calculate_overshoot(pos: f32, min: f32, max: f32) -> f32 {
458 if pos < min {
459 pos - min } else if pos > max {
461 pos - max } else {
463 0.0
464 }
465}
466
467fn rubber_band_clamp(
472 raw_pos: f32,
473 min: f32,
474 max: f32,
475 max_overscroll: f32,
476 elasticity: f32,
477) -> f32 {
478 if raw_pos >= min && raw_pos <= max {
479 return raw_pos;
480 }
481
482 let (boundary, overshoot) = if raw_pos < min {
483 (min, min - raw_pos) } else {
485 (max, raw_pos - max)
486 };
487
488 let clamped_overscroll = if max_overscroll > 0.0 {
491 max_overscroll * (1.0 - (-elasticity * overshoot / max_overscroll).exp())
492 } else {
493 0.0
494 };
495
496 if raw_pos < min {
497 boundary - clamped_overscroll
498 } else {
499 boundary + clamped_overscroll
500 }
501}
502
503fn friction_from_deceleration(deceleration_rate: f32) -> f32 {
506 (1.0 - deceleration_rate.clamp(0.0, 0.999)).max(0.001)
509}
510
511fn spring_constant_from_bounce_duration(duration_ms: u32) -> f32 {
514 let duration_s = duration_ms.max(50) as f32 / 1000.0;
515 let omega = core::f32::consts::TAU / duration_s;
516 omega * omega
517}