1#![expect(clippy::needless_range_loop)]
4
5use core::ops::{Add, AddAssign, BitOr, BitOrAssign};
6
7use emath::GuiRounding as _;
8use epaint::{Color32, Direction, Margin, Shape};
9
10use crate::{
11 AsIdSalt, Context, CursorIcon, Id, IdSalt, NumExt as _, Pos2, Rangef, Rect, Response, Sense,
12 Ui, UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b, WidgetInfo, emath, epaint, lerp, pass_state,
13 pos2, remap, 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, Default)]
146#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
147pub enum DragScroll {
148 Never,
150
151 #[default]
154 OnTouch,
155
156 Always,
158}
159
160impl DragScroll {
161 pub fn enabled(self, ctx: &Context) -> bool {
166 match self {
167 Self::Never => false,
168 Self::OnTouch => ctx.input(|i| i.has_touch_screen()),
169 Self::Always => true,
170 }
171 }
172}
173
174impl BitOr for DragScroll {
175 type Output = Self;
176
177 #[inline]
180 fn bitor(self, rhs: Self) -> Self::Output {
181 match (self, rhs) {
182 (Self::Always, _) | (_, Self::Always) => Self::Always,
183 (Self::OnTouch, _) | (_, Self::OnTouch) => Self::OnTouch,
184 (Self::Never, Self::Never) => Self::Never,
185 }
186 }
187}
188
189#[derive(Clone, Copy, Debug, PartialEq, Eq)]
191#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
192pub struct ScrollSource {
193 pub scroll_bar: bool,
198
199 pub drag: DragScroll,
205
206 pub mouse_wheel: bool,
209}
210
211impl Default for ScrollSource {
212 fn default() -> Self {
214 Self {
215 scroll_bar: true,
216 drag: DragScroll::OnTouch,
217 mouse_wheel: true,
218 }
219 }
220}
221
222impl ScrollSource {
223 pub const NONE: Self = Self {
224 scroll_bar: false,
225 drag: DragScroll::Never,
226 mouse_wheel: false,
227 };
228 pub const ALL: Self = Self {
229 scroll_bar: true,
230 drag: DragScroll::Always,
231 mouse_wheel: true,
232 };
233 pub const SCROLL_BAR: Self = Self {
234 scroll_bar: true,
235 drag: DragScroll::Never,
236 mouse_wheel: false,
237 };
238 pub const DRAG: Self = Self {
239 scroll_bar: false,
240 drag: DragScroll::Always,
241 mouse_wheel: false,
242 };
243 pub const MOUSE_WHEEL: Self = Self {
244 scroll_bar: false,
245 drag: DragScroll::Never,
246 mouse_wheel: true,
247 };
248
249 #[inline]
251 pub fn is_none(&self) -> bool {
252 self == &Self::NONE
253 }
254
255 #[inline]
257 pub fn any(&self) -> bool {
258 self.scroll_bar || self.drag != DragScroll::Never || self.mouse_wheel
259 }
260
261 #[inline]
263 pub fn is_all(&self) -> bool {
264 self.scroll_bar && self.drag == DragScroll::Always && self.mouse_wheel
265 }
266}
267
268impl BitOr for ScrollSource {
269 type Output = Self;
270
271 #[inline]
272 fn bitor(self, rhs: Self) -> Self::Output {
273 Self {
274 scroll_bar: self.scroll_bar | rhs.scroll_bar,
275 drag: self.drag | rhs.drag,
276 mouse_wheel: self.mouse_wheel | rhs.mouse_wheel,
277 }
278 }
279}
280
281#[expect(clippy::suspicious_arithmetic_impl)]
282impl Add for ScrollSource {
283 type Output = Self;
284
285 #[inline]
286 fn add(self, rhs: Self) -> Self::Output {
287 self | rhs
288 }
289}
290
291impl BitOrAssign for ScrollSource {
292 #[inline]
293 fn bitor_assign(&mut self, rhs: Self) {
294 *self = *self | rhs;
295 }
296}
297
298impl AddAssign for ScrollSource {
299 #[inline]
300 fn add_assign(&mut self, rhs: Self) {
301 *self = *self + rhs;
302 }
303}
304
305#[derive(Clone, Debug)]
337#[must_use = "You should call .show()"]
338pub struct ScrollArea {
339 direction_enabled: Vec2b,
341
342 auto_shrink: Vec2b,
343 max_size: Vec2,
344 min_scrolled_size: Vec2,
345 scroll_bar_visibility: ScrollBarVisibility,
346 scroll_bar_rect: Option<Rect>,
347 id_salt: Option<IdSalt>,
348 offset_x: Option<f32>,
349 offset_y: Option<f32>,
350 on_hover_cursor: Option<CursorIcon>,
351 on_drag_cursor: Option<CursorIcon>,
352 scroll_source: ScrollSource,
353 wheel_scroll_multiplier: Vec2,
354
355 content_margin: Option<Margin>,
356
357 stick_to_end: Vec2b,
361
362 animated: bool,
364}
365
366impl ScrollArea {
367 #[inline]
369 pub fn horizontal() -> Self {
370 Self::new([true, false])
371 }
372
373 #[inline]
375 pub fn vertical() -> Self {
376 Self::new([false, true])
377 }
378
379 #[inline]
381 pub fn both() -> Self {
382 Self::new([true, true])
383 }
384
385 #[inline]
388 pub fn neither() -> Self {
389 Self::new([false, false])
390 }
391
392 pub fn new(direction_enabled: impl Into<Vec2b>) -> Self {
395 Self {
396 direction_enabled: direction_enabled.into(),
397 auto_shrink: Vec2b::TRUE,
398 max_size: Vec2::INFINITY,
399 min_scrolled_size: Vec2::splat(64.0),
400 scroll_bar_visibility: Default::default(),
401 scroll_bar_rect: None,
402 id_salt: None,
403 offset_x: None,
404 offset_y: None,
405 on_hover_cursor: None,
406 on_drag_cursor: None,
407 scroll_source: ScrollSource::default(),
408 wheel_scroll_multiplier: Vec2::splat(1.0),
409 content_margin: None,
410 stick_to_end: Vec2b::FALSE,
411 animated: true,
412 }
413 }
414
415 #[inline]
421 pub fn max_width(mut self, max_width: f32) -> Self {
422 self.max_size.x = max_width;
423 self
424 }
425
426 #[inline]
432 pub fn max_height(mut self, max_height: f32) -> Self {
433 self.max_size.y = max_height;
434 self
435 }
436
437 #[inline]
444 pub fn min_scrolled_width(mut self, min_scrolled_width: f32) -> Self {
445 self.min_scrolled_size.x = min_scrolled_width;
446 self
447 }
448
449 #[inline]
456 pub fn min_scrolled_height(mut self, min_scrolled_height: f32) -> Self {
457 self.min_scrolled_size.y = min_scrolled_height;
458 self
459 }
460
461 #[inline]
465 pub fn scroll_bar_visibility(mut self, scroll_bar_visibility: ScrollBarVisibility) -> Self {
466 self.scroll_bar_visibility = scroll_bar_visibility;
467 self
468 }
469
470 #[inline]
475 pub fn scroll_bar_rect(mut self, scroll_bar_rect: Rect) -> Self {
476 self.scroll_bar_rect = Some(scroll_bar_rect);
477 self
478 }
479
480 #[inline]
482 pub fn id_salt(mut self, id_salt: impl AsIdSalt) -> Self {
483 self.id_salt = Some(IdSalt::new(id_salt));
484 self
485 }
486
487 #[inline]
495 pub fn scroll_offset(mut self, offset: Vec2) -> Self {
496 self.offset_x = Some(offset.x);
497 self.offset_y = Some(offset.y);
498 self
499 }
500
501 #[inline]
508 pub fn vertical_scroll_offset(mut self, offset: f32) -> Self {
509 self.offset_y = Some(offset);
510 self
511 }
512
513 #[inline]
520 pub fn horizontal_scroll_offset(mut self, offset: f32) -> Self {
521 self.offset_x = Some(offset);
522 self
523 }
524
525 #[inline]
532 pub fn on_hover_cursor(mut self, cursor: CursorIcon) -> Self {
533 self.on_hover_cursor = Some(cursor);
534 self
535 }
536
537 #[inline]
544 pub fn on_drag_cursor(mut self, cursor: CursorIcon) -> Self {
545 self.on_drag_cursor = Some(cursor);
546 self
547 }
548
549 #[inline]
551 pub fn hscroll(mut self, hscroll: bool) -> Self {
552 self.direction_enabled[0] = hscroll;
553 self
554 }
555
556 #[inline]
558 pub fn vscroll(mut self, vscroll: bool) -> Self {
559 self.direction_enabled[1] = vscroll;
560 self
561 }
562
563 #[inline]
567 pub fn scroll(mut self, direction_enabled: impl Into<Vec2b>) -> Self {
568 self.direction_enabled = direction_enabled.into();
569 self
570 }
571
572 #[inline]
582 pub fn scroll_source(mut self, scroll_source: ScrollSource) -> Self {
583 self.scroll_source = scroll_source;
584 self
585 }
586
587 #[inline]
593 pub fn wheel_scroll_multiplier(mut self, multiplier: Vec2) -> Self {
594 self.wheel_scroll_multiplier = multiplier;
595 self
596 }
597
598 #[inline]
605 pub fn auto_shrink(mut self, auto_shrink: impl Into<Vec2b>) -> Self {
606 self.auto_shrink = auto_shrink.into();
607 self
608 }
609
610 #[inline]
614 pub fn animated(mut self, animated: bool) -> Self {
615 self.animated = animated;
616 self
617 }
618
619 pub(crate) fn is_any_scroll_enabled(&self) -> bool {
621 self.direction_enabled[0] || self.direction_enabled[1]
622 }
623
624 #[inline]
631 pub fn content_margin(mut self, margin: impl Into<Margin>) -> Self {
632 self.content_margin = Some(margin.into());
633 self
634 }
635
636 #[inline]
643 pub fn stick_to_right(mut self, stick: bool) -> Self {
644 self.stick_to_end[0] = stick;
645 self
646 }
647
648 #[inline]
655 pub fn stick_to_bottom(mut self, stick: bool) -> Self {
656 self.stick_to_end[1] = stick;
657 self
658 }
659}
660
661struct Prepared {
662 id: Id,
663 state: State,
664
665 auto_shrink: Vec2b,
666
667 direction_enabled: Vec2b,
669
670 show_bars_factor: Vec2,
672
673 current_bar_use: Vec2,
683
684 scroll_bar_visibility: ScrollBarVisibility,
685 scroll_bar_rect: Option<Rect>,
686
687 inner_rect: Rect,
689
690 content_ui: Ui,
691
692 viewport: Rect,
695
696 scroll_source: ScrollSource,
697 wheel_scroll_multiplier: Vec2,
698 stick_to_end: Vec2b,
699
700 saved_scroll_target: [Option<pass_state::ScrollTarget>; 2],
703
704 background_drag_response: Option<Response>,
706
707 animated: bool,
708}
709
710impl ScrollArea {
711 fn begin(self, ui: &mut Ui) -> Prepared {
712 let Self {
713 direction_enabled,
714 auto_shrink,
715 max_size,
716 min_scrolled_size,
717 scroll_bar_visibility,
718 scroll_bar_rect,
719 id_salt,
720 offset_x,
721 offset_y,
722 on_hover_cursor,
723 on_drag_cursor,
724 scroll_source,
725 wheel_scroll_multiplier,
726 content_margin: _, stick_to_end,
728 animated,
729 } = self;
730
731 let ctx = ui.ctx().clone();
732
733 let id_salt = id_salt.unwrap_or_else(|| IdSalt::new("scroll_area"));
734 let id = ui.make_persistent_id(id_salt);
735 ctx.check_for_id_clash(
736 id,
737 Rect::from_min_size(ui.available_rect_before_wrap().min, Vec2::ZERO),
738 "ScrollArea",
739 );
740 let mut state = State::load(&ctx, id).unwrap_or_default();
741
742 state.offset.x = offset_x.unwrap_or(state.offset.x);
743 state.offset.y = offset_y.unwrap_or(state.offset.y);
744
745 let show_bars: Vec2b = match scroll_bar_visibility {
746 ScrollBarVisibility::AlwaysHidden => Vec2b::FALSE,
747 ScrollBarVisibility::VisibleWhenNeeded => state.show_scroll,
748 ScrollBarVisibility::AlwaysVisible => direction_enabled,
749 };
750
751 let show_bars_factor = Vec2::new(
752 ctx.animate_bool_responsive(id.with("h"), show_bars[0]),
753 ctx.animate_bool_responsive(id.with("v"), show_bars[1]),
754 );
755
756 let scroll_style = ui.spacing().scroll;
757 let current_bar_use = if scroll_style.floating {
758 show_bars.to_vec2().yx() * scroll_style.allocated_width()
759 } else {
760 show_bars_factor.yx() * scroll_style.allocated_width()
761 };
762
763 let available_outer = ui.available_rect_before_wrap();
764
765 let outer_size = available_outer.size().at_most(max_size);
766
767 let inner_size = {
768 let mut inner_size = outer_size - current_bar_use;
769
770 for d in 0..2 {
775 if direction_enabled[d] {
776 inner_size[d] = inner_size[d].max(min_scrolled_size[d]);
777 }
778 }
779 inner_size
780 };
781
782 let inner_rect = Rect::from_min_size(available_outer.min, inner_size);
783
784 let mut content_max_size = inner_size;
785
786 if true {
787 } else {
790 for d in 0..2 {
792 if direction_enabled[d] {
793 content_max_size[d] = f32::INFINITY;
794 }
795 }
796 }
797
798 let content_max_rect = Rect::from_min_size(inner_rect.min - state.offset, content_max_size);
799
800 let content_max_rect = content_max_rect
802 .round_to_pixels(ui.pixels_per_point())
803 .round_ui();
804
805 let mut content_ui = ui.new_child(
806 UiBuilder::new()
807 .ui_stack_info(UiStackInfo::new(UiKind::ScrollArea))
808 .max_rect(content_max_rect),
809 );
810
811 {
812 let mut content_clip_rect = ui.clip_rect();
814 for d in 0..2 {
815 if direction_enabled[d] {
816 content_clip_rect.min[d] = inner_rect.min[d];
817 content_clip_rect.max[d] = inner_rect.max[d];
818 } else {
819 content_clip_rect.max[d] = ui.clip_rect().max[d] - current_bar_use[d];
821 }
822 }
823 content_clip_rect = content_clip_rect.intersect(ui.clip_rect());
825 content_ui.set_clip_rect(content_clip_rect);
826 }
827
828 let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size);
829 let dt = ui.input(|i| i.stable_dt).at_most(0.1);
830
831 let background_drag_response = if scroll_source.drag.enabled(ui.ctx())
832 && ui.is_enabled()
833 && state.content_is_too_large.any()
834 {
835 let content_response_option = state
839 .interact_rect
840 .map(|rect| ui.interact(rect, id.with("area"), Sense::DRAG));
841
842 if content_response_option
843 .as_ref()
844 .is_some_and(|response| response.dragged())
845 {
846 for d in 0..2 {
847 if direction_enabled[d] {
848 ui.input(|input| {
849 state.offset[d] -= input.pointer.delta()[d];
850 });
851 state.scroll_stuck_to_end[d] = false;
852 state.offset_target[d] = None;
853 }
854 }
855 } else {
856 if content_response_option
858 .as_ref()
859 .is_some_and(|response| response.drag_stopped())
860 {
861 state.vel =
862 direction_enabled.to_vec2() * ui.input(|input| input.pointer.velocity());
863 }
864 for d in 0..2 {
865 let stop_speed = 20.0; let friction_coeff = 1000.0; let friction = friction_coeff * dt;
870 if friction > state.vel[d].abs() || state.vel[d].abs() < stop_speed {
871 state.vel[d] = 0.0;
872 } else {
873 state.vel[d] -= friction * state.vel[d].signum();
874 state.offset[d] -= state.vel[d] * dt;
877 ctx.request_repaint();
878 }
879 }
880 }
881
882 if let Some(response) = &content_response_option {
884 if response.dragged()
885 && let Some(cursor) = on_drag_cursor
886 {
887 ui.set_cursor_icon(cursor);
888 } else if response.hovered()
889 && let Some(cursor) = on_hover_cursor
890 {
891 ui.set_cursor_icon(cursor);
892 }
893 }
894
895 content_response_option
896 } else {
897 None
898 };
899
900 for d in 0..2 {
903 if let Some(scroll_target) = state.offset_target[d] {
904 state.vel[d] = 0.0;
905
906 if (state.offset[d] - scroll_target.target_offset).abs() < 1.0 {
907 state.offset[d] = scroll_target.target_offset;
909 state.offset_target[d] = None;
910 } else {
911 let t = emath::interpolation_factor(
913 scroll_target.animation_time_span,
914 ui.input(|i| i.time),
915 dt,
916 emath::ease_in_ease_out,
917 );
918 if t < 1.0 {
919 state.offset[d] =
920 emath::lerp(state.offset[d]..=scroll_target.target_offset, t);
921 ctx.request_repaint();
922 } else {
923 state.offset[d] = scroll_target.target_offset;
925 state.offset_target[d] = None;
926 }
927 }
928 }
929 }
930
931 let saved_scroll_target = content_ui
932 .ctx()
933 .pass_state_mut(|state| core::mem::take(&mut state.scroll_target));
934
935 Prepared {
936 id,
937 state,
938 auto_shrink,
939 direction_enabled,
940 show_bars_factor,
941 current_bar_use,
942 scroll_bar_visibility,
943 scroll_bar_rect,
944 inner_rect,
945 content_ui,
946 viewport,
947 scroll_source,
948 wheel_scroll_multiplier,
949 stick_to_end,
950 saved_scroll_target,
951 background_drag_response,
952 animated,
953 }
954 }
955
956 pub fn show<R>(
960 self,
961 ui: &mut Ui,
962 add_contents: impl FnOnce(&mut Ui) -> R,
963 ) -> ScrollAreaOutput<R> {
964 self.show_viewport_dyn(ui, Box::new(|ui, _viewport| add_contents(ui)))
965 }
966
967 pub fn show_rows<R>(
984 self,
985 ui: &mut Ui,
986 row_height_sans_spacing: f32,
987 total_rows: usize,
988 add_contents: impl FnOnce(&mut Ui, core::ops::Range<usize>) -> R,
989 ) -> ScrollAreaOutput<R> {
990 let spacing = ui.spacing().item_spacing;
991 let row_height_with_spacing = row_height_sans_spacing + spacing.y;
992 self.show_viewport(ui, |ui, viewport| {
993 ui.set_height((row_height_with_spacing * total_rows as f32 - spacing.y).at_least(0.0));
994
995 let mut min_row = (viewport.min.y / row_height_with_spacing).floor() as usize;
996 let mut max_row = (viewport.max.y / row_height_with_spacing).ceil() as usize + 1;
997 if max_row > total_rows {
998 let diff = max_row.saturating_sub(min_row);
999 max_row = total_rows;
1000 min_row = total_rows.saturating_sub(diff);
1001 }
1002
1003 let y_min = ui.max_rect().top() + min_row as f32 * row_height_with_spacing;
1004 let y_max = ui.max_rect().top() + max_row as f32 * row_height_with_spacing;
1005
1006 let rect = Rect::from_x_y_ranges(ui.max_rect().x_range(), y_min..=y_max);
1007
1008 ui.scope_builder(UiBuilder::new().max_rect(rect), |viewport_ui| {
1009 viewport_ui.skip_ahead_auto_ids(min_row); add_contents(viewport_ui, min_row..max_row)
1011 })
1012 .inner
1013 })
1014 }
1015
1016 pub fn show_viewport<R>(
1021 self,
1022 ui: &mut Ui,
1023 add_contents: impl FnOnce(&mut Ui, Rect) -> R,
1024 ) -> ScrollAreaOutput<R> {
1025 self.show_viewport_dyn(ui, Box::new(add_contents))
1026 }
1027
1028 fn show_viewport_dyn<'c, R>(
1029 self,
1030 ui: &mut Ui,
1031 add_contents: Box<dyn FnOnce(&mut Ui, Rect) -> R + 'c>,
1032 ) -> ScrollAreaOutput<R> {
1033 let margin = self
1034 .content_margin
1035 .unwrap_or_else(|| ui.spacing().scroll.content_margin);
1036
1037 let mut prepared = self.begin(ui);
1038 let id = prepared.id;
1039 let inner_rect = prepared.inner_rect;
1040
1041 let inner = crate::Frame::NONE
1042 .inner_margin(margin)
1043 .show(&mut prepared.content_ui, |ui| {
1044 add_contents(ui, prepared.viewport)
1045 })
1046 .inner;
1047
1048 let (content_size, state) = prepared.end(ui);
1049 ScrollAreaOutput {
1050 inner,
1051 id,
1052 state,
1053 content_size,
1054 inner_rect,
1055 }
1056 }
1057}
1058
1059impl Prepared {
1060 fn end(self, ui: &mut Ui) -> (Vec2, State) {
1062 let Self {
1063 id,
1064 mut state,
1065 inner_rect,
1066 auto_shrink,
1067 direction_enabled,
1068 mut show_bars_factor,
1069 current_bar_use,
1070 scroll_bar_visibility,
1071 scroll_bar_rect,
1072 content_ui,
1073 viewport: _,
1074 scroll_source,
1075 wheel_scroll_multiplier,
1076 stick_to_end,
1077 saved_scroll_target,
1078 background_drag_response,
1079 animated,
1080 } = self;
1081
1082 let content_size = content_ui.min_size();
1083
1084 let mut had_explicit_scroll_adjustment = Vec2b::FALSE;
1085
1086 for d in 0..2 {
1087 let scroll_target = content_ui
1090 .ctx()
1091 .pass_state_mut(|state| state.scroll_target[d].take());
1092
1093 if direction_enabled[d] {
1094 let (scroll_delta, scroll_animation) = content_ui.ctx().pass_state_mut(|state| {
1095 (
1096 core::mem::take(&mut state.scroll_delta.0[d]),
1097 state.scroll_delta.1,
1098 )
1099 });
1100
1101 let mut delta = -scroll_delta;
1103 let mut animation = scroll_animation;
1104
1105 if let Some(target) = scroll_target {
1106 let pass_state::ScrollTarget {
1107 range,
1108 align,
1109 animation: animation_update,
1110 } = target;
1111 let min = content_ui.min_rect().min[d];
1112 let clip_rect = content_ui.clip_rect();
1113 let visible_range = min..=min + clip_rect.size()[d];
1114 let (start, end) = (range.min, range.max);
1115 let clip_start = clip_rect.min[d];
1116 let clip_end = clip_rect.max[d];
1117 let mut spacing = content_ui.spacing().item_spacing[d];
1118
1119 let delta_update = if let Some(align) = align {
1120 let center_factor = align.to_factor();
1121
1122 let offset =
1123 lerp(range, center_factor) - lerp(visible_range, center_factor);
1124
1125 spacing *= remap(center_factor, 0.0..=1.0, -1.0..=1.0);
1127
1128 offset + spacing - state.offset[d]
1129 } else if start < clip_start && end < clip_end {
1130 -(clip_start - start + spacing).min(clip_end - end - spacing)
1131 } else if end > clip_end && start > clip_start {
1132 (end - clip_end + spacing).min(start - clip_start - spacing)
1133 } else {
1134 0.0
1136 };
1137
1138 animation = animation_update;
1139 delta += delta_update;
1140 }
1141
1142 if delta != 0.0 {
1143 let target_offset = state.offset[d] + delta;
1144
1145 if !animated {
1146 state.offset[d] = target_offset;
1147 } else if let Some(animation) = &mut state.offset_target[d] {
1148 animation.target_offset = target_offset;
1151 } else {
1152 let now = ui.input(|i| i.time);
1154 let animation_duration = (delta.abs() / animation.points_per_second)
1155 .clamp(animation.duration.min, animation.duration.max);
1156 state.offset_target[d] = Some(ScrollingToTarget {
1157 animation_time_span: (now, now + animation_duration as f64),
1158 target_offset,
1159 });
1160 }
1161 ui.request_repaint();
1162 }
1163
1164 if delta != 0.0 {
1165 had_explicit_scroll_adjustment[d] = true;
1166 }
1167 }
1168 }
1169
1170 ui.ctx().pass_state_mut(|state| {
1172 for d in 0..2 {
1173 if saved_scroll_target[d].is_some() {
1174 state.scroll_target[d] = saved_scroll_target[d].clone();
1175 }
1176 }
1177 });
1178
1179 let inner_rect = {
1180 let mut inner_size = inner_rect.size();
1182
1183 for d in 0..2 {
1184 inner_size[d] = match (direction_enabled[d], auto_shrink[d]) {
1185 (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]), };
1190 }
1191
1192 Rect::from_min_size(inner_rect.min, inner_size)
1193 };
1194
1195 let outer_rect = Rect::from_min_size(inner_rect.min, inner_rect.size() + current_bar_use);
1196
1197 let limit_rect = if ui.spacing().scroll.floating {
1198 outer_rect
1199 } else {
1200 inner_rect
1201 };
1202
1203 let content_is_too_large = Vec2b::new(
1204 direction_enabled[0] && (limit_rect.width().ceil() < content_size.x),
1205 direction_enabled[1] && (limit_rect.height().ceil() < content_size.y),
1206 );
1207
1208 let max_offset = content_size - inner_rect.size();
1209
1210 let is_dragging_background = background_drag_response
1212 .as_ref()
1213 .is_some_and(|r| r.dragged());
1214
1215 let is_hovering_outer_rect = ui.rect_contains_pointer(outer_rect)
1216 && ui.ctx().dragged_id().is_none()
1217 || is_dragging_background;
1218
1219 if scroll_source.mouse_wheel && ui.is_enabled() && is_hovering_outer_rect {
1220 let always_scroll_enabled_direction = ui.style().always_scroll_the_only_direction
1221 && direction_enabled[0] != direction_enabled[1];
1222 for d in 0..2 {
1223 if direction_enabled[d] {
1224 let scroll_delta = ui.input(|input| {
1225 if always_scroll_enabled_direction {
1226 input.smooth_scroll_delta()[0] + input.smooth_scroll_delta()[1]
1228 } else {
1229 input.smooth_scroll_delta()[d]
1230 }
1231 });
1232 let scroll_delta = scroll_delta * wheel_scroll_multiplier[d];
1233
1234 let scrolling_up = state.offset[d] > 0.0 && scroll_delta > 0.0;
1235 let scrolling_down = state.offset[d] < max_offset[d] && scroll_delta < 0.0;
1236
1237 if scrolling_up || scrolling_down {
1238 state.offset[d] -= scroll_delta;
1239
1240 ui.input_mut(|input| {
1242 if always_scroll_enabled_direction {
1243 input.smooth_scroll_delta = Vec2::ZERO;
1244 } else {
1245 input.smooth_scroll_delta[d] = 0.0;
1246 }
1247 });
1248
1249 state.scroll_stuck_to_end[d] = false;
1250 state.offset_target[d] = None;
1251 }
1252 }
1253 }
1254 }
1255
1256 let show_scroll_this_frame = match scroll_bar_visibility {
1257 ScrollBarVisibility::AlwaysHidden => Vec2b::FALSE,
1258 ScrollBarVisibility::VisibleWhenNeeded => content_is_too_large,
1259 ScrollBarVisibility::AlwaysVisible => direction_enabled,
1260 };
1261
1262 if show_scroll_this_frame[0] && show_bars_factor.x <= 0.0 {
1264 show_bars_factor.x = ui.ctx().animate_bool_responsive(id.with("h"), true);
1265 }
1266 if show_scroll_this_frame[1] && show_bars_factor.y <= 0.0 {
1267 show_bars_factor.y = ui.ctx().animate_bool_responsive(id.with("v"), true);
1268 }
1269
1270 let scroll_style = ui.spacing().scroll;
1271
1272 ui.advance_cursor_after_rect(outer_rect);
1274
1275 paint_fade_areas_impl(ui, inner_rect, content_size, state.offset);
1276
1277 let scroll_bar_rect = scroll_bar_rect.unwrap_or(inner_rect);
1279 for d in 0..2 {
1280 if stick_to_end[d] && state.scroll_stuck_to_end[d] && !had_explicit_scroll_adjustment[d]
1283 {
1284 state.offset[d] = content_size[d] - inner_rect.size()[d];
1285 }
1286
1287 let show_factor = show_bars_factor[d];
1288 if show_factor == 0.0 {
1289 state.scroll_bar_interaction[d] = false;
1290 continue;
1291 }
1292
1293 let interact_id = id.with(d);
1294
1295 let inner_margin = show_factor * scroll_style.bar_inner_margin;
1297 let outer_margin = show_factor * scroll_style.bar_outer_margin;
1298
1299 let mut max_cross = outer_rect.max[1 - d] - outer_margin;
1302
1303 if ui.clip_rect().max[1 - d] - outer_margin < max_cross {
1304 max_cross = ui.clip_rect().max[1 - d] - outer_margin;
1312 }
1313
1314 let full_width = scroll_style.bar_width;
1315
1316 let max_bar_rect = if d == 0 {
1319 outer_rect.with_min_y(max_cross - full_width)
1320 } else {
1321 outer_rect.with_min_x(max_cross - full_width)
1322 };
1323
1324 let sense = if scroll_source.scroll_bar && ui.is_enabled() {
1325 Sense::CLICK | Sense::DRAG
1326 } else {
1327 Sense::hover()
1328 };
1329
1330 let response = ui.interact(max_bar_rect, interact_id, sense);
1335
1336 response.widget_info(|| WidgetInfo::new(crate::WidgetType::ScrollBar));
1337
1338 let cross = if scroll_style.floating {
1341 let is_hovering_bar_area = response.hovered() || state.scroll_bar_interaction[d];
1342
1343 let is_hovering_bar_area_t = ui
1344 .ctx()
1345 .animate_bool_responsive(id.with((d, "bar_hover")), is_hovering_bar_area);
1346
1347 let width = show_factor
1348 * lerp(
1349 scroll_style.floating_width..=full_width,
1350 is_hovering_bar_area_t,
1351 );
1352
1353 let min_cross = max_cross - width;
1354 Rangef::new(min_cross, max_cross)
1355 } else {
1356 let min_cross = inner_rect.max[1 - d] + inner_margin;
1357 Rangef::new(min_cross, max_cross)
1358 };
1359
1360 let outer_scroll_bar_rect = if d == 0 {
1361 Rect::from_x_y_ranges(scroll_bar_rect.x_range(), cross)
1362 } else {
1363 Rect::from_x_y_ranges(cross, scroll_bar_rect.y_range())
1364 };
1365
1366 let from_content = |content| {
1367 remap_clamp(
1368 content,
1369 0.0..=content_size[d],
1370 scroll_bar_rect.min[d]..=scroll_bar_rect.max[d],
1371 )
1372 };
1373
1374 let calculate_handle_rect = |d, offset: &Vec2| {
1375 let handle_size = if d == 0 {
1376 from_content(offset.x + inner_rect.width()) - from_content(offset.x)
1377 } else {
1378 from_content(offset.y + inner_rect.height()) - from_content(offset.y)
1379 }
1380 .max(scroll_style.handle_min_length);
1381
1382 let handle_start_point = remap_clamp(
1383 offset[d],
1384 0.0..=max_offset[d],
1385 scroll_bar_rect.min[d]..=(scroll_bar_rect.max[d] - handle_size),
1386 );
1387
1388 if d == 0 {
1389 Rect::from_min_max(
1390 pos2(handle_start_point, cross.min),
1391 pos2(handle_start_point + handle_size, cross.max),
1392 )
1393 } else {
1394 Rect::from_min_max(
1395 pos2(cross.min, handle_start_point),
1396 pos2(cross.max, handle_start_point + handle_size),
1397 )
1398 }
1399 };
1400
1401 let handle_rect = calculate_handle_rect(d, &state.offset);
1402
1403 state.scroll_bar_interaction[d] = response.hovered() || response.dragged();
1404
1405 if let Some(pointer_pos) = response.interact_pointer_pos() {
1406 let scroll_start_offset_from_top_left = state.scroll_start_offset_from_top_left[d]
1407 .get_or_insert_with(|| {
1408 if handle_rect.contains(pointer_pos) {
1409 pointer_pos[d] - handle_rect.min[d]
1410 } else {
1411 let handle_top_pos_at_bottom =
1412 scroll_bar_rect.max[d] - handle_rect.size()[d];
1413 let new_handle_top_pos = (pointer_pos[d] - handle_rect.size()[d] / 2.0)
1415 .clamp(scroll_bar_rect.min[d], handle_top_pos_at_bottom);
1416 pointer_pos[d] - new_handle_top_pos
1417 }
1418 });
1419
1420 let new_handle_top = pointer_pos[d] - *scroll_start_offset_from_top_left;
1421 let handle_travel =
1422 scroll_bar_rect.min[d]..=(scroll_bar_rect.max[d] - handle_rect.size()[d]);
1423 state.offset[d] = if handle_travel.start() == handle_travel.end() {
1424 0.0
1425 } else {
1426 remap(new_handle_top, handle_travel, 0.0..=max_offset[d])
1427 };
1428
1429 state.scroll_stuck_to_end[d] = false;
1431 state.offset_target[d] = None;
1432 } else {
1433 state.scroll_start_offset_from_top_left[d] = None;
1434 }
1435
1436 let unbounded_offset = state.offset[d];
1437 state.offset[d] = state.offset[d].max(0.0);
1438 state.offset[d] = state.offset[d].min(max_offset[d]);
1439
1440 if state.offset[d] != unbounded_offset {
1441 state.vel[d] = 0.0;
1442 }
1443
1444 if ui.is_rect_visible(outer_scroll_bar_rect) {
1445 let handle_rect = calculate_handle_rect(d, &state.offset);
1447
1448 let visuals = if scroll_source.scroll_bar && ui.is_enabled() {
1449 let is_hovering_handle = response.hovered()
1452 && ui.input(|i| {
1453 i.pointer
1454 .latest_pos()
1455 .is_some_and(|p| handle_rect.contains(p))
1456 });
1457 let visuals = ui.visuals();
1458 if response.is_pointer_button_down_on() {
1459 &visuals.widgets.active
1460 } else if is_hovering_handle {
1461 &visuals.widgets.hovered
1462 } else {
1463 &visuals.widgets.inactive
1464 }
1465 } else {
1466 &ui.visuals().widgets.inactive
1467 };
1468
1469 let handle_opacity = if scroll_style.floating {
1470 if response.hovered() || response.dragged() {
1471 scroll_style.interact_handle_opacity
1472 } else {
1473 let is_hovering_outer_rect_t = ui.ctx().animate_bool_responsive(
1474 id.with((d, "is_hovering_outer_rect")),
1475 is_hovering_outer_rect,
1476 );
1477 lerp(
1478 scroll_style.dormant_handle_opacity
1479 ..=scroll_style.active_handle_opacity,
1480 is_hovering_outer_rect_t,
1481 )
1482 }
1483 } else {
1484 1.0
1485 };
1486
1487 let background_opacity = if scroll_style.floating {
1488 if response.hovered() || response.dragged() {
1489 scroll_style.interact_background_opacity
1490 } else if is_hovering_outer_rect {
1491 scroll_style.active_background_opacity
1492 } else {
1493 scroll_style.dormant_background_opacity
1494 }
1495 } else {
1496 1.0
1497 };
1498
1499 let handle_color = if scroll_style.foreground_color {
1500 visuals.fg_stroke.color
1501 } else {
1502 visuals.bg_fill
1503 };
1504
1505 ui.painter().add(epaint::Shape::rect_filled(
1507 outer_scroll_bar_rect,
1508 visuals.corner_radius,
1509 ui.visuals()
1510 .extreme_bg_color
1511 .gamma_multiply(background_opacity),
1512 ));
1513
1514 ui.painter().add(epaint::Shape::rect_filled(
1516 handle_rect,
1517 visuals.corner_radius,
1518 handle_color.gamma_multiply(handle_opacity),
1519 ));
1520 }
1521 }
1522
1523 if show_scroll_this_frame != state.show_scroll {
1524 ui.request_repaint();
1525 }
1526
1527 let available_offset = content_size - inner_rect.size();
1528 state.offset = state.offset.min(available_offset);
1529 state.offset = state.offset.max(Vec2::ZERO);
1530
1531 let suppress_stuck_recompute = Vec2b::new(
1532 had_explicit_scroll_adjustment[0] && state.offset_target[0].is_some(),
1533 had_explicit_scroll_adjustment[1] && state.offset_target[1].is_some(),
1534 );
1535
1536 state.scroll_stuck_to_end = Vec2b::new(
1544 !suppress_stuck_recompute[0]
1545 && ((state.offset[0] == available_offset[0])
1546 || (stick_to_end[0] && available_offset[0] < 0.0)),
1547 !suppress_stuck_recompute[1]
1548 && ((state.offset[1] == available_offset[1])
1549 || (stick_to_end[1] && available_offset[1] < 0.0)),
1550 );
1551
1552 state.show_scroll = show_scroll_this_frame;
1553 state.content_is_too_large = content_is_too_large;
1554 state.interact_rect = Some(inner_rect);
1555
1556 state.store(ui.ctx(), id);
1557
1558 (content_size, state)
1559 }
1560}
1561
1562fn paint_fade_areas_impl(ui: &Ui, inner_rect: Rect, content_size: Vec2, offset: Vec2) {
1565 let crate::style::ScrollFadeStyle {
1566 strength,
1567 size: fade_size,
1568 } = ui.spacing().scroll.fade;
1569
1570 if strength <= 0.0 {
1571 return;
1572 }
1573
1574 let bg = ui.stack().bg_color();
1575
1576 let overflow = content_size - inner_rect.size();
1577
1578 let paint_rect = inner_rect.intersect(ui.min_rect());
1579
1580 if 0.0 < offset.y {
1582 let t = (offset.y / fade_size).clamp(0.0, 1.0) * strength;
1583 let bg_faded = bg.gamma_multiply(t);
1584 let rect = Rect::from_min_max(
1585 paint_rect.left_top(),
1586 pos2(paint_rect.right(), paint_rect.top() + fade_size),
1587 );
1588 ui.painter().add(Shape::gradient_rect(
1589 rect,
1590 Direction::TopDown,
1591 [bg_faded, Color32::TRANSPARENT],
1592 ));
1593 }
1594
1595 let distance_from_bottom = overflow.y - offset.y;
1597 if 0.0 < distance_from_bottom {
1598 let t = (distance_from_bottom / fade_size).clamp(0.0, 1.0) * strength;
1599 let bg_faded = bg.gamma_multiply(t);
1600 let rect = Rect::from_min_max(
1601 pos2(paint_rect.left(), paint_rect.bottom() - fade_size),
1602 paint_rect.right_bottom(),
1603 );
1604 ui.painter().add(Shape::gradient_rect(
1605 rect,
1606 Direction::BottomUp,
1607 [bg_faded, Color32::TRANSPARENT],
1608 ));
1609 }
1610
1611 if 0.0 < offset.x {
1613 let t = (offset.x / fade_size).clamp(0.0, 1.0) * strength;
1614 let bg_faded = bg.gamma_multiply(t);
1615 let rect = Rect::from_min_max(
1616 paint_rect.left_top(),
1617 pos2(paint_rect.left() + fade_size, paint_rect.bottom()),
1618 );
1619 ui.painter().add(Shape::gradient_rect(
1620 rect,
1621 Direction::LeftToRight,
1622 [bg_faded, Color32::TRANSPARENT],
1623 ));
1624 }
1625
1626 let distance_from_right = overflow.x - offset.x;
1628 if 0.0 < distance_from_right {
1629 let t = (distance_from_right / fade_size).clamp(0.0, 1.0) * strength;
1630 let bg_faded = bg.gamma_multiply(t);
1631 let rect = Rect::from_min_max(
1632 pos2(paint_rect.right() - fade_size, paint_rect.top()),
1633 paint_rect.right_bottom(),
1634 );
1635 ui.painter().add(Shape::gradient_rect(
1636 rect,
1637 Direction::RightToLeft,
1638 [bg_faded, Color32::TRANSPARENT],
1639 ));
1640 }
1641}