1#![allow(clippy::needless_range_loop)]
2
3use std::ops::{Add, AddAssign, BitOr, BitOrAssign};
4
5use emath::GuiRounding as _;
6
7use crate::{
8 Context, CursorIcon, Id, NumExt as _, Pos2, Rangef, Rect, Response, Sense, Ui, UiBuilder,
9 UiKind, UiStackInfo, Vec2, Vec2b, emath, epaint, lerp, pass_state, pos2, remap, remap_clamp,
10};
11
12#[derive(Clone, Copy, Debug)]
13#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
14struct ScrollingToTarget {
15 animation_time_span: (f64, f64),
16 target_offset: f32,
17}
18
19#[derive(Clone, Copy, Debug)]
20#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
21#[cfg_attr(feature = "serde", serde(default))]
22pub struct State {
23 pub offset: Vec2,
25
26 offset_target: [Option<ScrollingToTarget>; 2],
28
29 show_scroll: Vec2b,
31
32 content_is_too_large: Vec2b,
34
35 scroll_bar_interaction: Vec2b,
37
38 #[cfg_attr(feature = "serde", serde(skip))]
40 vel: Vec2,
41
42 scroll_start_offset_from_top_left: [Option<f32>; 2],
44
45 scroll_stuck_to_end: Vec2b,
49
50 interact_rect: Option<Rect>,
52}
53
54impl Default for State {
55 fn default() -> Self {
56 Self {
57 offset: Vec2::ZERO,
58 offset_target: Default::default(),
59 show_scroll: Vec2b::FALSE,
60 content_is_too_large: Vec2b::FALSE,
61 scroll_bar_interaction: Vec2b::FALSE,
62 vel: Vec2::ZERO,
63 scroll_start_offset_from_top_left: [None; 2],
64 scroll_stuck_to_end: Vec2b::TRUE,
65 interact_rect: None,
66 }
67 }
68}
69
70impl State {
71 pub fn load(ctx: &Context, id: Id) -> Option<Self> {
72 ctx.data_mut(|d| d.get_persisted(id))
73 }
74
75 pub fn store(self, ctx: &Context, id: Id) {
76 ctx.data_mut(|d| d.insert_persisted(id, self));
77 }
78
79 pub fn velocity(&self) -> Vec2 {
81 self.vel
82 }
83}
84
85pub struct ScrollAreaOutput<R> {
86 pub inner: R,
88
89 pub id: Id,
91
92 pub state: State,
94
95 pub content_size: Vec2,
98
99 pub inner_rect: Rect,
101}
102
103#[derive(Clone, Copy, Debug, PartialEq, Eq)]
105#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
106pub enum ScrollBarVisibility {
107 AlwaysHidden,
113
114 VisibleWhenNeeded,
119
120 AlwaysVisible,
123}
124
125impl Default for ScrollBarVisibility {
126 #[inline]
127 fn default() -> Self {
128 Self::VisibleWhenNeeded
129 }
130}
131
132impl ScrollBarVisibility {
133 pub const ALL: [Self; 3] = [
134 Self::AlwaysHidden,
135 Self::VisibleWhenNeeded,
136 Self::AlwaysVisible,
137 ];
138}
139
140#[derive(Clone, Copy, Debug, PartialEq, Eq)]
142#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
143pub struct ScrollSource {
144 pub scroll_bar: bool,
149
150 pub drag: bool,
152
153 pub mouse_wheel: bool,
156}
157
158impl Default for ScrollSource {
159 fn default() -> Self {
160 Self::ALL
161 }
162}
163
164impl ScrollSource {
165 pub const NONE: Self = Self {
166 scroll_bar: false,
167 drag: false,
168 mouse_wheel: false,
169 };
170 pub const ALL: Self = Self {
171 scroll_bar: true,
172 drag: true,
173 mouse_wheel: true,
174 };
175 pub const SCROLL_BAR: Self = Self {
176 scroll_bar: true,
177 drag: false,
178 mouse_wheel: false,
179 };
180 pub const DRAG: Self = Self {
181 scroll_bar: false,
182 drag: true,
183 mouse_wheel: false,
184 };
185 pub const MOUSE_WHEEL: Self = Self {
186 scroll_bar: false,
187 drag: false,
188 mouse_wheel: true,
189 };
190
191 #[inline]
193 pub fn is_none(&self) -> bool {
194 self == &Self::NONE
195 }
196
197 #[inline]
199 pub fn any(&self) -> bool {
200 self.scroll_bar | self.drag | self.mouse_wheel
201 }
202
203 #[inline]
205 pub fn is_all(&self) -> bool {
206 self.scroll_bar & self.drag & self.mouse_wheel
207 }
208}
209
210impl BitOr for ScrollSource {
211 type Output = Self;
212
213 #[inline]
214 fn bitor(self, rhs: Self) -> Self::Output {
215 Self {
216 scroll_bar: self.scroll_bar | rhs.scroll_bar,
217 drag: self.drag | rhs.drag,
218 mouse_wheel: self.mouse_wheel | rhs.mouse_wheel,
219 }
220 }
221}
222
223#[expect(clippy::suspicious_arithmetic_impl)]
224impl Add for ScrollSource {
225 type Output = Self;
226
227 #[inline]
228 fn add(self, rhs: Self) -> Self::Output {
229 self | rhs
230 }
231}
232
233impl BitOrAssign for ScrollSource {
234 #[inline]
235 fn bitor_assign(&mut self, rhs: Self) {
236 *self = *self | rhs;
237 }
238}
239
240impl AddAssign for ScrollSource {
241 #[inline]
242 fn add_assign(&mut self, rhs: Self) {
243 *self = *self + rhs;
244 }
245}
246
247#[derive(Clone, Debug)]
279#[must_use = "You should call .show()"]
280pub struct ScrollArea {
281 direction_enabled: Vec2b,
283
284 auto_shrink: Vec2b,
285 max_size: Vec2,
286 min_scrolled_size: Vec2,
287 scroll_bar_visibility: ScrollBarVisibility,
288 scroll_bar_rect: Option<Rect>,
289 id_salt: Option<Id>,
290 offset_x: Option<f32>,
291 offset_y: Option<f32>,
292 on_hover_cursor: Option<CursorIcon>,
293 on_drag_cursor: Option<CursorIcon>,
294 scroll_source: ScrollSource,
295 wheel_scroll_multiplier: Vec2,
296
297 stick_to_end: Vec2b,
301
302 animated: bool,
304}
305
306impl ScrollArea {
307 #[inline]
309 pub fn horizontal() -> Self {
310 Self::new([true, false])
311 }
312
313 #[inline]
315 pub fn vertical() -> Self {
316 Self::new([false, true])
317 }
318
319 #[inline]
321 pub fn both() -> Self {
322 Self::new([true, true])
323 }
324
325 #[inline]
328 pub fn neither() -> Self {
329 Self::new([false, false])
330 }
331
332 pub fn new(direction_enabled: impl Into<Vec2b>) -> Self {
335 Self {
336 direction_enabled: direction_enabled.into(),
337 auto_shrink: Vec2b::TRUE,
338 max_size: Vec2::INFINITY,
339 min_scrolled_size: Vec2::splat(64.0),
340 scroll_bar_visibility: Default::default(),
341 scroll_bar_rect: None,
342 id_salt: None,
343 offset_x: None,
344 offset_y: None,
345 on_hover_cursor: None,
346 on_drag_cursor: None,
347 scroll_source: ScrollSource::default(),
348 wheel_scroll_multiplier: Vec2::splat(1.0),
349 stick_to_end: Vec2b::FALSE,
350 animated: true,
351 }
352 }
353
354 #[inline]
360 pub fn max_width(mut self, max_width: f32) -> Self {
361 self.max_size.x = max_width;
362 self
363 }
364
365 #[inline]
371 pub fn max_height(mut self, max_height: f32) -> Self {
372 self.max_size.y = max_height;
373 self
374 }
375
376 #[inline]
383 pub fn min_scrolled_width(mut self, min_scrolled_width: f32) -> Self {
384 self.min_scrolled_size.x = min_scrolled_width;
385 self
386 }
387
388 #[inline]
395 pub fn min_scrolled_height(mut self, min_scrolled_height: f32) -> Self {
396 self.min_scrolled_size.y = min_scrolled_height;
397 self
398 }
399
400 #[inline]
404 pub fn scroll_bar_visibility(mut self, scroll_bar_visibility: ScrollBarVisibility) -> Self {
405 self.scroll_bar_visibility = scroll_bar_visibility;
406 self
407 }
408
409 #[inline]
414 pub fn scroll_bar_rect(mut self, scroll_bar_rect: Rect) -> Self {
415 self.scroll_bar_rect = Some(scroll_bar_rect);
416 self
417 }
418
419 #[inline]
421 #[deprecated = "Renamed id_salt"]
422 pub fn id_source(self, id_salt: impl std::hash::Hash) -> Self {
423 self.id_salt(id_salt)
424 }
425
426 #[inline]
428 pub fn id_salt(mut self, id_salt: impl std::hash::Hash) -> Self {
429 self.id_salt = Some(Id::new(id_salt));
430 self
431 }
432
433 #[inline]
441 pub fn scroll_offset(mut self, offset: Vec2) -> Self {
442 self.offset_x = Some(offset.x);
443 self.offset_y = Some(offset.y);
444 self
445 }
446
447 #[inline]
454 pub fn vertical_scroll_offset(mut self, offset: f32) -> Self {
455 self.offset_y = Some(offset);
456 self
457 }
458
459 #[inline]
466 pub fn horizontal_scroll_offset(mut self, offset: f32) -> Self {
467 self.offset_x = Some(offset);
468 self
469 }
470
471 #[inline]
478 pub fn on_hover_cursor(mut self, cursor: CursorIcon) -> Self {
479 self.on_hover_cursor = Some(cursor);
480 self
481 }
482
483 #[inline]
490 pub fn on_drag_cursor(mut self, cursor: CursorIcon) -> Self {
491 self.on_drag_cursor = Some(cursor);
492 self
493 }
494
495 #[inline]
497 pub fn hscroll(mut self, hscroll: bool) -> Self {
498 self.direction_enabled[0] = hscroll;
499 self
500 }
501
502 #[inline]
504 pub fn vscroll(mut self, vscroll: bool) -> Self {
505 self.direction_enabled[1] = vscroll;
506 self
507 }
508
509 #[inline]
513 pub fn scroll(mut self, direction_enabled: impl Into<Vec2b>) -> Self {
514 self.direction_enabled = direction_enabled.into();
515 self
516 }
517
518 #[deprecated = "Use `ScrollArea::scroll_source()"]
528 #[inline]
529 pub fn enable_scrolling(mut self, enable: bool) -> Self {
530 self.scroll_source = if enable {
531 ScrollSource::ALL
532 } else {
533 ScrollSource::NONE
534 };
535 self
536 }
537
538 #[deprecated = "Use `ScrollArea::scroll_source()"]
546 #[inline]
547 pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self {
548 self.scroll_source.drag = drag_to_scroll;
549 self
550 }
551
552 #[inline]
554 pub fn scroll_source(mut self, scroll_source: ScrollSource) -> Self {
555 self.scroll_source = scroll_source;
556 self
557 }
558
559 #[inline]
565 pub fn wheel_scroll_multiplier(mut self, multiplier: Vec2) -> Self {
566 self.wheel_scroll_multiplier = multiplier;
567 self
568 }
569
570 #[inline]
577 pub fn auto_shrink(mut self, auto_shrink: impl Into<Vec2b>) -> Self {
578 self.auto_shrink = auto_shrink.into();
579 self
580 }
581
582 #[inline]
586 pub fn animated(mut self, animated: bool) -> Self {
587 self.animated = animated;
588 self
589 }
590
591 pub(crate) fn is_any_scroll_enabled(&self) -> bool {
593 self.direction_enabled[0] || self.direction_enabled[1]
594 }
595
596 #[inline]
603 pub fn stick_to_right(mut self, stick: bool) -> Self {
604 self.stick_to_end[0] = stick;
605 self
606 }
607
608 #[inline]
615 pub fn stick_to_bottom(mut self, stick: bool) -> Self {
616 self.stick_to_end[1] = stick;
617 self
618 }
619}
620
621struct Prepared {
622 id: Id,
623 state: State,
624
625 auto_shrink: Vec2b,
626
627 direction_enabled: Vec2b,
629
630 show_bars_factor: Vec2,
632
633 current_bar_use: Vec2,
643
644 scroll_bar_visibility: ScrollBarVisibility,
645 scroll_bar_rect: Option<Rect>,
646
647 inner_rect: Rect,
649
650 content_ui: Ui,
651
652 viewport: Rect,
655
656 scroll_source: ScrollSource,
657 wheel_scroll_multiplier: Vec2,
658 stick_to_end: Vec2b,
659
660 saved_scroll_target: [Option<pass_state::ScrollTarget>; 2],
663
664 background_drag_response: Option<Response>,
666
667 animated: bool,
668}
669
670impl ScrollArea {
671 fn begin(self, ui: &mut Ui) -> Prepared {
672 let Self {
673 direction_enabled,
674 auto_shrink,
675 max_size,
676 min_scrolled_size,
677 scroll_bar_visibility,
678 scroll_bar_rect,
679 id_salt,
680 offset_x,
681 offset_y,
682 on_hover_cursor,
683 on_drag_cursor,
684 scroll_source,
685 wheel_scroll_multiplier,
686 stick_to_end,
687 animated,
688 } = self;
689
690 let ctx = ui.ctx().clone();
691
692 let id_salt = id_salt.unwrap_or_else(|| Id::new("scroll_area"));
693 let id = ui.make_persistent_id(id_salt);
694 ctx.check_for_id_clash(
695 id,
696 Rect::from_min_size(ui.available_rect_before_wrap().min, Vec2::ZERO),
697 "ScrollArea",
698 );
699 let mut state = State::load(&ctx, id).unwrap_or_default();
700
701 state.offset.x = offset_x.unwrap_or(state.offset.x);
702 state.offset.y = offset_y.unwrap_or(state.offset.y);
703
704 let show_bars: Vec2b = match scroll_bar_visibility {
705 ScrollBarVisibility::AlwaysHidden => Vec2b::FALSE,
706 ScrollBarVisibility::VisibleWhenNeeded => state.show_scroll,
707 ScrollBarVisibility::AlwaysVisible => direction_enabled,
708 };
709
710 let show_bars_factor = Vec2::new(
711 ctx.animate_bool_responsive(id.with("h"), show_bars[0]),
712 ctx.animate_bool_responsive(id.with("v"), show_bars[1]),
713 );
714
715 let current_bar_use = show_bars_factor.yx() * ui.spacing().scroll.allocated_width();
716
717 let available_outer = ui.available_rect_before_wrap();
718
719 let outer_size = available_outer.size().at_most(max_size);
720
721 let inner_size = {
722 let mut inner_size = outer_size - current_bar_use;
723
724 for d in 0..2 {
729 if direction_enabled[d] {
730 inner_size[d] = inner_size[d].max(min_scrolled_size[d]);
731 }
732 }
733 inner_size
734 };
735
736 let inner_rect = Rect::from_min_size(available_outer.min, inner_size);
737
738 let mut content_max_size = inner_size;
739
740 if true {
741 } else {
744 for d in 0..2 {
746 if direction_enabled[d] {
747 content_max_size[d] = f32::INFINITY;
748 }
749 }
750 }
751
752 let content_max_rect = Rect::from_min_size(inner_rect.min - state.offset, content_max_size);
753
754 let content_max_rect = content_max_rect
756 .round_to_pixels(ui.pixels_per_point())
757 .round_ui();
758
759 let mut content_ui = ui.new_child(
760 UiBuilder::new()
761 .ui_stack_info(UiStackInfo::new(UiKind::ScrollArea))
762 .max_rect(content_max_rect),
763 );
764
765 {
766 let clip_rect_margin = ui.visuals().clip_rect_margin;
768 let mut content_clip_rect = ui.clip_rect();
769 for d in 0..2 {
770 if direction_enabled[d] {
771 content_clip_rect.min[d] = inner_rect.min[d] - clip_rect_margin;
772 content_clip_rect.max[d] = inner_rect.max[d] + clip_rect_margin;
773 } else {
774 content_clip_rect.max[d] = ui.clip_rect().max[d] - current_bar_use[d];
776 }
777 }
778 content_clip_rect = content_clip_rect.intersect(ui.clip_rect());
780 content_ui.set_clip_rect(content_clip_rect);
781 }
782
783 let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size);
784 let dt = ui.input(|i| i.stable_dt).at_most(0.1);
785
786 let background_drag_response =
787 if scroll_source.drag && ui.is_enabled() && state.content_is_too_large.any() {
788 let content_response_option = state
792 .interact_rect
793 .map(|rect| ui.interact(rect, id.with("area"), Sense::drag()));
794
795 if content_response_option
796 .as_ref()
797 .is_some_and(|response| response.dragged())
798 {
799 for d in 0..2 {
800 if direction_enabled[d] {
801 ui.input(|input| {
802 state.offset[d] -= input.pointer.delta()[d];
803 });
804 state.scroll_stuck_to_end[d] = false;
805 state.offset_target[d] = None;
806 }
807 }
808 } else {
809 if content_response_option
811 .as_ref()
812 .is_some_and(|response| response.drag_stopped())
813 {
814 state.vel = direction_enabled.to_vec2()
815 * ui.input(|input| input.pointer.velocity());
816 }
817 for d in 0..2 {
818 let stop_speed = 20.0; let friction_coeff = 1000.0; let friction = friction_coeff * dt;
823 if friction > state.vel[d].abs() || state.vel[d].abs() < stop_speed {
824 state.vel[d] = 0.0;
825 } else {
826 state.vel[d] -= friction * state.vel[d].signum();
827 state.offset[d] -= state.vel[d] * dt;
830 ctx.request_repaint();
831 }
832 }
833 }
834
835 if let Some(response) = &content_response_option {
837 if response.dragged()
838 && let Some(cursor) = on_drag_cursor
839 {
840 ui.ctx().set_cursor_icon(cursor);
841 } else if response.hovered()
842 && let Some(cursor) = on_hover_cursor
843 {
844 ui.ctx().set_cursor_icon(cursor);
845 }
846 }
847
848 content_response_option
849 } else {
850 None
851 };
852
853 for d in 0..2 {
856 if let Some(scroll_target) = state.offset_target[d] {
857 state.vel[d] = 0.0;
858
859 if (state.offset[d] - scroll_target.target_offset).abs() < 1.0 {
860 state.offset[d] = scroll_target.target_offset;
862 state.offset_target[d] = None;
863 } else {
864 let t = emath::interpolation_factor(
866 scroll_target.animation_time_span,
867 ui.input(|i| i.time),
868 dt,
869 emath::ease_in_ease_out,
870 );
871 if t < 1.0 {
872 state.offset[d] =
873 emath::lerp(state.offset[d]..=scroll_target.target_offset, t);
874 ctx.request_repaint();
875 } else {
876 state.offset[d] = scroll_target.target_offset;
878 state.offset_target[d] = None;
879 }
880 }
881 }
882 }
883
884 let saved_scroll_target = content_ui
885 .ctx()
886 .pass_state_mut(|state| std::mem::take(&mut state.scroll_target));
887
888 Prepared {
889 id,
890 state,
891 auto_shrink,
892 direction_enabled,
893 show_bars_factor,
894 current_bar_use,
895 scroll_bar_visibility,
896 scroll_bar_rect,
897 inner_rect,
898 content_ui,
899 viewport,
900 scroll_source,
901 wheel_scroll_multiplier,
902 stick_to_end,
903 saved_scroll_target,
904 background_drag_response,
905 animated,
906 }
907 }
908
909 pub fn show<R>(
913 self,
914 ui: &mut Ui,
915 add_contents: impl FnOnce(&mut Ui) -> R,
916 ) -> ScrollAreaOutput<R> {
917 self.show_viewport_dyn(ui, Box::new(|ui, _viewport| add_contents(ui)))
918 }
919
920 pub fn show_rows<R>(
937 self,
938 ui: &mut Ui,
939 row_height_sans_spacing: f32,
940 total_rows: usize,
941 add_contents: impl FnOnce(&mut Ui, std::ops::Range<usize>) -> R,
942 ) -> ScrollAreaOutput<R> {
943 let spacing = ui.spacing().item_spacing;
944 let row_height_with_spacing = row_height_sans_spacing + spacing.y;
945 self.show_viewport(ui, |ui, viewport| {
946 ui.set_height((row_height_with_spacing * total_rows as f32 - spacing.y).at_least(0.0));
947
948 let mut min_row = (viewport.min.y / row_height_with_spacing).floor() as usize;
949 let mut max_row = (viewport.max.y / row_height_with_spacing).ceil() as usize + 1;
950 if max_row > total_rows {
951 let diff = max_row.saturating_sub(min_row);
952 max_row = total_rows;
953 min_row = total_rows.saturating_sub(diff);
954 }
955
956 let y_min = ui.max_rect().top() + min_row as f32 * row_height_with_spacing;
957 let y_max = ui.max_rect().top() + max_row as f32 * row_height_with_spacing;
958
959 let rect = Rect::from_x_y_ranges(ui.max_rect().x_range(), y_min..=y_max);
960
961 ui.scope_builder(UiBuilder::new().max_rect(rect), |viewport_ui| {
962 viewport_ui.skip_ahead_auto_ids(min_row); add_contents(viewport_ui, min_row..max_row)
964 })
965 .inner
966 })
967 }
968
969 pub fn show_viewport<R>(
974 self,
975 ui: &mut Ui,
976 add_contents: impl FnOnce(&mut Ui, Rect) -> R,
977 ) -> ScrollAreaOutput<R> {
978 self.show_viewport_dyn(ui, Box::new(add_contents))
979 }
980
981 fn show_viewport_dyn<'c, R>(
982 self,
983 ui: &mut Ui,
984 add_contents: Box<dyn FnOnce(&mut Ui, Rect) -> R + 'c>,
985 ) -> ScrollAreaOutput<R> {
986 let mut prepared = self.begin(ui);
987 let id = prepared.id;
988 let inner_rect = prepared.inner_rect;
989 let inner = add_contents(&mut prepared.content_ui, prepared.viewport);
990 let (content_size, state) = prepared.end(ui);
991 ScrollAreaOutput {
992 inner,
993 id,
994 state,
995 content_size,
996 inner_rect,
997 }
998 }
999}
1000
1001impl Prepared {
1002 fn end(self, ui: &mut Ui) -> (Vec2, State) {
1004 let Self {
1005 id,
1006 mut state,
1007 inner_rect,
1008 auto_shrink,
1009 direction_enabled,
1010 mut show_bars_factor,
1011 current_bar_use,
1012 scroll_bar_visibility,
1013 scroll_bar_rect,
1014 content_ui,
1015 viewport: _,
1016 scroll_source,
1017 wheel_scroll_multiplier,
1018 stick_to_end,
1019 saved_scroll_target,
1020 background_drag_response,
1021 animated,
1022 } = self;
1023
1024 let content_size = content_ui.min_size();
1025
1026 let scroll_delta = content_ui
1027 .ctx()
1028 .pass_state_mut(|state| std::mem::take(&mut state.scroll_delta));
1029
1030 for d in 0..2 {
1031 let mut delta = -scroll_delta.0[d];
1033 let mut animation = scroll_delta.1;
1034
1035 let scroll_target = content_ui
1038 .ctx()
1039 .pass_state_mut(|state| state.scroll_target[d].take());
1040
1041 if direction_enabled[d] {
1042 if let Some(target) = scroll_target {
1043 let pass_state::ScrollTarget {
1044 range,
1045 align,
1046 animation: animation_update,
1047 } = target;
1048 let min = content_ui.min_rect().min[d];
1049 let clip_rect = content_ui.clip_rect();
1050 let visible_range = min..=min + clip_rect.size()[d];
1051 let (start, end) = (range.min, range.max);
1052 let clip_start = clip_rect.min[d];
1053 let clip_end = clip_rect.max[d];
1054 let mut spacing = content_ui.spacing().item_spacing[d];
1055
1056 let delta_update = if let Some(align) = align {
1057 let center_factor = align.to_factor();
1058
1059 let offset =
1060 lerp(range, center_factor) - lerp(visible_range, center_factor);
1061
1062 spacing *= remap(center_factor, 0.0..=1.0, -1.0..=1.0);
1064
1065 offset + spacing - state.offset[d]
1066 } else if start < clip_start && end < clip_end {
1067 -(clip_start - start + spacing).min(clip_end - end - spacing)
1068 } else if end > clip_end && start > clip_start {
1069 (end - clip_end + spacing).min(start - clip_start - spacing)
1070 } else {
1071 0.0
1073 };
1074
1075 delta += delta_update;
1076 animation = animation_update;
1077 }
1078
1079 if delta != 0.0 {
1080 let target_offset = state.offset[d] + delta;
1081
1082 if !animated {
1083 state.offset[d] = target_offset;
1084 } else if let Some(animation) = &mut state.offset_target[d] {
1085 animation.target_offset = target_offset;
1088 } else {
1089 let now = ui.input(|i| i.time);
1091 let animation_duration = (delta.abs() / animation.points_per_second)
1092 .clamp(animation.duration.min, animation.duration.max);
1093 state.offset_target[d] = Some(ScrollingToTarget {
1094 animation_time_span: (now, now + animation_duration as f64),
1095 target_offset,
1096 });
1097 }
1098 ui.ctx().request_repaint();
1099 }
1100 }
1101 }
1102
1103 ui.ctx().pass_state_mut(|state| {
1105 for d in 0..2 {
1106 if saved_scroll_target[d].is_some() {
1107 state.scroll_target[d] = saved_scroll_target[d].clone();
1108 }
1109 }
1110 });
1111
1112 let inner_rect = {
1113 let mut inner_size = inner_rect.size();
1115
1116 for d in 0..2 {
1117 inner_size[d] = match (direction_enabled[d], auto_shrink[d]) {
1118 (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]), };
1123 }
1124
1125 Rect::from_min_size(inner_rect.min, inner_size)
1126 };
1127
1128 let outer_rect = Rect::from_min_size(inner_rect.min, inner_rect.size() + current_bar_use);
1129
1130 let content_is_too_large = Vec2b::new(
1131 direction_enabled[0] && inner_rect.width() < content_size.x,
1132 direction_enabled[1] && inner_rect.height() < content_size.y,
1133 );
1134
1135 let max_offset = content_size - inner_rect.size();
1136
1137 let is_dragging_background = background_drag_response
1139 .as_ref()
1140 .is_some_and(|r| r.dragged());
1141
1142 let is_hovering_outer_rect = ui.rect_contains_pointer(outer_rect)
1143 && ui.ctx().dragged_id().is_none()
1144 || is_dragging_background;
1145
1146 if scroll_source.mouse_wheel && ui.is_enabled() && is_hovering_outer_rect {
1147 let always_scroll_enabled_direction = ui.style().always_scroll_the_only_direction
1148 && direction_enabled[0] != direction_enabled[1];
1149 for d in 0..2 {
1150 if direction_enabled[d] {
1151 let scroll_delta = ui.ctx().input(|input| {
1152 if always_scroll_enabled_direction {
1153 input.smooth_scroll_delta[0] + input.smooth_scroll_delta[1]
1155 } else {
1156 input.smooth_scroll_delta[d]
1157 }
1158 });
1159 let scroll_delta = scroll_delta * wheel_scroll_multiplier[d];
1160
1161 let scrolling_up = state.offset[d] > 0.0 && scroll_delta > 0.0;
1162 let scrolling_down = state.offset[d] < max_offset[d] && scroll_delta < 0.0;
1163
1164 if scrolling_up || scrolling_down {
1165 state.offset[d] -= scroll_delta;
1166
1167 ui.ctx().input_mut(|input| {
1169 if always_scroll_enabled_direction {
1170 input.smooth_scroll_delta[0] = 0.0;
1171 input.smooth_scroll_delta[1] = 0.0;
1172 } else {
1173 input.smooth_scroll_delta[d] = 0.0;
1174 }
1175 });
1176
1177 state.scroll_stuck_to_end[d] = false;
1178 state.offset_target[d] = None;
1179 }
1180 }
1181 }
1182 }
1183
1184 let show_scroll_this_frame = match scroll_bar_visibility {
1185 ScrollBarVisibility::AlwaysHidden => Vec2b::FALSE,
1186 ScrollBarVisibility::VisibleWhenNeeded => content_is_too_large,
1187 ScrollBarVisibility::AlwaysVisible => direction_enabled,
1188 };
1189
1190 if show_scroll_this_frame[0] && show_bars_factor.x <= 0.0 {
1192 show_bars_factor.x = ui.ctx().animate_bool_responsive(id.with("h"), true);
1193 }
1194 if show_scroll_this_frame[1] && show_bars_factor.y <= 0.0 {
1195 show_bars_factor.y = ui.ctx().animate_bool_responsive(id.with("v"), true);
1196 }
1197
1198 let scroll_style = ui.spacing().scroll;
1199
1200 let scroll_bar_rect = scroll_bar_rect.unwrap_or(inner_rect);
1202 for d in 0..2 {
1203 if stick_to_end[d] && state.scroll_stuck_to_end[d] {
1205 state.offset[d] = content_size[d] - inner_rect.size()[d];
1206 }
1207
1208 let show_factor = show_bars_factor[d];
1209 if show_factor == 0.0 {
1210 state.scroll_bar_interaction[d] = false;
1211 continue;
1212 }
1213
1214 let inner_margin = show_factor * scroll_style.bar_inner_margin;
1216 let outer_margin = show_factor * scroll_style.bar_outer_margin;
1217
1218 let mut cross = if scroll_style.floating {
1221 let max_bar_rect = if d == 0 {
1224 outer_rect.with_min_y(outer_rect.max.y - outer_margin - scroll_style.bar_width)
1225 } else {
1226 outer_rect.with_min_x(outer_rect.max.x - outer_margin - scroll_style.bar_width)
1227 };
1228
1229 let is_hovering_bar_area = is_hovering_outer_rect
1230 && ui.rect_contains_pointer(max_bar_rect)
1231 && !is_dragging_background
1232 || state.scroll_bar_interaction[d];
1233
1234 let is_hovering_bar_area_t = ui
1235 .ctx()
1236 .animate_bool_responsive(id.with((d, "bar_hover")), is_hovering_bar_area);
1237
1238 let width = show_factor
1239 * lerp(
1240 scroll_style.floating_width..=scroll_style.bar_width,
1241 is_hovering_bar_area_t,
1242 );
1243
1244 let max_cross = outer_rect.max[1 - d] - outer_margin;
1245 let min_cross = max_cross - width;
1246 Rangef::new(min_cross, max_cross)
1247 } else {
1248 let min_cross = inner_rect.max[1 - d] + inner_margin;
1249 let max_cross = outer_rect.max[1 - d] - outer_margin;
1250 Rangef::new(min_cross, max_cross)
1251 };
1252
1253 if ui.clip_rect().max[1 - d] < cross.max + outer_margin {
1254 let width = cross.max - cross.min;
1264 cross.max = ui.clip_rect().max[1 - d] - outer_margin;
1265 cross.min = cross.max - width;
1266 }
1267
1268 let outer_scroll_bar_rect = if d == 0 {
1269 Rect::from_min_max(
1270 pos2(scroll_bar_rect.left(), cross.min),
1271 pos2(scroll_bar_rect.right(), cross.max),
1272 )
1273 } else {
1274 Rect::from_min_max(
1275 pos2(cross.min, scroll_bar_rect.top()),
1276 pos2(cross.max, scroll_bar_rect.bottom()),
1277 )
1278 };
1279
1280 let from_content = |content| {
1281 remap_clamp(
1282 content,
1283 0.0..=content_size[d],
1284 scroll_bar_rect.min[d]..=scroll_bar_rect.max[d],
1285 )
1286 };
1287
1288 let calculate_handle_rect = |d, offset: &Vec2| {
1289 let handle_size = if d == 0 {
1290 from_content(offset.x + inner_rect.width()) - from_content(offset.x)
1291 } else {
1292 from_content(offset.y + inner_rect.height()) - from_content(offset.y)
1293 }
1294 .max(scroll_style.handle_min_length);
1295
1296 let handle_start_point = remap_clamp(
1297 offset[d],
1298 0.0..=max_offset[d],
1299 scroll_bar_rect.min[d]..=(scroll_bar_rect.max[d] - handle_size),
1300 );
1301
1302 if d == 0 {
1303 Rect::from_min_max(
1304 pos2(handle_start_point, cross.min),
1305 pos2(handle_start_point + handle_size, cross.max),
1306 )
1307 } else {
1308 Rect::from_min_max(
1309 pos2(cross.min, handle_start_point),
1310 pos2(cross.max, handle_start_point + handle_size),
1311 )
1312 }
1313 };
1314
1315 let handle_rect = calculate_handle_rect(d, &state.offset);
1316
1317 let interact_id = id.with(d);
1318 let sense = if scroll_source.scroll_bar && ui.is_enabled() {
1319 Sense::click_and_drag()
1320 } else {
1321 Sense::hover()
1322 };
1323 let response = ui.interact(outer_scroll_bar_rect, interact_id, sense);
1324
1325 state.scroll_bar_interaction[d] = response.hovered() || response.dragged();
1326
1327 if let Some(pointer_pos) = response.interact_pointer_pos() {
1328 let scroll_start_offset_from_top_left = state.scroll_start_offset_from_top_left[d]
1329 .get_or_insert_with(|| {
1330 if handle_rect.contains(pointer_pos) {
1331 pointer_pos[d] - handle_rect.min[d]
1332 } else {
1333 let handle_top_pos_at_bottom =
1334 scroll_bar_rect.max[d] - handle_rect.size()[d];
1335 let new_handle_top_pos = (pointer_pos[d] - handle_rect.size()[d] / 2.0)
1337 .clamp(scroll_bar_rect.min[d], handle_top_pos_at_bottom);
1338 pointer_pos[d] - new_handle_top_pos
1339 }
1340 });
1341
1342 let new_handle_top = pointer_pos[d] - *scroll_start_offset_from_top_left;
1343 let handle_travel =
1344 scroll_bar_rect.min[d]..=(scroll_bar_rect.max[d] - handle_rect.size()[d]);
1345 state.offset[d] = if handle_travel.start() == handle_travel.end() {
1346 0.0
1347 } else {
1348 remap(new_handle_top, handle_travel, 0.0..=max_offset[d])
1349 };
1350
1351 state.scroll_stuck_to_end[d] = false;
1353 state.offset_target[d] = None;
1354 } else {
1355 state.scroll_start_offset_from_top_left[d] = None;
1356 }
1357
1358 let unbounded_offset = state.offset[d];
1359 state.offset[d] = state.offset[d].max(0.0);
1360 state.offset[d] = state.offset[d].min(max_offset[d]);
1361
1362 if state.offset[d] != unbounded_offset {
1363 state.vel[d] = 0.0;
1364 }
1365
1366 if ui.is_rect_visible(outer_scroll_bar_rect) {
1367 let handle_rect = calculate_handle_rect(d, &state.offset);
1369
1370 let visuals = if scroll_source.scroll_bar && ui.is_enabled() {
1371 let is_hovering_handle = response.hovered()
1374 && ui.input(|i| {
1375 i.pointer
1376 .latest_pos()
1377 .is_some_and(|p| handle_rect.contains(p))
1378 });
1379 let visuals = ui.visuals();
1380 if response.is_pointer_button_down_on() {
1381 &visuals.widgets.active
1382 } else if is_hovering_handle {
1383 &visuals.widgets.hovered
1384 } else {
1385 &visuals.widgets.inactive
1386 }
1387 } else {
1388 &ui.visuals().widgets.inactive
1389 };
1390
1391 let handle_opacity = if scroll_style.floating {
1392 if response.hovered() || response.dragged() {
1393 scroll_style.interact_handle_opacity
1394 } else {
1395 let is_hovering_outer_rect_t = ui.ctx().animate_bool_responsive(
1396 id.with((d, "is_hovering_outer_rect")),
1397 is_hovering_outer_rect,
1398 );
1399 lerp(
1400 scroll_style.dormant_handle_opacity
1401 ..=scroll_style.active_handle_opacity,
1402 is_hovering_outer_rect_t,
1403 )
1404 }
1405 } else {
1406 1.0
1407 };
1408
1409 let background_opacity = if scroll_style.floating {
1410 if response.hovered() || response.dragged() {
1411 scroll_style.interact_background_opacity
1412 } else if is_hovering_outer_rect {
1413 scroll_style.active_background_opacity
1414 } else {
1415 scroll_style.dormant_background_opacity
1416 }
1417 } else {
1418 1.0
1419 };
1420
1421 let handle_color = if scroll_style.foreground_color {
1422 visuals.fg_stroke.color
1423 } else {
1424 visuals.bg_fill
1425 };
1426
1427 ui.painter().add(epaint::Shape::rect_filled(
1429 outer_scroll_bar_rect,
1430 visuals.corner_radius,
1431 ui.visuals()
1432 .extreme_bg_color
1433 .gamma_multiply(background_opacity),
1434 ));
1435
1436 ui.painter().add(epaint::Shape::rect_filled(
1438 handle_rect,
1439 visuals.corner_radius,
1440 handle_color.gamma_multiply(handle_opacity),
1441 ));
1442 }
1443 }
1444
1445 ui.advance_cursor_after_rect(outer_rect);
1446
1447 if show_scroll_this_frame != state.show_scroll {
1448 ui.ctx().request_repaint();
1449 }
1450
1451 let available_offset = content_size - inner_rect.size();
1452 state.offset = state.offset.min(available_offset);
1453 state.offset = state.offset.max(Vec2::ZERO);
1454
1455 state.scroll_stuck_to_end = Vec2b::new(
1461 (state.offset[0] == available_offset[0])
1462 || (self.stick_to_end[0] && available_offset[0] < 0.0),
1463 (state.offset[1] == available_offset[1])
1464 || (self.stick_to_end[1] && available_offset[1] < 0.0),
1465 );
1466
1467 state.show_scroll = show_scroll_this_frame;
1468 state.content_is_too_large = content_is_too_large;
1469 state.interact_rect = Some(inner_rect);
1470
1471 state.store(ui.ctx(), id);
1472
1473 (content_size, state)
1474 }
1475}