1#![allow(clippy::needless_range_loop)]
2
3use std::ops::{Add, AddAssign, BitOr, BitOrAssign};
4
5use crate::{
6 Context, CursorIcon, Id, NumExt as _, Pos2, Rangef, Rect, Sense, Ui, UiBuilder, UiKind,
7 UiStackInfo, Vec2, Vec2b, emath, epaint, lerp, pass_state, pos2, remap, remap_clamp,
8};
9
10#[derive(Clone, Copy, Debug)]
11#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
12struct ScrollingToTarget {
13 animation_time_span: (f64, f64),
14 target_offset: f32,
15}
16
17#[derive(Clone, Copy, Debug)]
18#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
19#[cfg_attr(feature = "serde", serde(default))]
20pub struct State {
21 pub offset: Vec2,
23
24 offset_target: [Option<ScrollingToTarget>; 2],
26
27 show_scroll: Vec2b,
29
30 content_is_too_large: Vec2b,
32
33 scroll_bar_interaction: Vec2b,
35
36 #[cfg_attr(feature = "serde", serde(skip))]
38 vel: Vec2,
39
40 scroll_start_offset_from_top_left: [Option<f32>; 2],
42
43 scroll_stuck_to_end: Vec2b,
47
48 interact_rect: Option<Rect>,
50}
51
52impl Default for State {
53 fn default() -> Self {
54 Self {
55 offset: Vec2::ZERO,
56 offset_target: Default::default(),
57 show_scroll: Vec2b::FALSE,
58 content_is_too_large: Vec2b::FALSE,
59 scroll_bar_interaction: Vec2b::FALSE,
60 vel: Vec2::ZERO,
61 scroll_start_offset_from_top_left: [None; 2],
62 scroll_stuck_to_end: Vec2b::TRUE,
63 interact_rect: None,
64 }
65 }
66}
67
68impl State {
69 pub fn load(ctx: &Context, id: Id) -> Option<Self> {
70 ctx.data_mut(|d| d.get_persisted(id))
71 }
72
73 pub fn store(self, ctx: &Context, id: Id) {
74 ctx.data_mut(|d| d.insert_persisted(id, self));
75 }
76
77 pub fn velocity(&self) -> Vec2 {
79 self.vel
80 }
81}
82
83pub struct ScrollAreaOutput<R> {
84 pub inner: R,
86
87 pub id: Id,
89
90 pub state: State,
92
93 pub content_size: Vec2,
96
97 pub inner_rect: Rect,
99}
100
101#[derive(Clone, Copy, Debug, PartialEq, Eq)]
103#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
104pub enum ScrollBarVisibility {
105 AlwaysHidden,
111
112 VisibleWhenNeeded,
117
118 AlwaysVisible,
121}
122
123impl Default for ScrollBarVisibility {
124 #[inline]
125 fn default() -> Self {
126 Self::VisibleWhenNeeded
127 }
128}
129
130impl ScrollBarVisibility {
131 pub const ALL: [Self; 3] = [
132 Self::AlwaysHidden,
133 Self::VisibleWhenNeeded,
134 Self::AlwaysVisible,
135 ];
136}
137
138#[derive(Clone, Copy, Debug, PartialEq, Eq)]
140#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
141pub struct ScrollSource {
142 pub scroll_bar: bool,
147
148 pub drag: bool,
150
151 pub mouse_wheel: bool,
154}
155
156impl Default for ScrollSource {
157 fn default() -> Self {
158 Self::ALL
159 }
160}
161
162impl ScrollSource {
163 pub const NONE: Self = Self {
164 scroll_bar: false,
165 drag: false,
166 mouse_wheel: false,
167 };
168 pub const ALL: Self = Self {
169 scroll_bar: true,
170 drag: true,
171 mouse_wheel: true,
172 };
173 pub const SCROLL_BAR: Self = Self {
174 scroll_bar: true,
175 drag: false,
176 mouse_wheel: false,
177 };
178 pub const DRAG: Self = Self {
179 scroll_bar: false,
180 drag: true,
181 mouse_wheel: false,
182 };
183 pub const MOUSE_WHEEL: Self = Self {
184 scroll_bar: false,
185 drag: false,
186 mouse_wheel: true,
187 };
188
189 #[inline]
191 pub fn is_none(&self) -> bool {
192 self == &Self::NONE
193 }
194
195 #[inline]
197 pub fn any(&self) -> bool {
198 self.scroll_bar | self.drag | self.mouse_wheel
199 }
200
201 #[inline]
203 pub fn is_all(&self) -> bool {
204 self.scroll_bar & self.drag & self.mouse_wheel
205 }
206}
207
208impl BitOr for ScrollSource {
209 type Output = Self;
210
211 #[inline]
212 fn bitor(self, rhs: Self) -> Self::Output {
213 Self {
214 scroll_bar: self.scroll_bar | rhs.scroll_bar,
215 drag: self.drag | rhs.drag,
216 mouse_wheel: self.mouse_wheel | rhs.mouse_wheel,
217 }
218 }
219}
220
221#[expect(clippy::suspicious_arithmetic_impl)]
222impl Add for ScrollSource {
223 type Output = Self;
224
225 #[inline]
226 fn add(self, rhs: Self) -> Self::Output {
227 self | rhs
228 }
229}
230
231impl BitOrAssign for ScrollSource {
232 #[inline]
233 fn bitor_assign(&mut self, rhs: Self) {
234 *self = *self | rhs;
235 }
236}
237
238impl AddAssign for ScrollSource {
239 #[inline]
240 fn add_assign(&mut self, rhs: Self) {
241 *self = *self + rhs;
242 }
243}
244
245#[derive(Clone, Debug)]
277#[must_use = "You should call .show()"]
278pub struct ScrollArea {
279 direction_enabled: Vec2b,
281
282 auto_shrink: Vec2b,
283 max_size: Vec2,
284 min_scrolled_size: Vec2,
285 scroll_bar_visibility: ScrollBarVisibility,
286 scroll_bar_rect: Option<Rect>,
287 id_salt: Option<Id>,
288 offset_x: Option<f32>,
289 offset_y: Option<f32>,
290 on_hover_cursor: Option<CursorIcon>,
291 on_drag_cursor: Option<CursorIcon>,
292 scroll_source: ScrollSource,
293 wheel_scroll_multiplier: Vec2,
294
295 stick_to_end: Vec2b,
299
300 animated: bool,
302}
303
304impl ScrollArea {
305 #[inline]
307 pub fn horizontal() -> Self {
308 Self::new([true, false])
309 }
310
311 #[inline]
313 pub fn vertical() -> Self {
314 Self::new([false, true])
315 }
316
317 #[inline]
319 pub fn both() -> Self {
320 Self::new([true, true])
321 }
322
323 #[inline]
326 pub fn neither() -> Self {
327 Self::new([false, false])
328 }
329
330 pub fn new(direction_enabled: impl Into<Vec2b>) -> Self {
333 Self {
334 direction_enabled: direction_enabled.into(),
335 auto_shrink: Vec2b::TRUE,
336 max_size: Vec2::INFINITY,
337 min_scrolled_size: Vec2::splat(64.0),
338 scroll_bar_visibility: Default::default(),
339 scroll_bar_rect: None,
340 id_salt: None,
341 offset_x: None,
342 offset_y: None,
343 on_hover_cursor: None,
344 on_drag_cursor: None,
345 scroll_source: ScrollSource::default(),
346 wheel_scroll_multiplier: Vec2::splat(1.0),
347 stick_to_end: Vec2b::FALSE,
348 animated: true,
349 }
350 }
351
352 #[inline]
358 pub fn max_width(mut self, max_width: f32) -> Self {
359 self.max_size.x = max_width;
360 self
361 }
362
363 #[inline]
369 pub fn max_height(mut self, max_height: f32) -> Self {
370 self.max_size.y = max_height;
371 self
372 }
373
374 #[inline]
381 pub fn min_scrolled_width(mut self, min_scrolled_width: f32) -> Self {
382 self.min_scrolled_size.x = min_scrolled_width;
383 self
384 }
385
386 #[inline]
393 pub fn min_scrolled_height(mut self, min_scrolled_height: f32) -> Self {
394 self.min_scrolled_size.y = min_scrolled_height;
395 self
396 }
397
398 #[inline]
402 pub fn scroll_bar_visibility(mut self, scroll_bar_visibility: ScrollBarVisibility) -> Self {
403 self.scroll_bar_visibility = scroll_bar_visibility;
404 self
405 }
406
407 #[inline]
412 pub fn scroll_bar_rect(mut self, scroll_bar_rect: Rect) -> Self {
413 self.scroll_bar_rect = Some(scroll_bar_rect);
414 self
415 }
416
417 #[inline]
419 #[deprecated = "Renamed id_salt"]
420 pub fn id_source(self, id_salt: impl std::hash::Hash) -> Self {
421 self.id_salt(id_salt)
422 }
423
424 #[inline]
426 pub fn id_salt(mut self, id_salt: impl std::hash::Hash) -> Self {
427 self.id_salt = Some(Id::new(id_salt));
428 self
429 }
430
431 #[inline]
439 pub fn scroll_offset(mut self, offset: Vec2) -> Self {
440 self.offset_x = Some(offset.x);
441 self.offset_y = Some(offset.y);
442 self
443 }
444
445 #[inline]
452 pub fn vertical_scroll_offset(mut self, offset: f32) -> Self {
453 self.offset_y = Some(offset);
454 self
455 }
456
457 #[inline]
464 pub fn horizontal_scroll_offset(mut self, offset: f32) -> Self {
465 self.offset_x = Some(offset);
466 self
467 }
468
469 #[inline]
476 pub fn on_hover_cursor(mut self, cursor: CursorIcon) -> Self {
477 self.on_hover_cursor = Some(cursor);
478 self
479 }
480
481 #[inline]
488 pub fn on_drag_cursor(mut self, cursor: CursorIcon) -> Self {
489 self.on_drag_cursor = Some(cursor);
490 self
491 }
492
493 #[inline]
495 pub fn hscroll(mut self, hscroll: bool) -> Self {
496 self.direction_enabled[0] = hscroll;
497 self
498 }
499
500 #[inline]
502 pub fn vscroll(mut self, vscroll: bool) -> Self {
503 self.direction_enabled[1] = vscroll;
504 self
505 }
506
507 #[inline]
511 pub fn scroll(mut self, direction_enabled: impl Into<Vec2b>) -> Self {
512 self.direction_enabled = direction_enabled.into();
513 self
514 }
515
516 #[deprecated = "Use `ScrollArea::scroll_source()"]
526 #[inline]
527 pub fn enable_scrolling(mut self, enable: bool) -> Self {
528 self.scroll_source = if enable {
529 ScrollSource::ALL
530 } else {
531 ScrollSource::NONE
532 };
533 self
534 }
535
536 #[deprecated = "Use `ScrollArea::scroll_source()"]
544 #[inline]
545 pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self {
546 self.scroll_source.drag = drag_to_scroll;
547 self
548 }
549
550 #[inline]
552 pub fn scroll_source(mut self, scroll_source: ScrollSource) -> Self {
553 self.scroll_source = scroll_source;
554 self
555 }
556
557 #[inline]
563 pub fn wheel_scroll_multiplier(mut self, multiplier: Vec2) -> Self {
564 self.wheel_scroll_multiplier = multiplier;
565 self
566 }
567
568 #[inline]
575 pub fn auto_shrink(mut self, auto_shrink: impl Into<Vec2b>) -> Self {
576 self.auto_shrink = auto_shrink.into();
577 self
578 }
579
580 #[inline]
584 pub fn animated(mut self, animated: bool) -> Self {
585 self.animated = animated;
586 self
587 }
588
589 pub(crate) fn is_any_scroll_enabled(&self) -> bool {
591 self.direction_enabled[0] || self.direction_enabled[1]
592 }
593
594 #[inline]
601 pub fn stick_to_right(mut self, stick: bool) -> Self {
602 self.stick_to_end[0] = stick;
603 self
604 }
605
606 #[inline]
613 pub fn stick_to_bottom(mut self, stick: bool) -> Self {
614 self.stick_to_end[1] = stick;
615 self
616 }
617}
618
619struct Prepared {
620 id: Id,
621 state: State,
622
623 auto_shrink: Vec2b,
624
625 direction_enabled: Vec2b,
627
628 show_bars_factor: Vec2,
630
631 current_bar_use: Vec2,
641
642 scroll_bar_visibility: ScrollBarVisibility,
643 scroll_bar_rect: Option<Rect>,
644
645 inner_rect: Rect,
647
648 content_ui: Ui,
649
650 viewport: Rect,
653
654 scroll_source: ScrollSource,
655 wheel_scroll_multiplier: Vec2,
656 stick_to_end: Vec2b,
657
658 saved_scroll_target: [Option<pass_state::ScrollTarget>; 2],
661
662 animated: bool,
663}
664
665impl ScrollArea {
666 fn begin(self, ui: &mut Ui) -> Prepared {
667 let Self {
668 direction_enabled,
669 auto_shrink,
670 max_size,
671 min_scrolled_size,
672 scroll_bar_visibility,
673 scroll_bar_rect,
674 id_salt,
675 offset_x,
676 offset_y,
677 on_hover_cursor,
678 on_drag_cursor,
679 scroll_source,
680 wheel_scroll_multiplier,
681 stick_to_end,
682 animated,
683 } = self;
684
685 let ctx = ui.ctx().clone();
686
687 let id_salt = id_salt.unwrap_or_else(|| Id::new("scroll_area"));
688 let id = ui.make_persistent_id(id_salt);
689 ctx.check_for_id_clash(
690 id,
691 Rect::from_min_size(ui.available_rect_before_wrap().min, Vec2::ZERO),
692 "ScrollArea",
693 );
694 let mut state = State::load(&ctx, id).unwrap_or_default();
695
696 state.offset.x = offset_x.unwrap_or(state.offset.x);
697 state.offset.y = offset_y.unwrap_or(state.offset.y);
698
699 let show_bars: Vec2b = match scroll_bar_visibility {
700 ScrollBarVisibility::AlwaysHidden => Vec2b::FALSE,
701 ScrollBarVisibility::VisibleWhenNeeded => state.show_scroll,
702 ScrollBarVisibility::AlwaysVisible => direction_enabled,
703 };
704
705 let show_bars_factor = Vec2::new(
706 ctx.animate_bool_responsive(id.with("h"), show_bars[0]),
707 ctx.animate_bool_responsive(id.with("v"), show_bars[1]),
708 );
709
710 let current_bar_use = show_bars_factor.yx() * ui.spacing().scroll.allocated_width();
711
712 let available_outer = ui.available_rect_before_wrap();
713
714 let outer_size = available_outer.size().at_most(max_size);
715
716 let inner_size = {
717 let mut inner_size = outer_size - current_bar_use;
718
719 for d in 0..2 {
724 if direction_enabled[d] {
725 inner_size[d] = inner_size[d].max(min_scrolled_size[d]);
726 }
727 }
728 inner_size
729 };
730
731 let inner_rect = Rect::from_min_size(available_outer.min, inner_size);
732
733 let mut content_max_size = inner_size;
734
735 if true {
736 } else {
739 for d in 0..2 {
741 if direction_enabled[d] {
742 content_max_size[d] = f32::INFINITY;
743 }
744 }
745 }
746
747 let content_max_rect = Rect::from_min_size(inner_rect.min - state.offset, content_max_size);
748 let mut content_ui = ui.new_child(
749 UiBuilder::new()
750 .ui_stack_info(UiStackInfo::new(UiKind::ScrollArea))
751 .max_rect(content_max_rect),
752 );
753
754 {
755 let clip_rect_margin = ui.visuals().clip_rect_margin;
757 let mut content_clip_rect = ui.clip_rect();
758 for d in 0..2 {
759 if direction_enabled[d] {
760 content_clip_rect.min[d] = inner_rect.min[d] - clip_rect_margin;
761 content_clip_rect.max[d] = inner_rect.max[d] + clip_rect_margin;
762 } else {
763 content_clip_rect.max[d] = ui.clip_rect().max[d] - current_bar_use[d];
765 }
766 }
767 content_clip_rect = content_clip_rect.intersect(ui.clip_rect());
769 content_ui.set_clip_rect(content_clip_rect);
770 }
771
772 let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size);
773 let dt = ui.input(|i| i.stable_dt).at_most(0.1);
774
775 if scroll_source.drag
776 && ui.is_enabled()
777 && (state.content_is_too_large[0] || state.content_is_too_large[1])
778 {
779 let content_response_option = state
783 .interact_rect
784 .map(|rect| ui.interact(rect, id.with("area"), Sense::drag()));
785
786 if content_response_option
787 .as_ref()
788 .is_some_and(|response| response.dragged())
789 {
790 for d in 0..2 {
791 if direction_enabled[d] {
792 ui.input(|input| {
793 state.offset[d] -= input.pointer.delta()[d];
794 });
795 state.scroll_stuck_to_end[d] = false;
796 state.offset_target[d] = None;
797 }
798 }
799 } else {
800 if content_response_option
802 .as_ref()
803 .is_some_and(|response| response.drag_stopped())
804 {
805 state.vel =
806 direction_enabled.to_vec2() * ui.input(|input| input.pointer.velocity());
807 }
808 for d in 0..2 {
809 let stop_speed = 20.0; let friction_coeff = 1000.0; let friction = friction_coeff * dt;
814 if friction > state.vel[d].abs() || state.vel[d].abs() < stop_speed {
815 state.vel[d] = 0.0;
816 } else {
817 state.vel[d] -= friction * state.vel[d].signum();
818 state.offset[d] -= state.vel[d] * dt;
821 ctx.request_repaint();
822 }
823 }
824 }
825
826 if let Some(response) = content_response_option {
828 if response.dragged() {
829 if let Some(cursor) = on_drag_cursor {
830 response.on_hover_cursor(cursor);
831 }
832 } else if response.hovered() {
833 if let Some(cursor) = on_hover_cursor {
834 response.on_hover_cursor(cursor);
835 }
836 }
837 }
838 }
839
840 for d in 0..2 {
843 if let Some(scroll_target) = state.offset_target[d] {
844 state.vel[d] = 0.0;
845
846 if (state.offset[d] - scroll_target.target_offset).abs() < 1.0 {
847 state.offset[d] = scroll_target.target_offset;
849 state.offset_target[d] = None;
850 } else {
851 let t = emath::interpolation_factor(
853 scroll_target.animation_time_span,
854 ui.input(|i| i.time),
855 dt,
856 emath::ease_in_ease_out,
857 );
858 if t < 1.0 {
859 state.offset[d] =
860 emath::lerp(state.offset[d]..=scroll_target.target_offset, t);
861 ctx.request_repaint();
862 } else {
863 state.offset[d] = scroll_target.target_offset;
865 state.offset_target[d] = None;
866 }
867 }
868 }
869 }
870
871 let saved_scroll_target = content_ui
872 .ctx()
873 .pass_state_mut(|state| std::mem::take(&mut state.scroll_target));
874
875 Prepared {
876 id,
877 state,
878 auto_shrink,
879 direction_enabled,
880 show_bars_factor,
881 current_bar_use,
882 scroll_bar_visibility,
883 scroll_bar_rect,
884 inner_rect,
885 content_ui,
886 viewport,
887 scroll_source,
888 wheel_scroll_multiplier,
889 stick_to_end,
890 saved_scroll_target,
891 animated,
892 }
893 }
894
895 pub fn show<R>(
899 self,
900 ui: &mut Ui,
901 add_contents: impl FnOnce(&mut Ui) -> R,
902 ) -> ScrollAreaOutput<R> {
903 self.show_viewport_dyn(ui, Box::new(|ui, _viewport| add_contents(ui)))
904 }
905
906 pub fn show_rows<R>(
923 self,
924 ui: &mut Ui,
925 row_height_sans_spacing: f32,
926 total_rows: usize,
927 add_contents: impl FnOnce(&mut Ui, std::ops::Range<usize>) -> R,
928 ) -> ScrollAreaOutput<R> {
929 let spacing = ui.spacing().item_spacing;
930 let row_height_with_spacing = row_height_sans_spacing + spacing.y;
931 self.show_viewport(ui, |ui, viewport| {
932 ui.set_height((row_height_with_spacing * total_rows as f32 - spacing.y).at_least(0.0));
933
934 let mut min_row = (viewport.min.y / row_height_with_spacing).floor() as usize;
935 let mut max_row = (viewport.max.y / row_height_with_spacing).ceil() as usize + 1;
936 if max_row > total_rows {
937 let diff = max_row.saturating_sub(min_row);
938 max_row = total_rows;
939 min_row = total_rows.saturating_sub(diff);
940 }
941
942 let y_min = ui.max_rect().top() + min_row as f32 * row_height_with_spacing;
943 let y_max = ui.max_rect().top() + max_row as f32 * row_height_with_spacing;
944
945 let rect = Rect::from_x_y_ranges(ui.max_rect().x_range(), y_min..=y_max);
946
947 ui.scope_builder(UiBuilder::new().max_rect(rect), |viewport_ui| {
948 viewport_ui.skip_ahead_auto_ids(min_row); add_contents(viewport_ui, min_row..max_row)
950 })
951 .inner
952 })
953 }
954
955 pub fn show_viewport<R>(
960 self,
961 ui: &mut Ui,
962 add_contents: impl FnOnce(&mut Ui, Rect) -> R,
963 ) -> ScrollAreaOutput<R> {
964 self.show_viewport_dyn(ui, Box::new(add_contents))
965 }
966
967 fn show_viewport_dyn<'c, R>(
968 self,
969 ui: &mut Ui,
970 add_contents: Box<dyn FnOnce(&mut Ui, Rect) -> R + 'c>,
971 ) -> ScrollAreaOutput<R> {
972 let mut prepared = self.begin(ui);
973 let id = prepared.id;
974 let inner_rect = prepared.inner_rect;
975 let inner = add_contents(&mut prepared.content_ui, prepared.viewport);
976 let (content_size, state) = prepared.end(ui);
977 ScrollAreaOutput {
978 inner,
979 id,
980 state,
981 content_size,
982 inner_rect,
983 }
984 }
985}
986
987impl Prepared {
988 fn end(self, ui: &mut Ui) -> (Vec2, State) {
990 let Self {
991 id,
992 mut state,
993 inner_rect,
994 auto_shrink,
995 direction_enabled,
996 mut show_bars_factor,
997 current_bar_use,
998 scroll_bar_visibility,
999 scroll_bar_rect,
1000 content_ui,
1001 viewport: _,
1002 scroll_source,
1003 wheel_scroll_multiplier,
1004 stick_to_end,
1005 saved_scroll_target,
1006 animated,
1007 } = self;
1008
1009 let content_size = content_ui.min_size();
1010
1011 let scroll_delta = content_ui
1012 .ctx()
1013 .pass_state_mut(|state| std::mem::take(&mut state.scroll_delta));
1014
1015 for d in 0..2 {
1016 let mut delta = -scroll_delta.0[d];
1018 let mut animation = scroll_delta.1;
1019
1020 let scroll_target = content_ui
1023 .ctx()
1024 .pass_state_mut(|state| state.scroll_target[d].take());
1025
1026 if direction_enabled[d] {
1027 if let Some(target) = scroll_target {
1028 let pass_state::ScrollTarget {
1029 range,
1030 align,
1031 animation: animation_update,
1032 } = target;
1033 let min = content_ui.min_rect().min[d];
1034 let clip_rect = content_ui.clip_rect();
1035 let visible_range = min..=min + clip_rect.size()[d];
1036 let (start, end) = (range.min, range.max);
1037 let clip_start = clip_rect.min[d];
1038 let clip_end = clip_rect.max[d];
1039 let mut spacing = content_ui.spacing().item_spacing[d];
1040
1041 let delta_update = if let Some(align) = align {
1042 let center_factor = align.to_factor();
1043
1044 let offset =
1045 lerp(range, center_factor) - lerp(visible_range, center_factor);
1046
1047 spacing *= remap(center_factor, 0.0..=1.0, -1.0..=1.0);
1049
1050 offset + spacing - state.offset[d]
1051 } else if start < clip_start && end < clip_end {
1052 -(clip_start - start + spacing).min(clip_end - end - spacing)
1053 } else if end > clip_end && start > clip_start {
1054 (end - clip_end + spacing).min(start - clip_start - spacing)
1055 } else {
1056 0.0
1058 };
1059
1060 delta += delta_update;
1061 animation = animation_update;
1062 };
1063
1064 if delta != 0.0 {
1065 let target_offset = state.offset[d] + delta;
1066
1067 if !animated {
1068 state.offset[d] = target_offset;
1069 } else if let Some(animation) = &mut state.offset_target[d] {
1070 animation.target_offset = target_offset;
1073 } else {
1074 let now = ui.input(|i| i.time);
1076 let animation_duration = (delta.abs() / animation.points_per_second)
1077 .clamp(animation.duration.min, animation.duration.max);
1078 state.offset_target[d] = Some(ScrollingToTarget {
1079 animation_time_span: (now, now + animation_duration as f64),
1080 target_offset,
1081 });
1082 }
1083 ui.ctx().request_repaint();
1084 }
1085 }
1086 }
1087
1088 ui.ctx().pass_state_mut(|state| {
1090 for d in 0..2 {
1091 if saved_scroll_target[d].is_some() {
1092 state.scroll_target[d] = saved_scroll_target[d].clone();
1093 };
1094 }
1095 });
1096
1097 let inner_rect = {
1098 let mut inner_size = inner_rect.size();
1100
1101 for d in 0..2 {
1102 inner_size[d] = match (direction_enabled[d], auto_shrink[d]) {
1103 (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]), };
1108 }
1109
1110 Rect::from_min_size(inner_rect.min, inner_size)
1111 };
1112
1113 let outer_rect = Rect::from_min_size(inner_rect.min, inner_rect.size() + current_bar_use);
1114
1115 let content_is_too_large = Vec2b::new(
1116 direction_enabled[0] && inner_rect.width() < content_size.x,
1117 direction_enabled[1] && inner_rect.height() < content_size.y,
1118 );
1119
1120 let max_offset = content_size - inner_rect.size();
1121 let is_hovering_outer_rect = ui.rect_contains_pointer(outer_rect);
1122 if scroll_source.mouse_wheel && ui.is_enabled() && is_hovering_outer_rect {
1123 let always_scroll_enabled_direction = ui.style().always_scroll_the_only_direction
1124 && direction_enabled[0] != direction_enabled[1];
1125 for d in 0..2 {
1126 if direction_enabled[d] {
1127 let scroll_delta = ui.ctx().input(|input| {
1128 if always_scroll_enabled_direction {
1129 input.smooth_scroll_delta[0] + input.smooth_scroll_delta[1]
1131 } else {
1132 input.smooth_scroll_delta[d]
1133 }
1134 });
1135 let scroll_delta = scroll_delta * wheel_scroll_multiplier[d];
1136
1137 let scrolling_up = state.offset[d] > 0.0 && scroll_delta > 0.0;
1138 let scrolling_down = state.offset[d] < max_offset[d] && scroll_delta < 0.0;
1139
1140 if scrolling_up || scrolling_down {
1141 state.offset[d] -= scroll_delta;
1142
1143 ui.ctx().input_mut(|input| {
1145 if always_scroll_enabled_direction {
1146 input.smooth_scroll_delta[0] = 0.0;
1147 input.smooth_scroll_delta[1] = 0.0;
1148 } else {
1149 input.smooth_scroll_delta[d] = 0.0;
1150 }
1151 });
1152
1153 state.scroll_stuck_to_end[d] = false;
1154 state.offset_target[d] = None;
1155 }
1156 }
1157 }
1158 }
1159
1160 let show_scroll_this_frame = match scroll_bar_visibility {
1161 ScrollBarVisibility::AlwaysHidden => Vec2b::FALSE,
1162 ScrollBarVisibility::VisibleWhenNeeded => content_is_too_large,
1163 ScrollBarVisibility::AlwaysVisible => direction_enabled,
1164 };
1165
1166 if show_scroll_this_frame[0] && show_bars_factor.x <= 0.0 {
1168 show_bars_factor.x = ui.ctx().animate_bool_responsive(id.with("h"), true);
1169 }
1170 if show_scroll_this_frame[1] && show_bars_factor.y <= 0.0 {
1171 show_bars_factor.y = ui.ctx().animate_bool_responsive(id.with("v"), true);
1172 }
1173
1174 let scroll_style = ui.spacing().scroll;
1175
1176 let scroll_bar_rect = scroll_bar_rect.unwrap_or(inner_rect);
1178 for d in 0..2 {
1179 if stick_to_end[d] && state.scroll_stuck_to_end[d] {
1181 state.offset[d] = content_size[d] - inner_rect.size()[d];
1182 }
1183
1184 let show_factor = show_bars_factor[d];
1185 if show_factor == 0.0 {
1186 state.scroll_bar_interaction[d] = false;
1187 continue;
1188 }
1189
1190 let inner_margin = show_factor * scroll_style.bar_inner_margin;
1192 let outer_margin = show_factor * scroll_style.bar_outer_margin;
1193
1194 let mut cross = if scroll_style.floating {
1197 let max_bar_rect = if d == 0 {
1200 outer_rect.with_min_y(outer_rect.max.y - outer_margin - scroll_style.bar_width)
1201 } else {
1202 outer_rect.with_min_x(outer_rect.max.x - outer_margin - scroll_style.bar_width)
1203 };
1204
1205 let is_hovering_bar_area = is_hovering_outer_rect
1206 && ui.rect_contains_pointer(max_bar_rect)
1207 || state.scroll_bar_interaction[d];
1208
1209 let is_hovering_bar_area_t = ui
1210 .ctx()
1211 .animate_bool_responsive(id.with((d, "bar_hover")), is_hovering_bar_area);
1212
1213 let width = show_factor
1214 * lerp(
1215 scroll_style.floating_width..=scroll_style.bar_width,
1216 is_hovering_bar_area_t,
1217 );
1218
1219 let max_cross = outer_rect.max[1 - d] - outer_margin;
1220 let min_cross = max_cross - width;
1221 Rangef::new(min_cross, max_cross)
1222 } else {
1223 let min_cross = inner_rect.max[1 - d] + inner_margin;
1224 let max_cross = outer_rect.max[1 - d] - outer_margin;
1225 Rangef::new(min_cross, max_cross)
1226 };
1227
1228 if ui.clip_rect().max[1 - d] < cross.max + outer_margin {
1229 let width = cross.max - cross.min;
1239 cross.max = ui.clip_rect().max[1 - d] - outer_margin;
1240 cross.min = cross.max - width;
1241 }
1242
1243 let outer_scroll_bar_rect = if d == 0 {
1244 Rect::from_min_max(
1245 pos2(scroll_bar_rect.left(), cross.min),
1246 pos2(scroll_bar_rect.right(), cross.max),
1247 )
1248 } else {
1249 Rect::from_min_max(
1250 pos2(cross.min, scroll_bar_rect.top()),
1251 pos2(cross.max, scroll_bar_rect.bottom()),
1252 )
1253 };
1254
1255 let from_content = |content| {
1256 remap_clamp(
1257 content,
1258 0.0..=content_size[d],
1259 scroll_bar_rect.min[d]..=scroll_bar_rect.max[d],
1260 )
1261 };
1262
1263 let calculate_handle_rect = |d, offset: &Vec2| {
1264 let handle_size = if d == 0 {
1265 from_content(offset.x + inner_rect.width()) - from_content(offset.x)
1266 } else {
1267 from_content(offset.y + inner_rect.height()) - from_content(offset.y)
1268 }
1269 .max(scroll_style.handle_min_length);
1270
1271 let handle_start_point = remap_clamp(
1272 offset[d],
1273 0.0..=max_offset[d],
1274 scroll_bar_rect.min[d]..=(scroll_bar_rect.max[d] - handle_size),
1275 );
1276
1277 if d == 0 {
1278 Rect::from_min_max(
1279 pos2(handle_start_point, cross.min),
1280 pos2(handle_start_point + handle_size, cross.max),
1281 )
1282 } else {
1283 Rect::from_min_max(
1284 pos2(cross.min, handle_start_point),
1285 pos2(cross.max, handle_start_point + handle_size),
1286 )
1287 }
1288 };
1289
1290 let handle_rect = calculate_handle_rect(d, &state.offset);
1291
1292 let interact_id = id.with(d);
1293 let sense = if scroll_source.scroll_bar && ui.is_enabled() {
1294 Sense::click_and_drag()
1295 } else {
1296 Sense::hover()
1297 };
1298 let response = ui.interact(outer_scroll_bar_rect, interact_id, sense);
1299
1300 state.scroll_bar_interaction[d] = response.hovered() || response.dragged();
1301
1302 if let Some(pointer_pos) = response.interact_pointer_pos() {
1303 let scroll_start_offset_from_top_left = state.scroll_start_offset_from_top_left[d]
1304 .get_or_insert_with(|| {
1305 if handle_rect.contains(pointer_pos) {
1306 pointer_pos[d] - handle_rect.min[d]
1307 } else {
1308 let handle_top_pos_at_bottom =
1309 scroll_bar_rect.max[d] - handle_rect.size()[d];
1310 let new_handle_top_pos = (pointer_pos[d] - handle_rect.size()[d] / 2.0)
1312 .clamp(scroll_bar_rect.min[d], handle_top_pos_at_bottom);
1313 pointer_pos[d] - new_handle_top_pos
1314 }
1315 });
1316
1317 let new_handle_top = pointer_pos[d] - *scroll_start_offset_from_top_left;
1318 state.offset[d] = remap(
1319 new_handle_top,
1320 scroll_bar_rect.min[d]..=(scroll_bar_rect.max[d] - handle_rect.size()[d]),
1321 0.0..=max_offset[d],
1322 );
1323
1324 state.scroll_stuck_to_end[d] = false;
1326 state.offset_target[d] = None;
1327 } else {
1328 state.scroll_start_offset_from_top_left[d] = None;
1329 }
1330
1331 let unbounded_offset = state.offset[d];
1332 state.offset[d] = state.offset[d].max(0.0);
1333 state.offset[d] = state.offset[d].min(max_offset[d]);
1334
1335 if state.offset[d] != unbounded_offset {
1336 state.vel[d] = 0.0;
1337 }
1338
1339 if ui.is_rect_visible(outer_scroll_bar_rect) {
1340 let handle_rect = calculate_handle_rect(d, &state.offset);
1342
1343 let visuals = if scroll_source.scroll_bar && ui.is_enabled() {
1344 let is_hovering_handle = response.hovered()
1347 && ui.input(|i| {
1348 i.pointer
1349 .latest_pos()
1350 .is_some_and(|p| handle_rect.contains(p))
1351 });
1352 let visuals = ui.visuals();
1353 if response.is_pointer_button_down_on() {
1354 &visuals.widgets.active
1355 } else if is_hovering_handle {
1356 &visuals.widgets.hovered
1357 } else {
1358 &visuals.widgets.inactive
1359 }
1360 } else {
1361 &ui.visuals().widgets.inactive
1362 };
1363
1364 let handle_opacity = if scroll_style.floating {
1365 if response.hovered() || response.dragged() {
1366 scroll_style.interact_handle_opacity
1367 } else {
1368 let is_hovering_outer_rect_t = ui.ctx().animate_bool_responsive(
1369 id.with((d, "is_hovering_outer_rect")),
1370 is_hovering_outer_rect,
1371 );
1372 lerp(
1373 scroll_style.dormant_handle_opacity
1374 ..=scroll_style.active_handle_opacity,
1375 is_hovering_outer_rect_t,
1376 )
1377 }
1378 } else {
1379 1.0
1380 };
1381
1382 let background_opacity = if scroll_style.floating {
1383 if response.hovered() || response.dragged() {
1384 scroll_style.interact_background_opacity
1385 } else if is_hovering_outer_rect {
1386 scroll_style.active_background_opacity
1387 } else {
1388 scroll_style.dormant_background_opacity
1389 }
1390 } else {
1391 1.0
1392 };
1393
1394 let handle_color = if scroll_style.foreground_color {
1395 visuals.fg_stroke.color
1396 } else {
1397 visuals.bg_fill
1398 };
1399
1400 ui.painter().add(epaint::Shape::rect_filled(
1402 outer_scroll_bar_rect,
1403 visuals.corner_radius,
1404 ui.visuals()
1405 .extreme_bg_color
1406 .gamma_multiply(background_opacity),
1407 ));
1408
1409 ui.painter().add(epaint::Shape::rect_filled(
1411 handle_rect,
1412 visuals.corner_radius,
1413 handle_color.gamma_multiply(handle_opacity),
1414 ));
1415 }
1416 }
1417
1418 ui.advance_cursor_after_rect(outer_rect);
1419
1420 if show_scroll_this_frame != state.show_scroll {
1421 ui.ctx().request_repaint();
1422 }
1423
1424 let available_offset = content_size - inner_rect.size();
1425 state.offset = state.offset.min(available_offset);
1426 state.offset = state.offset.max(Vec2::ZERO);
1427
1428 state.scroll_stuck_to_end = Vec2b::new(
1434 (state.offset[0] == available_offset[0])
1435 || (self.stick_to_end[0] && available_offset[0] < 0.0),
1436 (state.offset[1] == available_offset[1])
1437 || (self.stick_to_end[1] && available_offset[1] < 0.0),
1438 );
1439
1440 state.show_scroll = show_scroll_this_frame;
1441 state.content_is_too_large = content_is_too_large;
1442 state.interact_rect = Some(inner_rect);
1443
1444 state.store(ui.ctx(), id);
1445
1446 (content_size, state)
1447 }
1448}