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)]
25pub struct ScrollableArgs {
26 #[builder(default = "tessera_ui::DimensionValue::FILLED")]
29 pub width: tessera_ui::DimensionValue,
30 #[builder(default = "tessera_ui::DimensionValue::FILLED")]
33 pub height: tessera_ui::DimensionValue,
34 #[builder(default = "true")]
37 pub vertical: bool,
38 #[builder(default = "false")]
41 pub horizontal: bool,
42 #[builder(default = "0.05")]
45 pub scroll_smoothing: f32,
46 #[builder(default = "ScrollBarBehavior::AlwaysVisible")]
48 pub scrollbar_behavior: ScrollBarBehavior,
49 #[builder(default = "Color::new(0.0, 0.0, 0.0, 0.1)")]
51 pub scrollbar_track_color: Color,
52 #[builder(default = "Color::new(0.0, 0.0, 0.0, 0.3)")]
54 pub scrollbar_thumb_color: Color,
55 #[builder(default = "Color::new(0.0, 0.0, 0.0, 0.5)")]
57 pub scrollbar_thumb_hover_color: Color,
58 #[builder(default = "ScrollBarLayout::Alongside")]
60 pub scrollbar_layout: ScrollBarLayout,
61}
62
63#[derive(Debug, Clone)]
65pub enum ScrollBarBehavior {
66 AlwaysVisible,
68 AutoHide,
70 Hidden,
72}
73
74#[derive(Debug, Clone)]
76pub enum ScrollBarLayout {
77 Alongside,
79 Overlay,
81}
82
83impl Default for ScrollableArgs {
84 fn default() -> Self {
85 ScrollableArgsBuilder::default()
86 .build()
87 .expect("builder construction failed")
88 }
89}
90
91#[derive(Clone, Default)]
97pub struct ScrollableState {
98 inner: Arc<RwLock<ScrollableStateInner>>,
100 scrollbar_state_v: ScrollBarState,
102 scrollbar_state_h: ScrollBarState,
104}
105
106impl ScrollableState {
107 pub fn new() -> Self {
109 Self::default()
110 }
111
112 pub fn child_position(&self) -> PxPosition {
119 self.inner.read().child_position
120 }
121
122 pub fn visible_size(&self) -> ComputedData {
124 self.inner.read().visible_size
125 }
126
127 pub fn child_size(&self) -> ComputedData {
129 self.inner.read().child_size
130 }
131
132 pub fn override_child_size(&self, size: ComputedData) {
134 self.inner.write().override_child_size = Some(size);
135 }
136}
137
138#[derive(Clone, Debug)]
139struct ScrollableStateInner {
140 child_position: PxPosition,
142 target_position: PxPosition,
144 child_size: ComputedData,
146 visible_size: ComputedData,
148 override_child_size: Option<ComputedData>,
150 last_frame_time: Option<Instant>,
152}
153
154impl Default for ScrollableStateInner {
155 fn default() -> Self {
156 Self::new()
157 }
158}
159
160impl ScrollableStateInner {
161 pub fn new() -> Self {
163 Self {
164 child_position: PxPosition::ZERO,
165 target_position: PxPosition::ZERO,
166 child_size: ComputedData::ZERO,
167 visible_size: ComputedData::ZERO,
168 override_child_size: None,
169 last_frame_time: None,
170 }
171 }
172
173 fn update_scroll_position(&mut self, smoothing: f32) -> bool {
176 let current_time = Instant::now();
177
178 let delta_time = if let Some(last_time) = self.last_frame_time {
180 current_time.duration_since(last_time).as_secs_f32()
181 } else {
182 0.016 };
184
185 self.last_frame_time = Some(current_time);
186
187 let diff_x = self.target_position.x.to_f32() - self.child_position.x.to_f32();
189 let diff_y = self.target_position.y.to_f32() - self.child_position.y.to_f32();
190
191 if diff_x.abs() < 1.0 && diff_y.abs() < 1.0 {
193 if self.child_position != self.target_position {
194 self.child_position = self.target_position;
195 return true;
196 }
197 return false;
198 }
199
200 let mut movement_factor = (1.0 - smoothing) * delta_time * 60.0;
203
204 if movement_factor > 1.0 {
210 movement_factor = 1.0;
211 }
212 let old_position = self.child_position;
213
214 self.child_position = PxPosition {
215 x: Px::saturating_from_f32(self.child_position.x.to_f32() + diff_x * movement_factor),
216 y: Px::saturating_from_f32(self.child_position.y.to_f32() + diff_y * movement_factor),
217 };
218
219 old_position != self.child_position
221 }
222
223 fn set_target_position(&mut self, target: PxPosition) {
225 self.target_position = target;
226 }
227}
228
229#[tessera]
275pub fn scrollable(
276 args: impl Into<ScrollableArgs>,
277 state: ScrollableState,
278 child: impl FnOnce() + Send + Sync + 'static,
279) {
280 let args: ScrollableArgs = args.into();
281
282 let scrollbar_args_v = ScrollBarArgs {
284 total: state.inner.read().child_size.height,
285 visible: state.inner.read().visible_size.height,
286 offset: state.inner.read().child_position.y,
287 thickness: Dp(8.0), state: state.inner.clone(),
289 scrollbar_behavior: args.scrollbar_behavior.clone(),
290 track_color: args.scrollbar_track_color,
291 thumb_color: args.scrollbar_thumb_color,
292 thumb_hover_color: args.scrollbar_thumb_hover_color,
293 };
294
295 let scrollbar_args_h = ScrollBarArgs {
296 total: state.inner.read().child_size.width,
297 visible: state.inner.read().visible_size.width,
298 offset: state.inner.read().child_position.x,
299 thickness: Dp(8.0), state: state.inner.clone(),
301 scrollbar_behavior: args.scrollbar_behavior.clone(),
302 track_color: args.scrollbar_track_color,
303 thumb_color: args.scrollbar_thumb_color,
304 thumb_hover_color: args.scrollbar_thumb_hover_color,
305 };
306
307 match args.scrollbar_layout {
308 ScrollBarLayout::Alongside => {
309 scrollable_with_alongside_scrollbar(
310 state,
311 args,
312 scrollbar_args_v,
313 scrollbar_args_h,
314 child,
315 );
316 }
317 ScrollBarLayout::Overlay => {
318 scrollable_with_overlay_scrollbar(
319 state,
320 args,
321 scrollbar_args_v,
322 scrollbar_args_h,
323 child,
324 );
325 }
326 }
327}
328
329#[tessera]
330fn scrollable_with_alongside_scrollbar(
331 state: ScrollableState,
332 args: ScrollableArgs,
333 scrollbar_args_v: ScrollBarArgs,
334 scrollbar_args_h: ScrollBarArgs,
335 child: impl FnOnce() + Send + Sync + 'static,
336) {
337 scrollable_inner(
338 args.clone(),
339 state.inner.clone(),
340 state.scrollbar_state_v.clone(),
341 state.scrollbar_state_h.clone(),
342 child,
343 );
344
345 if args.vertical {
346 scrollbar_v(scrollbar_args_v, state.scrollbar_state_v.clone());
347 }
348
349 if args.horizontal {
350 scrollbar_h(scrollbar_args_h, state.scrollbar_state_h.clone());
351 }
352
353 measure(Box::new(move |input| {
354 let mut final_size = ComputedData::ZERO;
356 let self_constraint = Constraint {
358 width: args.width,
359 height: args.height,
360 };
361 let mut content_contraint = self_constraint.merge(input.parent_constraint);
362 if args.vertical {
364 let scrollbar_node_id = input.children_ids[1];
365 let size = input.measure_child(scrollbar_node_id, input.parent_constraint)?;
366 content_contraint.width -= size.width;
368 final_size.width += size.width;
370 }
371 if args.horizontal {
372 let scrollbar_node_id = if args.vertical {
373 input.children_ids[2]
374 } else {
375 input.children_ids[1]
376 };
377 let size = input.measure_child(scrollbar_node_id, input.parent_constraint)?;
378 content_contraint.height -= size.height;
379 final_size.height += size.height;
381 }
382 let content_node_id = input.children_ids[0];
384 let content_measurement = input.measure_child(content_node_id, &content_contraint)?;
385 final_size.width += content_measurement.width;
387 final_size.height += content_measurement.height;
388 input.place_child(content_node_id, PxPosition::ZERO);
391 if args.vertical {
393 input.place_child(
394 input.children_ids[1],
395 PxPosition::new(content_measurement.width, Px::ZERO),
396 );
397 }
398 if args.horizontal {
399 let scrollbar_node_id = if args.vertical {
400 input.children_ids[2]
401 } else {
402 input.children_ids[1]
403 };
404 input.place_child(
405 scrollbar_node_id,
406 PxPosition::new(Px::ZERO, content_measurement.height),
407 );
408 }
409 Ok(final_size)
411 }));
412}
413
414#[tessera]
415fn scrollable_with_overlay_scrollbar(
416 state: ScrollableState,
417 args: ScrollableArgs,
418 scrollbar_args_v: ScrollBarArgs,
419 scrollbar_args_h: ScrollBarArgs,
420 child: impl FnOnce() + Send + Sync + 'static,
421) {
422 boxed(
423 BoxedArgsBuilder::default()
424 .width(args.width)
425 .height(args.height)
426 .alignment(Alignment::BottomEnd)
427 .build()
428 .expect("builder construction failed"),
429 |scope| {
430 scope.child({
431 let state = state.clone();
432 let args = args.clone();
433 move || {
434 scrollable_inner(
435 args,
436 state.inner.clone(),
437 state.scrollbar_state_v.clone(),
438 state.scrollbar_state_h.clone(),
439 child,
440 );
441 }
442 });
443 scope.child({
444 let scrollbar_args_v = scrollbar_args_v.clone();
445 let args = args.clone();
446 let state = state.clone();
447 move || {
448 if args.vertical {
449 scrollbar_v(scrollbar_args_v, state.scrollbar_state_v.clone());
450 }
451 }
452 });
453 scope.child({
454 let scrollbar_args_h = scrollbar_args_h.clone();
455 let args = args.clone();
456 let state = state.clone();
457 move || {
458 if args.horizontal {
459 scrollbar_h(scrollbar_args_h, state.scrollbar_state_h.clone());
460 }
461 }
462 });
463 },
464 );
465}
466
467fn clamp_wrap(min: Option<Px>, max: Option<Px>, measure: Px) -> Px {
470 min.unwrap_or(Px(0))
471 .max(measure)
472 .min(max.unwrap_or(Px::MAX))
473}
474
475fn fill_value(min: Option<Px>, max: Option<Px>, measure: Px) -> Px {
476 max.expect("Seems that you are trying to fill an infinite dimension, which is not allowed")
477 .max(measure)
478 .max(min.unwrap_or(Px(0)))
479}
480
481fn resolve_dimension(dim: DimensionValue, measure: Px) -> Px {
482 match dim {
483 DimensionValue::Fixed(v) => v,
484 DimensionValue::Wrap { min, max } => clamp_wrap(min, max, measure),
485 DimensionValue::Fill { min, max } => fill_value(min, max, measure),
486 }
487}
488
489#[tessera]
490fn scrollable_inner(
491 args: impl Into<ScrollableArgs>,
492 state: Arc<RwLock<ScrollableStateInner>>,
493 scrollbar_state_v: ScrollBarState,
494 scrollbar_state_h: ScrollBarState,
495 child: impl FnOnce(),
496) {
497 let args: ScrollableArgs = args.into();
498 {
499 let state = state.clone();
500 measure(Box::new(move |input| {
501 input.enable_clipping();
503 let arg_constraint = Constraint {
505 width: args.width,
506 height: args.height,
507 };
508 let merged_constraint = input.parent_constraint.merge(&arg_constraint);
509 let mut child_constraint = merged_constraint;
511 if args.vertical {
513 child_constraint.height = tessera_ui::DimensionValue::Wrap {
514 min: None,
515 max: None,
516 };
517 }
518 if args.horizontal {
520 child_constraint.width = tessera_ui::DimensionValue::Wrap {
521 min: None,
522 max: None,
523 };
524 }
525 let child_node_id = input.children_ids[0]; let child_measurement = input.measure_child(child_node_id, &child_constraint)?;
528 let current_child_position = {
532 let mut state_guard = state.write();
533 if let Some(override_size) = state_guard.override_child_size.take() {
534 state_guard.child_size = override_size;
535 } else {
536 state_guard.child_size = child_measurement;
537 }
538 state_guard.update_scroll_position(args.scroll_smoothing);
539 state_guard.child_position
540 };
541
542 input.place_child(child_node_id, current_child_position);
544
545 let mut width = resolve_dimension(merged_constraint.width, child_measurement.width);
547 let mut height = resolve_dimension(merged_constraint.height, child_measurement.height);
548
549 if let Some(parent_max_width) = input.parent_constraint.width.get_max() {
550 width = width.min(parent_max_width);
551 }
552 if let Some(parent_max_height) = input.parent_constraint.height.get_max() {
553 height = height.min(parent_max_height);
554 }
555
556 let computed_data = ComputedData { width, height };
558 state.write().visible_size = computed_data;
560 Ok(computed_data)
562 }));
563 }
564
565 input_handler(Box::new(move |input| {
567 let size = input.computed_data;
568 let cursor_pos_option = input.cursor_position_rel;
569 let is_cursor_in_component = cursor_pos_option
570 .map(|pos| is_position_in_component(size, pos))
571 .unwrap_or(false);
572
573 if is_cursor_in_component {
574 for event in input
576 .cursor_events
577 .iter()
578 .filter_map(|event| match &event.content {
579 CursorEventContent::Scroll(event) => Some(event),
580 _ => None,
581 })
582 {
583 let mut state_guard = state.write();
584
585 let scroll_delta_x = event.delta_x;
587 let scroll_delta_y = event.delta_y;
588
589 let current_target = state_guard.target_position;
591 let new_target = current_target.saturating_offset(
592 Px::saturating_from_f32(scroll_delta_x),
593 Px::saturating_from_f32(scroll_delta_y),
594 );
595
596 let child_size = state_guard.child_size;
598 let constrained_target = constrain_position(
599 new_target,
600 &child_size,
601 &input.computed_data,
602 args.vertical,
603 args.horizontal,
604 );
605
606 state_guard.set_target_position(constrained_target);
608
609 if matches!(args.scrollbar_behavior, ScrollBarBehavior::AutoHide) {
611 if args.vertical {
613 let mut scrollbar_state = scrollbar_state_v.write();
614 scrollbar_state.last_scroll_activity = Some(std::time::Instant::now());
615 scrollbar_state.should_be_visible = true;
616 }
617 if args.horizontal {
619 let mut scrollbar_state = scrollbar_state_h.write();
620 scrollbar_state.last_scroll_activity = Some(std::time::Instant::now());
621 scrollbar_state.should_be_visible = true;
622 }
623 }
624 }
625
626 let target = state.read().target_position;
629 let child_size = state.read().child_size;
630 let constrained_position = constrain_position(
631 target,
632 &child_size,
633 &input.computed_data,
634 args.vertical,
635 args.horizontal,
636 );
637 state.write().set_target_position(constrained_position);
638
639 input.cursor_events.clear();
641 }
642
643 state.write().update_scroll_position(args.scroll_smoothing);
645 }));
646
647 child();
649}
650
651fn constrain_axis(pos: Px, child_len: Px, container_len: Px) -> Px {
655 if child_len <= container_len {
656 return Px::ZERO;
657 }
658
659 if pos > Px::ZERO {
660 Px::ZERO
661 } else if pos.saturating_add(child_len) < container_len {
662 container_len.saturating_sub(child_len)
663 } else {
664 pos
665 }
666}
667
668fn constrain_position(
669 position: PxPosition,
670 child_size: &ComputedData,
671 container_size: &ComputedData,
672 vertical_scrollable: bool,
673 horizontal_scrollable: bool,
674) -> PxPosition {
675 let x = if horizontal_scrollable {
676 constrain_axis(position.x, child_size.width, container_size.width)
677 } else {
678 Px::ZERO
679 };
680
681 let y = if vertical_scrollable {
682 constrain_axis(position.y, child_size.height, container_size.height)
683 } else {
684 Px::ZERO
685 };
686
687 PxPosition { x, y }
688}