damascene_core/state/
scroll.rs1use crate::hit_test::scroll_targets_at;
4use crate::tree::{El, Rect};
5
6use super::{
7 UiState,
8 types::{SCROLL_MOMENTUM_DECAY_PER_SEC, SCROLL_MOMENTUM_STOP_VELOCITY, ScrollMomentum},
9};
10use web_time::Instant;
11
12const WHEEL_EPSILON: f32 = 0.5;
13
14#[derive(Clone, Debug)]
15pub(crate) struct ScrollStep {
16 pub scroll_id: String,
17 pub applied_delta: f32,
18}
19
20impl UiState {
21 pub fn set_scroll_offset(&mut self, id: impl Into<String>, value: f32) {
26 self.scroll.offsets.insert(id.into(), value);
27 }
28
29 pub fn scroll_offset(&self, id: &str) -> f32 {
31 self.scroll.offsets.get(id).copied().unwrap_or(0.0)
32 }
33
34 pub fn push_scroll_requests(&mut self, requests: Vec<crate::scroll::ScrollRequest>) {
42 self.scroll.pending_requests.extend(requests);
43 }
44
45 pub fn clear_pending_scroll_requests(&mut self) {
50 self.scroll.pending_requests.clear();
51 }
52
53 pub fn scrollbar_tracks(&self) -> impl Iterator<Item = (&str, &Rect)> {
59 self.scroll
60 .thumb_tracks
61 .iter()
62 .map(|(id, rect)| (id.as_str(), rect))
63 }
64
65 pub fn thumb_at(&self, x: f32, y: f32) -> Option<(String, Rect, Rect)> {
73 for (id, track) in &self.scroll.thumb_tracks {
74 if track.contains(x, y) {
75 let thumb = self
76 .scroll
77 .thumb_rects
78 .get(id)
79 .copied()
80 .unwrap_or_else(|| Rect::new(track.x, track.y, track.w, 0.0));
81 return Some((id.clone(), *track, thumb));
82 }
83 }
84 None
85 }
86
87 pub fn pointer_wheel(&mut self, root: &El, point: (f32, f32), dy: f32) -> bool {
96 self.scroll_by_pointer(root, point, dy).is_some()
97 }
98
99 pub(crate) fn scroll_by_pointer(
100 &mut self,
101 root: &El,
102 point: (f32, f32),
103 dy: f32,
104 ) -> Option<ScrollStep> {
105 if dy.abs() <= f32::EPSILON {
106 return None;
107 }
108 for id in scroll_targets_at(root, self, point).into_iter().rev() {
109 if let Some(step) = self.scroll_by_id(&id, dy) {
110 return Some(step);
111 }
112 }
113 None
114 }
115
116 pub(crate) fn scroll_by_id(&mut self, id: &str, dy: f32) -> Option<ScrollStep> {
117 if dy.abs() <= f32::EPSILON {
118 return None;
119 }
120 let metrics = self.scroll.metrics.get(id).copied()?;
121 if metrics.max_offset <= WHEEL_EPSILON {
122 return None;
123 }
124 let current = self
125 .scroll
126 .offsets
127 .get(id)
128 .copied()
129 .unwrap_or(0.0)
130 .clamp(0.0, metrics.max_offset);
131 let can_scroll = if dy > 0.0 {
132 current < metrics.max_offset - WHEEL_EPSILON
133 } else {
134 current > WHEEL_EPSILON
135 };
136 if !can_scroll {
137 return None;
138 }
139 let next = (current + dy).clamp(0.0, metrics.max_offset);
140 if (next - current).abs() <= f32::EPSILON {
141 return None;
142 }
143 self.scroll.offsets.insert(id.to_owned(), next);
144 Some(ScrollStep {
145 scroll_id: id.to_owned(),
146 applied_delta: next - current,
147 })
148 }
149
150 pub(crate) fn start_scroll_momentum(
151 &mut self,
152 scroll_id: Option<String>,
153 velocity: f32,
154 now: Instant,
155 ) {
156 let Some(scroll_id) = scroll_id else {
157 self.scroll.momentum = None;
158 return;
159 };
160 if velocity.abs() < super::types::SCROLL_MOMENTUM_MIN_VELOCITY {
161 self.scroll.momentum = None;
162 return;
163 }
164 self.scroll.momentum = Some(ScrollMomentum {
165 scroll_id,
166 velocity,
167 last_tick: now,
168 });
169 }
170
171 pub(crate) fn cancel_scroll_momentum(&mut self) {
172 self.scroll.momentum = None;
173 }
174
175 pub(crate) fn has_scroll_momentum(&self) -> bool {
176 self.scroll.momentum.is_some()
177 }
178
179 pub(crate) fn tick_scroll_momentum(&mut self, now: Instant) -> bool {
180 let Some(mut momentum) = self.scroll.momentum.take() else {
181 return false;
182 };
183 let dt = now
184 .duration_since(momentum.last_tick)
185 .as_secs_f32()
186 .clamp(0.0, 0.050);
187 momentum.last_tick = now;
188 if dt <= f32::EPSILON {
189 self.scroll.momentum = Some(momentum);
190 return true;
191 }
192
193 let Some(metrics) = self.scroll.metrics.get(&momentum.scroll_id).copied() else {
194 return false;
195 };
196 if metrics.max_offset <= WHEEL_EPSILON {
197 return false;
198 }
199 let current = self
200 .scroll
201 .offsets
202 .get(&momentum.scroll_id)
203 .copied()
204 .unwrap_or(0.0)
205 .clamp(0.0, metrics.max_offset);
206 let next = (current + momentum.velocity * dt).clamp(0.0, metrics.max_offset);
207 let changed = (next - current).abs() > f32::EPSILON;
208 if changed {
209 self.scroll.offsets.insert(momentum.scroll_id.clone(), next);
210 }
211
212 let hit_edge = (next <= WHEEL_EPSILON && momentum.velocity < 0.0)
213 || (next >= metrics.max_offset - WHEEL_EPSILON && momentum.velocity > 0.0);
214 momentum.velocity *= (-SCROLL_MOMENTUM_DECAY_PER_SEC * dt).exp();
215 if !hit_edge && momentum.velocity.abs() > SCROLL_MOMENTUM_STOP_VELOCITY {
216 self.scroll.momentum = Some(momentum);
217 }
218 changed || self.scroll.momentum.is_some()
219 }
220}