1use crate::container;
23use crate::core::alignment;
24use crate::core::border::{self, Border};
25use crate::core::keyboard;
26use crate::core::layout;
27use crate::core::mouse;
28use crate::core::overlay;
29use crate::core::renderer;
30use crate::core::text;
31use crate::core::time::{Duration, Instant};
32use crate::core::touch;
33use crate::core::widget;
34use crate::core::widget::operation::{self, Animation, Operation};
35use crate::core::widget::tree::{self, Tree};
36use crate::core::window;
37use crate::core::{
38 self, Background, Color, Event, InputMethod, Layout, Length, Padding, Pixels, Point, Rectangle,
39 Shadow, Shell, Size, Theme, Vector, Widget,
40};
41
42pub use operation::scrollable::{AbsoluteOffset, RelativeOffset};
43
44pub struct Scrollable<'a, Message, W = crate::Element<'a, Message>, Theme = crate::Theme>
84where
85 Theme: Catalog,
86{
87 id: Option<widget::Id>,
88 width: Length,
89 height: Length,
90 direction: Direction,
91 auto_scroll: bool,
92 smooth_scroll: bool,
93 click_to_scroll: bool,
94 content: W,
95 on_scroll: Option<Box<dyn Fn(Scroll) -> Action<Message> + 'a>>,
96 class: Theme::Class<'a>,
97}
98
99impl<'a, Message, W, Theme> Scrollable<'a, Message, W, Theme>
100where
101 Theme: Catalog,
102{
103 pub fn new(content: W) -> Self {
105 Self::with_direction(content, Direction::default())
106 }
107
108 pub fn with_direction(content: W, direction: impl Into<Direction>) -> Self {
110 Scrollable {
111 id: None,
112 width: Length::Fit,
113 height: Length::Fit,
114 direction: direction.into(),
115 auto_scroll: false,
116 smooth_scroll: true,
117 click_to_scroll: false,
118 content,
119 on_scroll: None,
120 class: Theme::default(),
121 }
122 }
123
124 pub fn horizontal(self) -> Self {
126 self.direction(Direction::Horizontal(Scrollbar::default()))
127 }
128
129 pub fn direction(mut self, direction: impl Into<Direction>) -> Self {
131 self.direction = direction.into();
132 self
133 }
134
135 pub fn id(mut self, id: impl Into<widget::Id>) -> Self {
137 self.id = Some(id.into());
138 self
139 }
140
141 pub fn width(mut self, width: impl Into<Length>) -> Self {
143 self.width = width.into();
144 self
145 }
146
147 pub fn height(mut self, height: impl Into<Length>) -> Self {
149 self.height = height.into();
150 self
151 }
152
153 pub fn on_scroll<T>(mut self, f: impl Fn(Scroll) -> T + 'a) -> Self
159 where
160 T: Into<Action<Message>>,
161 {
162 self.on_scroll = Some(Box::new(move |scroll| f(scroll).into()));
163 self
164 }
165
166 pub fn anchor_top(self) -> Self {
168 self.anchor_y(Anchor::Start)
169 }
170
171 pub fn anchor_bottom(self) -> Self {
173 self.anchor_y(Anchor::End)
174 }
175
176 pub fn anchor_left(self) -> Self {
178 self.anchor_x(Anchor::Start)
179 }
180
181 pub fn anchor_right(self) -> Self {
183 self.anchor_x(Anchor::End)
184 }
185
186 pub fn anchor_x(mut self, alignment: Anchor) -> Self {
188 match &mut self.direction {
189 Direction::Horizontal(horizontal) | Direction::Both { horizontal, .. } => {
190 horizontal.alignment = alignment;
191 }
192 Direction::Vertical { .. } => {}
193 }
194
195 self
196 }
197
198 pub fn anchor_y(mut self, alignment: Anchor) -> Self {
200 match &mut self.direction {
201 Direction::Vertical(vertical) | Direction::Both { vertical, .. } => {
202 vertical.alignment = alignment;
203 }
204 Direction::Horizontal { .. } => {}
205 }
206
207 self
208 }
209
210 pub fn spacing(mut self, new_spacing: impl Into<Pixels>) -> Self {
216 match &mut self.direction {
217 Direction::Horizontal(scrollbar) | Direction::Vertical(scrollbar) => {
218 scrollbar.spacing = Some(new_spacing.into().0);
219 }
220 Direction::Both { .. } => {}
221 }
222
223 self
224 }
225
226 pub fn padding(mut self, new_padding: impl Into<Pixels>) -> Self {
235 let padding = new_padding.into().0;
236
237 match &mut self.direction {
238 Direction::Horizontal(scrollbar) | Direction::Vertical(scrollbar) => {
239 scrollbar.padding = padding;
240 }
241 Direction::Both {
242 horizontal,
243 vertical,
244 } => {
245 horizontal.padding = padding;
246 vertical.padding = padding;
247 }
248 }
249
250 self
251 }
252
253 pub fn auto_scroll(mut self, auto_scroll: bool) -> Self {
258 self.auto_scroll = auto_scroll;
259 self
260 }
261
262 pub fn smooth_scroll(mut self, smooth_scroll: bool) -> Self {
277 self.smooth_scroll = smooth_scroll;
278 self
279 }
280
281 pub fn click_to_scroll(mut self, click_to_scroll: bool) -> Self {
295 self.click_to_scroll = click_to_scroll;
296 self
297 }
298
299 #[must_use]
301 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
302 where
303 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
304 {
305 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
306 self
307 }
308
309 #[cfg(feature = "advanced")]
311 #[must_use]
312 pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
313 self.class = class.into();
314 self
315 }
316}
317
318#[derive(Debug, Clone, Copy, PartialEq)]
320pub enum Direction {
321 Vertical(Scrollbar),
323 Horizontal(Scrollbar),
325 Both {
327 vertical: Scrollbar,
329 horizontal: Scrollbar,
331 },
332}
333
334impl Direction {
335 pub fn horizontal(&self) -> Option<&Scrollbar> {
337 match self {
338 Self::Horizontal(scrollbar) => Some(scrollbar),
339 Self::Both { horizontal, .. } => Some(horizontal),
340 Self::Vertical(_) => None,
341 }
342 }
343
344 pub fn vertical(&self) -> Option<&Scrollbar> {
346 match self {
347 Self::Vertical(scrollbar) => Some(scrollbar),
348 Self::Both { vertical, .. } => Some(vertical),
349 Self::Horizontal(_) => None,
350 }
351 }
352
353 fn align(&self, delta: Vector) -> Vector {
354 let horizontal_alignment = self.horizontal().map(|p| p.alignment).unwrap_or_default();
355
356 let vertical_alignment = self.vertical().map(|p| p.alignment).unwrap_or_default();
357
358 let align = |alignment: Anchor, delta: f32| match alignment {
359 Anchor::Start => delta,
360 Anchor::End => -delta,
361 };
362
363 Vector::new(
364 align(horizontal_alignment, delta.x),
365 align(vertical_alignment, delta.y),
366 )
367 }
368}
369
370impl Default for Direction {
371 fn default() -> Self {
372 Self::Vertical(Scrollbar::default())
373 }
374}
375
376#[derive(Debug, Clone, Copy, PartialEq)]
378pub struct Scrollbar {
379 width: f32,
380 margin: f32,
381 scroller_width: f32,
382 alignment: Anchor,
383 spacing: Option<f32>,
384 padding: f32,
385}
386
387impl Default for Scrollbar {
388 fn default() -> Self {
389 Self {
390 width: 10.0,
391 margin: 0.0,
392 scroller_width: 10.0,
393 alignment: Anchor::Start,
394 spacing: None,
395 padding: 0.0,
396 }
397 }
398}
399
400impl Scrollbar {
401 pub fn new() -> Self {
403 Self::default()
404 }
405
406 pub fn hidden() -> Self {
409 Self::default().width(0).scroller_width(0)
410 }
411
412 pub fn width(mut self, width: impl Into<Pixels>) -> Self {
414 self.width = width.into().0.max(0.0);
415 self
416 }
417
418 pub fn margin(mut self, margin: impl Into<Pixels>) -> Self {
420 self.margin = margin.into().0;
421 self
422 }
423
424 pub fn scroller_width(mut self, scroller_width: impl Into<Pixels>) -> Self {
426 self.scroller_width = scroller_width.into().0.max(0.0);
427 self
428 }
429
430 pub fn anchor(mut self, alignment: Anchor) -> Self {
432 self.alignment = alignment;
433 self
434 }
435
436 pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
442 self.spacing = Some(spacing.into().0);
443 self
444 }
445
446 pub fn padding(mut self, padding: impl Into<Pixels>) -> Self {
454 self.padding = padding.into().0;
455 self
456 }
457}
458
459#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
462pub enum Anchor {
463 #[default]
465 Start,
466 End,
468}
469
470impl<Message, W, Theme> widget::Meta for Scrollable<'_, Message, W, Theme> where Theme: Catalog {}
471
472impl<Message, W, Theme, Renderer> Widget<Message, Theme, Renderer>
473 for Scrollable<'_, Message, W, Theme>
474where
475 Theme: Catalog,
476 Renderer: text::Renderer,
477 W: Widget<Message, Theme, Renderer>,
478{
479 fn tag(&self) -> tree::Tag {
480 tree::Tag::of::<State>()
481 }
482
483 fn state(&self) -> tree::State {
484 tree::State::new(State::new())
485 }
486
487 fn diff(&mut self, tree: &mut Tree) {
488 tree.diff_children(std::slice::from_mut(&mut self.content));
489
490 let state = tree.state.downcast_mut::<State>();
491
492 if state.last_id != self.id {
493 *state = State {
494 last_id: self.id.clone(),
495 ..State::default()
496 };
497 }
498
499 let size = self.content.size();
500
501 self.width = self.width.stack(size.width);
502 self.height = self.height.stack(size.height);
503 }
504
505 fn size(&self) -> Size<Length> {
506 Size {
507 width: self.width,
508 height: self.height,
509 }
510 }
511
512 fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits) {
513 let mut layout = |tree: &mut Tree, right_padding: f32, bottom_padding: f32| {
514 let is_horizontal = self.direction.horizontal().is_some();
515 let is_vertical = self.direction.vertical().is_some();
516
517 layout::padded(
518 tree,
519 limits,
520 self.width,
521 self.height,
522 Padding {
523 right: right_padding,
524 bottom: bottom_padding,
525 ..Padding::ZERO
526 },
527 |tree, limits| {
528 let child_limits = layout::Limits::with_flags(
529 limits.min,
530 limits.max,
531 limits.compression,
532 Size::new(
533 limits.infinite.width || is_horizontal,
534 limits.infinite.height || is_vertical,
535 ),
536 );
537
538 self.content.layout(tree, renderer, &child_limits);
539
540 tree.size
541 },
542 );
543 };
544
545 match self.direction {
546 Direction::Vertical(Scrollbar {
547 width,
548 margin,
549 spacing: Some(spacing),
550 ..
551 })
552 | Direction::Horizontal(Scrollbar {
553 width,
554 margin,
555 spacing: Some(spacing),
556 ..
557 }) => {
558 let is_vertical = matches!(self.direction, Direction::Vertical(_));
559
560 let padding = width + margin * 2.0 + spacing;
561 let was_scrollbar_visible = tree.state.downcast_mut::<State>().is_scrollbar_visible;
562
563 layout(
564 tree,
565 if is_vertical && was_scrollbar_visible {
566 padding
567 } else {
568 0.0
569 },
570 if !is_vertical && was_scrollbar_visible {
571 padding
572 } else {
573 0.0
574 },
575 );
576
577 let is_scrollbar_visible = if is_vertical {
578 tree.children[0].size.height > tree.size.height
579 } else {
580 tree.children[0].size.width > tree.size.width
581 };
582
583 if was_scrollbar_visible != is_scrollbar_visible {
584 log::trace!("Scrollbar status quo has changed");
585 tree.state.downcast_mut::<State>().is_scrollbar_visible = is_scrollbar_visible;
586
587 layout(
588 tree,
589 if is_vertical && is_scrollbar_visible {
590 padding
591 } else {
592 0.0
593 },
594 if !is_vertical && is_scrollbar_visible {
595 padding
596 } else {
597 0.0
598 },
599 );
600 }
601 }
602 _ => layout(tree, 0.0, 0.0),
603 }
604 }
605
606 fn operate(
607 &mut self,
608 tree: &mut Tree,
609 layout: Layout,
610 viewport: &Rectangle,
611 renderer: &Renderer,
612 operation: &mut dyn Operation,
613 ) {
614 let bounds = layout.bounds();
615 let (content_layout, content_tree) = layout.iter_mut(&mut tree.children).next().unwrap();
616 let content_bounds = content_layout.bounds();
617 let content = content_layout.size();
618
619 let state = tree.state.downcast_mut::<State>();
620
621 state.smooth_scroll = self.smooth_scroll;
623
624 let translation = state.last_translation;
625 let viewport = viewport.intersection(&bounds).unwrap_or_default() + translation;
626
627 operation.scrollable(self.id.as_ref(), bounds, content, translation, state);
628 operation.container(self.id.as_ref(), content_bounds, &viewport);
629
630 operation.traverse(&mut |operation| {
631 self.content
632 .operate(content_tree, content_layout, &viewport, renderer, operation);
633 });
634 }
635
636 fn update(
637 &mut self,
638 tree: &mut Tree,
639 event: &Event,
640 layout: Layout,
641 cursor: mouse::Cursor,
642 renderer: &Renderer,
643 shell: &mut Shell<'_, Message>,
644 viewport: &Rectangle,
645 ) {
646 let bounds = layout.bounds();
647 let cursor_over_scrollable = cursor.position_over(bounds);
648
649 let (content_layout, content_tree) = layout.iter_mut(&mut tree.children).next().unwrap();
650 let content = content_layout.size();
651
652 let state = tree.state.downcast_mut::<State>();
653 let scrollbars = Scrollbars::new(state.last_translation, self.direction, bounds, content);
654 let mouse_over_scrollbar = scrollbars.is_mouse_over(cursor);
655 let last_offsets = (state.offset_x, state.offset_y);
656
657 let interact = state.interact(
658 event,
659 bounds,
660 content,
661 cursor,
662 cursor_over_scrollable,
663 &scrollbars,
664 mouse_over_scrollbar,
665 self.direction,
666 self.smooth_scroll,
667 self.click_to_scroll,
668 );
669
670 if let Some(scroll) = interact.scroll
671 && let Some(on_scroll) = &self.on_scroll
672 {
673 on_scroll(scroll).perform(state, bounds, content, shell);
674
675 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
676 state.last_translation = state.translation(self.direction, bounds, content);
677 }
678 }
679
680 if interact.capture {
681 shell.capture_event();
682 }
683
684 if interact.invalidate_layout {
685 shell.invalidate_layout();
686 }
687
688 if interact.request_redraw {
689 shell.request_redraw();
690 }
691
692 if !interact.stop {
693 if let Some(Content { cursor }) = interact.content {
694 let had_input_method = shell.input_method().is_enabled();
695
696 let viewport =
697 bounds.intersection(viewport).unwrap_or_default() + state.last_translation;
698
699 self.content.update(
700 content_tree,
701 event,
702 content_layout,
703 cursor,
704 renderer,
705 shell,
706 &viewport,
707 );
708
709 if !had_input_method
710 && let InputMethod::Enabled { cursor, .. } = shell.input_method_mut()
711 {
712 *cursor -= state.last_translation;
713 }
714 }
715
716 let update = state.update(
717 event,
718 bounds,
719 content,
720 cursor,
721 cursor_over_scrollable,
722 mouse_over_scrollbar,
723 self.direction,
724 self.smooth_scroll,
725 self.auto_scroll,
726 shell.is_event_captured(),
727 );
728
729 if let Some(scroll) = update.scroll
730 && let Some(on_scroll) = &self.on_scroll
731 {
732 on_scroll(scroll).perform(state, bounds, content, shell);
733 }
734
735 if update.capture {
736 shell.capture_event();
737 }
738
739 if update.invalidate_layout {
740 shell.invalidate_layout();
741 }
742
743 if update.request_redraw {
744 shell.request_redraw();
745 }
746 }
747
748 let status = if let Some(axis) = state.interaction.axis() {
749 Status::Dragged {
750 is_horizontal_scrollbar_dragged: axis == Axis::X,
751 is_vertical_scrollbar_dragged: axis == Axis::Y,
752 is_horizontal_scrollbar_disabled: scrollbars.is_x_disabled(),
753 is_vertical_scrollbar_disabled: scrollbars.is_y_disabled(),
754 }
755 } else if cursor_over_scrollable.is_some() {
756 Status::Hovered {
757 is_horizontal_scrollbar_hovered: mouse_over_scrollbar == Some(Axis::X),
758 is_vertical_scrollbar_hovered: mouse_over_scrollbar == Some(Axis::Y),
759 is_horizontal_scrollbar_disabled: scrollbars.is_x_disabled(),
760 is_vertical_scrollbar_disabled: scrollbars.is_y_disabled(),
761 }
762 } else {
763 Status::Active {
764 is_horizontal_scrollbar_disabled: scrollbars.is_x_disabled(),
765 is_vertical_scrollbar_disabled: scrollbars.is_y_disabled(),
766 }
767 };
768
769 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
770 state.last_status = Some(status);
771 }
772
773 if last_offsets != (state.offset_x, state.offset_y)
774 || state
775 .last_status
776 .is_some_and(|last_status| last_status != status)
777 {
778 shell.request_redraw();
779 }
780 }
781
782 fn draw(
783 &self,
784 tree: &Tree,
785 renderer: &mut Renderer,
786 theme: &Theme,
787 defaults: &renderer::Style,
788 layout: Layout,
789 cursor: mouse::Cursor,
790 viewport: &Rectangle,
791 ) {
792 let state = tree.state.downcast_ref::<State>();
793 let bounds = layout.bounds();
794
795 let Some(viewport) = viewport.intersection(&bounds) else {
796 return;
797 };
798
799 let (content_layout, content_tree) = layout.iter(&tree.children).next().unwrap();
800 let content = content_layout.size();
801
802 let translation = state.last_translation;
803 let scrollbars = Scrollbars::new(translation, self.direction, bounds, content);
804 let cursor_over_scrollable = cursor.position_over(bounds);
805 let mouse_over_scrollbar = scrollbars.is_mouse_over(cursor);
806
807 let cursor = match cursor_over_scrollable {
808 Some(cursor_position) if mouse_over_scrollbar.is_none() => {
809 mouse::Cursor::Available(cursor_position + translation)
810 }
811 _ => cursor.obstruct() + translation,
812 };
813
814 let style = theme.style(
815 &self.class,
816 state.last_status.unwrap_or(Status::Active {
817 is_horizontal_scrollbar_disabled: false,
818 is_vertical_scrollbar_disabled: false,
819 }),
820 );
821
822 container::draw_background(renderer, &style.container, layout.bounds());
823
824 if scrollbars.active() {
826 renderer.with_layer(viewport, |renderer| {
827 renderer.with_translation(
828 -translation.hint(renderer.hint_factor().unwrap_or(1.0)),
829 |renderer| {
830 self.content.draw(
831 content_tree,
832 renderer,
833 theme,
834 defaults,
835 content_layout,
836 cursor,
837 &(viewport + translation),
838 );
839 },
840 );
841 });
842
843 let draw_scrollbar =
844 |renderer: &mut Renderer, style: Rail, scrollbar: &internals::Scrollbar| {
845 if scrollbar.bounds.width > 0.0
846 && scrollbar.bounds.height > 0.0
847 && (style.background.is_some()
848 || (style.border.color != Color::TRANSPARENT
849 && style.border.width > 0.0))
850 {
851 renderer.fill_quad(
852 renderer::Quad {
853 bounds: scrollbar.bounds,
854 border: style.border,
855 ..renderer::Quad::default()
856 },
857 style
858 .background
859 .unwrap_or(Background::Color(Color::TRANSPARENT)),
860 );
861 }
862
863 if let Some(scroller) = scrollbar.scroller
864 && scroller.bounds.width > 0.0
865 && scroller.bounds.height > 0.0
866 && (style.scroller.background != Background::Color(Color::TRANSPARENT)
867 || (style.scroller.border.color != Color::TRANSPARENT
868 && style.scroller.border.width > 0.0))
869 {
870 renderer.fill_quad(
871 renderer::Quad {
872 bounds: scroller.bounds,
873 border: style.scroller.border,
874 ..renderer::Quad::default()
875 },
876 style.scroller.background,
877 );
878 }
879 };
880
881 let has_floating_scrollbar = scrollbars.is_any_floating();
882
883 if has_floating_scrollbar {
884 renderer.start_layer(viewport);
885 }
886
887 if let Some(scrollbar) = scrollbars.y {
888 draw_scrollbar(renderer, style.vertical_rail, &scrollbar);
889 }
890
891 if let Some(scrollbar) = scrollbars.x {
892 draw_scrollbar(renderer, style.horizontal_rail, &scrollbar);
893 }
894
895 if let (Some(x), Some(y)) = (scrollbars.x, scrollbars.y) {
896 let background = style.gap.or(style.container.background);
897
898 if let Some(background) = background {
899 renderer.fill_quad(
900 renderer::Quad {
901 bounds: Rectangle {
902 x: y.bounds.x,
903 y: x.bounds.y,
904 width: y.bounds.width,
905 height: x.bounds.height,
906 },
907 ..renderer::Quad::default()
908 },
909 background,
910 );
911 }
912 }
913
914 if has_floating_scrollbar {
915 renderer.end_layer();
916 }
917 } else {
918 self.content.draw(
919 content_tree,
920 renderer,
921 theme,
922 defaults,
923 content_layout,
924 cursor,
925 &(viewport + translation),
926 );
927 }
928 }
929
930 fn mouse_interaction(
931 &self,
932 tree: &Tree,
933 layout: Layout,
934 cursor: mouse::Cursor,
935 viewport: &Rectangle,
936 renderer: &Renderer,
937 ) -> mouse::Interaction {
938 let bounds = layout.bounds();
939 let state = tree.state.downcast_ref::<State>();
940
941 if state.interaction.scrollers_grabbed() {
942 return mouse::Interaction::Idle;
943 }
944
945 let Some(viewport) = viewport.intersection(&bounds) else {
946 return mouse::Interaction::None;
947 };
948
949 let cursor_over_scrollable = cursor.position_over(bounds);
950 let (content_layout, content_tree) = layout.iter(&tree.children).next().unwrap();
951 let content = content_layout.size();
952
953 let translation = state.last_translation;
954 let scrollbars = Scrollbars::new(translation, self.direction, bounds, content);
955 let mouse_over_scrollbar = scrollbars.is_mouse_over(cursor);
956
957 let cursor = match cursor_over_scrollable {
958 Some(cursor_position) if mouse_over_scrollbar.is_none() => {
959 mouse::Cursor::Available(cursor_position + translation)
960 }
961 _ => cursor.obstruct() + translation,
962 };
963
964 self.content.mouse_interaction(
965 content_tree,
966 content_layout,
967 cursor,
968 &(viewport + translation),
969 renderer,
970 )
971 }
972
973 fn overlay<'b>(
974 &'b mut self,
975 tree: &'b mut Tree,
976 layout: Layout,
977 renderer: &Renderer,
978 viewport: &Rectangle,
979 translation: Vector,
980 window: Size,
981 ) -> Vec<overlay::Element<'b, Message, Theme, Renderer>> {
982 let state = tree.state.downcast_ref::<State>();
983 let bounds = layout.bounds();
984 let (content_layout, content_tree) = layout.iter_mut(&mut tree.children).next().unwrap();
985 let content = content_layout.size();
986 let viewport = viewport.intersection(&bounds).unwrap_or(*viewport);
987
988 let offset = state.last_translation;
989
990 let overlay = self.content.overlay(
991 content_tree,
992 content_layout,
993 renderer,
994 &(viewport + offset),
995 translation - offset,
996 window,
997 );
998
999 let icon = if let Interaction::AutoScrolling { origin, .. } = state.interaction {
1000 let scrollbars = Scrollbars::new(offset, self.direction, bounds, content);
1001
1002 Some(overlay::Element::new(Box::new(AutoScrollIcon {
1003 origin,
1004 vertical: scrollbars.y.is_some(),
1005 horizontal: scrollbars.x.is_some(),
1006 class: &self.class,
1007 })))
1008 } else {
1009 None
1010 };
1011
1012 overlay.into_iter().chain(icon).collect()
1013 }
1014}
1015
1016struct AutoScrollIcon<'a, Class> {
1017 origin: Point,
1018 vertical: bool,
1019 horizontal: bool,
1020 class: &'a Class,
1021}
1022
1023impl<Class> AutoScrollIcon<'_, Class> {
1024 const SIZE: f32 = 40.0;
1025 const DOT: f32 = Self::SIZE / 10.0;
1026 const PADDING: f32 = Self::SIZE / 10.0;
1027}
1028
1029impl<Message, Theme, Renderer> core::Overlay<Message, Theme, Renderer>
1030 for AutoScrollIcon<'_, Theme::Class<'_>>
1031where
1032 Renderer: text::Renderer,
1033 Theme: Catalog,
1034{
1035 fn draw(
1036 &self,
1037 renderer: &mut Renderer,
1038 theme: &Theme,
1039 _style: &renderer::Style,
1040 _cursor: mouse::Cursor,
1041 ) {
1042 let bounds = Rectangle::new(
1043 self.origin - Vector::new(Self::SIZE, Self::SIZE) / 2.0,
1044 Size::new(Self::SIZE, Self::SIZE),
1045 );
1046
1047 let style = theme
1048 .style(
1049 self.class,
1050 Status::Active {
1051 is_horizontal_scrollbar_disabled: false,
1052 is_vertical_scrollbar_disabled: false,
1053 },
1054 )
1055 .auto_scroll;
1056
1057 renderer.with_layer(bounds, |renderer| {
1058 renderer.fill_quad(
1059 renderer::Quad {
1060 bounds,
1061 border: style.border,
1062 shadow: style.shadow,
1063 snap: false,
1064 },
1065 style.background,
1066 );
1067
1068 renderer.fill_quad(
1069 renderer::Quad {
1070 bounds: Rectangle::new(
1071 bounds.center() - Vector::new(Self::DOT, Self::DOT) / 2.0,
1072 Size::new(Self::DOT, Self::DOT),
1073 ),
1074 border: border::rounded(bounds.width),
1075 snap: false,
1076 ..renderer::Quad::default()
1077 },
1078 style.icon,
1079 );
1080
1081 let arrow = core::Text {
1082 content: String::new(),
1083 bounds: bounds.size(),
1084 size: Pixels::from(12),
1085 line_height: text::LineHeight::from(1.0),
1086 font: Renderer::ICON_FONT,
1087 align_x: text::Alignment::Center,
1088 align_y: alignment::Vertical::Center,
1089 shaping: text::Shaping::Basic,
1090 wrapping: text::Wrapping::None,
1091 ellipsis: text::Ellipsis::None,
1092 hint_factor: None,
1093 };
1094
1095 if self.vertical {
1096 renderer.fill_text(
1097 core::Text {
1098 content: Renderer::SCROLL_UP_ICON.to_string(),
1099 align_y: alignment::Vertical::Top,
1100 ..arrow
1101 },
1102 Point::new(bounds.center_x(), bounds.y + Self::PADDING),
1103 style.icon,
1104 bounds,
1105 );
1106
1107 renderer.fill_text(
1108 core::Text {
1109 content: Renderer::SCROLL_DOWN_ICON.to_string(),
1110 align_y: alignment::Vertical::Bottom,
1111 ..arrow
1112 },
1113 Point::new(
1114 bounds.center_x(),
1115 bounds.y + bounds.height - Self::PADDING - 0.5,
1116 ),
1117 style.icon,
1118 bounds,
1119 );
1120 }
1121
1122 if self.horizontal {
1123 renderer.fill_text(
1124 core::Text {
1125 content: Renderer::SCROLL_LEFT_ICON.to_string(),
1126 align_x: text::Alignment::Left,
1127 ..arrow
1128 },
1129 Point::new(bounds.x + Self::PADDING + 1.0, bounds.center_y() + 1.0),
1130 style.icon,
1131 bounds,
1132 );
1133
1134 renderer.fill_text(
1135 core::Text {
1136 content: Renderer::SCROLL_RIGHT_ICON.to_string(),
1137 align_x: text::Alignment::Right,
1138 ..arrow
1139 },
1140 Point::new(
1141 bounds.x + bounds.width - Self::PADDING - 1.0,
1142 bounds.center_y() + 1.0,
1143 ),
1144 style.icon,
1145 bounds,
1146 );
1147 }
1148 });
1149 }
1150
1151 fn index(&self) -> f32 {
1152 f32::MAX
1153 }
1154}
1155
1156fn rail_moved(event: &Event, cursor: mouse::Cursor, last: Point) -> Option<Point> {
1161 if matches!(
1162 event,
1163 Event::Mouse(mouse::Event::CursorMoved { .. })
1164 | Event::Touch(touch::Event::FingerMoved { .. })
1165 ) {
1166 cursor
1167 .observe()
1168 .position()
1169 .filter(|position| *position != last)
1170 } else {
1171 None
1172 }
1173}
1174
1175#[derive(Debug)]
1178struct Content {
1179 cursor: mouse::Cursor,
1181}
1182
1183#[derive(Debug, Default)]
1186struct Interact {
1187 scroll: Option<Scroll>,
1190
1191 capture: bool,
1193
1194 invalidate_layout: bool,
1196
1197 request_redraw: bool,
1199
1200 stop: bool,
1203
1204 content: Option<Content>,
1207}
1208
1209#[derive(Debug, Default)]
1211struct Update {
1212 scroll: Option<Scroll>,
1215
1216 capture: bool,
1218
1219 invalidate_layout: bool,
1221
1222 request_redraw: bool,
1224}
1225
1226#[derive(Debug, Clone)]
1227struct State {
1228 offset_y: Offset,
1229 offset_x: Offset,
1230 smooth_scroll: bool,
1231 target: Option<Target>,
1232 source: Option<Source>,
1233 segment: Segment,
1234 last_frame: Option<Instant>,
1235 last_translation: Vector,
1236 interaction: Interaction,
1237 keyboard_modifiers: keyboard::Modifiers,
1238 last_notified: Option<Viewport>,
1239 last_scrolled: Option<Instant>,
1240 is_scrollbar_visible: bool,
1241 last_status: Option<Status>,
1242 last_id: Option<widget::Id>,
1243}
1244
1245#[derive(Debug, Clone, Copy)]
1246struct Segment {
1247 start: Point,
1248 started: Instant,
1249 duration: f32,
1250 slope: f32,
1251}
1252
1253#[derive(Debug, Clone, Copy)]
1254enum Interaction {
1255 None,
1256 ScrollerGrabbed(Axis, f32),
1257 TouchScrolling(Point),
1258 AutoScrolling {
1259 origin: Point,
1260 current: Point,
1261 last_frame: Option<Instant>,
1262 },
1263 RailHeld(RailHeld),
1264}
1265
1266impl Interaction {
1267 fn axis(&self) -> Option<Axis> {
1270 match self {
1271 Interaction::ScrollerGrabbed(axis, _) => Some(*axis),
1272 Interaction::RailHeld(rail) => Some(rail.axis),
1273 _ => None,
1274 }
1275 }
1276
1277 fn scrollers_grabbed(&self) -> bool {
1280 self.axis().is_some()
1281 }
1282
1283 fn scroller_grabbed(&self) -> Option<(Axis, f32)> {
1286 let Interaction::ScrollerGrabbed(axis, grabbed_at) = self else {
1287 return None;
1288 };
1289
1290 Some((*axis, *grabbed_at))
1291 }
1292}
1293
1294#[derive(Debug, Clone, Copy)]
1301struct RailHeld {
1302 axis: Axis,
1304
1305 direction: f32,
1308
1309 cursor_direction: f32,
1316
1317 velocity: f32,
1319
1320 pressed_at: Instant,
1322
1323 pointer: Point,
1325
1326 last_frame: Option<Instant>,
1329}
1330
1331#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1333enum RailStep {
1334 Moved,
1336
1337 Waiting,
1340
1341 Idle,
1343}
1344
1345#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1347pub(super) enum Axis {
1348 X,
1350
1351 Y,
1353}
1354
1355impl Axis {
1356 fn length(self, size: impl Into<Size>) -> f32 {
1358 let size = size.into();
1359 match self {
1360 Axis::X => size.width,
1361 Axis::Y => size.height,
1362 }
1363 }
1364
1365 fn coordinate(self, point: Point) -> f32 {
1367 match self {
1368 Axis::X => point.x,
1369 Axis::Y => point.y,
1370 }
1371 }
1372
1373 fn vector(self, value: f32) -> Vector {
1376 match self {
1377 Axis::X => Vector::new(value, 0.0),
1378 Axis::Y => Vector::new(0.0, value),
1379 }
1380 }
1381}
1382
1383impl Default for State {
1384 fn default() -> Self {
1385 Self {
1386 offset_y: Offset::Absolute(0.0),
1387 offset_x: Offset::Absolute(0.0),
1388 smooth_scroll: true,
1389 target: None,
1390 source: None,
1391 segment: Segment {
1392 start: Point::ORIGIN,
1393 started: Instant::now(),
1394 duration: 0.0,
1395 slope: 0.0,
1396 },
1397 last_frame: None,
1398 last_translation: Vector::ZERO,
1399 interaction: Interaction::None,
1400 keyboard_modifiers: keyboard::Modifiers::default(),
1401 last_notified: None,
1402 last_scrolled: None,
1403 is_scrollbar_visible: true,
1404 last_status: None,
1405 last_id: None,
1406 }
1407 }
1408}
1409
1410impl operation::Scrollable for State {
1411 fn snap_to(
1412 &mut self,
1413 offset: RelativeOffset<Option<f32>>,
1414 animation: Animation,
1415 bounds: Rectangle,
1416 content: Size,
1417 ) {
1418 State::snap_to(self, offset, animation, bounds, content, Source::Operation);
1419 }
1420
1421 fn scroll_to(
1422 &mut self,
1423 offset: AbsoluteOffset<Option<f32>>,
1424 animation: Animation,
1425 bounds: Rectangle,
1426 content: Size,
1427 ) {
1428 State::scroll_to(self, offset, animation, bounds, content, Source::Operation);
1429 }
1430
1431 fn scroll_by(
1432 &mut self,
1433 offset: AbsoluteOffset,
1434 animation: Animation,
1435 bounds: Rectangle,
1436 content: Size,
1437 ) {
1438 State::scroll_by(self, offset, animation, bounds, content);
1439 }
1440}
1441
1442#[derive(Debug, Clone, Copy, PartialEq)]
1444pub enum Offset {
1445 Absolute(f32),
1447
1448 Relative(f32),
1450}
1451
1452impl Offset {
1453 pub fn is_snapped(self) -> bool {
1455 matches!(self, Offset::Relative(_))
1456 }
1457
1458 fn absolute(self, viewport: f32, content: f32) -> f32 {
1459 match self {
1460 Offset::Absolute(absolute) => absolute.min((content - viewport).max(0.0)),
1461 Offset::Relative(percentage) => ((content - viewport) * percentage).max(0.0),
1462 }
1463 }
1464
1465 fn translation(self, viewport: f32, content: f32, alignment: Anchor) -> f32 {
1466 let offset = self.absolute(viewport, content);
1467
1468 match alignment {
1469 Anchor::Start => offset,
1470 Anchor::End => ((content - viewport).max(0.0) - offset).max(0.0),
1471 }
1472 }
1473}
1474
1475#[derive(Debug, Clone, Copy)]
1480pub struct Target {
1481 pub x: Offset,
1483
1484 pub y: Offset,
1486
1487 pub destination: Vector,
1490}
1491
1492impl Target {
1493 fn new(x: Offset, y: Offset, bounds: Rectangle, content: Size) -> Self {
1495 let destination = Vector::new(
1496 x.absolute(bounds.width, content.width),
1497 y.absolute(bounds.height, content.height),
1498 );
1499
1500 Self { x, y, destination }
1501 }
1502
1503 fn absolute(destination: Vector) -> Self {
1505 Self {
1506 x: Offset::Absolute(destination.x),
1507 y: Offset::Absolute(destination.y),
1508 destination,
1509 }
1510 }
1511}
1512
1513#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1515pub enum Source {
1516 Wheel,
1518
1519 Touch,
1521
1522 Scrollbar,
1524
1525 AutoScroll,
1527
1528 Operation,
1530
1531 Action,
1533
1534 Content,
1536
1537 Resize,
1539}
1540
1541#[derive(Debug, Clone, Copy)]
1543pub struct Scroll {
1544 pub viewport: Viewport,
1546
1547 pub origin: Option<Viewport>,
1549
1550 pub source: Source,
1552
1553 pub target: Option<Target>,
1555}
1556
1557impl Scroll {
1558 pub fn destination(&self) -> Viewport {
1564 Viewport {
1565 x: self
1566 .target
1567 .map(|target| target.x)
1568 .unwrap_or(self.viewport.x),
1569 y: self
1570 .target
1571 .map(|target| target.y)
1572 .unwrap_or(self.viewport.y),
1573 ..self.viewport
1574 }
1575 }
1576}
1577
1578#[derive(Debug, Clone, Copy)]
1580pub struct Viewport {
1581 pub x: Offset,
1583
1584 pub y: Offset,
1586
1587 pub bounds: Rectangle,
1589
1590 pub content: Size,
1592}
1593
1594impl Viewport {
1595 pub fn end(&self) -> AbsoluteOffset {
1597 self.absolute_offset() + self.distance_to_end()
1598 }
1599
1600 pub fn slide(self, other: Self) -> Self {
1603 Self {
1604 x: other.x,
1605 y: other.y,
1606 ..self
1607 }
1608 }
1609
1610 pub fn distance_to_end(&self) -> Vector {
1616 let AbsoluteOffset { x, y } = self.absolute_offset();
1617
1618 Vector::new(
1619 (self.content.width - self.bounds.width - x).max(0.0),
1620 (self.content.height - self.bounds.height - y).max(0.0),
1621 )
1622 }
1623
1624 pub fn absolute_offset(&self) -> AbsoluteOffset {
1626 let x = self.x.absolute(self.bounds.width, self.content.width);
1627 let y = self.y.absolute(self.bounds.height, self.content.height);
1628
1629 AbsoluteOffset { x, y }
1630 }
1631
1632 pub fn absolute_offset_reversed(&self) -> AbsoluteOffset {
1638 let AbsoluteOffset { x, y } = self.absolute_offset();
1639
1640 AbsoluteOffset {
1641 x: (self.content.width - self.bounds.width).max(0.0) - x,
1642 y: (self.content.height - self.bounds.height).max(0.0) - y,
1643 }
1644 }
1645
1646 pub fn relative_offset(&self) -> RelativeOffset {
1648 let AbsoluteOffset { x, y } = self.absolute_offset();
1649
1650 let x = x / (self.content.width - self.bounds.width);
1651 let y = y / (self.content.height - self.bounds.height);
1652
1653 RelativeOffset { x, y }
1654 }
1655}
1656
1657#[derive(Debug)]
1663pub enum Action<Message> {
1664 None,
1666
1667 ScrollTo(AbsoluteOffset<Option<f32>>, Animation),
1671
1672 SnapTo(RelativeOffset<Option<f32>>, Animation),
1676
1677 Custom(Message),
1679}
1680
1681impl<Message> Action<Message> {
1682 fn perform(
1683 self,
1684 state: &mut State,
1685 bounds: Rectangle,
1686 content: Size,
1687 shell: &mut Shell<'_, Message>,
1688 ) {
1689 match self {
1690 Action::None => {}
1691 Action::ScrollTo(absolute_offset, animation) => {
1692 state.scroll_to(absolute_offset, animation, bounds, content, Source::Action);
1693 }
1694 Action::SnapTo(relative_offset, animation) => {
1695 state.snap_to(relative_offset, animation, bounds, content, Source::Action);
1696 }
1697 Action::Custom(message) => {
1698 shell.publish(message);
1699 }
1700 }
1701 }
1702}
1703
1704impl<Message> From<Message> for Action<Message> {
1705 fn from(message: Message) -> Self {
1706 Self::Custom(message)
1707 }
1708}
1709
1710impl<Message> From<Option<Message>> for Action<Message> {
1711 fn from(message: Option<Message>) -> Self {
1712 match message {
1713 Some(message) => Self::Custom(message),
1714 None => Self::None,
1715 }
1716 }
1717}
1718
1719const WHEEL_PX_PER_LINE: f32 = 120.0;
1729
1730const SMOOTH_SCROLL_BEZIER_X1: f32 = 0.42;
1743const SMOOTH_SCROLL_BEZIER_X2: f32 = 0.58;
1744
1745const SMOOTH_SCROLL_DURATION_DIVISOR: f32 = 60.0;
1748
1749const SMOOTH_SCROLL_FRAME_DELAY: f32 = 1.0 / SMOOTH_SCROLL_DURATION_DIVISOR;
1763
1764const SMOOTH_SCROLL_DURATION_RAMP_START: f32 = WHEEL_PX_PER_LINE;
1767const SMOOTH_SCROLL_DURATION_RAMP_END: f32 = 480.0;
1768
1769const SMOOTH_SCROLL_DURATION_MIN: f32 = 6.0;
1776const SMOOTH_SCROLL_DURATION_MAX: f32 = 12.0;
1777
1778const SMOOTH_SCROLL_RETARGET_VELOCITY_BOUND: f32 = 2.5;
1785
1786const SMOOTH_SCROLL_SLOPE_CLAMP: f32 = 1000.0;
1789
1790const RAIL_AUTOSCROLL_DELAY: Duration = Duration::from_millis(250);
1803
1804const RAIL_AUTOSCROLL_MULTIPLIER: f32 = 20.0;
1810
1811const RAIL_PAGE_STEP_FRACTION: f32 = 0.875;
1815
1816const AUTOSCROLL_DEADZONE: f32 = 20.0;
1819
1820const AUTOSCROLL_SMOOTHNESS: f32 = 1.5;
1823
1824impl State {
1825 fn rail_page_step(viewport: f32) -> f32 {
1828 (viewport * RAIL_PAGE_STEP_FRACTION).max(1.0)
1829 }
1830
1831 fn new() -> Self {
1832 State::default()
1833 }
1834
1835 fn scroll(&mut self, delta: Vector<f32>, bounds: Rectangle, content: Size) {
1836 self.cancel();
1837
1838 if bounds.height < content.height {
1839 self.offset_y = Offset::Absolute(
1840 (self.offset_y.absolute(bounds.height, content.height) + delta.y)
1841 .clamp(0.0, content.height - bounds.height),
1842 );
1843 }
1844
1845 if bounds.width < content.width {
1846 self.offset_x = Offset::Absolute(
1847 (self.offset_x.absolute(bounds.width, content.width) + delta.x)
1848 .clamp(0.0, content.width - bounds.width),
1849 );
1850 }
1851 }
1852
1853 fn scroll_smoothly(
1859 &mut self,
1860 delta: Vector<f32>,
1861 bounds: Rectangle,
1862 content: Size,
1863 now: Instant,
1864 ) {
1865 let current = Point::new(
1866 self.offset_x.absolute(bounds.width, content.width),
1867 self.offset_y.absolute(bounds.height, content.height),
1868 );
1869
1870 let target = match self.target {
1873 Some(target) => Vector::new(
1874 Self::clamp_offset(target.destination.x + delta.x, bounds.width, content.width),
1875 Self::clamp_offset(
1876 target.destination.y + delta.y,
1877 bounds.height,
1878 content.height,
1879 ),
1880 ),
1881 None => Vector::new(
1882 Self::clamp_offset(current.x + delta.x, bounds.width, content.width),
1883 Self::clamp_offset(current.y + delta.y, bounds.height, content.height),
1884 ),
1885 };
1886
1887 self.scroll_smoothly_to(Target::absolute(target), bounds, content, now);
1888 }
1889
1890 fn scroll_smoothly_to(
1901 &mut self,
1902 target: Target,
1903 bounds: Rectangle,
1904 content: Size,
1905 now: Instant,
1906 ) {
1907 let now = now - Duration::from_secs_f32(SMOOTH_SCROLL_FRAME_DELAY);
1911
1912 let current = Point::new(
1913 self.offset_x.absolute(bounds.width, content.width),
1914 self.offset_y.absolute(bounds.height, content.height),
1915 );
1916
1917 if target.destination.x == current.x && target.destination.y == current.y {
1919 self.offset_x = target.x;
1920 self.offset_y = target.y;
1921 self.target = None;
1922 self.last_frame = None;
1923 return;
1924 }
1925
1926 let Some(ongoing) = self.target else {
1927 let distance = (target.destination.x - current.x)
1930 .abs()
1931 .max((target.destination.y - current.y).abs());
1932
1933 self.target = Some(target);
1934 self.segment = Segment {
1935 start: current,
1936 started: now,
1937 duration: Self::smooth_scroll_duration(distance),
1938 slope: 0.0,
1939 };
1940 self.last_frame = Some(now);
1941 return;
1942 };
1943
1944 if ongoing.destination == target.destination {
1946 return;
1947 }
1948
1949 self.retarget(target, now);
1950 }
1951
1952 fn retarget(&mut self, target: Target, now: Instant) {
1955 let start = self.animated_position(now);
1958 let velocity = self.animated_velocity(now);
1959 let new = Vector::new(
1960 target.destination.x - start.x,
1961 target.destination.y - start.y,
1962 );
1963
1964 let max_dimension = if new.x.abs() > new.y.abs() {
1966 new.x
1967 } else {
1968 new.y
1969 };
1970
1971 let mut duration = Self::smooth_scroll_duration(max_dimension.abs());
1975 if velocity.abs() > 0.01 {
1976 let bound = SMOOTH_SCROLL_RETARGET_VELOCITY_BOUND * max_dimension / velocity;
1977
1978 if bound > 0.0 {
1979 duration = duration.min(bound);
1980 }
1981 }
1982
1983 if max_dimension.abs() < 0.01 || duration < 0.01 {
1984 self.offset_x = target.x;
1986 self.offset_y = target.y;
1987 self.target = None;
1988 self.last_frame = None;
1989 return;
1990 }
1991
1992 let slope = (velocity * (duration / max_dimension))
1995 .clamp(-SMOOTH_SCROLL_SLOPE_CLAMP, SMOOTH_SCROLL_SLOPE_CLAMP);
1996
1997 self.target = Some(target);
1998 self.segment = Segment {
1999 start,
2000 started: now,
2001 duration,
2002 slope,
2003 };
2004 self.last_frame = Some(now);
2005 }
2006
2007 fn step(&mut self, now: Instant, bounds: Rectangle, content: Size) -> bool {
2012 let Some(target) = self.target else {
2013 return false;
2014 };
2015
2016 let resolved = Target::new(target.x, target.y, bounds, content);
2019 if resolved.destination != target.destination {
2020 self.retarget(resolved, now);
2021 }
2022
2023 let Some(target) = self.target else {
2024 return false;
2025 };
2026 let started = self.segment.started;
2027
2028 let t = (now - started).as_secs_f32();
2029 let progress = (t / self.segment.duration).clamp(0.0, 1.0);
2030
2031 if progress >= 1.0 {
2032 self.offset_x = target.x;
2035 self.offset_y = target.y;
2036 self.target = None;
2037 self.last_frame = None;
2038 return false;
2039 }
2040
2041 let bez = Self::smooth_scroll_progress(progress, self.segment.slope);
2042 self.offset_x = Offset::Absolute(
2043 self.segment.start.x + (target.destination.x - self.segment.start.x) * bez,
2044 );
2045 self.offset_y = Offset::Absolute(
2046 self.segment.start.y + (target.destination.y - self.segment.start.y) * bez,
2047 );
2048
2049 self.last_frame = Some(now);
2050 true
2051 }
2052
2053 fn cancel(&mut self) {
2060 self.target = None;
2061 self.last_frame = None;
2062 }
2063
2064 fn clamp_offset(offset: f32, viewport: f32, content: f32) -> f32 {
2067 if content > viewport {
2068 offset.clamp(0.0, content - viewport)
2069 } else {
2070 0.0
2071 }
2072 }
2073
2074 fn animated_position(&self, now: Instant) -> Point {
2077 let Some(target) = self.target else {
2078 return Point::ORIGIN;
2079 };
2080
2081 let started = self.segment.started;
2082
2083 let t = (now - started).as_secs_f32();
2084 let progress = (t / self.segment.duration).clamp(0.0, 1.0);
2085 let bez = Self::smooth_scroll_progress(progress, self.segment.slope);
2086
2087 Point::new(
2088 self.segment.start.x + (target.destination.x - self.segment.start.x) * bez,
2089 self.segment.start.y + (target.destination.y - self.segment.start.y) * bez,
2090 )
2091 }
2092
2093 fn animated_velocity(&self, now: Instant) -> f32 {
2096 let Some(target) = self.target else {
2097 return 0.0;
2098 };
2099
2100 let started = self.segment.started;
2101
2102 let t = (now - started).as_secs_f32();
2103 let progress = (t / self.segment.duration).clamp(0.0, 1.0);
2104
2105 if progress >= 1.0 {
2106 return 0.0;
2107 }
2108
2109 let dx = target.destination.x - self.segment.start.x;
2110 let dy = target.destination.y - self.segment.start.y;
2111 let max_dimension = if dx.abs() > dy.abs() { dx } else { dy };
2112
2113 Self::smooth_scroll_curve_slope(progress, self.segment.slope) * max_dimension
2114 / self.segment.duration
2115 }
2116
2117 fn smooth_scroll_duration(distance: f32) -> f32 {
2124 let slope = (SMOOTH_SCROLL_DURATION_MIN - SMOOTH_SCROLL_DURATION_MAX)
2125 / (SMOOTH_SCROLL_DURATION_RAMP_END - SMOOTH_SCROLL_DURATION_RAMP_START);
2126 let offset = SMOOTH_SCROLL_DURATION_MAX - SMOOTH_SCROLL_DURATION_RAMP_START * slope;
2127
2128 (offset + distance * slope).clamp(SMOOTH_SCROLL_DURATION_MIN, SMOOTH_SCROLL_DURATION_MAX)
2129 / SMOOTH_SCROLL_DURATION_DIVISOR
2130 }
2131
2132 fn smooth_scroll_progress(time: f32, slope: f32) -> f32 {
2138 if time <= 0.0 {
2139 return 0.0;
2140 }
2141
2142 if time >= 1.0 {
2143 return 1.0;
2144 }
2145
2146 let s = Self::bezier_solve(time);
2147 let y1 = SMOOTH_SCROLL_BEZIER_X1 * slope;
2148 let os = 1.0 - s;
2149
2150 3.0 * y1 * s * os * os + 3.0 * s * s * os + s * s * s
2152 }
2153
2154 fn smooth_scroll_curve_slope(time: f32, slope: f32) -> f32 {
2157 let s = if time <= 0.0 {
2158 0.0
2159 } else if time >= 1.0 {
2160 1.0
2161 } else {
2162 Self::bezier_solve(time)
2163 };
2164
2165 let x1 = SMOOTH_SCROLL_BEZIER_X1;
2166 let x2 = SMOOTH_SCROLL_BEZIER_X2;
2167 let y1 = x1 * slope;
2168 let os = 1.0 - s;
2169
2170 let x_prime =
2172 3.0 * x1 * os * (1.0 - 3.0 * s) + 3.0 * x2 * s * (2.0 - 3.0 * s) + 3.0 * s * s;
2173 let y_prime = 3.0 * y1 * os * (1.0 - 3.0 * s) + 3.0 * s * (2.0 - 3.0 * s) + 3.0 * s * s;
2174
2175 y_prime / x_prime
2176 }
2177
2178 fn bezier_solve(time: f32) -> f32 {
2184 let x1 = SMOOTH_SCROLL_BEZIER_X1;
2185 let x2 = SMOOTH_SCROLL_BEZIER_X2;
2186
2187 let x = |s: f32| {
2188 let os = 1.0 - s;
2189 3.0 * x1 * s * os * os + 3.0 * x2 * s * s * os + s * s * s
2190 };
2191
2192 let mut low = 0.0;
2193 let mut high = 1.0;
2194
2195 for _ in 0..40 {
2196 let mid = (low + high) / 2.0;
2197
2198 if x(mid) < time {
2199 low = mid;
2200 } else {
2201 high = mid;
2202 }
2203 }
2204
2205 (low + high) / 2.0
2206 }
2207
2208 fn notify_scroll(&mut self, bounds: Rectangle, content: Size) -> Option<Scroll> {
2211 let notification = self.notify_viewport(bounds, content);
2212
2213 if notification.is_some() {
2214 self.last_scrolled = Some(Instant::now());
2215 }
2216
2217 notification
2218 }
2219
2220 fn notify_viewport(&mut self, bounds: Rectangle, content: Size) -> Option<Scroll> {
2223 if content.width <= bounds.width && content.height <= bounds.height {
2224 return None;
2225 }
2226
2227 let viewport = Viewport {
2228 x: self.offset_x,
2229 y: self.offset_y,
2230 bounds,
2231 content,
2232 };
2233
2234 if let Some(last_notified) = self.last_notified {
2236 let last_absolute_offset = last_notified.absolute_offset();
2237 let current_absolute_offset = viewport.absolute_offset();
2238
2239 let unchanged =
2240 |a: f32, b: f32| (a - b).abs() <= f32::EPSILON || (a.is_nan() && b.is_nan());
2241
2242 if last_notified.bounds == bounds
2243 && last_notified.content == content
2244 && unchanged(last_absolute_offset.x, current_absolute_offset.x)
2245 && unchanged(last_absolute_offset.y, current_absolute_offset.y)
2246 {
2247 return None;
2248 }
2249 }
2250
2251 let source = if self.target.is_some() {
2255 self.source
2258 } else {
2259 self.source.take()
2261 }
2262 .unwrap_or_else(|| {
2263 if let Some(last_notified) = self.last_notified {
2264 if last_notified.content != content {
2265 Source::Content
2266 } else {
2267 Source::Resize
2268 }
2269 } else {
2270 Source::Content
2272 }
2273 });
2274
2275 let origin = self.last_notified;
2276 self.last_notified = Some(viewport);
2277
2278 Some(Scroll {
2279 viewport,
2280 origin,
2281 source,
2282 target: self.target,
2283 })
2284 }
2285
2286 fn scroll_to_percentage(
2289 &mut self,
2290 axis: Axis,
2291 percentage: f32,
2292 bounds: Rectangle,
2293 content: Size,
2294 ) {
2295 self.cancel();
2296
2297 match axis {
2298 Axis::X => self.offset_x = Offset::Relative(percentage.clamp(0.0, 1.0)),
2299 Axis::Y => self.offset_y = Offset::Relative(percentage.clamp(0.0, 1.0)),
2300 }
2301
2302 self.unsnap(axis, bounds, content);
2303 }
2304
2305 fn press_rail(
2315 &mut self,
2316 axis: Axis,
2317 scrollbar: &internals::Scrollbar,
2318 cursor_position: Point,
2319 smooth: bool,
2320 now: Instant,
2321 bounds: Rectangle,
2322 content: Size,
2323 ) {
2324 let Some(scroller) = scrollbar.scroller else {
2325 return;
2326 };
2327
2328 let cursor_direction =
2332 if axis.coordinate(cursor_position) < axis.coordinate(scroller.bounds.position()) {
2333 -1.0
2334 } else {
2335 1.0
2336 };
2337
2338 let grabbed_at = if cursor_direction < 0.0 { 0.0 } else { 1.0 };
2342 let stop = Offset::Relative(scrollbar.scroll_percentage(axis, grabbed_at, cursor_position))
2343 .absolute(axis.length(bounds), axis.length(content));
2344
2345 let direction = (stop - self.axis_offset(axis, bounds, content)).signum();
2348 if direction == 0.0 {
2349 return;
2351 }
2352
2353 let page_step = Self::rail_page_step(axis.length(bounds));
2354
2355 if smooth {
2358 self.scroll_smoothly(axis.vector(direction * page_step), bounds, content, now);
2359 } else {
2360 self.scroll(axis.vector(direction * page_step), bounds, content);
2361 }
2362
2363 self.interaction = Interaction::RailHeld(RailHeld {
2364 axis,
2365 direction,
2366 cursor_direction,
2367 velocity: page_step * RAIL_AUTOSCROLL_MULTIPLIER,
2368 pressed_at: now,
2369 pointer: cursor_position,
2370 last_frame: None,
2371 });
2372 }
2373
2374 fn step_rail(
2381 &mut self,
2382 rail: &mut RailHeld,
2383 now: Instant,
2384 scrollbar: Option<&internals::Scrollbar>,
2385 bounds: Rectangle,
2386 content: Size,
2387 ) -> Option<RailStep> {
2388 let axis = rail.axis;
2389
2390 let scrollbar = scrollbar?;
2391
2392 if now - rail.pressed_at < RAIL_AUTOSCROLL_DELAY {
2395 return Some(RailStep::Waiting);
2396 }
2397
2398 if rail.last_frame == Some(now) {
2400 return Some(RailStep::Waiting);
2401 }
2402
2403 let last_frame = rail.last_frame;
2406 rail.last_frame = Some(now);
2407 let time_delta = last_frame.map_or(
2408 (now - rail.pressed_at) - RAIL_AUTOSCROLL_DELAY,
2409 |last_frame| now - last_frame,
2410 );
2411 if time_delta.is_zero() {
2412 return Some(RailStep::Waiting);
2413 }
2414
2415 let scroller = scrollbar.scroller?;
2416
2417 let current = self.axis_offset(axis, bounds, content);
2418
2419 let grabbed_at = if rail.cursor_direction > 0.0 {
2422 1.0
2423 } else {
2424 0.0
2425 };
2426 let stop = Offset::Relative(scrollbar.scroll_percentage(axis, grabbed_at, rail.pointer))
2427 .absolute(axis.length(bounds), axis.length(content));
2428
2429 let on_pressed_side = if rail.cursor_direction > 0.0 {
2434 axis.coordinate(rail.pointer)
2435 > axis.coordinate(scroller.bounds.position()) + axis.length(scroller.bounds)
2436 } else {
2437 axis.coordinate(rail.pointer) < axis.coordinate(scroller.bounds.position())
2438 };
2439 if scrollbar.total_bounds.contains(rail.pointer) && !on_pressed_side {
2440 return Some(RailStep::Idle);
2441 }
2442
2443 let remaining = if rail.direction > 0.0 {
2446 stop - current
2447 } else {
2448 current - stop
2449 };
2450 if remaining <= 0.0 {
2451 return Some(RailStep::Idle);
2452 }
2453
2454 let delta = rail.direction * (rail.velocity * time_delta.as_secs_f32()).min(remaining);
2457
2458 self.scroll(axis.vector(delta), bounds, content);
2459
2460 Some(if self.axis_offset(axis, bounds, content) != current {
2461 RailStep::Moved
2462 } else {
2463 RailStep::Idle
2466 })
2467 }
2468
2469 fn snap_to(
2472 &mut self,
2473 offset: RelativeOffset<Option<f32>>,
2474 animation: Animation,
2475 bounds: Rectangle,
2476 content: Size,
2477 source: Source,
2478 ) {
2479 self.source = Some(source);
2480
2481 if !self.should_scroll_smoothly(animation) {
2482 self.cancel();
2483
2484 if let Some(x) = offset.x {
2485 self.offset_x = Offset::Relative(x.clamp(0.0, 1.0));
2486 }
2487
2488 if let Some(y) = offset.y {
2489 self.offset_y = Offset::Relative(y.clamp(0.0, 1.0));
2490 }
2491
2492 return;
2493 }
2494
2495 let x = offset
2498 .x
2499 .map(|x| Offset::Relative(x.clamp(0.0, 1.0)))
2500 .unwrap_or(self.offset_x);
2501 let y = offset
2502 .y
2503 .map(|y| Offset::Relative(y.clamp(0.0, 1.0)))
2504 .unwrap_or(self.offset_y);
2505
2506 self.scroll_smoothly_to(
2507 Target::new(x, y, bounds, content),
2508 bounds,
2509 content,
2510 Instant::now(),
2511 );
2512 }
2513
2514 fn scroll_to(
2516 &mut self,
2517 offset: AbsoluteOffset<Option<f32>>,
2518 animation: Animation,
2519 bounds: Rectangle,
2520 content: Size,
2521 source: Source,
2522 ) {
2523 self.source = Some(source);
2524
2525 if !self.should_scroll_smoothly(animation) {
2526 self.cancel();
2527
2528 if let Some(x) = offset.x {
2529 self.offset_x = Offset::Absolute(x.max(0.0));
2530 }
2531
2532 if let Some(y) = offset.y {
2533 self.offset_y = Offset::Absolute(y.max(0.0));
2534 }
2535
2536 return;
2537 }
2538
2539 let x = offset
2542 .x
2543 .map(|x| Offset::Absolute(Self::clamp_offset(x, bounds.width, content.width)))
2544 .unwrap_or(self.offset_x);
2545 let y = offset
2546 .y
2547 .map(|y| Offset::Absolute(Self::clamp_offset(y, bounds.height, content.height)))
2548 .unwrap_or(self.offset_y);
2549
2550 self.scroll_smoothly_to(
2551 Target::new(x, y, bounds, content),
2552 bounds,
2553 content,
2554 Instant::now(),
2555 );
2556 }
2557
2558 fn scroll_by(
2561 &mut self,
2562 offset: AbsoluteOffset,
2563 animation: Animation,
2564 bounds: Rectangle,
2565 content: Size,
2566 ) {
2567 self.source = Some(Source::Operation);
2568
2569 let delta = Vector::new(offset.x, offset.y);
2570
2571 if self.should_scroll_smoothly(animation) {
2572 self.scroll_smoothly(delta, bounds, content, Instant::now());
2573 } else {
2574 self.scroll(delta, bounds, content);
2575 }
2576 }
2577
2578 fn should_scroll_smoothly(&self, animation: Animation) -> bool {
2581 match animation {
2582 Animation::Auto => self.smooth_scroll,
2583 Animation::Instant => false,
2584 Animation::Smooth => true,
2585 }
2586 }
2587
2588 fn interact(
2594 &mut self,
2595 event: &Event,
2596 bounds: Rectangle,
2597 content: Size,
2598 cursor: mouse::Cursor,
2599 cursor_over_scrollable: Option<Point>,
2600 scrollbars: &Scrollbars,
2601 mouse_over_scrollbar: Option<Axis>,
2602 direction: Direction,
2603 smooth_scroll: bool,
2604 click_to_scroll: bool,
2605 ) -> Interact {
2606 let mut interact = Interact::default();
2607
2608 if let Some(last_scrolled) = self.last_scrolled {
2610 let clear_transaction = match event {
2611 Event::Mouse(
2612 mouse::Event::ButtonPressed(_)
2613 | mouse::Event::ButtonReleased(_)
2614 | mouse::Event::CursorLeft,
2615 ) => true,
2616 Event::Mouse(mouse::Event::CursorMoved { .. }) => {
2617 last_scrolled.elapsed() > Duration::from_millis(100)
2618 }
2619 _ => last_scrolled.elapsed() > Duration::from_millis(1500),
2620 };
2621
2622 if clear_transaction {
2623 self.last_scrolled = None;
2624 }
2625 }
2626
2627 let mut translation = self.last_translation;
2628
2629 if let Event::Window(window::Event::RedrawRequested(now)) = event {
2630 if self.last_frame != Some(*now)
2633 && self.step(*now, bounds, content)
2634 && let Some(notification) = self.notify_scroll(bounds, content)
2635 {
2636 interact.scroll = Some(notification);
2637 }
2638
2639 translation = self.translation(direction, bounds, content);
2640 self.last_translation = translation;
2641
2642 if let Interaction::RailHeld(mut rail) = self.interaction {
2646 let scrollbars = Scrollbars::new(translation, direction, bounds, content);
2647
2648 let scrollbar = match rail.axis {
2649 Axis::X => scrollbars.x.as_ref(),
2650 Axis::Y => scrollbars.y.as_ref(),
2651 };
2652
2653 let step = self
2654 .step_rail(&mut rail, *now, scrollbar, bounds, content)
2655 .map(|step| (Interaction::RailHeld(rail), step));
2656
2657 match step {
2658 Some((interaction, step)) => {
2659 self.interaction = interaction;
2660
2661 match step {
2662 RailStep::Moved => {
2663 self.source = Some(Source::Scrollbar);
2664
2665 if let Some(notification) = self.notify_scroll(bounds, content) {
2666 interact.scroll = Some(notification);
2667 }
2668
2669 interact.request_redraw = true;
2670 }
2671 RailStep::Waiting => interact.request_redraw = true,
2672 RailStep::Idle => {}
2673 }
2674 }
2675 None => self.interaction = Interaction::None,
2679 }
2680 }
2681
2682 if self.target.is_some() {
2683 interact.request_redraw = true;
2684 } else if let Interaction::AutoScrolling {
2685 origin,
2686 current,
2687 last_frame,
2688 } = self.interaction
2689 {
2690 if last_frame == Some(*now) {
2691 interact.request_redraw = true;
2692 } else {
2693 self.interaction = Interaction::AutoScrolling {
2694 origin,
2695 current,
2696 last_frame: None,
2697 };
2698
2699 let mut delta = current - origin;
2700
2701 if delta.x.abs() < AUTOSCROLL_DEADZONE {
2702 delta.x = 0.0;
2703 }
2704
2705 if delta.y.abs() < AUTOSCROLL_DEADZONE {
2706 delta.y = 0.0;
2707 }
2708
2709 if delta.x != 0.0 || delta.y != 0.0 {
2710 let time_delta =
2711 last_frame.map_or(Duration::ZERO, |last_frame| *now - last_frame);
2712
2713 let scroll_factor = time_delta.as_secs_f32();
2714
2715 self.scroll(
2716 direction.align(Vector::new(
2717 delta.x.signum()
2718 * delta.x.abs().powf(AUTOSCROLL_SMOOTHNESS)
2719 * scroll_factor,
2720 delta.y.signum()
2721 * delta.y.abs().powf(AUTOSCROLL_SMOOTHNESS)
2722 * scroll_factor,
2723 )),
2724 bounds,
2725 content,
2726 );
2727
2728 self.source = Some(Source::AutoScroll);
2729
2730 let notification = self.notify_scroll(bounds, content);
2731 let has_scrolled = notification.is_some();
2732
2733 if let Some(notification) = notification {
2734 interact.scroll = Some(notification);
2735 }
2736
2737 if has_scrolled || time_delta.is_zero() {
2738 self.interaction = Interaction::AutoScrolling {
2739 origin,
2740 current,
2741 last_frame: Some(*now),
2742 };
2743
2744 interact.request_redraw = true;
2745 }
2746 }
2747 }
2748 }
2749
2750 if interact.scroll.is_none() {
2751 interact.scroll = self.notify_viewport(bounds, content);
2752 }
2753
2754 if self.target.is_none() {
2757 self.source = None;
2758 }
2759 }
2760
2761 if let Interaction::RailHeld(mut rail) = self.interaction
2764 && let Some(position) = rail_moved(event, cursor, rail.pointer)
2765 {
2766 rail.pointer = position;
2767 self.interaction = Interaction::RailHeld(rail);
2768 interact.request_redraw = true;
2769 }
2770
2771 if let Some((axis, scroller_grabbed_at)) = self.interaction.scroller_grabbed() {
2774 match event {
2775 Event::Mouse(mouse::Event::CursorMoved { .. })
2776 | Event::Touch(touch::Event::FingerMoved { .. }) => {
2777 if let Some(scrollbar) = scrollbars.scrollbar(axis) {
2778 let Some(cursor_position) = cursor.observe().position() else {
2779 interact.stop = true;
2780 return interact;
2781 };
2782
2783 self.scroll_to_percentage(
2784 axis,
2785 scrollbar.scroll_percentage(axis, scroller_grabbed_at, cursor_position),
2786 bounds,
2787 content,
2788 );
2789
2790 self.source = Some(Source::Scrollbar);
2791
2792 if let Some(notification) = self.notify_scroll(bounds, content) {
2793 interact.scroll = Some(notification);
2794 }
2795
2796 interact.capture = true;
2797 }
2798 }
2799 _ => {}
2800 }
2801 } else if let Some(axis) = mouse_over_scrollbar {
2802 match event {
2806 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
2807 | Event::Touch(touch::Event::FingerPressed { .. }) => {
2808 let Some(cursor_position) = cursor.position() else {
2809 interact.stop = true;
2810 return interact;
2811 };
2812
2813 if let Some((scrollbar, hit)) = scrollbars.hit(axis, cursor_position) {
2814 match hit {
2815 Hit::Scroller { grabbed_at } => {
2818 self.scroll_to_percentage(
2819 axis,
2820 scrollbar.scroll_percentage(axis, grabbed_at, cursor_position),
2821 bounds,
2822 content,
2823 );
2824
2825 self.interaction = Interaction::ScrollerGrabbed(axis, grabbed_at);
2826 }
2827 Hit::Rail if self.keyboard_modifiers.shift() != click_to_scroll => {
2833 self.scroll_to_percentage(
2834 axis,
2835 scrollbar.scroll_percentage(axis, 0.5, cursor_position),
2836 bounds,
2837 content,
2838 );
2839
2840 self.interaction = Interaction::ScrollerGrabbed(axis, 0.5);
2841 }
2842 Hit::Rail => {
2850 self.press_rail(
2851 axis,
2852 scrollbar,
2853 cursor_position,
2854 smooth_scroll,
2855 Instant::now(),
2856 bounds,
2857 content,
2858 );
2859
2860 interact.request_redraw = true;
2861 }
2862 }
2863
2864 self.source = Some(Source::Scrollbar);
2865
2866 if let Some(notification) = self.notify_scroll(bounds, content) {
2867 interact.scroll = Some(notification);
2868 }
2869
2870 interact.capture = true;
2871 }
2872 }
2873 _ => {}
2874 }
2875 }
2876
2877 if matches!(self.interaction, Interaction::AutoScrolling { .. })
2878 && matches!(
2879 event,
2880 Event::Mouse(mouse::Event::ButtonPressed(_) | mouse::Event::WheelScrolled { .. })
2881 | Event::Touch(_)
2882 | Event::Keyboard(_)
2883 )
2884 {
2885 self.interaction = Interaction::None;
2886 interact.capture = true;
2887 interact.invalidate_layout = true;
2888 interact.request_redraw = true;
2889 interact.stop = true;
2890 return interact;
2891 }
2892
2893 if self.last_scrolled.is_none()
2894 || !matches!(event, Event::Mouse(mouse::Event::WheelScrolled { .. }))
2895 {
2896 let cursor = match cursor_over_scrollable {
2897 Some(cursor_position)
2898 if mouse_over_scrollbar.is_none() && !self.interaction.scrollers_grabbed() =>
2899 {
2900 mouse::Cursor::Available(cursor_position + translation)
2901 }
2902 _ => cursor.obstruct() + translation,
2903 };
2904
2905 interact.content = Some(Content { cursor });
2906 }
2907
2908 interact
2909 }
2910
2911 fn update(
2916 &mut self,
2917 event: &Event,
2918 bounds: Rectangle,
2919 content: Size,
2920 cursor: mouse::Cursor,
2921 cursor_over_scrollable: Option<Point>,
2922 mouse_over_scrollbar: Option<Axis>,
2923 direction: Direction,
2924 smooth_scroll: bool,
2925 auto_scroll: bool,
2926 is_event_captured: bool,
2927 ) -> Update {
2928 let mut update = Update::default();
2929
2930 if matches!(
2931 event,
2932 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
2933 | Event::Touch(touch::Event::FingerLifted { .. } | touch::Event::FingerLost { .. })
2934 ) {
2935 self.interaction = Interaction::None;
2936 return update;
2937 }
2938
2939 if is_event_captured {
2940 return update;
2941 }
2942
2943 match event {
2944 Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
2945 if !cursor.land().is_over(bounds) {
2946 return update;
2947 }
2948
2949 let (delta, is_significant) = match *delta {
2950 mouse::ScrollDelta::Lines { x, y } => {
2951 let is_shift_pressed = self.keyboard_modifiers.shift();
2952
2953 let (x, y) = if cfg!(target_os = "macos") && is_shift_pressed {
2956 (y, x)
2957 } else {
2958 (x, y)
2959 };
2960
2961 let movement = if !is_shift_pressed {
2962 Vector::new(x, y)
2963 } else {
2964 Vector::new(y, x)
2965 };
2966
2967 (
2968 -movement * WHEEL_PX_PER_LINE,
2969 x.abs() >= 0.5 || y.abs() >= 0.5,
2970 )
2971 }
2972 mouse::ScrollDelta::Pixels { x, y } => (-Vector::new(x, y), false),
2976 };
2977
2978 let delta = direction.align(delta);
2979
2980 if smooth_scroll && is_significant {
2981 self.scroll_smoothly(delta, bounds, content, Instant::now());
2982 } else {
2983 self.scroll(delta, bounds, content);
2984 }
2985
2986 self.source = Some(Source::Wheel);
2987
2988 let notification = self.notify_scroll(bounds, content);
2989 let has_scrolled = notification.is_some();
2990
2991 if let Some(notification) = notification {
2992 update.scroll = Some(notification);
2993 }
2994
2995 let in_transaction = self.last_scrolled.is_some() || self.target.is_some();
2996
2997 if has_scrolled || in_transaction {
2998 update.capture = true;
2999 }
3000
3001 if self.target.is_some() {
3002 update.request_redraw = true;
3003 }
3004 }
3005 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle))
3006 if auto_scroll && matches!(self.interaction, Interaction::None) =>
3007 {
3008 let Some(origin) = cursor_over_scrollable else {
3009 return update;
3010 };
3011
3012 self.interaction = Interaction::AutoScrolling {
3013 origin,
3014 current: origin,
3015 last_frame: None,
3016 };
3017
3018 update.capture = true;
3019 update.invalidate_layout = true;
3020 update.request_redraw = true;
3021 }
3022 Event::Touch(event)
3023 if matches!(self.interaction, Interaction::TouchScrolling(_))
3024 || mouse_over_scrollbar.is_none() =>
3025 {
3026 match event {
3027 touch::Event::FingerPressed { .. } => {
3028 let Some(position) = cursor_over_scrollable else {
3029 return update;
3030 };
3031
3032 self.interaction = Interaction::TouchScrolling(position);
3033 }
3034 touch::Event::FingerMoved { .. } => {
3035 let Interaction::TouchScrolling(scroll_box_touched_at) = self.interaction
3036 else {
3037 return update;
3038 };
3039
3040 let Some(cursor_position) = cursor.position() else {
3041 return update;
3042 };
3043
3044 let delta = Vector::new(
3045 scroll_box_touched_at.x - cursor_position.x,
3046 scroll_box_touched_at.y - cursor_position.y,
3047 );
3048
3049 self.scroll(direction.align(delta), bounds, content);
3050
3051 self.interaction = Interaction::TouchScrolling(cursor_position);
3052 self.source = Some(Source::Touch);
3053
3054 if let Some(notification) = self.notify_scroll(bounds, content) {
3055 update.scroll = Some(notification);
3056 }
3057 }
3058 _ => {}
3059 }
3060
3061 update.capture = true;
3062 }
3063 Event::Mouse(mouse::Event::CursorMoved { position }) => {
3064 if let Interaction::AutoScrolling {
3065 origin, last_frame, ..
3066 } = self.interaction
3067 {
3068 let delta = *position - origin;
3069
3070 self.interaction = Interaction::AutoScrolling {
3071 origin,
3072 current: *position,
3073 last_frame,
3074 };
3075
3076 if (delta.x.abs() >= AUTOSCROLL_DEADZONE
3077 || delta.y.abs() >= AUTOSCROLL_DEADZONE)
3078 && last_frame.is_none()
3079 {
3080 update.request_redraw = true;
3081 }
3082 }
3083 }
3084 Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
3085 self.keyboard_modifiers = *modifiers;
3086 }
3087 _ => {}
3088 }
3089
3090 update
3091 }
3092
3093 fn unsnap(&mut self, axis: Axis, bounds: Rectangle, content: Size) {
3096 match axis {
3097 Axis::X => {
3098 self.offset_x =
3099 Offset::Absolute(self.offset_x.absolute(bounds.width, content.width));
3100 }
3101 Axis::Y => {
3102 self.offset_y =
3103 Offset::Absolute(self.offset_y.absolute(bounds.height, content.height));
3104 }
3105 }
3106 }
3107
3108 fn axis_offset(&self, axis: Axis, bounds: Rectangle, content: Size) -> f32 {
3110 match axis {
3111 Axis::X => self.offset_x.absolute(bounds.width, content.width),
3112 Axis::Y => self.offset_y.absolute(bounds.height, content.height),
3113 }
3114 }
3115
3116 fn translation(&self, direction: Direction, bounds: Rectangle, content: Size) -> Vector {
3119 Vector::new(
3120 if let Some(horizontal) = direction.horizontal() {
3121 self.offset_x
3122 .translation(bounds.width, content.width, horizontal.alignment)
3123 } else {
3124 0.0
3125 },
3126 if let Some(vertical) = direction.vertical() {
3127 self.offset_y
3128 .translation(bounds.height, content.height, vertical.alignment)
3129 } else {
3130 0.0
3131 },
3132 )
3133 }
3134}
3135
3136#[derive(Debug, Clone, Copy, PartialEq)]
3138enum Hit {
3139 Scroller {
3142 grabbed_at: f32,
3145 },
3146
3147 Rail,
3149}
3150
3151#[derive(Debug)]
3152struct Scrollbars {
3154 y: Option<internals::Scrollbar>,
3155 x: Option<internals::Scrollbar>,
3156}
3157
3158impl Scrollbars {
3159 fn new(translation: Vector, direction: Direction, bounds: Rectangle, content: Size) -> Self {
3161 let show_scrollbar_x = direction
3162 .horizontal()
3163 .filter(|_scrollbar| content.width > bounds.width);
3164
3165 let show_scrollbar_y = direction
3166 .vertical()
3167 .filter(|_scrollbar| content.height > bounds.height);
3168
3169 let y_scrollbar = if let Some(vertical) = show_scrollbar_y {
3170 let Scrollbar {
3171 width,
3172 margin,
3173 scroller_width,
3174 spacing,
3175 padding,
3176 ..
3177 } = *vertical;
3178
3179 let x_scrollbar_height =
3182 show_scrollbar_x.map_or(0.0, |h| h.width.max(h.scroller_width) + h.margin);
3183
3184 let total_scrollbar_width = width.max(scroller_width) + 2.0 * margin;
3185
3186 let scrollbar_height = (bounds.height - x_scrollbar_height - 2.0 * padding).max(0.0);
3189
3190 let total_scrollbar_bounds = Rectangle {
3192 x: bounds.x + bounds.width - total_scrollbar_width,
3193 y: bounds.y + padding,
3194 width: total_scrollbar_width,
3195 height: scrollbar_height,
3196 };
3197
3198 let scrollbar_bounds = Rectangle {
3200 x: bounds.x + bounds.width - total_scrollbar_width / 2.0 - width / 2.0,
3201 y: bounds.y + padding,
3202 width,
3203 height: scrollbar_height,
3204 };
3205
3206 let ratio = bounds.height / content.height;
3207
3208 let scroller = if ratio >= 1.0 {
3209 None
3210 } else {
3211 let scroller_height = (scrollbar_bounds.height * ratio).max(2.0);
3213 let scroller_offset =
3214 translation.y * ratio * scrollbar_bounds.height / bounds.height;
3215
3216 let scroller_bounds = Rectangle {
3217 x: bounds.x + bounds.width - total_scrollbar_width / 2.0 - scroller_width / 2.0,
3218 y: (scrollbar_bounds.y + scroller_offset).max(0.0),
3219 width: scroller_width,
3220 height: scroller_height,
3221 };
3222
3223 Some(internals::Scroller {
3224 bounds: scroller_bounds,
3225 })
3226 };
3227
3228 Some(internals::Scrollbar {
3229 total_bounds: total_scrollbar_bounds,
3230 bounds: scrollbar_bounds,
3231 scroller,
3232 alignment: vertical.alignment,
3233 disabled: content.height <= bounds.height,
3234 floating: spacing.is_none(),
3235 })
3236 } else {
3237 None
3238 };
3239
3240 let x_scrollbar = if let Some(horizontal) = show_scrollbar_x {
3241 let Scrollbar {
3242 width,
3243 margin,
3244 scroller_width,
3245 spacing,
3246 padding,
3247 ..
3248 } = *horizontal;
3249
3250 let scrollbar_y_width =
3253 y_scrollbar.map_or(0.0, |scrollbar| scrollbar.total_bounds.width);
3254
3255 let total_scrollbar_height = width.max(scroller_width) + 2.0 * margin;
3256
3257 let scrollbar_width = (bounds.width - scrollbar_y_width - 2.0 * padding).max(0.0);
3260
3261 let total_scrollbar_bounds = Rectangle {
3263 x: bounds.x + padding,
3264 y: bounds.y + bounds.height - total_scrollbar_height,
3265 width: scrollbar_width,
3266 height: total_scrollbar_height,
3267 };
3268
3269 let scrollbar_bounds = Rectangle {
3271 x: bounds.x + padding,
3272 y: bounds.y + bounds.height - total_scrollbar_height / 2.0 - width / 2.0,
3273 width: scrollbar_width,
3274 height: width,
3275 };
3276
3277 let ratio = bounds.width / content.width;
3278
3279 let scroller = if ratio >= 1.0 {
3280 None
3281 } else {
3282 let scroller_length = (scrollbar_bounds.width * ratio).max(2.0);
3284 let scroller_offset = translation.x * ratio * scrollbar_bounds.width / bounds.width;
3285
3286 let scroller_bounds = Rectangle {
3287 x: (scrollbar_bounds.x + scroller_offset).max(0.0),
3288 y: bounds.y + bounds.height
3289 - total_scrollbar_height / 2.0
3290 - scroller_width / 2.0,
3291 width: scroller_length,
3292 height: scroller_width,
3293 };
3294
3295 Some(internals::Scroller {
3296 bounds: scroller_bounds,
3297 })
3298 };
3299
3300 Some(internals::Scrollbar {
3301 total_bounds: total_scrollbar_bounds,
3302 bounds: scrollbar_bounds,
3303 scroller,
3304 alignment: horizontal.alignment,
3305 disabled: content.width <= bounds.width,
3306 floating: spacing.is_none(),
3307 })
3308 } else {
3309 None
3310 };
3311
3312 Self {
3313 y: y_scrollbar,
3314 x: x_scrollbar,
3315 }
3316 }
3317
3318 fn scrollbar(&self, axis: Axis) -> Option<&internals::Scrollbar> {
3320 match axis {
3321 Axis::X => self.x.as_ref(),
3322 Axis::Y => self.y.as_ref(),
3323 }
3324 }
3325
3326 fn is_mouse_over(&self, cursor: mouse::Cursor) -> Option<Axis> {
3331 let cursor_position = cursor.position()?;
3332
3333 if self
3334 .y
3335 .as_ref()
3336 .is_some_and(|scrollbar| scrollbar.is_mouse_over(cursor_position))
3337 {
3338 Some(Axis::Y)
3339 } else if self
3340 .x
3341 .as_ref()
3342 .is_some_and(|scrollbar| scrollbar.is_mouse_over(cursor_position))
3343 {
3344 Some(Axis::X)
3345 } else {
3346 None
3347 }
3348 }
3349
3350 fn is_y_disabled(&self) -> bool {
3351 self.y.map(|y| y.disabled).unwrap_or(false)
3352 }
3353
3354 fn is_x_disabled(&self) -> bool {
3355 self.x.map(|x| x.disabled).unwrap_or(false)
3356 }
3357
3358 fn hit(&self, axis: Axis, cursor_position: Point) -> Option<(&internals::Scrollbar, Hit)> {
3361 let scrollbar = match axis {
3362 Axis::X => self.x.as_ref(),
3363 Axis::Y => self.y.as_ref(),
3364 }?;
3365 let scroller = scrollbar.scroller?;
3366
3367 if !scrollbar.total_bounds.contains(cursor_position) {
3368 return None;
3369 }
3370
3371 Some((
3372 scrollbar,
3373 if scroller.bounds.contains(cursor_position) {
3374 Hit::Scroller {
3375 grabbed_at: (axis.coordinate(cursor_position)
3376 - axis.coordinate(scroller.bounds.position()))
3377 / axis.length(scroller.bounds),
3378 }
3379 } else {
3380 Hit::Rail
3381 },
3382 ))
3383 }
3384
3385 fn is_any_floating(&self) -> bool {
3386 self.y.is_some_and(|scrollbar| scrollbar.floating)
3387 || self.x.is_some_and(|scrollbar| scrollbar.floating)
3388 }
3389
3390 fn active(&self) -> bool {
3391 self.y.is_some() || self.x.is_some()
3392 }
3393}
3394
3395pub(super) mod internals {
3396 use crate::core::{Point, Rectangle};
3397
3398 use super::Anchor;
3399
3400 #[derive(Debug, Copy, Clone)]
3401 pub struct Scrollbar {
3402 pub total_bounds: Rectangle,
3403 pub bounds: Rectangle,
3404 pub scroller: Option<Scroller>,
3405 pub alignment: Anchor,
3406 pub disabled: bool,
3407 pub floating: bool,
3408 }
3409
3410 impl Scrollbar {
3411 pub fn is_mouse_over(&self, cursor_position: Point) -> bool {
3413 self.total_bounds.contains(cursor_position)
3414 }
3415
3416 pub fn scroll_percentage(
3419 &self,
3420 axis: super::Axis,
3421 grabbed_at: f32,
3422 cursor_position: Point,
3423 ) -> f32 {
3424 let Some(scroller) = self.scroller else {
3425 return 0.0;
3426 };
3427
3428 let percentage = (axis.coordinate(cursor_position)
3429 - axis.coordinate(self.bounds.position())
3430 - axis.length(scroller.bounds) * grabbed_at)
3431 / (axis.length(self.bounds) - axis.length(scroller.bounds));
3432
3433 match self.alignment {
3434 Anchor::Start => percentage,
3435 Anchor::End => 1.0 - percentage,
3436 }
3437 }
3438 }
3439
3440 #[derive(Debug, Clone, Copy)]
3442 pub struct Scroller {
3443 pub bounds: Rectangle,
3445 }
3446}
3447
3448#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3450pub enum Status {
3451 Active {
3453 is_horizontal_scrollbar_disabled: bool,
3455 is_vertical_scrollbar_disabled: bool,
3457 },
3458 Hovered {
3460 is_horizontal_scrollbar_hovered: bool,
3462 is_vertical_scrollbar_hovered: bool,
3464 is_horizontal_scrollbar_disabled: bool,
3466 is_vertical_scrollbar_disabled: bool,
3468 },
3469 Dragged {
3471 is_horizontal_scrollbar_dragged: bool,
3473 is_vertical_scrollbar_dragged: bool,
3475 is_horizontal_scrollbar_disabled: bool,
3477 is_vertical_scrollbar_disabled: bool,
3479 },
3480}
3481
3482#[derive(Debug, Clone, Copy, PartialEq)]
3484pub struct Style {
3485 pub container: container::Style,
3487 pub vertical_rail: Rail,
3489 pub horizontal_rail: Rail,
3491 pub gap: Option<Background>,
3493 pub auto_scroll: AutoScroll,
3495}
3496
3497#[derive(Debug, Clone, Copy, PartialEq)]
3499pub struct Rail {
3500 pub background: Option<Background>,
3502 pub border: Border,
3504 pub scroller: Scroller,
3506}
3507
3508#[derive(Debug, Clone, Copy, PartialEq)]
3510pub struct Scroller {
3511 pub background: Background,
3513 pub border: Border,
3515}
3516
3517#[derive(Debug, Clone, Copy, PartialEq)]
3519pub struct AutoScroll {
3520 pub background: Background,
3522 pub border: Border,
3524 pub shadow: Shadow,
3526 pub icon: Color,
3528}
3529
3530pub trait Catalog {
3532 type Class<'a>;
3534
3535 fn default<'a>() -> Self::Class<'a>;
3537
3538 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
3540}
3541
3542pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
3544
3545impl Catalog for Theme {
3546 type Class<'a> = StyleFn<'a, Self>;
3547
3548 fn default<'a>() -> Self::Class<'a> {
3549 Box::new(default)
3550 }
3551
3552 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
3553 class(self, status)
3554 }
3555}
3556
3557pub fn default(theme: &Theme, status: Status) -> Style {
3559 let palette = theme.palette();
3560
3561 let scrollbar = Rail {
3562 background: Some(palette.background.weak.color.into()),
3563 border: border::rounded(2),
3564 scroller: Scroller {
3565 background: palette.background.strongest.color.into(),
3566 border: border::rounded(2),
3567 },
3568 };
3569
3570 let auto_scroll = AutoScroll {
3571 background: palette.background.base.color.scale_alpha(0.9).into(),
3572 border: border::rounded(u32::MAX)
3573 .width(1)
3574 .color(palette.background.base.text.scale_alpha(0.8)),
3575 shadow: Shadow {
3576 color: Color::BLACK.scale_alpha(0.7),
3577 offset: Vector::ZERO,
3578 blur_radius: 2.0,
3579 },
3580 icon: palette.background.base.text.scale_alpha(0.8),
3581 };
3582
3583 match status {
3584 Status::Active { .. } => Style {
3585 container: container::Style::default(),
3586 vertical_rail: scrollbar,
3587 horizontal_rail: scrollbar,
3588 gap: None,
3589 auto_scroll,
3590 },
3591 Status::Hovered {
3592 is_horizontal_scrollbar_hovered,
3593 is_vertical_scrollbar_hovered,
3594 ..
3595 } => {
3596 let hovered_scrollbar = Rail {
3597 scroller: Scroller {
3598 background: palette.primary.strong.color.into(),
3599 ..scrollbar.scroller
3600 },
3601 ..scrollbar
3602 };
3603
3604 Style {
3605 container: container::Style::default(),
3606 vertical_rail: if is_vertical_scrollbar_hovered {
3607 hovered_scrollbar
3608 } else {
3609 scrollbar
3610 },
3611 horizontal_rail: if is_horizontal_scrollbar_hovered {
3612 hovered_scrollbar
3613 } else {
3614 scrollbar
3615 },
3616 gap: None,
3617 auto_scroll,
3618 }
3619 }
3620 Status::Dragged {
3621 is_horizontal_scrollbar_dragged,
3622 is_vertical_scrollbar_dragged,
3623 ..
3624 } => {
3625 let dragged_scrollbar = Rail {
3626 scroller: Scroller {
3627 background: palette.primary.base.color.into(),
3628 ..scrollbar.scroller
3629 },
3630 ..scrollbar
3631 };
3632
3633 Style {
3634 container: container::Style::default(),
3635 vertical_rail: if is_vertical_scrollbar_dragged {
3636 dragged_scrollbar
3637 } else {
3638 scrollbar
3639 },
3640 horizontal_rail: if is_horizontal_scrollbar_dragged {
3641 dragged_scrollbar
3642 } else {
3643 scrollbar
3644 },
3645 gap: None,
3646 auto_scroll,
3647 }
3648 }
3649 }
3650}