1mod scrollbar;
7use std::{sync::Arc, time::Instant};
8
9use derive_builder::Builder;
10use parking_lot::RwLock;
11use tessera_ui::{
12 Color, ComputedData, Constraint, CursorEventContent, DimensionValue, Dp, Px, PxPosition,
13 tessera,
14};
15
16use crate::{
17 alignment::Alignment,
18 boxed::{BoxedArgsBuilder, boxed},
19 pos_misc::is_position_in_component,
20 scrollable::scrollbar::{ScrollBarArgs, ScrollBarState, scrollbar_h, scrollbar_v},
21};
22
23#[derive(Debug, Builder, Clone)]
24pub struct ScrollableArgs {
25 #[builder(default = "tessera_ui::DimensionValue::Wrap { min: None, max: None }")]
28 pub width: tessera_ui::DimensionValue,
29 #[builder(default = "tessera_ui::DimensionValue::Wrap { min: None, max: None }")]
32 pub height: tessera_ui::DimensionValue,
33 #[builder(default = "true")]
36 pub vertical: bool,
37 #[builder(default = "false")]
40 pub horizontal: bool,
41 #[builder(default = "0.05")]
44 pub scroll_smoothing: f32,
45 #[builder(default = "ScrollBarBehavior::AlwaysVisible")]
47 pub scrollbar_behavior: ScrollBarBehavior,
48 #[builder(default = "Color::new(0.0, 0.0, 0.0, 0.1)")]
50 pub scrollbar_track_color: Color,
51 #[builder(default = "Color::new(0.0, 0.0, 0.0, 0.3)")]
53 pub scrollbar_thumb_color: Color,
54 #[builder(default = "Color::new(0.0, 0.0, 0.0, 0.5)")]
56 pub scrollbar_thumb_hover_color: Color,
57 #[builder(default = "ScrollBarLayout::Alongside")]
59 pub scrollbar_layout: ScrollBarLayout,
60}
61
62#[derive(Debug, Clone)]
64pub enum ScrollBarBehavior {
65 AlwaysVisible,
67 AutoHide,
69 Hidden,
71}
72
73#[derive(Debug, Clone)]
75pub enum ScrollBarLayout {
76 Alongside,
78 Overlay,
80}
81
82impl Default for ScrollableArgs {
83 fn default() -> Self {
84 ScrollableArgsBuilder::default().build().unwrap()
85 }
86}
87
88#[derive(Clone, Default)]
94pub struct ScrollableState {
95 inner: Arc<RwLock<ScrollableStateInner>>,
97 scrollbar_state_v: ScrollBarState,
99 scrollbar_state_h: ScrollBarState,
101}
102
103impl ScrollableState {
104 pub fn new() -> Self {
106 Self::default()
107 }
108
109 pub fn child_position(&self) -> PxPosition {
116 self.inner.read().child_position
117 }
118
119 pub fn visible_size(&self) -> ComputedData {
121 self.inner.read().visible_size
122 }
123
124 pub fn child_size(&self) -> ComputedData {
126 self.inner.read().child_size
127 }
128
129 pub fn override_child_size(&self, size: ComputedData) {
131 self.inner.write().override_child_size = Some(size);
132 }
133}
134
135#[derive(Clone, Debug)]
136struct ScrollableStateInner {
137 child_position: PxPosition,
139 target_position: PxPosition,
141 child_size: ComputedData,
143 visible_size: ComputedData,
145 override_child_size: Option<ComputedData>,
147 last_frame_time: Option<Instant>,
149}
150
151impl Default for ScrollableStateInner {
152 fn default() -> Self {
153 Self::new()
154 }
155}
156
157impl ScrollableStateInner {
158 pub fn new() -> Self {
160 Self {
161 child_position: PxPosition::ZERO,
162 target_position: PxPosition::ZERO,
163 child_size: ComputedData::ZERO,
164 visible_size: ComputedData::ZERO,
165 override_child_size: None,
166 last_frame_time: None,
167 }
168 }
169
170 fn update_scroll_position(&mut self, smoothing: f32) -> bool {
173 let current_time = Instant::now();
174
175 let delta_time = if let Some(last_time) = self.last_frame_time {
177 current_time.duration_since(last_time).as_secs_f32()
178 } else {
179 0.016 };
181
182 self.last_frame_time = Some(current_time);
183
184 let diff_x = self.target_position.x.to_f32() - self.child_position.x.to_f32();
186 let diff_y = self.target_position.y.to_f32() - self.child_position.y.to_f32();
187
188 if diff_x.abs() < 1.0 && diff_y.abs() < 1.0 {
190 if self.child_position != self.target_position {
191 self.child_position = self.target_position;
192 return true;
193 }
194 return false;
195 }
196
197 let mut movement_factor = (1.0 - smoothing) * delta_time * 60.0;
200
201 if movement_factor > 1.0 {
207 movement_factor = 1.0;
208 }
209 let old_position = self.child_position;
210
211 self.child_position = PxPosition {
212 x: Px::saturating_from_f32(self.child_position.x.to_f32() + diff_x * movement_factor),
213 y: Px::saturating_from_f32(self.child_position.y.to_f32() + diff_y * movement_factor),
214 };
215
216 old_position != self.child_position
218 }
219
220 fn set_target_position(&mut self, target: PxPosition) {
222 self.target_position = target;
223 }
224}
225
226#[tessera]
272pub fn scrollable(
273 args: impl Into<ScrollableArgs>,
274 state: ScrollableState,
275 child: impl FnOnce() + Send + Sync + 'static,
276) {
277 let args: ScrollableArgs = args.into();
278
279 let scrollbar_args_v = ScrollBarArgs {
281 total: state.inner.read().child_size.height,
282 visible: state.inner.read().visible_size.height,
283 offset: state.inner.read().child_position.y,
284 thickness: Dp(8.0), state: state.inner.clone(),
286 scrollbar_behavior: args.scrollbar_behavior.clone(),
287 track_color: args.scrollbar_track_color,
288 thumb_color: args.scrollbar_thumb_color,
289 thumb_hover_color: args.scrollbar_thumb_hover_color,
290 };
291
292 let scrollbar_args_h = ScrollBarArgs {
293 total: state.inner.read().child_size.width,
294 visible: state.inner.read().visible_size.width,
295 offset: state.inner.read().child_position.x,
296 thickness: Dp(8.0), state: state.inner.clone(),
298 scrollbar_behavior: args.scrollbar_behavior.clone(),
299 track_color: args.scrollbar_track_color,
300 thumb_color: args.scrollbar_thumb_color,
301 thumb_hover_color: args.scrollbar_thumb_hover_color,
302 };
303
304 match args.scrollbar_layout {
305 ScrollBarLayout::Alongside => {
306 scrollable_with_alongside_scrollbar(
307 state,
308 args,
309 scrollbar_args_v,
310 scrollbar_args_h,
311 child,
312 );
313 }
314 ScrollBarLayout::Overlay => {
315 scrollable_with_overlay_scrollbar(
316 state,
317 args,
318 scrollbar_args_v,
319 scrollbar_args_h,
320 child,
321 );
322 }
323 }
324}
325
326#[tessera]
327fn scrollable_with_alongside_scrollbar(
328 state: ScrollableState,
329 args: ScrollableArgs,
330 scrollbar_args_v: ScrollBarArgs,
331 scrollbar_args_h: ScrollBarArgs,
332 child: impl FnOnce() + Send + Sync + 'static,
333) {
334 scrollable_inner(
335 args.clone(),
336 state.inner.clone(),
337 state.scrollbar_state_v.clone(),
338 state.scrollbar_state_h.clone(),
339 child,
340 );
341
342 if args.vertical {
343 scrollbar_v(scrollbar_args_v, state.scrollbar_state_v.clone());
344 }
345
346 if args.horizontal {
347 scrollbar_h(scrollbar_args_h, state.scrollbar_state_h.clone());
348 }
349
350 measure(Box::new(move |input| {
351 let mut final_size = ComputedData::ZERO;
353 let self_constraint = Constraint {
355 width: args.width,
356 height: args.height,
357 };
358 let mut content_contraint = self_constraint.merge(input.parent_constraint);
359 if args.vertical {
361 let scrollbar_node_id = input.children_ids[1];
362 let size = input.measure_child(scrollbar_node_id, input.parent_constraint)?;
363 content_contraint.width -= size.width;
365 final_size.width += size.width;
367 }
368 if args.horizontal {
369 let scrollbar_node_id = if args.vertical {
370 input.children_ids[2]
371 } else {
372 input.children_ids[1]
373 };
374 let size = input.measure_child(scrollbar_node_id, input.parent_constraint)?;
375 content_contraint.height -= size.height;
376 final_size.height += size.height;
378 }
379 let content_node_id = input.children_ids[0];
381 let content_measurement = input.measure_child(content_node_id, &content_contraint)?;
382 final_size.width += content_measurement.width;
384 final_size.height += content_measurement.height;
385 input.place_child(content_node_id, PxPosition::ZERO);
388 if args.vertical {
390 input.place_child(
391 input.children_ids[1],
392 PxPosition::new(content_measurement.width, Px::ZERO),
393 );
394 }
395 if args.horizontal {
396 let scrollbar_node_id = if args.vertical {
397 input.children_ids[2]
398 } else {
399 input.children_ids[1]
400 };
401 input.place_child(
402 scrollbar_node_id,
403 PxPosition::new(Px::ZERO, content_measurement.height),
404 );
405 }
406 Ok(final_size)
408 }));
409}
410
411#[tessera]
412fn scrollable_with_overlay_scrollbar(
413 state: ScrollableState,
414 args: ScrollableArgs,
415 scrollbar_args_v: ScrollBarArgs,
416 scrollbar_args_h: ScrollBarArgs,
417 child: impl FnOnce() + Send + Sync + 'static,
418) {
419 boxed(
420 BoxedArgsBuilder::default()
421 .width(args.width)
422 .height(args.height)
423 .alignment(Alignment::BottomEnd)
424 .build()
425 .unwrap(),
426 |scope| {
427 scope.child({
428 let state = state.clone();
429 let args = args.clone();
430 move || {
431 scrollable_inner(
432 args,
433 state.inner.clone(),
434 state.scrollbar_state_v.clone(),
435 state.scrollbar_state_h.clone(),
436 child,
437 );
438 }
439 });
440 scope.child({
441 let scrollbar_args_v = scrollbar_args_v.clone();
442 let args = args.clone();
443 let state = state.clone();
444 move || {
445 if args.vertical {
446 scrollbar_v(scrollbar_args_v, state.scrollbar_state_v.clone());
447 }
448 }
449 });
450 scope.child({
451 let scrollbar_args_h = scrollbar_args_h.clone();
452 let args = args.clone();
453 let state = state.clone();
454 move || {
455 if args.horizontal {
456 scrollbar_h(scrollbar_args_h, state.scrollbar_state_h.clone());
457 }
458 }
459 });
460 },
461 );
462}
463
464fn clamp_wrap(min: Option<Px>, max: Option<Px>, measure: Px) -> Px {
467 min.unwrap_or(Px(0))
468 .max(measure)
469 .min(max.unwrap_or(Px::MAX))
470}
471
472fn fill_value(min: Option<Px>, max: Option<Px>, measure: Px) -> Px {
473 max.expect("Seems that you are trying to fill an infinite dimension, which is not allowed")
474 .max(measure)
475 .max(min.unwrap_or(Px(0)))
476}
477
478fn resolve_dimension(dim: DimensionValue, measure: Px) -> Px {
479 match dim {
480 DimensionValue::Fixed(v) => v,
481 DimensionValue::Wrap { min, max } => clamp_wrap(min, max, measure),
482 DimensionValue::Fill { min, max } => fill_value(min, max, measure),
483 }
484}
485
486#[tessera]
487fn scrollable_inner(
488 args: impl Into<ScrollableArgs>,
489 state: Arc<RwLock<ScrollableStateInner>>,
490 scrollbar_state_v: ScrollBarState,
491 scrollbar_state_h: ScrollBarState,
492 child: impl FnOnce(),
493) {
494 let args: ScrollableArgs = args.into();
495 {
496 let state = state.clone();
497 measure(Box::new(move |input| {
498 input.enable_clipping();
500 let arg_constraint = Constraint {
502 width: args.width,
503 height: args.height,
504 };
505 let merged_constraint = input.parent_constraint.merge(&arg_constraint);
506 let mut child_constraint = merged_constraint;
508 if args.vertical {
510 child_constraint.height = tessera_ui::DimensionValue::Wrap {
511 min: None,
512 max: None,
513 };
514 }
515 if args.horizontal {
517 child_constraint.width = tessera_ui::DimensionValue::Wrap {
518 min: None,
519 max: None,
520 };
521 }
522 let child_node_id = input.children_ids[0]; let child_measurement = input.measure_child(child_node_id, &child_constraint)?;
525 let current_child_position = {
529 let mut state_guard = state.write();
530 if let Some(override_size) = state_guard.override_child_size.take() {
531 state_guard.child_size = override_size;
532 } else {
533 state_guard.child_size = child_measurement;
534 }
535 state_guard.update_scroll_position(args.scroll_smoothing);
536 state_guard.child_position
537 };
538
539 input.place_child(child_node_id, current_child_position);
541
542 let mut width = resolve_dimension(merged_constraint.width, child_measurement.width);
544 let mut height = resolve_dimension(merged_constraint.height, child_measurement.height);
545
546 if let Some(parent_max_width) = input.parent_constraint.width.get_max() {
547 width = width.min(parent_max_width);
548 }
549 if let Some(parent_max_height) = input.parent_constraint.height.get_max() {
550 height = height.min(parent_max_height);
551 }
552
553 let computed_data = ComputedData { width, height };
555 state.write().visible_size = computed_data;
557 Ok(computed_data)
559 }));
560 }
561
562 input_handler(Box::new(move |input| {
564 let size = input.computed_data;
565 let cursor_pos_option = input.cursor_position_rel;
566 let is_cursor_in_component = cursor_pos_option
567 .map(|pos| is_position_in_component(size, pos))
568 .unwrap_or(false);
569
570 if is_cursor_in_component {
571 for event in input
573 .cursor_events
574 .iter()
575 .filter_map(|event| match &event.content {
576 CursorEventContent::Scroll(event) => Some(event),
577 _ => None,
578 })
579 {
580 let mut state_guard = state.write();
581
582 let scroll_delta_x = event.delta_x;
584 let scroll_delta_y = event.delta_y;
585
586 let current_target = state_guard.target_position;
588 let new_target = current_target.saturating_offset(
589 Px::saturating_from_f32(scroll_delta_x),
590 Px::saturating_from_f32(scroll_delta_y),
591 );
592
593 let child_size = state_guard.child_size;
595 let constrained_target = constrain_position(
596 new_target,
597 &child_size,
598 &input.computed_data,
599 args.vertical,
600 args.horizontal,
601 );
602
603 state_guard.set_target_position(constrained_target);
605
606 if matches!(args.scrollbar_behavior, ScrollBarBehavior::AutoHide) {
608 if args.vertical {
610 let mut scrollbar_state = scrollbar_state_v.write();
611 scrollbar_state.last_scroll_activity = Some(std::time::Instant::now());
612 scrollbar_state.should_be_visible = true;
613 }
614 if args.horizontal {
616 let mut scrollbar_state = scrollbar_state_h.write();
617 scrollbar_state.last_scroll_activity = Some(std::time::Instant::now());
618 scrollbar_state.should_be_visible = true;
619 }
620 }
621 }
622
623 let target = state.read().target_position;
626 let child_size = state.read().child_size;
627 let constrained_position = constrain_position(
628 target,
629 &child_size,
630 &input.computed_data,
631 args.vertical,
632 args.horizontal,
633 );
634 state.write().set_target_position(constrained_position);
635
636 input.cursor_events.clear();
638 }
639
640 state.write().update_scroll_position(args.scroll_smoothing);
642 }));
643
644 child();
646}
647
648fn constrain_axis(pos: Px, child_len: Px, container_len: Px) -> Px {
652 if pos > Px::ZERO {
653 Px::ZERO
654 } else if pos.saturating_add(child_len) < container_len {
655 container_len.saturating_sub(child_len)
656 } else {
657 pos
658 }
659}
660
661fn constrain_position(
662 position: PxPosition,
663 child_size: &ComputedData,
664 container_size: &ComputedData,
665 vertical_scrollable: bool,
666 horizontal_scrollable: bool,
667) -> PxPosition {
668 let x = if horizontal_scrollable {
669 constrain_axis(position.x, child_size.width, container_size.width)
670 } else {
671 Px::ZERO
672 };
673
674 let y = if vertical_scrollable {
675 constrain_axis(position.y, child_size.height, container_size.height)
676 } else {
677 Px::ZERO
678 };
679
680 PxPosition { x, y }
681}