1#![expect(clippy::needless_range_loop)]
4
5use std::ops::{Add, AddAssign, BitOr, BitOrAssign};
6
7use emath::GuiRounding as _;
8use epaint::{Color32, Direction, Margin, Shape};
9
10use crate::{
11 Context, CursorIcon, Id, NumExt as _, Pos2, Rangef, Rect, Response, Sense, Ui, UiBuilder,
12 UiKind, UiStackInfo, Vec2, Vec2b, WidgetInfo, emath, epaint, lerp, pass_state, pos2, remap,
13 remap_clamp,
14};
15
16#[derive(Clone, Copy, Debug)]
17#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
18struct ScrollingToTarget {
19 animation_time_span: (f64, f64),
20 target_offset: f32,
21}
22
23#[derive(Clone, Copy, Debug)]
24#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
25#[cfg_attr(feature = "serde", serde(default))]
26pub struct State {
27 pub offset: Vec2,
29
30 offset_target: [Option<ScrollingToTarget>; 2],
32
33 show_scroll: Vec2b,
35
36 content_is_too_large: Vec2b,
38
39 scroll_bar_interaction: Vec2b,
41
42 #[cfg_attr(feature = "serde", serde(skip))]
44 vel: Vec2,
45
46 scroll_start_offset_from_top_left: [Option<f32>; 2],
48
49 scroll_stuck_to_end: Vec2b,
53
54 interact_rect: Option<Rect>,
56}
57
58impl Default for State {
59 fn default() -> Self {
60 Self {
61 offset: Vec2::ZERO,
62 offset_target: Default::default(),
63 show_scroll: Vec2b::FALSE,
64 content_is_too_large: Vec2b::FALSE,
65 scroll_bar_interaction: Vec2b::FALSE,
66 vel: Vec2::ZERO,
67 scroll_start_offset_from_top_left: [None; 2],
68 scroll_stuck_to_end: Vec2b::TRUE,
69 interact_rect: None,
70 }
71 }
72}
73
74impl State {
75 pub fn load(ctx: &Context, id: Id) -> Option<Self> {
76 ctx.data_mut(|d| d.get_persisted(id))
77 }
78
79 pub fn store(self, ctx: &Context, id: Id) {
80 ctx.data_mut(|d| d.insert_persisted(id, self));
81 }
82
83 pub fn velocity(&self) -> Vec2 {
85 self.vel
86 }
87}
88
89pub struct ScrollAreaOutput<R> {
90 pub inner: R,
92
93 pub id: Id,
95
96 pub state: State,
98
99 pub content_size: Vec2,
102
103 pub inner_rect: Rect,
105}
106
107#[derive(Clone, Copy, Debug, PartialEq, Eq)]
109#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
110pub enum ScrollBarVisibility {
111 AlwaysHidden,
117
118 VisibleWhenNeeded,
123
124 AlwaysVisible,
127}
128
129impl Default for ScrollBarVisibility {
130 #[inline]
131 fn default() -> Self {
132 Self::VisibleWhenNeeded
133 }
134}
135
136impl ScrollBarVisibility {
137 pub const ALL: [Self; 3] = [
138 Self::AlwaysHidden,
139 Self::VisibleWhenNeeded,
140 Self::AlwaysVisible,
141 ];
142}
143
144#[derive(Clone, Copy, Debug, PartialEq, Eq)]
146#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
147pub struct ScrollSource {
148 pub scroll_bar: bool,
153
154 pub drag: bool,
156
157 pub mouse_wheel: bool,
160}
161
162impl Default for ScrollSource {
163 fn default() -> Self {
164 Self::ALL
165 }
166}
167
168impl ScrollSource {
169 pub const NONE: Self = Self {
170 scroll_bar: false,
171 drag: false,
172 mouse_wheel: false,
173 };
174 pub const ALL: Self = Self {
175 scroll_bar: true,
176 drag: true,
177 mouse_wheel: true,
178 };
179 pub const SCROLL_BAR: Self = Self {
180 scroll_bar: true,
181 drag: false,
182 mouse_wheel: false,
183 };
184 pub const DRAG: Self = Self {
185 scroll_bar: false,
186 drag: true,
187 mouse_wheel: false,
188 };
189 pub const MOUSE_WHEEL: Self = Self {
190 scroll_bar: false,
191 drag: false,
192 mouse_wheel: true,
193 };
194
195 #[inline]
197 pub fn is_none(&self) -> bool {
198 self == &Self::NONE
199 }
200
201 #[inline]
203 pub fn any(&self) -> bool {
204 self.scroll_bar | self.drag | self.mouse_wheel
205 }
206
207 #[inline]
209 pub fn is_all(&self) -> bool {
210 self.scroll_bar & self.drag & self.mouse_wheel
211 }
212}
213
214impl BitOr for ScrollSource {
215 type Output = Self;
216
217 #[inline]
218 fn bitor(self, rhs: Self) -> Self::Output {
219 Self {
220 scroll_bar: self.scroll_bar | rhs.scroll_bar,
221 drag: self.drag | rhs.drag,
222 mouse_wheel: self.mouse_wheel | rhs.mouse_wheel,
223 }
224 }
225}
226
227#[expect(clippy::suspicious_arithmetic_impl)]
228impl Add for ScrollSource {
229 type Output = Self;
230
231 #[inline]
232 fn add(self, rhs: Self) -> Self::Output {
233 self | rhs
234 }
235}
236
237impl BitOrAssign for ScrollSource {
238 #[inline]
239 fn bitor_assign(&mut self, rhs: Self) {
240 *self = *self | rhs;
241 }
242}
243
244impl AddAssign for ScrollSource {
245 #[inline]
246 fn add_assign(&mut self, rhs: Self) {
247 *self = *self + rhs;
248 }
249}
250
251#[derive(Clone, Debug)]
283#[must_use = "You should call .show()"]
284pub struct ScrollArea {
285 direction_enabled: Vec2b,
287
288 auto_shrink: Vec2b,
289 max_size: Vec2,
290 min_scrolled_size: Vec2,
291 scroll_bar_visibility: ScrollBarVisibility,
292 scroll_bar_rect: Option<Rect>,
293 id_salt: Option<Id>,
294 offset_x: Option<f32>,
295 offset_y: Option<f32>,
296 on_hover_cursor: Option<CursorIcon>,
297 on_drag_cursor: Option<CursorIcon>,
298 scroll_source: ScrollSource,
299 wheel_scroll_multiplier: Vec2,
300
301 content_margin: Option<Margin>,
302
303 stick_to_end: Vec2b,
307
308 animated: bool,
310}
311
312impl ScrollArea {
313 #[inline]
315 pub fn horizontal() -> Self {
316 Self::new([true, false])
317 }
318
319 #[inline]
321 pub fn vertical() -> Self {
322 Self::new([false, true])
323 }
324
325 #[inline]
327 pub fn both() -> Self {
328 Self::new([true, true])
329 }
330
331 #[inline]
334 pub fn neither() -> Self {
335 Self::new([false, false])
336 }
337
338 pub fn new(direction_enabled: impl Into<Vec2b>) -> Self {
341 Self {
342 direction_enabled: direction_enabled.into(),
343 auto_shrink: Vec2b::TRUE,
344 max_size: Vec2::INFINITY,
345 min_scrolled_size: Vec2::splat(64.0),
346 scroll_bar_visibility: Default::default(),
347 scroll_bar_rect: None,
348 id_salt: None,
349 offset_x: None,
350 offset_y: None,
351 on_hover_cursor: None,
352 on_drag_cursor: None,
353 scroll_source: ScrollSource::default(),
354 wheel_scroll_multiplier: Vec2::splat(1.0),
355 content_margin: None,
356 stick_to_end: Vec2b::FALSE,
357 animated: true,
358 }
359 }
360
361 #[inline]
367 pub fn max_width(mut self, max_width: f32) -> Self {
368 self.max_size.x = max_width;
369 self
370 }
371
372 #[inline]
378 pub fn max_height(mut self, max_height: f32) -> Self {
379 self.max_size.y = max_height;
380 self
381 }
382
383 #[inline]
390 pub fn min_scrolled_width(mut self, min_scrolled_width: f32) -> Self {
391 self.min_scrolled_size.x = min_scrolled_width;
392 self
393 }
394
395 #[inline]
402 pub fn min_scrolled_height(mut self, min_scrolled_height: f32) -> Self {
403 self.min_scrolled_size.y = min_scrolled_height;
404 self
405 }
406
407 #[inline]
411 pub fn scroll_bar_visibility(mut self, scroll_bar_visibility: ScrollBarVisibility) -> Self {
412 self.scroll_bar_visibility = scroll_bar_visibility;
413 self
414 }
415
416 #[inline]
421 pub fn scroll_bar_rect(mut self, scroll_bar_rect: Rect) -> Self {
422 self.scroll_bar_rect = Some(scroll_bar_rect);
423 self
424 }
425
426 #[inline]
428 #[deprecated = "Renamed id_salt"]
429 pub fn id_source(self, id_salt: impl std::hash::Hash) -> Self {
430 self.id_salt(id_salt)
431 }
432
433 #[inline]
435 pub fn id_salt(mut self, id_salt: impl std::hash::Hash) -> Self {
436 self.id_salt = Some(Id::new(id_salt));
437 self
438 }
439
440 #[inline]
448 pub fn scroll_offset(mut self, offset: Vec2) -> Self {
449 self.offset_x = Some(offset.x);
450 self.offset_y = Some(offset.y);
451 self
452 }
453
454 #[inline]
461 pub fn vertical_scroll_offset(mut self, offset: f32) -> Self {
462 self.offset_y = Some(offset);
463 self
464 }
465
466 #[inline]
473 pub fn horizontal_scroll_offset(mut self, offset: f32) -> Self {
474 self.offset_x = Some(offset);
475 self
476 }
477
478 #[inline]
485 pub fn on_hover_cursor(mut self, cursor: CursorIcon) -> Self {
486 self.on_hover_cursor = Some(cursor);
487 self
488 }
489
490 #[inline]
497 pub fn on_drag_cursor(mut self, cursor: CursorIcon) -> Self {
498 self.on_drag_cursor = Some(cursor);
499 self
500 }
501
502 #[inline]
504 pub fn hscroll(mut self, hscroll: bool) -> Self {
505 self.direction_enabled[0] = hscroll;
506 self
507 }
508
509 #[inline]
511 pub fn vscroll(mut self, vscroll: bool) -> Self {
512 self.direction_enabled[1] = vscroll;
513 self
514 }
515
516 #[inline]
520 pub fn scroll(mut self, direction_enabled: impl Into<Vec2b>) -> Self {
521 self.direction_enabled = direction_enabled.into();
522 self
523 }
524
525 #[deprecated = "Use `ScrollArea::scroll_source()"]
535 #[inline]
536 pub fn enable_scrolling(mut self, enable: bool) -> Self {
537 self.scroll_source = if enable {
538 ScrollSource::ALL
539 } else {
540 ScrollSource::NONE
541 };
542 self
543 }
544
545 #[deprecated = "Use `ScrollArea::scroll_source()"]
553 #[inline]
554 pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self {
555 self.scroll_source.drag = drag_to_scroll;
556 self
557 }
558
559 #[inline]
561 pub fn scroll_source(mut self, scroll_source: ScrollSource) -> Self {
562 self.scroll_source = scroll_source;
563 self
564 }
565
566 #[inline]
572 pub fn wheel_scroll_multiplier(mut self, multiplier: Vec2) -> Self {
573 self.wheel_scroll_multiplier = multiplier;
574 self
575 }
576
577 #[inline]
584 pub fn auto_shrink(mut self, auto_shrink: impl Into<Vec2b>) -> Self {
585 self.auto_shrink = auto_shrink.into();
586 self
587 }
588
589 #[inline]
593 pub fn animated(mut self, animated: bool) -> Self {
594 self.animated = animated;
595 self
596 }
597
598 pub(crate) fn is_any_scroll_enabled(&self) -> bool {
600 self.direction_enabled[0] || self.direction_enabled[1]
601 }
602
603 #[inline]
610 pub fn content_margin(mut self, margin: impl Into<Margin>) -> Self {
611 self.content_margin = Some(margin.into());
612 self
613 }
614
615 #[inline]
622 pub fn stick_to_right(mut self, stick: bool) -> Self {
623 self.stick_to_end[0] = stick;
624 self
625 }
626
627 #[inline]
634 pub fn stick_to_bottom(mut self, stick: bool) -> Self {
635 self.stick_to_end[1] = stick;
636 self
637 }
638}
639
640struct Prepared {
641 id: Id,
642 state: State,
643
644 auto_shrink: Vec2b,
645
646 direction_enabled: Vec2b,
648
649 show_bars_factor: Vec2,
651
652 current_bar_use: Vec2,
662
663 scroll_bar_visibility: ScrollBarVisibility,
664 scroll_bar_rect: Option<Rect>,
665
666 inner_rect: Rect,
668
669 content_ui: Ui,
670
671 viewport: Rect,
674
675 scroll_source: ScrollSource,
676 wheel_scroll_multiplier: Vec2,
677 stick_to_end: Vec2b,
678
679 saved_scroll_target: [Option<pass_state::ScrollTarget>; 2],
682
683 background_drag_response: Option<Response>,
685
686 animated: bool,
687}
688
689impl ScrollArea {
690 fn begin(self, ui: &mut Ui) -> Prepared {
691 let Self {
692 direction_enabled,
693 auto_shrink,
694 max_size,
695 min_scrolled_size,
696 scroll_bar_visibility,
697 scroll_bar_rect,
698 id_salt,
699 offset_x,
700 offset_y,
701 on_hover_cursor,
702 on_drag_cursor,
703 scroll_source,
704 wheel_scroll_multiplier,
705 content_margin: _, stick_to_end,
707 animated,
708 } = self;
709
710 let ctx = ui.ctx().clone();
711
712 let id_salt = id_salt.unwrap_or_else(|| Id::new("scroll_area"));
713 let id = ui.make_persistent_id(id_salt);
714 ctx.check_for_id_clash(
715 id,
716 Rect::from_min_size(ui.available_rect_before_wrap().min, Vec2::ZERO),
717 "ScrollArea",
718 );
719 let mut state = State::load(&ctx, id).unwrap_or_default();
720
721 state.offset.x = offset_x.unwrap_or(state.offset.x);
722 state.offset.y = offset_y.unwrap_or(state.offset.y);
723
724 let show_bars: Vec2b = match scroll_bar_visibility {
725 ScrollBarVisibility::AlwaysHidden => Vec2b::FALSE,
726 ScrollBarVisibility::VisibleWhenNeeded => state.show_scroll,
727 ScrollBarVisibility::AlwaysVisible => direction_enabled,
728 };
729
730 let show_bars_factor = Vec2::new(
731 ctx.animate_bool_responsive(id.with("h"), show_bars[0]),
732 ctx.animate_bool_responsive(id.with("v"), show_bars[1]),
733 );
734
735 let current_bar_use = show_bars_factor.yx() * ui.spacing().scroll.allocated_width();
736
737 let available_outer = ui.available_rect_before_wrap();
738
739 let outer_size = available_outer.size().at_most(max_size);
740
741 let inner_size = {
742 let mut inner_size = outer_size - current_bar_use;
743
744 for d in 0..2 {
749 if direction_enabled[d] {
750 inner_size[d] = inner_size[d].max(min_scrolled_size[d]);
751 }
752 }
753 inner_size
754 };
755
756 let inner_rect = Rect::from_min_size(available_outer.min, inner_size);
757
758 let mut content_max_size = inner_size;
759
760 if true {
761 } else {
764 for d in 0..2 {
766 if direction_enabled[d] {
767 content_max_size[d] = f32::INFINITY;
768 }
769 }
770 }
771
772 let content_max_rect = Rect::from_min_size(inner_rect.min - state.offset, content_max_size);
773
774 let content_max_rect = content_max_rect
776 .round_to_pixels(ui.pixels_per_point())
777 .round_ui();
778
779 let mut content_ui = ui.new_child(
780 UiBuilder::new()
781 .ui_stack_info(UiStackInfo::new(UiKind::ScrollArea))
782 .max_rect(content_max_rect),
783 );
784
785 {
786 let clip_rect_margin = ui.visuals().clip_rect_margin;
788 let mut content_clip_rect = ui.clip_rect();
789 for d in 0..2 {
790 if direction_enabled[d] {
791 content_clip_rect.min[d] = inner_rect.min[d] - clip_rect_margin;
792 content_clip_rect.max[d] = inner_rect.max[d] + clip_rect_margin;
793 } else {
794 content_clip_rect.max[d] = ui.clip_rect().max[d] - current_bar_use[d];
796 }
797 }
798 content_clip_rect = content_clip_rect.intersect(ui.clip_rect());
800 content_ui.set_clip_rect(content_clip_rect);
801 }
802
803 let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size);
804 let dt = ui.input(|i| i.stable_dt).at_most(0.1);
805
806 let background_drag_response =
807 if scroll_source.drag && ui.is_enabled() && state.content_is_too_large.any() {
808 let content_response_option = state
812 .interact_rect
813 .map(|rect| ui.interact(rect, id.with("area"), Sense::DRAG));
814
815 if content_response_option
816 .as_ref()
817 .is_some_and(|response| response.dragged())
818 {
819 for d in 0..2 {
820 if direction_enabled[d] {
821 ui.input(|input| {
822 state.offset[d] -= input.pointer.delta()[d];
823 });
824 state.scroll_stuck_to_end[d] = false;
825 state.offset_target[d] = None;
826 }
827 }
828 } else {
829 if content_response_option
831 .as_ref()
832 .is_some_and(|response| response.drag_stopped())
833 {
834 state.vel = direction_enabled.to_vec2()
835 * ui.input(|input| input.pointer.velocity());
836 }
837 for d in 0..2 {
838 let stop_speed = 20.0; let friction_coeff = 1000.0; let friction = friction_coeff * dt;
843 if friction > state.vel[d].abs() || state.vel[d].abs() < stop_speed {
844 state.vel[d] = 0.0;
845 } else {
846 state.vel[d] -= friction * state.vel[d].signum();
847 state.offset[d] -= state.vel[d] * dt;
850 ctx.request_repaint();
851 }
852 }
853 }
854
855 if let Some(response) = &content_response_option {
857 if response.dragged()
858 && let Some(cursor) = on_drag_cursor
859 {
860 ui.set_cursor_icon(cursor);
861 } else if response.hovered()
862 && let Some(cursor) = on_hover_cursor
863 {
864 ui.set_cursor_icon(cursor);
865 }
866 }
867
868 content_response_option
869 } else {
870 None
871 };
872
873 for d in 0..2 {
876 if let Some(scroll_target) = state.offset_target[d] {
877 state.vel[d] = 0.0;
878
879 if (state.offset[d] - scroll_target.target_offset).abs() < 1.0 {
880 state.offset[d] = scroll_target.target_offset;
882 state.offset_target[d] = None;
883 } else {
884 let t = emath::interpolation_factor(
886 scroll_target.animation_time_span,
887 ui.input(|i| i.time),
888 dt,
889 emath::ease_in_ease_out,
890 );
891 if t < 1.0 {
892 state.offset[d] =
893 emath::lerp(state.offset[d]..=scroll_target.target_offset, t);
894 ctx.request_repaint();
895 } else {
896 state.offset[d] = scroll_target.target_offset;
898 state.offset_target[d] = None;
899 }
900 }
901 }
902 }
903
904 let saved_scroll_target = content_ui
905 .ctx()
906 .pass_state_mut(|state| std::mem::take(&mut state.scroll_target));
907
908 Prepared {
909 id,
910 state,
911 auto_shrink,
912 direction_enabled,
913 show_bars_factor,
914 current_bar_use,
915 scroll_bar_visibility,
916 scroll_bar_rect,
917 inner_rect,
918 content_ui,
919 viewport,
920 scroll_source,
921 wheel_scroll_multiplier,
922 stick_to_end,
923 saved_scroll_target,
924 background_drag_response,
925 animated,
926 }
927 }
928
929 pub fn show<R>(
933 self,
934 ui: &mut Ui,
935 add_contents: impl FnOnce(&mut Ui) -> R,
936 ) -> ScrollAreaOutput<R> {
937 self.show_viewport_dyn(ui, Box::new(|ui, _viewport| add_contents(ui)))
938 }
939
940 pub fn show_rows<R>(
957 self,
958 ui: &mut Ui,
959 row_height_sans_spacing: f32,
960 total_rows: usize,
961 add_contents: impl FnOnce(&mut Ui, std::ops::Range<usize>) -> R,
962 ) -> ScrollAreaOutput<R> {
963 let spacing = ui.spacing().item_spacing;
964 let row_height_with_spacing = row_height_sans_spacing + spacing.y;
965 self.show_viewport(ui, |ui, viewport| {
966 ui.set_height((row_height_with_spacing * total_rows as f32 - spacing.y).at_least(0.0));
967
968 let mut min_row = (viewport.min.y / row_height_with_spacing).floor() as usize;
969 let mut max_row = (viewport.max.y / row_height_with_spacing).ceil() as usize + 1;
970 if max_row > total_rows {
971 let diff = max_row.saturating_sub(min_row);
972 max_row = total_rows;
973 min_row = total_rows.saturating_sub(diff);
974 }
975
976 let y_min = ui.max_rect().top() + min_row as f32 * row_height_with_spacing;
977 let y_max = ui.max_rect().top() + max_row as f32 * row_height_with_spacing;
978
979 let rect = Rect::from_x_y_ranges(ui.max_rect().x_range(), y_min..=y_max);
980
981 ui.scope_builder(UiBuilder::new().max_rect(rect), |viewport_ui| {
982 viewport_ui.skip_ahead_auto_ids(min_row); add_contents(viewport_ui, min_row..max_row)
984 })
985 .inner
986 })
987 }
988
989 pub fn show_viewport<R>(
994 self,
995 ui: &mut Ui,
996 add_contents: impl FnOnce(&mut Ui, Rect) -> R,
997 ) -> ScrollAreaOutput<R> {
998 self.show_viewport_dyn(ui, Box::new(add_contents))
999 }
1000
1001 fn show_viewport_dyn<'c, R>(
1002 self,
1003 ui: &mut Ui,
1004 add_contents: Box<dyn FnOnce(&mut Ui, Rect) -> R + 'c>,
1005 ) -> ScrollAreaOutput<R> {
1006 let margin = self
1007 .content_margin
1008 .unwrap_or_else(|| ui.spacing().scroll.content_margin);
1009
1010 let mut prepared = self.begin(ui);
1011 let id = prepared.id;
1012 let inner_rect = prepared.inner_rect;
1013
1014 let inner = crate::Frame::NONE
1015 .inner_margin(margin)
1016 .show(&mut prepared.content_ui, |ui| {
1017 add_contents(ui, prepared.viewport)
1018 })
1019 .inner;
1020
1021 let (content_size, state) = prepared.end(ui);
1022 let output = ScrollAreaOutput {
1023 inner,
1024 id,
1025 state,
1026 content_size,
1027 inner_rect,
1028 };
1029
1030 paint_fade_areas(ui, &output);
1031
1032 output
1033 }
1034}
1035
1036impl Prepared {
1037 fn end(self, ui: &mut Ui) -> (Vec2, State) {
1039 let Self {
1040 id,
1041 mut state,
1042 inner_rect,
1043 auto_shrink,
1044 direction_enabled,
1045 mut show_bars_factor,
1046 current_bar_use,
1047 scroll_bar_visibility,
1048 scroll_bar_rect,
1049 content_ui,
1050 viewport: _,
1051 scroll_source,
1052 wheel_scroll_multiplier,
1053 stick_to_end,
1054 saved_scroll_target,
1055 background_drag_response,
1056 animated,
1057 } = self;
1058
1059 let content_size = content_ui.min_size();
1060
1061 let scroll_delta = content_ui
1062 .ctx()
1063 .pass_state_mut(|state| std::mem::take(&mut state.scroll_delta));
1064
1065 for d in 0..2 {
1066 let mut delta = -scroll_delta.0[d];
1068 let mut animation = scroll_delta.1;
1069
1070 let scroll_target = content_ui
1073 .ctx()
1074 .pass_state_mut(|state| state.scroll_target[d].take());
1075
1076 if direction_enabled[d] {
1077 if let Some(target) = scroll_target {
1078 let pass_state::ScrollTarget {
1079 range,
1080 align,
1081 animation: animation_update,
1082 } = target;
1083 let min = content_ui.min_rect().min[d];
1084 let clip_rect = content_ui.clip_rect();
1085 let visible_range = min..=min + clip_rect.size()[d];
1086 let (start, end) = (range.min, range.max);
1087 let clip_start = clip_rect.min[d];
1088 let clip_end = clip_rect.max[d];
1089 let mut spacing = content_ui.spacing().item_spacing[d];
1090
1091 let delta_update = if let Some(align) = align {
1092 let center_factor = align.to_factor();
1093
1094 let offset =
1095 lerp(range, center_factor) - lerp(visible_range, center_factor);
1096
1097 spacing *= remap(center_factor, 0.0..=1.0, -1.0..=1.0);
1099
1100 offset + spacing - state.offset[d]
1101 } else if start < clip_start && end < clip_end {
1102 -(clip_start - start + spacing).min(clip_end - end - spacing)
1103 } else if end > clip_end && start > clip_start {
1104 (end - clip_end + spacing).min(start - clip_start - spacing)
1105 } else {
1106 0.0
1108 };
1109
1110 delta += delta_update;
1111 animation = animation_update;
1112 }
1113
1114 if delta != 0.0 {
1115 let target_offset = state.offset[d] + delta;
1116
1117 if !animated {
1118 state.offset[d] = target_offset;
1119 } else if let Some(animation) = &mut state.offset_target[d] {
1120 animation.target_offset = target_offset;
1123 } else {
1124 let now = ui.input(|i| i.time);
1126 let animation_duration = (delta.abs() / animation.points_per_second)
1127 .clamp(animation.duration.min, animation.duration.max);
1128 state.offset_target[d] = Some(ScrollingToTarget {
1129 animation_time_span: (now, now + animation_duration as f64),
1130 target_offset,
1131 });
1132 }
1133 ui.request_repaint();
1134 }
1135 }
1136 }
1137
1138 ui.ctx().pass_state_mut(|state| {
1140 for d in 0..2 {
1141 if saved_scroll_target[d].is_some() {
1142 state.scroll_target[d] = saved_scroll_target[d].clone();
1143 }
1144 }
1145 });
1146
1147 let inner_rect = {
1148 let mut inner_size = inner_rect.size();
1150
1151 for d in 0..2 {
1152 inner_size[d] = match (direction_enabled[d], auto_shrink[d]) {
1153 (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]), };
1158 }
1159
1160 Rect::from_min_size(inner_rect.min, inner_size)
1161 };
1162
1163 let outer_rect = Rect::from_min_size(inner_rect.min, inner_rect.size() + current_bar_use);
1164
1165 let content_is_too_large = Vec2b::new(
1166 direction_enabled[0] && inner_rect.width() < content_size.x,
1167 direction_enabled[1] && inner_rect.height() < content_size.y,
1168 );
1169
1170 let max_offset = content_size - inner_rect.size();
1171
1172 let is_dragging_background = background_drag_response
1174 .as_ref()
1175 .is_some_and(|r| r.dragged());
1176
1177 let is_hovering_outer_rect = ui.rect_contains_pointer(outer_rect)
1178 && ui.ctx().dragged_id().is_none()
1179 || is_dragging_background;
1180
1181 if scroll_source.mouse_wheel && ui.is_enabled() && is_hovering_outer_rect {
1182 let always_scroll_enabled_direction = ui.style().always_scroll_the_only_direction
1183 && direction_enabled[0] != direction_enabled[1];
1184 for d in 0..2 {
1185 if direction_enabled[d] {
1186 let scroll_delta = ui.input(|input| {
1187 if always_scroll_enabled_direction {
1188 input.smooth_scroll_delta()[0] + input.smooth_scroll_delta()[1]
1190 } else {
1191 input.smooth_scroll_delta()[d]
1192 }
1193 });
1194 let scroll_delta = scroll_delta * wheel_scroll_multiplier[d];
1195
1196 let scrolling_up = state.offset[d] > 0.0 && scroll_delta > 0.0;
1197 let scrolling_down = state.offset[d] < max_offset[d] && scroll_delta < 0.0;
1198
1199 if scrolling_up || scrolling_down {
1200 state.offset[d] -= scroll_delta;
1201
1202 ui.input_mut(|input| {
1204 if always_scroll_enabled_direction {
1205 input.smooth_scroll_delta = Vec2::ZERO;
1206 } else {
1207 input.smooth_scroll_delta[d] = 0.0;
1208 }
1209 });
1210
1211 state.scroll_stuck_to_end[d] = false;
1212 state.offset_target[d] = None;
1213 }
1214 }
1215 }
1216 }
1217
1218 let show_scroll_this_frame = match scroll_bar_visibility {
1219 ScrollBarVisibility::AlwaysHidden => Vec2b::FALSE,
1220 ScrollBarVisibility::VisibleWhenNeeded => content_is_too_large,
1221 ScrollBarVisibility::AlwaysVisible => direction_enabled,
1222 };
1223
1224 if show_scroll_this_frame[0] && show_bars_factor.x <= 0.0 {
1226 show_bars_factor.x = ui.ctx().animate_bool_responsive(id.with("h"), true);
1227 }
1228 if show_scroll_this_frame[1] && show_bars_factor.y <= 0.0 {
1229 show_bars_factor.y = ui.ctx().animate_bool_responsive(id.with("v"), true);
1230 }
1231
1232 let scroll_style = ui.spacing().scroll;
1233
1234 let scroll_bar_rect = scroll_bar_rect.unwrap_or(inner_rect);
1236 for d in 0..2 {
1237 if stick_to_end[d] && state.scroll_stuck_to_end[d] {
1239 state.offset[d] = content_size[d] - inner_rect.size()[d];
1240 }
1241
1242 let show_factor = show_bars_factor[d];
1243 if show_factor == 0.0 {
1244 state.scroll_bar_interaction[d] = false;
1245 continue;
1246 }
1247
1248 let interact_id = id.with(d);
1249
1250 let inner_margin = show_factor * scroll_style.bar_inner_margin;
1252 let outer_margin = show_factor * scroll_style.bar_outer_margin;
1253
1254 let mut max_cross = outer_rect.max[1 - d] - outer_margin;
1257
1258 if ui.clip_rect().max[1 - d] - outer_margin < max_cross {
1259 max_cross = ui.clip_rect().max[1 - d] - outer_margin;
1269 }
1270
1271 let full_width = scroll_style.bar_width;
1272
1273 let max_bar_rect = if d == 0 {
1276 outer_rect.with_min_y(max_cross - full_width)
1277 } else {
1278 outer_rect.with_min_x(max_cross - full_width)
1279 };
1280
1281 let sense = if scroll_source.scroll_bar && ui.is_enabled() {
1282 Sense::CLICK | Sense::DRAG
1283 } else {
1284 Sense::hover()
1285 };
1286
1287 let response = ui.interact(max_bar_rect, interact_id, sense);
1292
1293 response.widget_info(|| WidgetInfo::new(crate::WidgetType::ScrollBar));
1294
1295 let cross = if scroll_style.floating {
1298 let is_hovering_bar_area = response.hovered() || state.scroll_bar_interaction[d];
1299
1300 let is_hovering_bar_area_t = ui
1301 .ctx()
1302 .animate_bool_responsive(id.with((d, "bar_hover")), is_hovering_bar_area);
1303
1304 let width = show_factor
1305 * lerp(
1306 scroll_style.floating_width..=full_width,
1307 is_hovering_bar_area_t,
1308 );
1309
1310 let min_cross = max_cross - width;
1311 Rangef::new(min_cross, max_cross)
1312 } else {
1313 let min_cross = inner_rect.max[1 - d] + inner_margin;
1314 Rangef::new(min_cross, max_cross)
1315 };
1316
1317 let outer_scroll_bar_rect = if d == 0 {
1318 Rect::from_x_y_ranges(scroll_bar_rect.x_range(), cross)
1319 } else {
1320 Rect::from_x_y_ranges(cross, scroll_bar_rect.y_range())
1321 };
1322
1323 let from_content = |content| {
1324 remap_clamp(
1325 content,
1326 0.0..=content_size[d],
1327 scroll_bar_rect.min[d]..=scroll_bar_rect.max[d],
1328 )
1329 };
1330
1331 let calculate_handle_rect = |d, offset: &Vec2| {
1332 let handle_size = if d == 0 {
1333 from_content(offset.x + inner_rect.width()) - from_content(offset.x)
1334 } else {
1335 from_content(offset.y + inner_rect.height()) - from_content(offset.y)
1336 }
1337 .max(scroll_style.handle_min_length);
1338
1339 let handle_start_point = remap_clamp(
1340 offset[d],
1341 0.0..=max_offset[d],
1342 scroll_bar_rect.min[d]..=(scroll_bar_rect.max[d] - handle_size),
1343 );
1344
1345 if d == 0 {
1346 Rect::from_min_max(
1347 pos2(handle_start_point, cross.min),
1348 pos2(handle_start_point + handle_size, cross.max),
1349 )
1350 } else {
1351 Rect::from_min_max(
1352 pos2(cross.min, handle_start_point),
1353 pos2(cross.max, handle_start_point + handle_size),
1354 )
1355 }
1356 };
1357
1358 let handle_rect = calculate_handle_rect(d, &state.offset);
1359
1360 state.scroll_bar_interaction[d] = response.hovered() || response.dragged();
1361
1362 if let Some(pointer_pos) = response.interact_pointer_pos() {
1363 let scroll_start_offset_from_top_left = state.scroll_start_offset_from_top_left[d]
1364 .get_or_insert_with(|| {
1365 if handle_rect.contains(pointer_pos) {
1366 pointer_pos[d] - handle_rect.min[d]
1367 } else {
1368 let handle_top_pos_at_bottom =
1369 scroll_bar_rect.max[d] - handle_rect.size()[d];
1370 let new_handle_top_pos = (pointer_pos[d] - handle_rect.size()[d] / 2.0)
1372 .clamp(scroll_bar_rect.min[d], handle_top_pos_at_bottom);
1373 pointer_pos[d] - new_handle_top_pos
1374 }
1375 });
1376
1377 let new_handle_top = pointer_pos[d] - *scroll_start_offset_from_top_left;
1378 let handle_travel =
1379 scroll_bar_rect.min[d]..=(scroll_bar_rect.max[d] - handle_rect.size()[d]);
1380 state.offset[d] = if handle_travel.start() == handle_travel.end() {
1381 0.0
1382 } else {
1383 remap(new_handle_top, handle_travel, 0.0..=max_offset[d])
1384 };
1385
1386 state.scroll_stuck_to_end[d] = false;
1388 state.offset_target[d] = None;
1389 } else {
1390 state.scroll_start_offset_from_top_left[d] = None;
1391 }
1392
1393 let unbounded_offset = state.offset[d];
1394 state.offset[d] = state.offset[d].max(0.0);
1395 state.offset[d] = state.offset[d].min(max_offset[d]);
1396
1397 if state.offset[d] != unbounded_offset {
1398 state.vel[d] = 0.0;
1399 }
1400
1401 if ui.is_rect_visible(outer_scroll_bar_rect) {
1402 let handle_rect = calculate_handle_rect(d, &state.offset);
1404
1405 let visuals = if scroll_source.scroll_bar && ui.is_enabled() {
1406 let is_hovering_handle = response.hovered()
1409 && ui.input(|i| {
1410 i.pointer
1411 .latest_pos()
1412 .is_some_and(|p| handle_rect.contains(p))
1413 });
1414 let visuals = ui.visuals();
1415 if response.is_pointer_button_down_on() {
1416 &visuals.widgets.active
1417 } else if is_hovering_handle {
1418 &visuals.widgets.hovered
1419 } else {
1420 &visuals.widgets.inactive
1421 }
1422 } else {
1423 &ui.visuals().widgets.inactive
1424 };
1425
1426 let handle_opacity = if scroll_style.floating {
1427 if response.hovered() || response.dragged() {
1428 scroll_style.interact_handle_opacity
1429 } else {
1430 let is_hovering_outer_rect_t = ui.ctx().animate_bool_responsive(
1431 id.with((d, "is_hovering_outer_rect")),
1432 is_hovering_outer_rect,
1433 );
1434 lerp(
1435 scroll_style.dormant_handle_opacity
1436 ..=scroll_style.active_handle_opacity,
1437 is_hovering_outer_rect_t,
1438 )
1439 }
1440 } else {
1441 1.0
1442 };
1443
1444 let background_opacity = if scroll_style.floating {
1445 if response.hovered() || response.dragged() {
1446 scroll_style.interact_background_opacity
1447 } else if is_hovering_outer_rect {
1448 scroll_style.active_background_opacity
1449 } else {
1450 scroll_style.dormant_background_opacity
1451 }
1452 } else {
1453 1.0
1454 };
1455
1456 let handle_color = if scroll_style.foreground_color {
1457 visuals.fg_stroke.color
1458 } else {
1459 visuals.bg_fill
1460 };
1461
1462 ui.painter().add(epaint::Shape::rect_filled(
1464 outer_scroll_bar_rect,
1465 visuals.corner_radius,
1466 ui.visuals()
1467 .extreme_bg_color
1468 .gamma_multiply(background_opacity),
1469 ));
1470
1471 ui.painter().add(epaint::Shape::rect_filled(
1473 handle_rect,
1474 visuals.corner_radius,
1475 handle_color.gamma_multiply(handle_opacity),
1476 ));
1477 }
1478 }
1479
1480 ui.advance_cursor_after_rect(outer_rect);
1481
1482 if show_scroll_this_frame != state.show_scroll {
1483 ui.request_repaint();
1484 }
1485
1486 let available_offset = content_size - inner_rect.size();
1487 state.offset = state.offset.min(available_offset);
1488 state.offset = state.offset.max(Vec2::ZERO);
1489
1490 state.scroll_stuck_to_end = Vec2b::new(
1496 (state.offset[0] == available_offset[0])
1497 || (self.stick_to_end[0] && available_offset[0] < 0.0),
1498 (state.offset[1] == available_offset[1])
1499 || (self.stick_to_end[1] && available_offset[1] < 0.0),
1500 );
1501
1502 state.show_scroll = show_scroll_this_frame;
1503 state.content_is_too_large = content_is_too_large;
1504 state.interact_rect = Some(inner_rect);
1505
1506 state.store(ui.ctx(), id);
1507
1508 (content_size, state)
1509 }
1510}
1511
1512fn paint_fade_areas<R>(ui: &Ui, scroll_output: &ScrollAreaOutput<R>) {
1515 let crate::style::ScrollFadeStyle {
1516 strength,
1517 size: fade_size,
1518 } = ui.spacing().scroll.fade;
1519
1520 if strength <= 0.0 {
1521 return;
1522 }
1523
1524 let bg = ui.stack().bg_color();
1525
1526 let offset = scroll_output.state.offset;
1527 let overflow = scroll_output.content_size - scroll_output.inner_rect.size();
1528
1529 let paint_rect = scroll_output
1530 .inner_rect
1531 .intersect(ui.min_rect())
1532 .expand(ui.visuals().clip_rect_margin);
1533
1534 if 0.0 < offset.y {
1536 let t = (offset.y / fade_size).clamp(0.0, 1.0) * strength;
1537 let bg_faded = bg.gamma_multiply(t);
1538 let rect = Rect::from_min_max(
1539 paint_rect.left_top(),
1540 pos2(paint_rect.right(), paint_rect.top() + fade_size),
1541 );
1542 ui.painter().add(Shape::gradient_rect(
1543 rect,
1544 Direction::TopDown,
1545 [bg_faded, Color32::TRANSPARENT],
1546 ));
1547 }
1548
1549 let distance_from_bottom = overflow.y - offset.y;
1551 if 0.0 < distance_from_bottom {
1552 let t = (distance_from_bottom / fade_size).clamp(0.0, 1.0) * strength;
1553 let bg_faded = bg.gamma_multiply(t);
1554 let rect = Rect::from_min_max(
1555 pos2(paint_rect.left(), paint_rect.bottom() - fade_size),
1556 paint_rect.right_bottom(),
1557 );
1558 ui.painter().add(Shape::gradient_rect(
1559 rect,
1560 Direction::BottomUp,
1561 [bg_faded, Color32::TRANSPARENT],
1562 ));
1563 }
1564
1565 if 0.0 < offset.x {
1567 let t = (offset.x / fade_size).clamp(0.0, 1.0) * strength;
1568 let bg_faded = bg.gamma_multiply(t);
1569 let rect = Rect::from_min_max(
1570 paint_rect.left_top(),
1571 pos2(paint_rect.left() + fade_size, paint_rect.bottom()),
1572 );
1573 ui.painter().add(Shape::gradient_rect(
1574 rect,
1575 Direction::LeftToRight,
1576 [bg_faded, Color32::TRANSPARENT],
1577 ));
1578 }
1579
1580 let distance_from_right = overflow.x - offset.x;
1582 if 0.0 < distance_from_right {
1583 let t = (distance_from_right / fade_size).clamp(0.0, 1.0) * strength;
1584 let bg_faded = bg.gamma_multiply(t);
1585 let rect = Rect::from_min_max(
1586 pos2(paint_rect.right() - fade_size, paint_rect.top()),
1587 paint_rect.right_bottom(),
1588 );
1589 ui.painter().add(Shape::gradient_rect(
1590 rect,
1591 Direction::RightToLeft,
1592 [bg_faded, Color32::TRANSPARENT],
1593 ));
1594 }
1595}