tessera_ui_basic_components/
scrollable.rs1use std::{sync::Arc, time::Instant};
2
3use derive_builder::Builder;
4use parking_lot::RwLock;
5use tessera_ui::{
6 ComputedData, Constraint, CursorEventContent, DimensionValue, Px, PxPosition,
7 focus_state::Focus, measure_node, place_node,
8};
9use tessera_ui_macros::tessera;
10
11use crate::pos_misc::is_position_in_component;
12
13#[derive(Debug, Builder)]
14pub struct ScrollableArgs {
15 #[builder(default = "tessera_ui::DimensionValue::Wrap { min: None, max: None }")]
18 pub width: tessera_ui::DimensionValue,
19 #[builder(default = "tessera_ui::DimensionValue::Wrap { min: None, max: None }")]
22 pub height: tessera_ui::DimensionValue,
23 #[builder(default = "true")]
26 pub vertical: bool,
27 #[builder(default = "false")]
30 pub horizontal: bool,
31 #[builder(default = "0.05")]
34 pub scroll_smoothing: f32,
35}
36
37pub struct ScrollableState {
39 child_position: PxPosition,
41 target_position: PxPosition,
43 child_size: ComputedData,
45 last_frame_time: Option<Instant>,
47 focus_handler: Focus,
49}
50
51impl Default for ScrollableState {
52 fn default() -> Self {
53 Self::new()
54 }
55}
56
57impl ScrollableState {
58 pub fn new() -> Self {
60 Self {
61 child_position: PxPosition::ZERO,
62 target_position: PxPosition::ZERO,
63 child_size: ComputedData::ZERO,
64 last_frame_time: None,
65 focus_handler: Focus::new(),
66 }
67 }
68
69 fn update_scroll_position(&mut self, smoothing: f32) -> bool {
72 let current_time = Instant::now();
73
74 let delta_time = if let Some(last_time) = self.last_frame_time {
76 current_time.duration_since(last_time).as_secs_f32()
77 } else {
78 0.016 };
80
81 self.last_frame_time = Some(current_time);
82
83 let diff_x = self.target_position.x.to_f32() - self.child_position.x.to_f32();
85 let diff_y = self.target_position.y.to_f32() - self.child_position.y.to_f32();
86
87 if diff_x.abs() < 1.0 && diff_y.abs() < 1.0 {
89 if self.child_position != self.target_position {
90 self.child_position = self.target_position;
91 return true;
92 }
93 return false;
94 }
95
96 let mut movement_factor = (1.0 - smoothing) * delta_time * 60.0;
99
100 if movement_factor > 1.0 {
106 movement_factor = 1.0;
107 }
108 let old_position = self.child_position;
109
110 self.child_position = PxPosition {
111 x: Px::saturating_from_f32(self.child_position.x.to_f32() + diff_x * movement_factor),
112 y: Px::saturating_from_f32(self.child_position.y.to_f32() + diff_y * movement_factor),
113 };
114
115 old_position != self.child_position
117 }
118
119 fn set_target_position(&mut self, target: PxPosition) {
121 self.target_position = target;
122 }
123
124 pub fn focus_handler(&self) -> &Focus {
126 &self.focus_handler
127 }
128
129 pub fn focus_handler_mut(&mut self) -> &mut Focus {
131 &mut self.focus_handler
132 }
133}
134
135#[tessera]
136pub fn scrollable(
137 args: impl Into<ScrollableArgs>,
138 state: Arc<RwLock<ScrollableState>>,
139 child: impl FnOnce(),
140) {
141 let args: ScrollableArgs = args.into();
142 {
143 let state = state.clone();
144 measure(Box::new(move |input| {
145 let arg_constraint = Constraint {
147 width: args.width,
148 height: args.height,
149 };
150 let merged_constraint = input.parent_constraint.merge(&arg_constraint);
151 let mut child_constraint = merged_constraint;
153 if args.vertical {
155 child_constraint.height = tessera_ui::DimensionValue::Wrap {
156 min: None,
157 max: None,
158 };
159 }
160 if args.horizontal {
162 child_constraint.width = tessera_ui::DimensionValue::Wrap {
163 min: None,
164 max: None,
165 };
166 }
167 let child_node_id = input.children_ids[0]; let child_measurement = measure_node(
170 child_node_id,
171 &child_constraint,
172 input.tree,
173 input.metadatas,
174 input.compute_resource_manager.clone(),
175 input.gpu,
176 )?;
177 state.write().child_size = child_measurement;
179
180 let current_child_position = {
182 let mut state_guard = state.write();
183 state_guard.update_scroll_position(args.scroll_smoothing);
184 state_guard.child_position
185 };
186
187 place_node(child_node_id, current_child_position, input.metadatas);
189 let width = match merged_constraint.width {
191 DimensionValue::Fixed(w) => w,
192 DimensionValue::Wrap { min, max } => {
193 let mut width = child_measurement.width;
194 if let Some(min_width) = min {
195 width = width.max(min_width);
196 }
197 if let Some(max_width) = max {
198 width = width.min(max_width);
199 }
200 width
201 }
202 DimensionValue::Fill { min: _, max } => max.unwrap(),
203 };
204 let height = match merged_constraint.height {
205 DimensionValue::Fixed(h) => h,
206 DimensionValue::Wrap { min, max } => {
207 let mut height = child_measurement.height;
208 if let Some(min_height) = min {
209 height = height.max(min_height)
210 }
211 if let Some(max_height) = max {
212 height = height.min(max_height)
213 }
214 height
215 }
216 DimensionValue::Fill { min: _, max } => max.unwrap(),
217 };
218 Ok(ComputedData { width, height })
220 }));
221 }
222
223 state_handler(Box::new(move |input| {
225 let size = input.computed_data;
226 let cursor_pos_option = input.cursor_position;
227 let is_cursor_in_component = cursor_pos_option
228 .map(|pos| is_position_in_component(size, pos))
229 .unwrap_or(false);
230
231 if is_cursor_in_component {
233 let click_events: Vec<_> = input
234 .cursor_events
235 .iter()
236 .filter(|event| matches!(event.content, CursorEventContent::Pressed(_)))
237 .collect();
238
239 if !click_events.is_empty() {
240 if !state.read().focus_handler().is_focused() {
242 state.write().focus_handler_mut().request_focus();
243 }
244 }
245
246 if state.read().focus_handler().is_focused() {
248 for event in input
249 .cursor_events
250 .iter()
251 .filter_map(|event| match &event.content {
252 CursorEventContent::Scroll(event) => Some(event),
253 _ => None,
254 })
255 {
256 let mut state_guard = state.write();
257
258 let scroll_delta_x = event.delta_x;
260 let scroll_delta_y = event.delta_y;
261
262 let current_target = state_guard.target_position;
264 let new_target = current_target.saturating_offset(
265 Px::saturating_from_f32(scroll_delta_x),
266 Px::saturating_from_f32(scroll_delta_y),
267 );
268
269 let child_size = state_guard.child_size;
271 let constrained_target = constrain_position(
272 new_target,
273 &child_size,
274 &input.computed_data,
275 args.vertical,
276 args.horizontal,
277 );
278
279 state_guard.set_target_position(constrained_target);
281 }
282 }
283
284 let target = state.read().target_position;
287 let child_size = state.read().child_size;
288 let constrained_position = constrain_position(
289 target,
290 &child_size,
291 &input.computed_data,
292 args.vertical,
293 args.horizontal,
294 );
295 state.write().set_target_position(constrained_position);
296
297 if state.read().focus_handler().is_focused() {
299 input.cursor_events.clear();
300 }
301 }
302
303 state.write().update_scroll_position(args.scroll_smoothing);
305 }));
306
307 child();
309}
310
311fn constrain_position(
313 position: PxPosition,
314 child_size: &ComputedData,
315 container_size: &ComputedData,
316 vertical_scrollable: bool,
317 horizontal_scrollable: bool,
318) -> PxPosition {
319 let mut constrained = position;
320
321 if horizontal_scrollable {
323 if constrained.x > Px::ZERO {
325 constrained.x = Px::ZERO;
326 }
327 if constrained.x.saturating_add(child_size.width) < container_size.width {
329 constrained.x = container_size.width.saturating_sub(child_size.width);
330 }
331 } else {
332 constrained.x = Px::ZERO;
334 }
335
336 if vertical_scrollable {
337 if constrained.y > Px::ZERO {
339 constrained.y = Px::ZERO;
340 }
341 if constrained.y.saturating_add(child_size.height) < container_size.height {
343 constrained.y = container_size.height.saturating_sub(child_size.height);
344 }
345 } else {
346 constrained.y = Px::ZERO;
348 }
349
350 constrained
351}