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 let mut had_explicit_scroll_adjustment = Vec2b::FALSE;
1066
1067 for d in 0..2 {
1068 let mut delta = -scroll_delta.0[d];
1070 let mut animation = scroll_delta.1;
1071
1072 let scroll_target = content_ui
1075 .ctx()
1076 .pass_state_mut(|state| state.scroll_target[d].take());
1077
1078 if direction_enabled[d] {
1079 if let Some(target) = scroll_target {
1080 let pass_state::ScrollTarget {
1081 range,
1082 align,
1083 animation: animation_update,
1084 } = target;
1085 let min = content_ui.min_rect().min[d];
1086 let clip_rect = content_ui.clip_rect();
1087 let visible_range = min..=min + clip_rect.size()[d];
1088 let (start, end) = (range.min, range.max);
1089 let clip_start = clip_rect.min[d];
1090 let clip_end = clip_rect.max[d];
1091 let mut spacing = content_ui.spacing().item_spacing[d];
1092
1093 let delta_update = if let Some(align) = align {
1094 let center_factor = align.to_factor();
1095
1096 let offset =
1097 lerp(range, center_factor) - lerp(visible_range, center_factor);
1098
1099 spacing *= remap(center_factor, 0.0..=1.0, -1.0..=1.0);
1101
1102 offset + spacing - state.offset[d]
1103 } else if start < clip_start && end < clip_end {
1104 -(clip_start - start + spacing).min(clip_end - end - spacing)
1105 } else if end > clip_end && start > clip_start {
1106 (end - clip_end + spacing).min(start - clip_start - spacing)
1107 } else {
1108 0.0
1110 };
1111
1112 delta += delta_update;
1113 animation = animation_update;
1114 }
1115
1116 if delta != 0.0 {
1117 let target_offset = state.offset[d] + delta;
1118
1119 if !animated {
1120 state.offset[d] = target_offset;
1121 } else if let Some(animation) = &mut state.offset_target[d] {
1122 animation.target_offset = target_offset;
1125 } else {
1126 let now = ui.input(|i| i.time);
1128 let animation_duration = (delta.abs() / animation.points_per_second)
1129 .clamp(animation.duration.min, animation.duration.max);
1130 state.offset_target[d] = Some(ScrollingToTarget {
1131 animation_time_span: (now, now + animation_duration as f64),
1132 target_offset,
1133 });
1134 }
1135 ui.request_repaint();
1136 }
1137 }
1138
1139 if delta != 0.0 {
1140 had_explicit_scroll_adjustment[d] = true;
1141 }
1142 }
1143
1144 ui.ctx().pass_state_mut(|state| {
1146 for d in 0..2 {
1147 if saved_scroll_target[d].is_some() {
1148 state.scroll_target[d] = saved_scroll_target[d].clone();
1149 }
1150 }
1151 });
1152
1153 let inner_rect = {
1154 let mut inner_size = inner_rect.size();
1156
1157 for d in 0..2 {
1158 inner_size[d] = match (direction_enabled[d], auto_shrink[d]) {
1159 (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]), };
1164 }
1165
1166 Rect::from_min_size(inner_rect.min, inner_size)
1167 };
1168
1169 let outer_rect = Rect::from_min_size(inner_rect.min, inner_rect.size() + current_bar_use);
1170
1171 let content_is_too_large = Vec2b::new(
1172 direction_enabled[0] && inner_rect.width() < content_size.x,
1173 direction_enabled[1] && inner_rect.height() < content_size.y,
1174 );
1175
1176 let max_offset = content_size - inner_rect.size();
1177
1178 let is_dragging_background = background_drag_response
1180 .as_ref()
1181 .is_some_and(|r| r.dragged());
1182
1183 let is_hovering_outer_rect = ui.rect_contains_pointer(outer_rect)
1184 && ui.ctx().dragged_id().is_none()
1185 || is_dragging_background;
1186
1187 if scroll_source.mouse_wheel && ui.is_enabled() && is_hovering_outer_rect {
1188 let always_scroll_enabled_direction = ui.style().always_scroll_the_only_direction
1189 && direction_enabled[0] != direction_enabled[1];
1190 for d in 0..2 {
1191 if direction_enabled[d] {
1192 let scroll_delta = ui.input(|input| {
1193 if always_scroll_enabled_direction {
1194 input.smooth_scroll_delta()[0] + input.smooth_scroll_delta()[1]
1196 } else {
1197 input.smooth_scroll_delta()[d]
1198 }
1199 });
1200 let scroll_delta = scroll_delta * wheel_scroll_multiplier[d];
1201
1202 let scrolling_up = state.offset[d] > 0.0 && scroll_delta > 0.0;
1203 let scrolling_down = state.offset[d] < max_offset[d] && scroll_delta < 0.0;
1204
1205 if scrolling_up || scrolling_down {
1206 state.offset[d] -= scroll_delta;
1207
1208 ui.input_mut(|input| {
1210 if always_scroll_enabled_direction {
1211 input.smooth_scroll_delta = Vec2::ZERO;
1212 } else {
1213 input.smooth_scroll_delta[d] = 0.0;
1214 }
1215 });
1216
1217 state.scroll_stuck_to_end[d] = false;
1218 state.offset_target[d] = None;
1219 }
1220 }
1221 }
1222 }
1223
1224 let show_scroll_this_frame = match scroll_bar_visibility {
1225 ScrollBarVisibility::AlwaysHidden => Vec2b::FALSE,
1226 ScrollBarVisibility::VisibleWhenNeeded => content_is_too_large,
1227 ScrollBarVisibility::AlwaysVisible => direction_enabled,
1228 };
1229
1230 if show_scroll_this_frame[0] && show_bars_factor.x <= 0.0 {
1232 show_bars_factor.x = ui.ctx().animate_bool_responsive(id.with("h"), true);
1233 }
1234 if show_scroll_this_frame[1] && show_bars_factor.y <= 0.0 {
1235 show_bars_factor.y = ui.ctx().animate_bool_responsive(id.with("v"), true);
1236 }
1237
1238 let scroll_style = ui.spacing().scroll;
1239
1240 let scroll_bar_rect = scroll_bar_rect.unwrap_or(inner_rect);
1242 for d in 0..2 {
1243 if stick_to_end[d] && state.scroll_stuck_to_end[d] && !had_explicit_scroll_adjustment[d]
1246 {
1247 state.offset[d] = content_size[d] - inner_rect.size()[d];
1248 }
1249
1250 let show_factor = show_bars_factor[d];
1251 if show_factor == 0.0 {
1252 state.scroll_bar_interaction[d] = false;
1253 continue;
1254 }
1255
1256 let interact_id = id.with(d);
1257
1258 let inner_margin = show_factor * scroll_style.bar_inner_margin;
1260 let outer_margin = show_factor * scroll_style.bar_outer_margin;
1261
1262 let mut max_cross = outer_rect.max[1 - d] - outer_margin;
1265
1266 if ui.clip_rect().max[1 - d] - outer_margin < max_cross {
1267 max_cross = ui.clip_rect().max[1 - d] - outer_margin;
1277 }
1278
1279 let full_width = scroll_style.bar_width;
1280
1281 let max_bar_rect = if d == 0 {
1284 outer_rect.with_min_y(max_cross - full_width)
1285 } else {
1286 outer_rect.with_min_x(max_cross - full_width)
1287 };
1288
1289 let sense = if scroll_source.scroll_bar && ui.is_enabled() {
1290 Sense::CLICK | Sense::DRAG
1291 } else {
1292 Sense::hover()
1293 };
1294
1295 let response = ui.interact(max_bar_rect, interact_id, sense);
1300
1301 response.widget_info(|| WidgetInfo::new(crate::WidgetType::ScrollBar));
1302
1303 let cross = if scroll_style.floating {
1306 let is_hovering_bar_area = response.hovered() || state.scroll_bar_interaction[d];
1307
1308 let is_hovering_bar_area_t = ui
1309 .ctx()
1310 .animate_bool_responsive(id.with((d, "bar_hover")), is_hovering_bar_area);
1311
1312 let width = show_factor
1313 * lerp(
1314 scroll_style.floating_width..=full_width,
1315 is_hovering_bar_area_t,
1316 );
1317
1318 let min_cross = max_cross - width;
1319 Rangef::new(min_cross, max_cross)
1320 } else {
1321 let min_cross = inner_rect.max[1 - d] + inner_margin;
1322 Rangef::new(min_cross, max_cross)
1323 };
1324
1325 let outer_scroll_bar_rect = if d == 0 {
1326 Rect::from_x_y_ranges(scroll_bar_rect.x_range(), cross)
1327 } else {
1328 Rect::from_x_y_ranges(cross, scroll_bar_rect.y_range())
1329 };
1330
1331 let from_content = |content| {
1332 remap_clamp(
1333 content,
1334 0.0..=content_size[d],
1335 scroll_bar_rect.min[d]..=scroll_bar_rect.max[d],
1336 )
1337 };
1338
1339 let calculate_handle_rect = |d, offset: &Vec2| {
1340 let handle_size = if d == 0 {
1341 from_content(offset.x + inner_rect.width()) - from_content(offset.x)
1342 } else {
1343 from_content(offset.y + inner_rect.height()) - from_content(offset.y)
1344 }
1345 .max(scroll_style.handle_min_length);
1346
1347 let handle_start_point = remap_clamp(
1348 offset[d],
1349 0.0..=max_offset[d],
1350 scroll_bar_rect.min[d]..=(scroll_bar_rect.max[d] - handle_size),
1351 );
1352
1353 if d == 0 {
1354 Rect::from_min_max(
1355 pos2(handle_start_point, cross.min),
1356 pos2(handle_start_point + handle_size, cross.max),
1357 )
1358 } else {
1359 Rect::from_min_max(
1360 pos2(cross.min, handle_start_point),
1361 pos2(cross.max, handle_start_point + handle_size),
1362 )
1363 }
1364 };
1365
1366 let handle_rect = calculate_handle_rect(d, &state.offset);
1367
1368 state.scroll_bar_interaction[d] = response.hovered() || response.dragged();
1369
1370 if let Some(pointer_pos) = response.interact_pointer_pos() {
1371 let scroll_start_offset_from_top_left = state.scroll_start_offset_from_top_left[d]
1372 .get_or_insert_with(|| {
1373 if handle_rect.contains(pointer_pos) {
1374 pointer_pos[d] - handle_rect.min[d]
1375 } else {
1376 let handle_top_pos_at_bottom =
1377 scroll_bar_rect.max[d] - handle_rect.size()[d];
1378 let new_handle_top_pos = (pointer_pos[d] - handle_rect.size()[d] / 2.0)
1380 .clamp(scroll_bar_rect.min[d], handle_top_pos_at_bottom);
1381 pointer_pos[d] - new_handle_top_pos
1382 }
1383 });
1384
1385 let new_handle_top = pointer_pos[d] - *scroll_start_offset_from_top_left;
1386 let handle_travel =
1387 scroll_bar_rect.min[d]..=(scroll_bar_rect.max[d] - handle_rect.size()[d]);
1388 state.offset[d] = if handle_travel.start() == handle_travel.end() {
1389 0.0
1390 } else {
1391 remap(new_handle_top, handle_travel, 0.0..=max_offset[d])
1392 };
1393
1394 state.scroll_stuck_to_end[d] = false;
1396 state.offset_target[d] = None;
1397 } else {
1398 state.scroll_start_offset_from_top_left[d] = None;
1399 }
1400
1401 let unbounded_offset = state.offset[d];
1402 state.offset[d] = state.offset[d].max(0.0);
1403 state.offset[d] = state.offset[d].min(max_offset[d]);
1404
1405 if state.offset[d] != unbounded_offset {
1406 state.vel[d] = 0.0;
1407 }
1408
1409 if ui.is_rect_visible(outer_scroll_bar_rect) {
1410 let handle_rect = calculate_handle_rect(d, &state.offset);
1412
1413 let visuals = if scroll_source.scroll_bar && ui.is_enabled() {
1414 let is_hovering_handle = response.hovered()
1417 && ui.input(|i| {
1418 i.pointer
1419 .latest_pos()
1420 .is_some_and(|p| handle_rect.contains(p))
1421 });
1422 let visuals = ui.visuals();
1423 if response.is_pointer_button_down_on() {
1424 &visuals.widgets.active
1425 } else if is_hovering_handle {
1426 &visuals.widgets.hovered
1427 } else {
1428 &visuals.widgets.inactive
1429 }
1430 } else {
1431 &ui.visuals().widgets.inactive
1432 };
1433
1434 let handle_opacity = if scroll_style.floating {
1435 if response.hovered() || response.dragged() {
1436 scroll_style.interact_handle_opacity
1437 } else {
1438 let is_hovering_outer_rect_t = ui.ctx().animate_bool_responsive(
1439 id.with((d, "is_hovering_outer_rect")),
1440 is_hovering_outer_rect,
1441 );
1442 lerp(
1443 scroll_style.dormant_handle_opacity
1444 ..=scroll_style.active_handle_opacity,
1445 is_hovering_outer_rect_t,
1446 )
1447 }
1448 } else {
1449 1.0
1450 };
1451
1452 let background_opacity = if scroll_style.floating {
1453 if response.hovered() || response.dragged() {
1454 scroll_style.interact_background_opacity
1455 } else if is_hovering_outer_rect {
1456 scroll_style.active_background_opacity
1457 } else {
1458 scroll_style.dormant_background_opacity
1459 }
1460 } else {
1461 1.0
1462 };
1463
1464 let handle_color = if scroll_style.foreground_color {
1465 visuals.fg_stroke.color
1466 } else {
1467 visuals.bg_fill
1468 };
1469
1470 ui.painter().add(epaint::Shape::rect_filled(
1472 outer_scroll_bar_rect,
1473 visuals.corner_radius,
1474 ui.visuals()
1475 .extreme_bg_color
1476 .gamma_multiply(background_opacity),
1477 ));
1478
1479 ui.painter().add(epaint::Shape::rect_filled(
1481 handle_rect,
1482 visuals.corner_radius,
1483 handle_color.gamma_multiply(handle_opacity),
1484 ));
1485 }
1486 }
1487
1488 ui.advance_cursor_after_rect(outer_rect);
1489
1490 if show_scroll_this_frame != state.show_scroll {
1491 ui.request_repaint();
1492 }
1493
1494 let available_offset = content_size - inner_rect.size();
1495 state.offset = state.offset.min(available_offset);
1496 state.offset = state.offset.max(Vec2::ZERO);
1497
1498 let suppress_stuck_recompute = Vec2b::new(
1499 had_explicit_scroll_adjustment[0] && state.offset_target[0].is_some(),
1500 had_explicit_scroll_adjustment[1] && state.offset_target[1].is_some(),
1501 );
1502
1503 state.scroll_stuck_to_end = Vec2b::new(
1511 !suppress_stuck_recompute[0]
1512 && ((state.offset[0] == available_offset[0])
1513 || (stick_to_end[0] && available_offset[0] < 0.0)),
1514 !suppress_stuck_recompute[1]
1515 && ((state.offset[1] == available_offset[1])
1516 || (stick_to_end[1] && available_offset[1] < 0.0)),
1517 );
1518
1519 state.show_scroll = show_scroll_this_frame;
1520 state.content_is_too_large = content_is_too_large;
1521 state.interact_rect = Some(inner_rect);
1522
1523 state.store(ui.ctx(), id);
1524
1525 (content_size, state)
1526 }
1527}
1528
1529fn paint_fade_areas<R>(ui: &Ui, scroll_output: &ScrollAreaOutput<R>) {
1532 let crate::style::ScrollFadeStyle {
1533 strength,
1534 size: fade_size,
1535 } = ui.spacing().scroll.fade;
1536
1537 if strength <= 0.0 {
1538 return;
1539 }
1540
1541 let bg = ui.stack().bg_color();
1542
1543 let offset = scroll_output.state.offset;
1544 let overflow = scroll_output.content_size - scroll_output.inner_rect.size();
1545
1546 let paint_rect = scroll_output
1547 .inner_rect
1548 .intersect(ui.min_rect())
1549 .expand(ui.visuals().clip_rect_margin);
1550
1551 if 0.0 < offset.y {
1553 let t = (offset.y / fade_size).clamp(0.0, 1.0) * strength;
1554 let bg_faded = bg.gamma_multiply(t);
1555 let rect = Rect::from_min_max(
1556 paint_rect.left_top(),
1557 pos2(paint_rect.right(), paint_rect.top() + fade_size),
1558 );
1559 ui.painter().add(Shape::gradient_rect(
1560 rect,
1561 Direction::TopDown,
1562 [bg_faded, Color32::TRANSPARENT],
1563 ));
1564 }
1565
1566 let distance_from_bottom = overflow.y - offset.y;
1568 if 0.0 < distance_from_bottom {
1569 let t = (distance_from_bottom / fade_size).clamp(0.0, 1.0) * strength;
1570 let bg_faded = bg.gamma_multiply(t);
1571 let rect = Rect::from_min_max(
1572 pos2(paint_rect.left(), paint_rect.bottom() - fade_size),
1573 paint_rect.right_bottom(),
1574 );
1575 ui.painter().add(Shape::gradient_rect(
1576 rect,
1577 Direction::BottomUp,
1578 [bg_faded, Color32::TRANSPARENT],
1579 ));
1580 }
1581
1582 if 0.0 < offset.x {
1584 let t = (offset.x / fade_size).clamp(0.0, 1.0) * strength;
1585 let bg_faded = bg.gamma_multiply(t);
1586 let rect = Rect::from_min_max(
1587 paint_rect.left_top(),
1588 pos2(paint_rect.left() + fade_size, paint_rect.bottom()),
1589 );
1590 ui.painter().add(Shape::gradient_rect(
1591 rect,
1592 Direction::LeftToRight,
1593 [bg_faded, Color32::TRANSPARENT],
1594 ));
1595 }
1596
1597 let distance_from_right = overflow.x - offset.x;
1599 if 0.0 < distance_from_right {
1600 let t = (distance_from_right / fade_size).clamp(0.0, 1.0) * strength;
1601 let bg_faded = bg.gamma_multiply(t);
1602 let rect = Rect::from_min_max(
1603 pos2(paint_rect.right() - fade_size, paint_rect.top()),
1604 paint_rect.right_bottom(),
1605 );
1606 ui.painter().add(Shape::gradient_rect(
1607 rect,
1608 Direction::RightToLeft,
1609 [bg_faded, Color32::TRANSPARENT],
1610 ));
1611 }
1612}