1#![allow(clippy::needless_range_loop)]
2
3use crate::{
4 emath, epaint, lerp, pass_state, pos2, remap, remap_clamp, vec2, Context, Id, NumExt, Pos2,
5 Rangef, Rect, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b,
6};
7
8#[derive(Clone, Copy, Debug)]
9#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
10struct ScrollingToTarget {
11 animation_time_span: (f64, f64),
12 target_offset: f32,
13}
14
15#[derive(Clone, Copy, Debug)]
16#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
17#[cfg_attr(feature = "serde", serde(default))]
18pub struct State {
19 pub offset: Vec2,
21
22 offset_target: [Option<ScrollingToTarget>; 2],
24
25 show_scroll: Vec2b,
27
28 content_is_too_large: Vec2b,
30
31 scroll_bar_interaction: Vec2b,
33
34 #[cfg_attr(feature = "serde", serde(skip))]
36 vel: Vec2,
37
38 scroll_start_offset_from_top_left: [Option<f32>; 2],
40
41 scroll_stuck_to_end: Vec2b,
45
46 interact_rect: Option<Rect>,
48}
49
50impl Default for State {
51 fn default() -> Self {
52 Self {
53 offset: Vec2::ZERO,
54 offset_target: Default::default(),
55 show_scroll: Vec2b::FALSE,
56 content_is_too_large: Vec2b::FALSE,
57 scroll_bar_interaction: Vec2b::FALSE,
58 vel: Vec2::ZERO,
59 scroll_start_offset_from_top_left: [None; 2],
60 scroll_stuck_to_end: Vec2b::TRUE,
61 interact_rect: None,
62 }
63 }
64}
65
66impl State {
67 pub fn load(ctx: &Context, id: Id) -> Option<Self> {
68 ctx.data_mut(|d| d.get_persisted(id))
69 }
70
71 pub fn store(self, ctx: &Context, id: Id) {
72 ctx.data_mut(|d| d.insert_persisted(id, self));
73 }
74
75 pub fn velocity(&self) -> Vec2 {
77 self.vel
78 }
79}
80
81pub struct ScrollAreaOutput<R> {
82 pub inner: R,
84
85 pub id: Id,
87
88 pub state: State,
90
91 pub content_size: Vec2,
94
95 pub inner_rect: Rect,
97}
98
99#[derive(Clone, Copy, Debug, PartialEq, Eq)]
101#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
102pub enum ScrollBarVisibility {
103 AlwaysHidden,
109
110 VisibleWhenNeeded,
115
116 AlwaysVisible,
119}
120
121impl Default for ScrollBarVisibility {
122 #[inline]
123 fn default() -> Self {
124 Self::VisibleWhenNeeded
125 }
126}
127
128impl ScrollBarVisibility {
129 pub const ALL: [Self; 3] = [
130 Self::AlwaysHidden,
131 Self::VisibleWhenNeeded,
132 Self::AlwaysVisible,
133 ];
134}
135
136#[derive(Clone, Debug)]
168#[must_use = "You should call .show()"]
169pub struct ScrollArea {
170 scroll_enabled: Vec2b,
172
173 auto_shrink: Vec2b,
174 max_size: Vec2,
175 min_scrolled_size: Vec2,
176 scroll_bar_visibility: ScrollBarVisibility,
177 scroll_bar_rect: Option<Rect>,
178 id_salt: Option<Id>,
179 offset_x: Option<f32>,
180 offset_y: Option<f32>,
181
182 scrolling_enabled: bool,
184 drag_to_scroll: bool,
185
186 stick_to_end: Vec2b,
190
191 animated: bool,
193}
194
195impl ScrollArea {
196 #[inline]
198 pub fn horizontal() -> Self {
199 Self::new([true, false])
200 }
201
202 #[inline]
204 pub fn vertical() -> Self {
205 Self::new([false, true])
206 }
207
208 #[inline]
210 pub fn both() -> Self {
211 Self::new([true, true])
212 }
213
214 #[inline]
217 pub fn neither() -> Self {
218 Self::new([false, false])
219 }
220
221 pub fn new(scroll_enabled: impl Into<Vec2b>) -> Self {
224 Self {
225 scroll_enabled: scroll_enabled.into(),
226 auto_shrink: Vec2b::TRUE,
227 max_size: Vec2::INFINITY,
228 min_scrolled_size: Vec2::splat(64.0),
229 scroll_bar_visibility: Default::default(),
230 scroll_bar_rect: None,
231 id_salt: None,
232 offset_x: None,
233 offset_y: None,
234 scrolling_enabled: true,
235 drag_to_scroll: true,
236 stick_to_end: Vec2b::FALSE,
237 animated: true,
238 }
239 }
240
241 #[inline]
247 pub fn max_width(mut self, max_width: f32) -> Self {
248 self.max_size.x = max_width;
249 self
250 }
251
252 #[inline]
258 pub fn max_height(mut self, max_height: f32) -> Self {
259 self.max_size.y = max_height;
260 self
261 }
262
263 #[inline]
270 pub fn min_scrolled_width(mut self, min_scrolled_width: f32) -> Self {
271 self.min_scrolled_size.x = min_scrolled_width;
272 self
273 }
274
275 #[inline]
282 pub fn min_scrolled_height(mut self, min_scrolled_height: f32) -> Self {
283 self.min_scrolled_size.y = min_scrolled_height;
284 self
285 }
286
287 #[inline]
291 pub fn scroll_bar_visibility(mut self, scroll_bar_visibility: ScrollBarVisibility) -> Self {
292 self.scroll_bar_visibility = scroll_bar_visibility;
293 self
294 }
295
296 #[inline]
301 pub fn scroll_bar_rect(mut self, scroll_bar_rect: Rect) -> Self {
302 self.scroll_bar_rect = Some(scroll_bar_rect);
303 self
304 }
305
306 #[inline]
308 #[deprecated = "Renamed id_salt"]
309 pub fn id_source(self, id_salt: impl std::hash::Hash) -> Self {
310 self.id_salt(id_salt)
311 }
312
313 #[inline]
315 pub fn id_salt(mut self, id_salt: impl std::hash::Hash) -> Self {
316 self.id_salt = Some(Id::new(id_salt));
317 self
318 }
319
320 #[inline]
328 pub fn scroll_offset(mut self, offset: Vec2) -> Self {
329 self.offset_x = Some(offset.x);
330 self.offset_y = Some(offset.y);
331 self
332 }
333
334 #[inline]
341 pub fn vertical_scroll_offset(mut self, offset: f32) -> Self {
342 self.offset_y = Some(offset);
343 self
344 }
345
346 #[inline]
353 pub fn horizontal_scroll_offset(mut self, offset: f32) -> Self {
354 self.offset_x = Some(offset);
355 self
356 }
357
358 #[inline]
360 pub fn hscroll(mut self, hscroll: bool) -> Self {
361 self.scroll_enabled[0] = hscroll;
362 self
363 }
364
365 #[inline]
367 pub fn vscroll(mut self, vscroll: bool) -> Self {
368 self.scroll_enabled[1] = vscroll;
369 self
370 }
371
372 #[inline]
376 pub fn scroll(mut self, scroll_enabled: impl Into<Vec2b>) -> Self {
377 self.scroll_enabled = scroll_enabled.into();
378 self
379 }
380
381 #[deprecated = "Renamed to `scroll`"]
383 #[inline]
384 pub fn scroll2(mut self, scroll_enabled: impl Into<Vec2b>) -> Self {
385 self.scroll_enabled = scroll_enabled.into();
386 self
387 }
388
389 #[inline]
399 pub fn enable_scrolling(mut self, enable: bool) -> Self {
400 self.scrolling_enabled = enable;
401 self
402 }
403
404 #[inline]
412 pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self {
413 self.drag_to_scroll = drag_to_scroll;
414 self
415 }
416
417 #[inline]
424 pub fn auto_shrink(mut self, auto_shrink: impl Into<Vec2b>) -> Self {
425 self.auto_shrink = auto_shrink.into();
426 self
427 }
428
429 #[inline]
433 pub fn animated(mut self, animated: bool) -> Self {
434 self.animated = animated;
435 self
436 }
437
438 pub(crate) fn is_any_scroll_enabled(&self) -> bool {
440 self.scroll_enabled[0] || self.scroll_enabled[1]
441 }
442
443 #[inline]
450 pub fn stick_to_right(mut self, stick: bool) -> Self {
451 self.stick_to_end[0] = stick;
452 self
453 }
454
455 #[inline]
462 pub fn stick_to_bottom(mut self, stick: bool) -> Self {
463 self.stick_to_end[1] = stick;
464 self
465 }
466}
467
468struct Prepared {
469 id: Id,
470 state: State,
471
472 auto_shrink: Vec2b,
473
474 scroll_enabled: Vec2b,
476
477 show_bars_factor: Vec2,
479
480 current_bar_use: Vec2,
490
491 scroll_bar_visibility: ScrollBarVisibility,
492 scroll_bar_rect: Option<Rect>,
493
494 inner_rect: Rect,
496
497 content_ui: Ui,
498
499 viewport: Rect,
502
503 scrolling_enabled: bool,
504 stick_to_end: Vec2b,
505
506 saved_scroll_target: [Option<pass_state::ScrollTarget>; 2],
509
510 animated: bool,
511}
512
513impl ScrollArea {
514 fn begin(self, ui: &mut Ui) -> Prepared {
515 let Self {
516 scroll_enabled,
517 auto_shrink,
518 max_size,
519 min_scrolled_size,
520 scroll_bar_visibility,
521 scroll_bar_rect,
522 id_salt,
523 offset_x,
524 offset_y,
525 scrolling_enabled,
526 drag_to_scroll,
527 stick_to_end,
528 animated,
529 } = self;
530
531 let ctx = ui.ctx().clone();
532 let scrolling_enabled = scrolling_enabled && ui.is_enabled();
533
534 let id_salt = id_salt.unwrap_or_else(|| Id::new("scroll_area"));
535 let id = ui.make_persistent_id(id_salt);
536 ctx.check_for_id_clash(
537 id,
538 Rect::from_min_size(ui.available_rect_before_wrap().min, Vec2::ZERO),
539 "ScrollArea",
540 );
541 let mut state = State::load(&ctx, id).unwrap_or_default();
542
543 state.offset.x = offset_x.unwrap_or(state.offset.x);
544 state.offset.y = offset_y.unwrap_or(state.offset.y);
545
546 let show_bars: Vec2b = match scroll_bar_visibility {
547 ScrollBarVisibility::AlwaysHidden => Vec2b::FALSE,
548 ScrollBarVisibility::VisibleWhenNeeded => state.show_scroll,
549 ScrollBarVisibility::AlwaysVisible => scroll_enabled,
550 };
551
552 let show_bars_factor = Vec2::new(
553 ctx.animate_bool_responsive(id.with("h"), show_bars[0]),
554 ctx.animate_bool_responsive(id.with("v"), show_bars[1]),
555 );
556
557 let current_bar_use = show_bars_factor.yx() * ui.spacing().scroll.allocated_width();
558
559 let available_outer = ui.available_rect_before_wrap();
560
561 let outer_size = available_outer.size().at_most(max_size);
562
563 let inner_size = {
564 let mut inner_size = outer_size - current_bar_use;
565
566 for d in 0..2 {
571 if scroll_enabled[d] {
572 inner_size[d] = inner_size[d].max(min_scrolled_size[d]);
573 }
574 }
575 inner_size
576 };
577
578 let inner_rect = Rect::from_min_size(available_outer.min, inner_size);
579
580 let mut content_max_size = inner_size;
581
582 if true {
583 } else {
586 for d in 0..2 {
588 if scroll_enabled[d] {
589 content_max_size[d] = f32::INFINITY;
590 }
591 }
592 }
593
594 let content_max_rect = Rect::from_min_size(inner_rect.min - state.offset, content_max_size);
595 let mut content_ui = ui.new_child(
596 UiBuilder::new()
597 .ui_stack_info(UiStackInfo::new(UiKind::ScrollArea))
598 .max_rect(content_max_rect),
599 );
600
601 {
602 let clip_rect_margin = ui.visuals().clip_rect_margin;
604 let mut content_clip_rect = ui.clip_rect();
605 for d in 0..2 {
606 if scroll_enabled[d] {
607 content_clip_rect.min[d] = inner_rect.min[d] - clip_rect_margin;
608 content_clip_rect.max[d] = inner_rect.max[d] + clip_rect_margin;
609 } else {
610 content_clip_rect.max[d] = ui.clip_rect().max[d] - current_bar_use[d];
612 }
613 }
614 content_clip_rect = content_clip_rect.intersect(ui.clip_rect());
616 content_ui.set_clip_rect(content_clip_rect);
617 }
618
619 let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size);
620 let dt = ui.input(|i| i.stable_dt).at_most(0.1);
621
622 if (scrolling_enabled && drag_to_scroll)
623 && (state.content_is_too_large[0] || state.content_is_too_large[1])
624 {
625 let content_response_option = state
629 .interact_rect
630 .map(|rect| ui.interact(rect, id.with("area"), Sense::drag()));
631
632 if content_response_option
633 .as_ref()
634 .is_some_and(|response| response.dragged())
635 {
636 for d in 0..2 {
637 if scroll_enabled[d] {
638 ui.input(|input| {
639 state.offset[d] -= input.pointer.delta()[d];
640 });
641 state.scroll_stuck_to_end[d] = false;
642 state.offset_target[d] = None;
643 }
644 }
645 } else {
646 if content_response_option
648 .as_ref()
649 .is_some_and(|response| response.drag_stopped())
650 {
651 state.vel =
652 scroll_enabled.to_vec2() * ui.input(|input| input.pointer.velocity());
653 }
654 for d in 0..2 {
655 let stop_speed = 20.0; let friction_coeff = 1000.0; let friction = friction_coeff * dt;
660 if friction > state.vel[d].abs() || state.vel[d].abs() < stop_speed {
661 state.vel[d] = 0.0;
662 } else {
663 state.vel[d] -= friction * state.vel[d].signum();
664 state.offset[d] -= state.vel[d] * dt;
667 ctx.request_repaint();
668 }
669 }
670 }
671 }
672
673 for d in 0..2 {
676 if let Some(scroll_target) = state.offset_target[d] {
677 state.vel[d] = 0.0;
678
679 if (state.offset[d] - scroll_target.target_offset).abs() < 1.0 {
680 state.offset[d] = scroll_target.target_offset;
682 state.offset_target[d] = None;
683 } else {
684 let t = emath::interpolation_factor(
686 scroll_target.animation_time_span,
687 ui.input(|i| i.time),
688 dt,
689 emath::ease_in_ease_out,
690 );
691 if t < 1.0 {
692 state.offset[d] =
693 emath::lerp(state.offset[d]..=scroll_target.target_offset, t);
694 ctx.request_repaint();
695 } else {
696 state.offset[d] = scroll_target.target_offset;
698 state.offset_target[d] = None;
699 }
700 }
701 }
702 }
703
704 let saved_scroll_target = content_ui
705 .ctx()
706 .pass_state_mut(|state| std::mem::take(&mut state.scroll_target));
707
708 Prepared {
709 id,
710 state,
711 auto_shrink,
712 scroll_enabled,
713 show_bars_factor,
714 current_bar_use,
715 scroll_bar_visibility,
716 scroll_bar_rect,
717 inner_rect,
718 content_ui,
719 viewport,
720 scrolling_enabled,
721 stick_to_end,
722 saved_scroll_target,
723 animated,
724 }
725 }
726
727 pub fn show<R>(
731 self,
732 ui: &mut Ui,
733 add_contents: impl FnOnce(&mut Ui) -> R,
734 ) -> ScrollAreaOutput<R> {
735 self.show_viewport_dyn(ui, Box::new(|ui, _viewport| add_contents(ui)))
736 }
737
738 pub fn show_rows<R>(
755 self,
756 ui: &mut Ui,
757 row_height_sans_spacing: f32,
758 total_rows: usize,
759 add_contents: impl FnOnce(&mut Ui, std::ops::Range<usize>) -> R,
760 ) -> ScrollAreaOutput<R> {
761 let spacing = ui.spacing().item_spacing;
762 let row_height_with_spacing = row_height_sans_spacing + spacing.y;
763 self.show_viewport(ui, |ui, viewport| {
764 ui.set_height((row_height_with_spacing * total_rows as f32 - spacing.y).at_least(0.0));
765
766 let mut min_row = (viewport.min.y / row_height_with_spacing).floor() as usize;
767 let mut max_row = (viewport.max.y / row_height_with_spacing).ceil() as usize + 1;
768 if max_row > total_rows {
769 let diff = max_row.saturating_sub(min_row);
770 max_row = total_rows;
771 min_row = total_rows.saturating_sub(diff);
772 }
773
774 let y_min = ui.max_rect().top() + min_row as f32 * row_height_with_spacing;
775 let y_max = ui.max_rect().top() + max_row as f32 * row_height_with_spacing;
776
777 let rect = Rect::from_x_y_ranges(ui.max_rect().x_range(), y_min..=y_max);
778
779 ui.allocate_new_ui(UiBuilder::new().max_rect(rect), |viewport_ui| {
780 viewport_ui.skip_ahead_auto_ids(min_row); add_contents(viewport_ui, min_row..max_row)
782 })
783 .inner
784 })
785 }
786
787 pub fn show_viewport<R>(
792 self,
793 ui: &mut Ui,
794 add_contents: impl FnOnce(&mut Ui, Rect) -> R,
795 ) -> ScrollAreaOutput<R> {
796 self.show_viewport_dyn(ui, Box::new(add_contents))
797 }
798
799 fn show_viewport_dyn<'c, R>(
800 self,
801 ui: &mut Ui,
802 add_contents: Box<dyn FnOnce(&mut Ui, Rect) -> R + 'c>,
803 ) -> ScrollAreaOutput<R> {
804 let mut prepared = self.begin(ui);
805 let id = prepared.id;
806 let inner_rect = prepared.inner_rect;
807 let inner = add_contents(&mut prepared.content_ui, prepared.viewport);
808 let (content_size, state) = prepared.end(ui);
809 ScrollAreaOutput {
810 inner,
811 id,
812 state,
813 content_size,
814 inner_rect,
815 }
816 }
817}
818
819impl Prepared {
820 fn end(self, ui: &mut Ui) -> (Vec2, State) {
822 let Self {
823 id,
824 mut state,
825 inner_rect,
826 auto_shrink,
827 scroll_enabled,
828 mut show_bars_factor,
829 current_bar_use,
830 scroll_bar_visibility,
831 scroll_bar_rect,
832 content_ui,
833 viewport: _,
834 scrolling_enabled,
835 stick_to_end,
836 saved_scroll_target,
837 animated,
838 } = self;
839
840 let content_size = content_ui.min_size();
841
842 let scroll_delta = content_ui
843 .ctx()
844 .pass_state_mut(|state| std::mem::take(&mut state.scroll_delta));
845
846 for d in 0..2 {
847 let mut delta = -scroll_delta.0[d];
849 let mut animation = scroll_delta.1;
850
851 let scroll_target = content_ui
854 .ctx()
855 .pass_state_mut(|state| state.scroll_target[d].take());
856
857 if scroll_enabled[d] {
858 if let Some(target) = scroll_target {
859 let pass_state::ScrollTarget {
860 range,
861 align,
862 animation: animation_update,
863 } = target;
864 let min = content_ui.min_rect().min[d];
865 let clip_rect = content_ui.clip_rect();
866 let visible_range = min..=min + clip_rect.size()[d];
867 let (start, end) = (range.min, range.max);
868 let clip_start = clip_rect.min[d];
869 let clip_end = clip_rect.max[d];
870 let mut spacing = content_ui.spacing().item_spacing[d];
871
872 let delta_update = if let Some(align) = align {
873 let center_factor = align.to_factor();
874
875 let offset =
876 lerp(range, center_factor) - lerp(visible_range, center_factor);
877
878 spacing *= remap(center_factor, 0.0..=1.0, -1.0..=1.0);
880
881 offset + spacing - state.offset[d]
882 } else if start < clip_start && end < clip_end {
883 -(clip_start - start + spacing).min(clip_end - end - spacing)
884 } else if end > clip_end && start > clip_start {
885 (end - clip_end + spacing).min(start - clip_start - spacing)
886 } else {
887 0.0
889 };
890
891 delta += delta_update;
892 animation = animation_update;
893 };
894
895 if delta != 0.0 {
896 let target_offset = state.offset[d] + delta;
897
898 if !animated {
899 state.offset[d] = target_offset;
900 } else if let Some(animation) = &mut state.offset_target[d] {
901 animation.target_offset = target_offset;
904 } else {
905 let now = ui.input(|i| i.time);
907 let animation_duration = (delta.abs() / animation.points_per_second)
908 .clamp(animation.duration.min, animation.duration.max);
909 state.offset_target[d] = Some(ScrollingToTarget {
910 animation_time_span: (now, now + animation_duration as f64),
911 target_offset,
912 });
913 }
914 ui.ctx().request_repaint();
915 }
916 }
917 }
918
919 ui.ctx().pass_state_mut(|state| {
921 for d in 0..2 {
922 if saved_scroll_target[d].is_some() {
923 state.scroll_target[d] = saved_scroll_target[d].clone();
924 };
925 }
926 });
927
928 let inner_rect = {
929 let mut inner_size = inner_rect.size();
931
932 for d in 0..2 {
933 inner_size[d] = match (scroll_enabled[d], auto_shrink[d]) {
934 (true, true) => inner_size[d].min(content_size[d]), (true, false) => inner_size[d], (false, true) => content_size[d], (false, false) => inner_size[d].max(content_size[d]), };
939 }
940
941 Rect::from_min_size(inner_rect.min, inner_size)
942 };
943
944 let outer_rect = Rect::from_min_size(inner_rect.min, inner_rect.size() + current_bar_use);
945
946 let content_is_too_large = Vec2b::new(
947 scroll_enabled[0] && inner_rect.width() < content_size.x,
948 scroll_enabled[1] && inner_rect.height() < content_size.y,
949 );
950
951 let max_offset = content_size - inner_rect.size();
952 let is_hovering_outer_rect = ui.rect_contains_pointer(outer_rect);
953 if scrolling_enabled && is_hovering_outer_rect {
954 let always_scroll_enabled_direction = ui.style().always_scroll_the_only_direction
955 && scroll_enabled[0] != scroll_enabled[1];
956 for d in 0..2 {
957 if scroll_enabled[d] {
958 let scroll_delta = ui.ctx().input_mut(|input| {
959 if always_scroll_enabled_direction {
960 input.smooth_scroll_delta[0] + input.smooth_scroll_delta[1]
962 } else {
963 input.smooth_scroll_delta[d]
964 }
965 });
966
967 let scrolling_up = state.offset[d] > 0.0 && scroll_delta > 0.0;
968 let scrolling_down = state.offset[d] < max_offset[d] && scroll_delta < 0.0;
969
970 if scrolling_up || scrolling_down {
971 state.offset[d] -= scroll_delta;
972
973 ui.ctx().input_mut(|input| {
975 if always_scroll_enabled_direction {
976 input.smooth_scroll_delta[0] = 0.0;
977 input.smooth_scroll_delta[1] = 0.0;
978 } else {
979 input.smooth_scroll_delta[d] = 0.0;
980 }
981 });
982
983 state.scroll_stuck_to_end[d] = false;
984 state.offset_target[d] = None;
985 }
986 }
987 }
988 }
989
990 let show_scroll_this_frame = match scroll_bar_visibility {
991 ScrollBarVisibility::AlwaysHidden => Vec2b::FALSE,
992 ScrollBarVisibility::VisibleWhenNeeded => content_is_too_large,
993 ScrollBarVisibility::AlwaysVisible => scroll_enabled,
994 };
995
996 if show_scroll_this_frame[0] && show_bars_factor.x <= 0.0 {
998 show_bars_factor.x = ui.ctx().animate_bool_responsive(id.with("h"), true);
999 }
1000 if show_scroll_this_frame[1] && show_bars_factor.y <= 0.0 {
1001 show_bars_factor.y = ui.ctx().animate_bool_responsive(id.with("v"), true);
1002 }
1003
1004 let scroll_style = ui.spacing().scroll;
1005
1006 let scroll_bar_rect = scroll_bar_rect.unwrap_or(inner_rect);
1008 for d in 0..2 {
1009 if stick_to_end[d] && state.scroll_stuck_to_end[d] {
1011 state.offset[d] = content_size[d] - inner_rect.size()[d];
1012 }
1013
1014 let show_factor = show_bars_factor[d];
1015 if show_factor == 0.0 {
1016 state.scroll_bar_interaction[d] = false;
1017 continue;
1018 }
1019
1020 let inner_margin = show_factor * scroll_style.bar_inner_margin;
1022 let outer_margin = show_factor * scroll_style.bar_outer_margin;
1023
1024 let mut cross = if scroll_style.floating {
1027 let max_bar_rect = if d == 0 {
1030 outer_rect.with_min_y(outer_rect.max.y - outer_margin - scroll_style.bar_width)
1031 } else {
1032 outer_rect.with_min_x(outer_rect.max.x - outer_margin - scroll_style.bar_width)
1033 };
1034
1035 let is_hovering_bar_area = is_hovering_outer_rect
1036 && ui.rect_contains_pointer(max_bar_rect)
1037 || state.scroll_bar_interaction[d];
1038
1039 let is_hovering_bar_area_t = ui
1040 .ctx()
1041 .animate_bool_responsive(id.with((d, "bar_hover")), is_hovering_bar_area);
1042
1043 let width = show_factor
1044 * lerp(
1045 scroll_style.floating_width..=scroll_style.bar_width,
1046 is_hovering_bar_area_t,
1047 );
1048
1049 let max_cross = outer_rect.max[1 - d] - outer_margin;
1050 let min_cross = max_cross - width;
1051 Rangef::new(min_cross, max_cross)
1052 } else {
1053 let min_cross = inner_rect.max[1 - d] + inner_margin;
1054 let max_cross = outer_rect.max[1 - d] - outer_margin;
1055 Rangef::new(min_cross, max_cross)
1056 };
1057
1058 if ui.clip_rect().max[1 - d] < cross.max + outer_margin {
1059 let width = cross.max - cross.min;
1069 cross.max = ui.clip_rect().max[1 - d] - outer_margin;
1070 cross.min = cross.max - width;
1071 }
1072
1073 let outer_scroll_bar_rect = if d == 0 {
1074 Rect::from_min_max(
1075 pos2(scroll_bar_rect.left(), cross.min),
1076 pos2(scroll_bar_rect.right(), cross.max),
1077 )
1078 } else {
1079 Rect::from_min_max(
1080 pos2(cross.min, scroll_bar_rect.top()),
1081 pos2(cross.max, scroll_bar_rect.bottom()),
1082 )
1083 };
1084
1085 let from_content = |content| {
1086 remap_clamp(
1087 content,
1088 0.0..=content_size[d],
1089 scroll_bar_rect.min[d]..=scroll_bar_rect.max[d],
1090 )
1091 };
1092
1093 let handle_rect = if d == 0 {
1094 Rect::from_min_max(
1095 pos2(from_content(state.offset.x), cross.min),
1096 pos2(from_content(state.offset.x + inner_rect.width()), cross.max),
1097 )
1098 } else {
1099 Rect::from_min_max(
1100 pos2(cross.min, from_content(state.offset.y)),
1101 pos2(
1102 cross.max,
1103 from_content(state.offset.y + inner_rect.height()),
1104 ),
1105 )
1106 };
1107
1108 let interact_id = id.with(d);
1109 let sense = if self.scrolling_enabled {
1110 Sense::click_and_drag()
1111 } else {
1112 Sense::hover()
1113 };
1114 let response = ui.interact(outer_scroll_bar_rect, interact_id, sense);
1115
1116 state.scroll_bar_interaction[d] = response.hovered() || response.dragged();
1117
1118 if let Some(pointer_pos) = response.interact_pointer_pos() {
1119 let scroll_start_offset_from_top_left = state.scroll_start_offset_from_top_left[d]
1120 .get_or_insert_with(|| {
1121 if handle_rect.contains(pointer_pos) {
1122 pointer_pos[d] - handle_rect.min[d]
1123 } else {
1124 let handle_top_pos_at_bottom =
1125 scroll_bar_rect.max[d] - handle_rect.size()[d];
1126 let new_handle_top_pos = (pointer_pos[d] - handle_rect.size()[d] / 2.0)
1128 .clamp(scroll_bar_rect.min[d], handle_top_pos_at_bottom);
1129 pointer_pos[d] - new_handle_top_pos
1130 }
1131 });
1132
1133 let new_handle_top = pointer_pos[d] - *scroll_start_offset_from_top_left;
1134 state.offset[d] = remap(
1135 new_handle_top,
1136 scroll_bar_rect.min[d]..=scroll_bar_rect.max[d],
1137 0.0..=content_size[d],
1138 );
1139
1140 state.scroll_stuck_to_end[d] = false;
1142 state.offset_target[d] = None;
1143 } else {
1144 state.scroll_start_offset_from_top_left[d] = None;
1145 }
1146
1147 let unbounded_offset = state.offset[d];
1148 state.offset[d] = state.offset[d].max(0.0);
1149 state.offset[d] = state.offset[d].min(max_offset[d]);
1150
1151 if state.offset[d] != unbounded_offset {
1152 state.vel[d] = 0.0;
1153 }
1154
1155 if ui.is_rect_visible(outer_scroll_bar_rect) {
1156 let mut handle_rect = if d == 0 {
1158 Rect::from_min_max(
1159 pos2(from_content(state.offset.x), cross.min),
1160 pos2(from_content(state.offset.x + inner_rect.width()), cross.max),
1161 )
1162 } else {
1163 Rect::from_min_max(
1164 pos2(cross.min, from_content(state.offset.y)),
1165 pos2(
1166 cross.max,
1167 from_content(state.offset.y + inner_rect.height()),
1168 ),
1169 )
1170 };
1171 let min_handle_size = scroll_style.handle_min_length;
1172 if handle_rect.size()[d] < min_handle_size {
1173 handle_rect = Rect::from_center_size(
1174 handle_rect.center(),
1175 if d == 0 {
1176 vec2(min_handle_size, handle_rect.size().y)
1177 } else {
1178 vec2(handle_rect.size().x, min_handle_size)
1179 },
1180 );
1181 }
1182
1183 let visuals = if scrolling_enabled {
1184 let is_hovering_handle = response.hovered()
1187 && ui.input(|i| {
1188 i.pointer
1189 .latest_pos()
1190 .is_some_and(|p| handle_rect.contains(p))
1191 });
1192 let visuals = ui.visuals();
1193 if response.is_pointer_button_down_on() {
1194 &visuals.widgets.active
1195 } else if is_hovering_handle {
1196 &visuals.widgets.hovered
1197 } else {
1198 &visuals.widgets.inactive
1199 }
1200 } else {
1201 &ui.visuals().widgets.inactive
1202 };
1203
1204 let handle_opacity = if scroll_style.floating {
1205 if response.hovered() || response.dragged() {
1206 scroll_style.interact_handle_opacity
1207 } else {
1208 let is_hovering_outer_rect_t = ui.ctx().animate_bool_responsive(
1209 id.with((d, "is_hovering_outer_rect")),
1210 is_hovering_outer_rect,
1211 );
1212 lerp(
1213 scroll_style.dormant_handle_opacity
1214 ..=scroll_style.active_handle_opacity,
1215 is_hovering_outer_rect_t,
1216 )
1217 }
1218 } else {
1219 1.0
1220 };
1221
1222 let background_opacity = if scroll_style.floating {
1223 if response.hovered() || response.dragged() {
1224 scroll_style.interact_background_opacity
1225 } else if is_hovering_outer_rect {
1226 scroll_style.active_background_opacity
1227 } else {
1228 scroll_style.dormant_background_opacity
1229 }
1230 } else {
1231 1.0
1232 };
1233
1234 let handle_color = if scroll_style.foreground_color {
1235 visuals.fg_stroke.color
1236 } else {
1237 visuals.bg_fill
1238 };
1239
1240 ui.painter().add(epaint::Shape::rect_filled(
1242 outer_scroll_bar_rect,
1243 visuals.corner_radius,
1244 ui.visuals()
1245 .extreme_bg_color
1246 .gamma_multiply(background_opacity),
1247 ));
1248
1249 ui.painter().add(epaint::Shape::rect_filled(
1251 handle_rect,
1252 visuals.corner_radius,
1253 handle_color.gamma_multiply(handle_opacity),
1254 ));
1255 }
1256 }
1257
1258 ui.advance_cursor_after_rect(outer_rect);
1259
1260 if show_scroll_this_frame != state.show_scroll {
1261 ui.ctx().request_repaint();
1262 }
1263
1264 let available_offset = content_size - inner_rect.size();
1265 state.offset = state.offset.min(available_offset);
1266 state.offset = state.offset.max(Vec2::ZERO);
1267
1268 state.scroll_stuck_to_end = Vec2b::new(
1274 (state.offset[0] == available_offset[0])
1275 || (self.stick_to_end[0] && available_offset[0] < 0.0),
1276 (state.offset[1] == available_offset[1])
1277 || (self.stick_to_end[1] && available_offset[1] < 0.0),
1278 );
1279
1280 state.show_scroll = show_scroll_this_frame;
1281 state.content_is_too_large = content_is_too_large;
1282 state.interact_rect = Some(inner_rect);
1283
1284 state.store(ui.ctx(), id);
1285
1286 (content_size, state)
1287 }
1288}