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)]
165#[must_use = "You should call .show()"]
166pub struct ScrollArea {
167 scroll_enabled: Vec2b,
169
170 auto_shrink: Vec2b,
171 max_size: Vec2,
172 min_scrolled_size: Vec2,
173 scroll_bar_visibility: ScrollBarVisibility,
174 scroll_bar_rect: Option<Rect>,
175 id_salt: Option<Id>,
176 offset_x: Option<f32>,
177 offset_y: Option<f32>,
178
179 scrolling_enabled: bool,
181 drag_to_scroll: bool,
182
183 stick_to_end: Vec2b,
187
188 animated: bool,
190}
191
192impl ScrollArea {
193 #[inline]
195 pub fn horizontal() -> Self {
196 Self::new([true, false])
197 }
198
199 #[inline]
201 pub fn vertical() -> Self {
202 Self::new([false, true])
203 }
204
205 #[inline]
207 pub fn both() -> Self {
208 Self::new([true, true])
209 }
210
211 #[inline]
214 pub fn neither() -> Self {
215 Self::new([false, false])
216 }
217
218 pub fn new(scroll_enabled: impl Into<Vec2b>) -> Self {
221 Self {
222 scroll_enabled: scroll_enabled.into(),
223 auto_shrink: Vec2b::TRUE,
224 max_size: Vec2::INFINITY,
225 min_scrolled_size: Vec2::splat(64.0),
226 scroll_bar_visibility: Default::default(),
227 scroll_bar_rect: None,
228 id_salt: None,
229 offset_x: None,
230 offset_y: None,
231 scrolling_enabled: true,
232 drag_to_scroll: true,
233 stick_to_end: Vec2b::FALSE,
234 animated: true,
235 }
236 }
237
238 #[inline]
244 pub fn max_width(mut self, max_width: f32) -> Self {
245 self.max_size.x = max_width;
246 self
247 }
248
249 #[inline]
255 pub fn max_height(mut self, max_height: f32) -> Self {
256 self.max_size.y = max_height;
257 self
258 }
259
260 #[inline]
267 pub fn min_scrolled_width(mut self, min_scrolled_width: f32) -> Self {
268 self.min_scrolled_size.x = min_scrolled_width;
269 self
270 }
271
272 #[inline]
279 pub fn min_scrolled_height(mut self, min_scrolled_height: f32) -> Self {
280 self.min_scrolled_size.y = min_scrolled_height;
281 self
282 }
283
284 #[inline]
288 pub fn scroll_bar_visibility(mut self, scroll_bar_visibility: ScrollBarVisibility) -> Self {
289 self.scroll_bar_visibility = scroll_bar_visibility;
290 self
291 }
292
293 #[inline]
298 pub fn scroll_bar_rect(mut self, scroll_bar_rect: Rect) -> Self {
299 self.scroll_bar_rect = Some(scroll_bar_rect);
300 self
301 }
302
303 #[inline]
305 #[deprecated = "Renamed id_salt"]
306 pub fn id_source(self, id_salt: impl std::hash::Hash) -> Self {
307 self.id_salt(id_salt)
308 }
309
310 #[inline]
312 pub fn id_salt(mut self, id_salt: impl std::hash::Hash) -> Self {
313 self.id_salt = Some(Id::new(id_salt));
314 self
315 }
316
317 #[inline]
325 pub fn scroll_offset(mut self, offset: Vec2) -> Self {
326 self.offset_x = Some(offset.x);
327 self.offset_y = Some(offset.y);
328 self
329 }
330
331 #[inline]
338 pub fn vertical_scroll_offset(mut self, offset: f32) -> Self {
339 self.offset_y = Some(offset);
340 self
341 }
342
343 #[inline]
350 pub fn horizontal_scroll_offset(mut self, offset: f32) -> Self {
351 self.offset_x = Some(offset);
352 self
353 }
354
355 #[inline]
357 pub fn hscroll(mut self, hscroll: bool) -> Self {
358 self.scroll_enabled[0] = hscroll;
359 self
360 }
361
362 #[inline]
364 pub fn vscroll(mut self, vscroll: bool) -> Self {
365 self.scroll_enabled[1] = vscroll;
366 self
367 }
368
369 #[inline]
373 pub fn scroll(mut self, scroll_enabled: impl Into<Vec2b>) -> Self {
374 self.scroll_enabled = scroll_enabled.into();
375 self
376 }
377
378 #[deprecated = "Renamed to `scroll`"]
380 #[inline]
381 pub fn scroll2(mut self, scroll_enabled: impl Into<Vec2b>) -> Self {
382 self.scroll_enabled = scroll_enabled.into();
383 self
384 }
385
386 #[inline]
396 pub fn enable_scrolling(mut self, enable: bool) -> Self {
397 self.scrolling_enabled = enable;
398 self
399 }
400
401 #[inline]
409 pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self {
410 self.drag_to_scroll = drag_to_scroll;
411 self
412 }
413
414 #[inline]
421 pub fn auto_shrink(mut self, auto_shrink: impl Into<Vec2b>) -> Self {
422 self.auto_shrink = auto_shrink.into();
423 self
424 }
425
426 #[inline]
430 pub fn animated(mut self, animated: bool) -> Self {
431 self.animated = animated;
432 self
433 }
434
435 pub(crate) fn is_any_scroll_enabled(&self) -> bool {
437 self.scroll_enabled[0] || self.scroll_enabled[1]
438 }
439
440 #[inline]
447 pub fn stick_to_right(mut self, stick: bool) -> Self {
448 self.stick_to_end[0] = stick;
449 self
450 }
451
452 #[inline]
459 pub fn stick_to_bottom(mut self, stick: bool) -> Self {
460 self.stick_to_end[1] = stick;
461 self
462 }
463}
464
465struct Prepared {
466 id: Id,
467 state: State,
468
469 auto_shrink: Vec2b,
470
471 scroll_enabled: Vec2b,
473
474 show_bars_factor: Vec2,
476
477 current_bar_use: Vec2,
487
488 scroll_bar_visibility: ScrollBarVisibility,
489 scroll_bar_rect: Option<Rect>,
490
491 inner_rect: Rect,
493
494 content_ui: Ui,
495
496 viewport: Rect,
499
500 scrolling_enabled: bool,
501 stick_to_end: Vec2b,
502
503 saved_scroll_target: [Option<pass_state::ScrollTarget>; 2],
506
507 animated: bool,
508}
509
510impl ScrollArea {
511 fn begin(self, ui: &mut Ui) -> Prepared {
512 let Self {
513 scroll_enabled,
514 auto_shrink,
515 max_size,
516 min_scrolled_size,
517 scroll_bar_visibility,
518 scroll_bar_rect,
519 id_salt,
520 offset_x,
521 offset_y,
522 scrolling_enabled,
523 drag_to_scroll,
524 stick_to_end,
525 animated,
526 } = self;
527
528 let ctx = ui.ctx().clone();
529 let scrolling_enabled = scrolling_enabled && ui.is_enabled();
530
531 let id_salt = id_salt.unwrap_or_else(|| Id::new("scroll_area"));
532 let id = ui.make_persistent_id(id_salt);
533 ctx.check_for_id_clash(
534 id,
535 Rect::from_min_size(ui.available_rect_before_wrap().min, Vec2::ZERO),
536 "ScrollArea",
537 );
538 let mut state = State::load(&ctx, id).unwrap_or_default();
539
540 state.offset.x = offset_x.unwrap_or(state.offset.x);
541 state.offset.y = offset_y.unwrap_or(state.offset.y);
542
543 let show_bars: Vec2b = match scroll_bar_visibility {
544 ScrollBarVisibility::AlwaysHidden => Vec2b::FALSE,
545 ScrollBarVisibility::VisibleWhenNeeded => state.show_scroll,
546 ScrollBarVisibility::AlwaysVisible => scroll_enabled,
547 };
548
549 let show_bars_factor = Vec2::new(
550 ctx.animate_bool_responsive(id.with("h"), show_bars[0]),
551 ctx.animate_bool_responsive(id.with("v"), show_bars[1]),
552 );
553
554 let current_bar_use = show_bars_factor.yx() * ui.spacing().scroll.allocated_width();
555
556 let available_outer = ui.available_rect_before_wrap();
557
558 let outer_size = available_outer.size().at_most(max_size);
559
560 let inner_size = {
561 let mut inner_size = outer_size - current_bar_use;
562
563 for d in 0..2 {
568 if scroll_enabled[d] {
569 inner_size[d] = inner_size[d].max(min_scrolled_size[d]);
570 }
571 }
572 inner_size
573 };
574
575 let inner_rect = Rect::from_min_size(available_outer.min, inner_size);
576
577 let mut content_max_size = inner_size;
578
579 if true {
580 } else {
583 for d in 0..2 {
585 if scroll_enabled[d] {
586 content_max_size[d] = f32::INFINITY;
587 }
588 }
589 }
590
591 let content_max_rect = Rect::from_min_size(inner_rect.min - state.offset, content_max_size);
592 let mut content_ui = ui.new_child(
593 UiBuilder::new()
594 .ui_stack_info(UiStackInfo::new(UiKind::ScrollArea))
595 .max_rect(content_max_rect),
596 );
597
598 {
599 let clip_rect_margin = ui.visuals().clip_rect_margin;
601 let mut content_clip_rect = ui.clip_rect();
602 for d in 0..2 {
603 if scroll_enabled[d] {
604 content_clip_rect.min[d] = inner_rect.min[d] - clip_rect_margin;
605 content_clip_rect.max[d] = inner_rect.max[d] + clip_rect_margin;
606 } else {
607 content_clip_rect.max[d] = ui.clip_rect().max[d] - current_bar_use[d];
609 }
610 }
611 content_clip_rect = content_clip_rect.intersect(ui.clip_rect());
613 content_ui.set_clip_rect(content_clip_rect);
614 }
615
616 let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size);
617 let dt = ui.input(|i| i.stable_dt).at_most(0.1);
618
619 if (scrolling_enabled && drag_to_scroll)
620 && (state.content_is_too_large[0] || state.content_is_too_large[1])
621 {
622 let content_response_option = state
626 .interact_rect
627 .map(|rect| ui.interact(rect, id.with("area"), Sense::drag()));
628
629 if content_response_option
630 .as_ref()
631 .is_some_and(|response| response.dragged())
632 {
633 for d in 0..2 {
634 if scroll_enabled[d] {
635 ui.input(|input| {
636 state.offset[d] -= input.pointer.delta()[d];
637 });
638 state.scroll_stuck_to_end[d] = false;
639 state.offset_target[d] = None;
640 }
641 }
642 } else {
643 if content_response_option
645 .as_ref()
646 .is_some_and(|response| response.drag_stopped())
647 {
648 state.vel =
649 scroll_enabled.to_vec2() * ui.input(|input| input.pointer.velocity());
650 }
651 for d in 0..2 {
652 let stop_speed = 20.0; let friction_coeff = 1000.0; let friction = friction_coeff * dt;
657 if friction > state.vel[d].abs() || state.vel[d].abs() < stop_speed {
658 state.vel[d] = 0.0;
659 } else {
660 state.vel[d] -= friction * state.vel[d].signum();
661 state.offset[d] -= state.vel[d] * dt;
664 ctx.request_repaint();
665 }
666 }
667 }
668 }
669
670 for d in 0..2 {
673 if let Some(scroll_target) = state.offset_target[d] {
674 state.vel[d] = 0.0;
675
676 if (state.offset[d] - scroll_target.target_offset).abs() < 1.0 {
677 state.offset[d] = scroll_target.target_offset;
679 state.offset_target[d] = None;
680 } else {
681 let t = emath::interpolation_factor(
683 scroll_target.animation_time_span,
684 ui.input(|i| i.time),
685 dt,
686 emath::ease_in_ease_out,
687 );
688 if t < 1.0 {
689 state.offset[d] =
690 emath::lerp(state.offset[d]..=scroll_target.target_offset, t);
691 ctx.request_repaint();
692 } else {
693 state.offset[d] = scroll_target.target_offset;
695 state.offset_target[d] = None;
696 }
697 }
698 }
699 }
700
701 let saved_scroll_target = content_ui
702 .ctx()
703 .pass_state_mut(|state| std::mem::take(&mut state.scroll_target));
704
705 Prepared {
706 id,
707 state,
708 auto_shrink,
709 scroll_enabled,
710 show_bars_factor,
711 current_bar_use,
712 scroll_bar_visibility,
713 scroll_bar_rect,
714 inner_rect,
715 content_ui,
716 viewport,
717 scrolling_enabled,
718 stick_to_end,
719 saved_scroll_target,
720 animated,
721 }
722 }
723
724 pub fn show<R>(
728 self,
729 ui: &mut Ui,
730 add_contents: impl FnOnce(&mut Ui) -> R,
731 ) -> ScrollAreaOutput<R> {
732 self.show_viewport_dyn(ui, Box::new(|ui, _viewport| add_contents(ui)))
733 }
734
735 pub fn show_rows<R>(
752 self,
753 ui: &mut Ui,
754 row_height_sans_spacing: f32,
755 total_rows: usize,
756 add_contents: impl FnOnce(&mut Ui, std::ops::Range<usize>) -> R,
757 ) -> ScrollAreaOutput<R> {
758 let spacing = ui.spacing().item_spacing;
759 let row_height_with_spacing = row_height_sans_spacing + spacing.y;
760 self.show_viewport(ui, |ui, viewport| {
761 ui.set_height((row_height_with_spacing * total_rows as f32 - spacing.y).at_least(0.0));
762
763 let mut min_row = (viewport.min.y / row_height_with_spacing).floor() as usize;
764 let mut max_row = (viewport.max.y / row_height_with_spacing).ceil() as usize + 1;
765 if max_row > total_rows {
766 let diff = max_row.saturating_sub(min_row);
767 max_row = total_rows;
768 min_row = total_rows.saturating_sub(diff);
769 }
770
771 let y_min = ui.max_rect().top() + min_row as f32 * row_height_with_spacing;
772 let y_max = ui.max_rect().top() + max_row as f32 * row_height_with_spacing;
773
774 let rect = Rect::from_x_y_ranges(ui.max_rect().x_range(), y_min..=y_max);
775
776 ui.allocate_new_ui(UiBuilder::new().max_rect(rect), |viewport_ui| {
777 viewport_ui.skip_ahead_auto_ids(min_row); add_contents(viewport_ui, min_row..max_row)
779 })
780 .inner
781 })
782 }
783
784 pub fn show_viewport<R>(
789 self,
790 ui: &mut Ui,
791 add_contents: impl FnOnce(&mut Ui, Rect) -> R,
792 ) -> ScrollAreaOutput<R> {
793 self.show_viewport_dyn(ui, Box::new(add_contents))
794 }
795
796 fn show_viewport_dyn<'c, R>(
797 self,
798 ui: &mut Ui,
799 add_contents: Box<dyn FnOnce(&mut Ui, Rect) -> R + 'c>,
800 ) -> ScrollAreaOutput<R> {
801 let mut prepared = self.begin(ui);
802 let id = prepared.id;
803 let inner_rect = prepared.inner_rect;
804 let inner = add_contents(&mut prepared.content_ui, prepared.viewport);
805 let (content_size, state) = prepared.end(ui);
806 ScrollAreaOutput {
807 inner,
808 id,
809 state,
810 content_size,
811 inner_rect,
812 }
813 }
814}
815
816impl Prepared {
817 fn end(self, ui: &mut Ui) -> (Vec2, State) {
819 let Self {
820 id,
821 mut state,
822 inner_rect,
823 auto_shrink,
824 scroll_enabled,
825 mut show_bars_factor,
826 current_bar_use,
827 scroll_bar_visibility,
828 scroll_bar_rect,
829 content_ui,
830 viewport: _,
831 scrolling_enabled,
832 stick_to_end,
833 saved_scroll_target,
834 animated,
835 } = self;
836
837 let content_size = content_ui.min_size();
838
839 let scroll_delta = content_ui
840 .ctx()
841 .pass_state_mut(|state| std::mem::take(&mut state.scroll_delta));
842
843 for d in 0..2 {
844 let mut delta = -scroll_delta.0[d];
846 let mut animation = scroll_delta.1;
847
848 let scroll_target = content_ui
851 .ctx()
852 .pass_state_mut(|state| state.scroll_target[d].take());
853
854 if scroll_enabled[d] {
855 if let Some(target) = scroll_target {
856 let pass_state::ScrollTarget {
857 range,
858 align,
859 animation: animation_update,
860 } = target;
861 let min = content_ui.min_rect().min[d];
862 let clip_rect = content_ui.clip_rect();
863 let visible_range = min..=min + clip_rect.size()[d];
864 let (start, end) = (range.min, range.max);
865 let clip_start = clip_rect.min[d];
866 let clip_end = clip_rect.max[d];
867 let mut spacing = content_ui.spacing().item_spacing[d];
868
869 let delta_update = if let Some(align) = align {
870 let center_factor = align.to_factor();
871
872 let offset =
873 lerp(range, center_factor) - lerp(visible_range, center_factor);
874
875 spacing *= remap(center_factor, 0.0..=1.0, -1.0..=1.0);
877
878 offset + spacing - state.offset[d]
879 } else if start < clip_start && end < clip_end {
880 -(clip_start - start + spacing).min(clip_end - end - spacing)
881 } else if end > clip_end && start > clip_start {
882 (end - clip_end + spacing).min(start - clip_start - spacing)
883 } else {
884 0.0
886 };
887
888 delta += delta_update;
889 animation = animation_update;
890 };
891
892 if delta != 0.0 {
893 let target_offset = state.offset[d] + delta;
894
895 if !animated {
896 state.offset[d] = target_offset;
897 } else if let Some(animation) = &mut state.offset_target[d] {
898 animation.target_offset = target_offset;
901 } else {
902 let now = ui.input(|i| i.time);
904 let animation_duration = (delta.abs() / animation.points_per_second)
905 .clamp(animation.duration.min, animation.duration.max);
906 state.offset_target[d] = Some(ScrollingToTarget {
907 animation_time_span: (now, now + animation_duration as f64),
908 target_offset,
909 });
910 }
911 ui.ctx().request_repaint();
912 }
913 }
914 }
915
916 ui.ctx().pass_state_mut(|state| {
918 for d in 0..2 {
919 if saved_scroll_target[d].is_some() {
920 state.scroll_target[d] = saved_scroll_target[d].clone();
921 };
922 }
923 });
924
925 let inner_rect = {
926 let mut inner_size = inner_rect.size();
928
929 for d in 0..2 {
930 inner_size[d] = match (scroll_enabled[d], auto_shrink[d]) {
931 (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]), };
936 }
937
938 Rect::from_min_size(inner_rect.min, inner_size)
939 };
940
941 let outer_rect = Rect::from_min_size(inner_rect.min, inner_rect.size() + current_bar_use);
942
943 let content_is_too_large = Vec2b::new(
944 scroll_enabled[0] && inner_rect.width() < content_size.x,
945 scroll_enabled[1] && inner_rect.height() < content_size.y,
946 );
947
948 let max_offset = content_size - inner_rect.size();
949 let is_hovering_outer_rect = ui.rect_contains_pointer(outer_rect);
950 if scrolling_enabled && is_hovering_outer_rect {
951 let always_scroll_enabled_direction = ui.style().always_scroll_the_only_direction
952 && scroll_enabled[0] != scroll_enabled[1];
953 for d in 0..2 {
954 if scroll_enabled[d] {
955 let scroll_delta = ui.ctx().input_mut(|input| {
956 if always_scroll_enabled_direction {
957 input.smooth_scroll_delta[0] + input.smooth_scroll_delta[1]
959 } else {
960 input.smooth_scroll_delta[d]
961 }
962 });
963
964 let scrolling_up = state.offset[d] > 0.0 && scroll_delta > 0.0;
965 let scrolling_down = state.offset[d] < max_offset[d] && scroll_delta < 0.0;
966
967 if scrolling_up || scrolling_down {
968 state.offset[d] -= scroll_delta;
969
970 ui.ctx().input_mut(|input| {
972 if always_scroll_enabled_direction {
973 input.smooth_scroll_delta[0] = 0.0;
974 input.smooth_scroll_delta[1] = 0.0;
975 } else {
976 input.smooth_scroll_delta[d] = 0.0;
977 }
978 });
979
980 state.scroll_stuck_to_end[d] = false;
981 state.offset_target[d] = None;
982 }
983 }
984 }
985 }
986
987 let show_scroll_this_frame = match scroll_bar_visibility {
988 ScrollBarVisibility::AlwaysHidden => Vec2b::FALSE,
989 ScrollBarVisibility::VisibleWhenNeeded => content_is_too_large,
990 ScrollBarVisibility::AlwaysVisible => scroll_enabled,
991 };
992
993 if show_scroll_this_frame[0] && show_bars_factor.x <= 0.0 {
995 show_bars_factor.x = ui.ctx().animate_bool_responsive(id.with("h"), true);
996 }
997 if show_scroll_this_frame[1] && show_bars_factor.y <= 0.0 {
998 show_bars_factor.y = ui.ctx().animate_bool_responsive(id.with("v"), true);
999 }
1000
1001 let scroll_style = ui.spacing().scroll;
1002
1003 let scroll_bar_rect = scroll_bar_rect.unwrap_or(inner_rect);
1005 for d in 0..2 {
1006 if stick_to_end[d] && state.scroll_stuck_to_end[d] {
1008 state.offset[d] = content_size[d] - inner_rect.size()[d];
1009 }
1010
1011 let show_factor = show_bars_factor[d];
1012 if show_factor == 0.0 {
1013 state.scroll_bar_interaction[d] = false;
1014 continue;
1015 }
1016
1017 let inner_margin = show_factor * scroll_style.bar_inner_margin;
1019 let outer_margin = show_factor * scroll_style.bar_outer_margin;
1020
1021 let mut cross = if scroll_style.floating {
1024 let max_bar_rect = if d == 0 {
1027 outer_rect.with_min_y(outer_rect.max.y - outer_margin - scroll_style.bar_width)
1028 } else {
1029 outer_rect.with_min_x(outer_rect.max.x - outer_margin - scroll_style.bar_width)
1030 };
1031
1032 let is_hovering_bar_area = is_hovering_outer_rect
1033 && ui.rect_contains_pointer(max_bar_rect)
1034 || state.scroll_bar_interaction[d];
1035
1036 let is_hovering_bar_area_t = ui
1037 .ctx()
1038 .animate_bool_responsive(id.with((d, "bar_hover")), is_hovering_bar_area);
1039
1040 let width = show_factor
1041 * lerp(
1042 scroll_style.floating_width..=scroll_style.bar_width,
1043 is_hovering_bar_area_t,
1044 );
1045
1046 let max_cross = outer_rect.max[1 - d] - outer_margin;
1047 let min_cross = max_cross - width;
1048 Rangef::new(min_cross, max_cross)
1049 } else {
1050 let min_cross = inner_rect.max[1 - d] + inner_margin;
1051 let max_cross = outer_rect.max[1 - d] - outer_margin;
1052 Rangef::new(min_cross, max_cross)
1053 };
1054
1055 if ui.clip_rect().max[1 - d] < cross.max + outer_margin {
1056 let width = cross.max - cross.min;
1066 cross.max = ui.clip_rect().max[1 - d] - outer_margin;
1067 cross.min = cross.max - width;
1068 }
1069
1070 let outer_scroll_bar_rect = if d == 0 {
1071 Rect::from_min_max(
1072 pos2(scroll_bar_rect.left(), cross.min),
1073 pos2(scroll_bar_rect.right(), cross.max),
1074 )
1075 } else {
1076 Rect::from_min_max(
1077 pos2(cross.min, scroll_bar_rect.top()),
1078 pos2(cross.max, scroll_bar_rect.bottom()),
1079 )
1080 };
1081
1082 let from_content = |content| {
1083 remap_clamp(
1084 content,
1085 0.0..=content_size[d],
1086 scroll_bar_rect.min[d]..=scroll_bar_rect.max[d],
1087 )
1088 };
1089
1090 let handle_rect = if d == 0 {
1091 Rect::from_min_max(
1092 pos2(from_content(state.offset.x), cross.min),
1093 pos2(from_content(state.offset.x + inner_rect.width()), cross.max),
1094 )
1095 } else {
1096 Rect::from_min_max(
1097 pos2(cross.min, from_content(state.offset.y)),
1098 pos2(
1099 cross.max,
1100 from_content(state.offset.y + inner_rect.height()),
1101 ),
1102 )
1103 };
1104
1105 let interact_id = id.with(d);
1106 let sense = if self.scrolling_enabled {
1107 Sense::click_and_drag()
1108 } else {
1109 Sense::hover()
1110 };
1111 let response = ui.interact(outer_scroll_bar_rect, interact_id, sense);
1112
1113 state.scroll_bar_interaction[d] = response.hovered() || response.dragged();
1114
1115 if let Some(pointer_pos) = response.interact_pointer_pos() {
1116 let scroll_start_offset_from_top_left = state.scroll_start_offset_from_top_left[d]
1117 .get_or_insert_with(|| {
1118 if handle_rect.contains(pointer_pos) {
1119 pointer_pos[d] - handle_rect.min[d]
1120 } else {
1121 let handle_top_pos_at_bottom =
1122 scroll_bar_rect.max[d] - handle_rect.size()[d];
1123 let new_handle_top_pos = (pointer_pos[d] - handle_rect.size()[d] / 2.0)
1125 .clamp(scroll_bar_rect.min[d], handle_top_pos_at_bottom);
1126 pointer_pos[d] - new_handle_top_pos
1127 }
1128 });
1129
1130 let new_handle_top = pointer_pos[d] - *scroll_start_offset_from_top_left;
1131 state.offset[d] = remap(
1132 new_handle_top,
1133 scroll_bar_rect.min[d]..=scroll_bar_rect.max[d],
1134 0.0..=content_size[d],
1135 );
1136
1137 state.scroll_stuck_to_end[d] = false;
1139 state.offset_target[d] = None;
1140 } else {
1141 state.scroll_start_offset_from_top_left[d] = None;
1142 }
1143
1144 let unbounded_offset = state.offset[d];
1145 state.offset[d] = state.offset[d].max(0.0);
1146 state.offset[d] = state.offset[d].min(max_offset[d]);
1147
1148 if state.offset[d] != unbounded_offset {
1149 state.vel[d] = 0.0;
1150 }
1151
1152 if ui.is_rect_visible(outer_scroll_bar_rect) {
1153 let mut handle_rect = if d == 0 {
1155 Rect::from_min_max(
1156 pos2(from_content(state.offset.x), cross.min),
1157 pos2(from_content(state.offset.x + inner_rect.width()), cross.max),
1158 )
1159 } else {
1160 Rect::from_min_max(
1161 pos2(cross.min, from_content(state.offset.y)),
1162 pos2(
1163 cross.max,
1164 from_content(state.offset.y + inner_rect.height()),
1165 ),
1166 )
1167 };
1168 let min_handle_size = scroll_style.handle_min_length;
1169 if handle_rect.size()[d] < min_handle_size {
1170 handle_rect = Rect::from_center_size(
1171 handle_rect.center(),
1172 if d == 0 {
1173 vec2(min_handle_size, handle_rect.size().y)
1174 } else {
1175 vec2(handle_rect.size().x, min_handle_size)
1176 },
1177 );
1178 }
1179
1180 let visuals = if scrolling_enabled {
1181 let is_hovering_handle = response.hovered()
1184 && ui.input(|i| {
1185 i.pointer
1186 .latest_pos()
1187 .map_or(false, |p| handle_rect.contains(p))
1188 });
1189 let visuals = ui.visuals();
1190 if response.is_pointer_button_down_on() {
1191 &visuals.widgets.active
1192 } else if is_hovering_handle {
1193 &visuals.widgets.hovered
1194 } else {
1195 &visuals.widgets.inactive
1196 }
1197 } else {
1198 &ui.visuals().widgets.inactive
1199 };
1200
1201 let handle_opacity = if scroll_style.floating {
1202 if response.hovered() || response.dragged() {
1203 scroll_style.interact_handle_opacity
1204 } else {
1205 let is_hovering_outer_rect_t = ui.ctx().animate_bool_responsive(
1206 id.with((d, "is_hovering_outer_rect")),
1207 is_hovering_outer_rect,
1208 );
1209 lerp(
1210 scroll_style.dormant_handle_opacity
1211 ..=scroll_style.active_handle_opacity,
1212 is_hovering_outer_rect_t,
1213 )
1214 }
1215 } else {
1216 1.0
1217 };
1218
1219 let background_opacity = if scroll_style.floating {
1220 if response.hovered() || response.dragged() {
1221 scroll_style.interact_background_opacity
1222 } else if is_hovering_outer_rect {
1223 scroll_style.active_background_opacity
1224 } else {
1225 scroll_style.dormant_background_opacity
1226 }
1227 } else {
1228 1.0
1229 };
1230
1231 let handle_color = if scroll_style.foreground_color {
1232 visuals.fg_stroke.color
1233 } else {
1234 visuals.bg_fill
1235 };
1236
1237 ui.painter().add(epaint::Shape::rect_filled(
1239 outer_scroll_bar_rect,
1240 visuals.rounding,
1241 ui.visuals()
1242 .extreme_bg_color
1243 .gamma_multiply(background_opacity),
1244 ));
1245
1246 ui.painter().add(epaint::Shape::rect_filled(
1248 handle_rect,
1249 visuals.rounding,
1250 handle_color.gamma_multiply(handle_opacity),
1251 ));
1252 }
1253 }
1254
1255 ui.advance_cursor_after_rect(outer_rect);
1256
1257 if show_scroll_this_frame != state.show_scroll {
1258 ui.ctx().request_repaint();
1259 }
1260
1261 let available_offset = content_size - inner_rect.size();
1262 state.offset = state.offset.min(available_offset);
1263 state.offset = state.offset.max(Vec2::ZERO);
1264
1265 state.scroll_stuck_to_end = Vec2b::new(
1271 (state.offset[0] == available_offset[0])
1272 || (self.stick_to_end[0] && available_offset[0] < 0.0),
1273 (state.offset[1] == available_offset[1])
1274 || (self.stick_to_end[1] && available_offset[1] < 0.0),
1275 );
1276
1277 state.show_scroll = show_scroll_this_frame;
1278 state.content_is_too_large = content_is_too_large;
1279 state.interact_rect = Some(inner_rect);
1280
1281 state.store(ui.ctx(), id);
1282
1283 (content_size, state)
1284 }
1285}