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, Operation};
35use crate::core::widget::tree::{self, Tree};
36use crate::core::window;
37use crate::core::{
38 self, Background, Clipboard, Color, Element, Event, InputMethod, Layout,
39 Length, Padding, Pixels, Point, Rectangle, Shadow, Shell, Size, Theme,
40 Vector, Widget,
41};
42
43pub use operation::scrollable::{AbsoluteOffset, RelativeOffset};
44
45pub struct Scrollable<
68 'a,
69 Message,
70 Theme = crate::Theme,
71 Renderer = crate::Renderer,
72> where
73 Theme: Catalog,
74 Renderer: text::Renderer,
75{
76 id: Option<widget::Id>,
77 width: Length,
78 height: Length,
79 direction: Direction,
80 auto_scroll: bool,
81 content: Element<'a, Message, Theme, Renderer>,
82 on_scroll: Option<Box<dyn Fn(Viewport) -> Message + 'a>>,
83 class: Theme::Class<'a>,
84 last_status: Option<Status>,
85}
86
87impl<'a, Message, Theme, Renderer> Scrollable<'a, Message, Theme, Renderer>
88where
89 Theme: Catalog,
90 Renderer: text::Renderer,
91{
92 pub fn new(
94 content: impl Into<Element<'a, Message, Theme, Renderer>>,
95 ) -> Self {
96 Self::with_direction(content, Direction::default())
97 }
98
99 pub fn with_direction(
101 content: impl Into<Element<'a, Message, Theme, Renderer>>,
102 direction: impl Into<Direction>,
103 ) -> Self {
104 Scrollable {
105 id: None,
106 width: Length::Shrink,
107 height: Length::Shrink,
108 direction: direction.into(),
109 auto_scroll: false,
110 content: content.into(),
111 on_scroll: None,
112 class: Theme::default(),
113 last_status: None,
114 }
115 .enclose()
116 }
117
118 fn enclose(mut self) -> Self {
119 let size_hint = self.content.as_widget().size_hint();
120
121 if self.direction.horizontal().is_none() {
122 self.width = self.width.enclose(size_hint.width);
123 }
124
125 if self.direction.vertical().is_none() {
126 self.height = self.height.enclose(size_hint.height);
127 }
128
129 self
130 }
131
132 pub fn horizontal(self) -> Self {
134 self.direction(Direction::Horizontal(Scrollbar::default()))
135 }
136
137 pub fn direction(mut self, direction: impl Into<Direction>) -> Self {
139 self.direction = direction.into();
140 self.enclose()
141 }
142
143 pub fn id(mut self, id: impl Into<widget::Id>) -> Self {
145 self.id = Some(id.into());
146 self
147 }
148
149 pub fn width(mut self, width: impl Into<Length>) -> Self {
151 self.width = width.into();
152 self
153 }
154
155 pub fn height(mut self, height: impl Into<Length>) -> Self {
157 self.height = height.into();
158 self
159 }
160
161 pub fn on_scroll(mut self, f: impl Fn(Viewport) -> Message + 'a) -> Self {
165 self.on_scroll = Some(Box::new(f));
166 self
167 }
168
169 pub fn anchor_top(self) -> Self {
171 self.anchor_y(Anchor::Start)
172 }
173
174 pub fn anchor_bottom(self) -> Self {
176 self.anchor_y(Anchor::End)
177 }
178
179 pub fn anchor_left(self) -> Self {
181 self.anchor_x(Anchor::Start)
182 }
183
184 pub fn anchor_right(self) -> Self {
186 self.anchor_x(Anchor::End)
187 }
188
189 pub fn anchor_x(mut self, alignment: Anchor) -> Self {
191 match &mut self.direction {
192 Direction::Horizontal(horizontal)
193 | Direction::Both { horizontal, .. } => {
194 horizontal.alignment = alignment;
195 }
196 Direction::Vertical { .. } => {}
197 }
198
199 self
200 }
201
202 pub fn anchor_y(mut self, alignment: Anchor) -> Self {
204 match &mut self.direction {
205 Direction::Vertical(vertical)
206 | Direction::Both { vertical, .. } => {
207 vertical.alignment = alignment;
208 }
209 Direction::Horizontal { .. } => {}
210 }
211
212 self
213 }
214
215 pub fn spacing(mut self, new_spacing: impl Into<Pixels>) -> Self {
221 match &mut self.direction {
222 Direction::Horizontal(scrollbar)
223 | Direction::Vertical(scrollbar) => {
224 scrollbar.spacing = Some(new_spacing.into().0);
225 }
226 Direction::Both { .. } => {}
227 }
228
229 self
230 }
231
232 pub fn auto_scroll(mut self, auto_scroll: bool) -> Self {
237 self.auto_scroll = auto_scroll;
238 self
239 }
240
241 #[must_use]
243 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
244 where
245 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
246 {
247 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
248 self
249 }
250
251 #[cfg(feature = "advanced")]
253 #[must_use]
254 pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
255 self.class = class.into();
256 self
257 }
258}
259
260#[derive(Debug, Clone, Copy, PartialEq)]
262pub enum Direction {
263 Vertical(Scrollbar),
265 Horizontal(Scrollbar),
267 Both {
269 vertical: Scrollbar,
271 horizontal: Scrollbar,
273 },
274}
275
276impl Direction {
277 pub fn horizontal(&self) -> Option<&Scrollbar> {
279 match self {
280 Self::Horizontal(scrollbar) => Some(scrollbar),
281 Self::Both { horizontal, .. } => Some(horizontal),
282 Self::Vertical(_) => None,
283 }
284 }
285
286 pub fn vertical(&self) -> Option<&Scrollbar> {
288 match self {
289 Self::Vertical(scrollbar) => Some(scrollbar),
290 Self::Both { vertical, .. } => Some(vertical),
291 Self::Horizontal(_) => None,
292 }
293 }
294
295 fn align(&self, delta: Vector) -> Vector {
296 let horizontal_alignment =
297 self.horizontal().map(|p| p.alignment).unwrap_or_default();
298
299 let vertical_alignment =
300 self.vertical().map(|p| p.alignment).unwrap_or_default();
301
302 let align = |alignment: Anchor, delta: f32| match alignment {
303 Anchor::Start => delta,
304 Anchor::End => -delta,
305 };
306
307 Vector::new(
308 align(horizontal_alignment, delta.x),
309 align(vertical_alignment, delta.y),
310 )
311 }
312}
313
314impl Default for Direction {
315 fn default() -> Self {
316 Self::Vertical(Scrollbar::default())
317 }
318}
319
320#[derive(Debug, Clone, Copy, PartialEq)]
322pub struct Scrollbar {
323 width: f32,
324 margin: f32,
325 scroller_width: f32,
326 alignment: Anchor,
327 spacing: Option<f32>,
328}
329
330impl Default for Scrollbar {
331 fn default() -> Self {
332 Self {
333 width: 10.0,
334 margin: 0.0,
335 scroller_width: 10.0,
336 alignment: Anchor::Start,
337 spacing: None,
338 }
339 }
340}
341
342impl Scrollbar {
343 pub fn new() -> Self {
345 Self::default()
346 }
347
348 pub fn hidden() -> Self {
351 Self::default().width(0).scroller_width(0)
352 }
353
354 pub fn width(mut self, width: impl Into<Pixels>) -> Self {
356 self.width = width.into().0.max(0.0);
357 self
358 }
359
360 pub fn margin(mut self, margin: impl Into<Pixels>) -> Self {
362 self.margin = margin.into().0;
363 self
364 }
365
366 pub fn scroller_width(mut self, scroller_width: impl Into<Pixels>) -> Self {
368 self.scroller_width = scroller_width.into().0.max(0.0);
369 self
370 }
371
372 pub fn anchor(mut self, alignment: Anchor) -> Self {
374 self.alignment = alignment;
375 self
376 }
377
378 pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
384 self.spacing = Some(spacing.into().0);
385 self
386 }
387}
388
389#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
392pub enum Anchor {
393 #[default]
395 Start,
396 End,
398}
399
400impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
401 for Scrollable<'_, Message, Theme, Renderer>
402where
403 Theme: Catalog,
404 Renderer: text::Renderer,
405{
406 fn tag(&self) -> tree::Tag {
407 tree::Tag::of::<State>()
408 }
409
410 fn state(&self) -> tree::State {
411 tree::State::new(State::new())
412 }
413
414 fn children(&self) -> Vec<Tree> {
415 vec![Tree::new(&self.content)]
416 }
417
418 fn diff(&self, tree: &mut Tree) {
419 tree.diff_children(std::slice::from_ref(&self.content));
420 }
421
422 fn size(&self) -> Size<Length> {
423 Size {
424 width: self.width,
425 height: self.height,
426 }
427 }
428
429 fn layout(
430 &mut self,
431 tree: &mut Tree,
432 renderer: &Renderer,
433 limits: &layout::Limits,
434 ) -> layout::Node {
435 let mut layout = |right_padding, bottom_padding| {
436 layout::padded(
437 limits,
438 self.width,
439 self.height,
440 Padding {
441 right: right_padding,
442 bottom: bottom_padding,
443 ..Padding::ZERO
444 },
445 |limits| {
446 let is_horizontal = self.direction.horizontal().is_some();
447 let is_vertical = self.direction.vertical().is_some();
448
449 let child_limits = layout::Limits::with_compression(
450 limits.min(),
451 Size::new(
452 if is_horizontal {
453 f32::INFINITY
454 } else {
455 limits.max().width
456 },
457 if is_vertical {
458 f32::INFINITY
459 } else {
460 limits.max().height
461 },
462 ),
463 Size::new(is_horizontal, is_vertical),
464 );
465
466 self.content.as_widget_mut().layout(
467 &mut tree.children[0],
468 renderer,
469 &child_limits,
470 )
471 },
472 )
473 };
474
475 match self.direction {
476 Direction::Vertical(Scrollbar {
477 width,
478 margin,
479 spacing: Some(spacing),
480 ..
481 })
482 | Direction::Horizontal(Scrollbar {
483 width,
484 margin,
485 spacing: Some(spacing),
486 ..
487 }) => {
488 let is_vertical =
489 matches!(self.direction, Direction::Vertical(_));
490
491 let padding = width + margin * 2.0 + spacing;
492 let state = tree.state.downcast_mut::<State>();
493
494 let status_quo = layout(
495 if is_vertical && state.is_scrollbar_visible {
496 padding
497 } else {
498 0.0
499 },
500 if !is_vertical && state.is_scrollbar_visible {
501 padding
502 } else {
503 0.0
504 },
505 );
506
507 let is_scrollbar_visible = if is_vertical {
508 status_quo.children()[0].size().height
509 > status_quo.size().height
510 } else {
511 status_quo.children()[0].size().width
512 > status_quo.size().width
513 };
514
515 if state.is_scrollbar_visible == is_scrollbar_visible {
516 status_quo
517 } else {
518 log::trace!("Scrollbar status quo has changed");
519 state.is_scrollbar_visible = is_scrollbar_visible;
520
521 layout(
522 if is_vertical && state.is_scrollbar_visible {
523 padding
524 } else {
525 0.0
526 },
527 if !is_vertical && state.is_scrollbar_visible {
528 padding
529 } else {
530 0.0
531 },
532 )
533 }
534 }
535 _ => layout(0.0, 0.0),
536 }
537 }
538
539 fn operate(
540 &mut self,
541 tree: &mut Tree,
542 layout: Layout<'_>,
543 renderer: &Renderer,
544 operation: &mut dyn Operation,
545 ) {
546 let state = tree.state.downcast_mut::<State>();
547
548 let bounds = layout.bounds();
549 let content_layout = layout.children().next().unwrap();
550 let content_bounds = content_layout.bounds();
551 let translation =
552 state.translation(self.direction, bounds, content_bounds);
553
554 operation.scrollable(
555 self.id.as_ref(),
556 bounds,
557 content_bounds,
558 translation,
559 state,
560 );
561
562 operation.traverse(&mut |operation| {
563 self.content.as_widget_mut().operate(
564 &mut tree.children[0],
565 layout.children().next().unwrap(),
566 renderer,
567 operation,
568 );
569 });
570 }
571
572 fn update(
573 &mut self,
574 tree: &mut Tree,
575 event: &Event,
576 layout: Layout<'_>,
577 cursor: mouse::Cursor,
578 renderer: &Renderer,
579 clipboard: &mut dyn Clipboard,
580 shell: &mut Shell<'_, Message>,
581 _viewport: &Rectangle,
582 ) {
583 const AUTOSCROLL_DEADZONE: f32 = 20.0;
584 const AUTOSCROLL_SMOOTHNESS: f32 = 1.5;
585
586 let state = tree.state.downcast_mut::<State>();
587 let bounds = layout.bounds();
588 let cursor_over_scrollable = cursor.position_over(bounds);
589
590 let content = layout.children().next().unwrap();
591 let content_bounds = content.bounds();
592
593 let scrollbars =
594 Scrollbars::new(state, self.direction, bounds, content_bounds);
595
596 let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
597 scrollbars.is_mouse_over(cursor);
598
599 let last_offsets = (state.offset_x, state.offset_y);
600
601 if let Some(last_scrolled) = state.last_scrolled {
602 let clear_transaction = match event {
603 Event::Mouse(
604 mouse::Event::ButtonPressed(_)
605 | mouse::Event::ButtonReleased(_)
606 | mouse::Event::CursorLeft,
607 ) => true,
608 Event::Mouse(mouse::Event::CursorMoved { .. }) => {
609 last_scrolled.elapsed() > Duration::from_millis(100)
610 }
611 _ => last_scrolled.elapsed() > Duration::from_millis(1500),
612 };
613
614 if clear_transaction {
615 state.last_scrolled = None;
616 }
617 }
618
619 let mut update = || {
620 if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at() {
621 match event {
622 Event::Mouse(mouse::Event::CursorMoved { .. })
623 | Event::Touch(touch::Event::FingerMoved { .. }) => {
624 if let Some(scrollbar) = scrollbars.y {
625 let Some(cursor_position) =
626 cursor.land().position()
627 else {
628 return;
629 };
630
631 state.scroll_y_to(
632 scrollbar.scroll_percentage_y(
633 scroller_grabbed_at,
634 cursor_position,
635 ),
636 bounds,
637 content_bounds,
638 );
639
640 let _ = notify_scroll(
641 state,
642 &self.on_scroll,
643 bounds,
644 content_bounds,
645 shell,
646 );
647
648 shell.capture_event();
649 }
650 }
651 _ => {}
652 }
653 } else if mouse_over_y_scrollbar {
654 match event {
655 Event::Mouse(mouse::Event::ButtonPressed(
656 mouse::Button::Left,
657 ))
658 | Event::Touch(touch::Event::FingerPressed { .. }) => {
659 let Some(cursor_position) = cursor.position() else {
660 return;
661 };
662
663 if let (Some(scroller_grabbed_at), Some(scrollbar)) = (
664 scrollbars.grab_y_scroller(cursor_position),
665 scrollbars.y,
666 ) {
667 state.scroll_y_to(
668 scrollbar.scroll_percentage_y(
669 scroller_grabbed_at,
670 cursor_position,
671 ),
672 bounds,
673 content_bounds,
674 );
675
676 state.interaction = Interaction::YScrollerGrabbed(
677 scroller_grabbed_at,
678 );
679
680 let _ = notify_scroll(
681 state,
682 &self.on_scroll,
683 bounds,
684 content_bounds,
685 shell,
686 );
687 }
688
689 shell.capture_event();
690 }
691 _ => {}
692 }
693 }
694
695 if let Some(scroller_grabbed_at) = state.x_scroller_grabbed_at() {
696 match event {
697 Event::Mouse(mouse::Event::CursorMoved { .. })
698 | Event::Touch(touch::Event::FingerMoved { .. }) => {
699 let Some(cursor_position) = cursor.land().position()
700 else {
701 return;
702 };
703
704 if let Some(scrollbar) = scrollbars.x {
705 state.scroll_x_to(
706 scrollbar.scroll_percentage_x(
707 scroller_grabbed_at,
708 cursor_position,
709 ),
710 bounds,
711 content_bounds,
712 );
713
714 let _ = notify_scroll(
715 state,
716 &self.on_scroll,
717 bounds,
718 content_bounds,
719 shell,
720 );
721 }
722
723 shell.capture_event();
724 }
725 _ => {}
726 }
727 } else if mouse_over_x_scrollbar {
728 match event {
729 Event::Mouse(mouse::Event::ButtonPressed(
730 mouse::Button::Left,
731 ))
732 | Event::Touch(touch::Event::FingerPressed { .. }) => {
733 let Some(cursor_position) = cursor.position() else {
734 return;
735 };
736
737 if let (Some(scroller_grabbed_at), Some(scrollbar)) = (
738 scrollbars.grab_x_scroller(cursor_position),
739 scrollbars.x,
740 ) {
741 state.scroll_x_to(
742 scrollbar.scroll_percentage_x(
743 scroller_grabbed_at,
744 cursor_position,
745 ),
746 bounds,
747 content_bounds,
748 );
749
750 state.interaction = Interaction::XScrollerGrabbed(
751 scroller_grabbed_at,
752 );
753
754 let _ = notify_scroll(
755 state,
756 &self.on_scroll,
757 bounds,
758 content_bounds,
759 shell,
760 );
761
762 shell.capture_event();
763 }
764 }
765 _ => {}
766 }
767 }
768
769 if matches!(state.interaction, Interaction::AutoScrolling { .. })
770 && matches!(
771 event,
772 Event::Mouse(
773 mouse::Event::ButtonPressed(_)
774 | mouse::Event::WheelScrolled { .. }
775 ) | Event::Touch(_)
776 | Event::Keyboard(_)
777 )
778 {
779 state.interaction = Interaction::None;
780 shell.capture_event();
781 shell.invalidate_layout();
782 shell.request_redraw();
783 return;
784 }
785
786 if state.last_scrolled.is_none()
787 || !matches!(
788 event,
789 Event::Mouse(mouse::Event::WheelScrolled { .. })
790 )
791 {
792 let translation =
793 state.translation(self.direction, bounds, content_bounds);
794
795 let cursor = match cursor_over_scrollable {
796 Some(cursor_position)
797 if !(mouse_over_x_scrollbar
798 || mouse_over_y_scrollbar) =>
799 {
800 mouse::Cursor::Available(cursor_position + translation)
801 }
802 _ => cursor.levitate() + translation,
803 };
804
805 let had_input_method = shell.input_method().is_enabled();
806
807 self.content.as_widget_mut().update(
808 &mut tree.children[0],
809 event,
810 content,
811 cursor,
812 renderer,
813 clipboard,
814 shell,
815 &Rectangle {
816 y: bounds.y + translation.y,
817 x: bounds.x + translation.x,
818 ..bounds
819 },
820 );
821
822 if !had_input_method
823 && let InputMethod::Enabled { cursor, .. } =
824 shell.input_method_mut()
825 {
826 *cursor = *cursor - translation;
827 }
828 };
829
830 if matches!(
831 event,
832 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
833 | Event::Touch(
834 touch::Event::FingerLifted { .. }
835 | touch::Event::FingerLost { .. }
836 )
837 ) {
838 state.interaction = Interaction::None;
839 return;
840 }
841
842 if shell.is_event_captured() {
843 return;
844 }
845
846 match event {
847 Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
848 if cursor_over_scrollable.is_none() {
849 return;
850 }
851
852 let delta = match *delta {
853 mouse::ScrollDelta::Lines { x, y } => {
854 let is_shift_pressed =
855 state.keyboard_modifiers.shift();
856
857 let (x, y) = if cfg!(target_os = "macos")
859 && is_shift_pressed
860 {
861 (y, x)
862 } else {
863 (x, y)
864 };
865
866 let movement = if !is_shift_pressed {
867 Vector::new(x, y)
868 } else {
869 Vector::new(y, x)
870 };
871
872 -movement * 60.0
874 }
875 mouse::ScrollDelta::Pixels { x, y } => {
876 -Vector::new(x, y)
877 }
878 };
879
880 state.scroll(
881 self.direction.align(delta),
882 bounds,
883 content_bounds,
884 );
885
886 let has_scrolled = notify_scroll(
887 state,
888 &self.on_scroll,
889 bounds,
890 content_bounds,
891 shell,
892 );
893
894 let in_transaction = state.last_scrolled.is_some();
895
896 if has_scrolled || in_transaction {
897 shell.capture_event();
898 }
899 }
900 Event::Mouse(mouse::Event::ButtonPressed(
901 mouse::Button::Middle,
902 )) if self.auto_scroll
903 && matches!(state.interaction, Interaction::None) =>
904 {
905 let Some(origin) = cursor_over_scrollable else {
906 return;
907 };
908
909 state.interaction = Interaction::AutoScrolling {
910 origin,
911 current: origin,
912 last_frame: None,
913 };
914
915 shell.capture_event();
916 shell.invalidate_layout();
917 shell.request_redraw();
918 }
919 Event::Touch(event)
920 if matches!(
921 state.interaction,
922 Interaction::TouchScrolling(_)
923 ) || (!mouse_over_y_scrollbar
924 && !mouse_over_x_scrollbar) =>
925 {
926 match event {
927 touch::Event::FingerPressed { .. } => {
928 let Some(position) = cursor_over_scrollable else {
929 return;
930 };
931
932 state.interaction =
933 Interaction::TouchScrolling(position);
934 }
935 touch::Event::FingerMoved { .. } => {
936 let Interaction::TouchScrolling(
937 scroll_box_touched_at,
938 ) = state.interaction
939 else {
940 return;
941 };
942
943 let Some(cursor_position) = cursor.position()
944 else {
945 return;
946 };
947
948 let delta = Vector::new(
949 scroll_box_touched_at.x - cursor_position.x,
950 scroll_box_touched_at.y - cursor_position.y,
951 );
952
953 state.scroll(
954 self.direction.align(delta),
955 bounds,
956 content_bounds,
957 );
958
959 state.interaction =
960 Interaction::TouchScrolling(cursor_position);
961
962 let _ = notify_scroll(
964 state,
965 &self.on_scroll,
966 bounds,
967 content_bounds,
968 shell,
969 );
970 }
971 _ => {}
972 }
973
974 shell.capture_event();
975 }
976 Event::Mouse(mouse::Event::CursorMoved { position }) => {
977 if let Interaction::AutoScrolling {
978 origin,
979 last_frame,
980 ..
981 } = state.interaction
982 {
983 let delta = *position - origin;
984
985 state.interaction = Interaction::AutoScrolling {
986 origin,
987 current: *position,
988 last_frame,
989 };
990
991 if (delta.x.abs() >= AUTOSCROLL_DEADZONE
992 || delta.y.abs() >= AUTOSCROLL_DEADZONE)
993 && last_frame.is_none()
994 {
995 shell.request_redraw();
996 }
997 }
998 }
999 Event::Keyboard(keyboard::Event::ModifiersChanged(
1000 modifiers,
1001 )) => {
1002 state.keyboard_modifiers = *modifiers;
1003 }
1004 Event::Window(window::Event::RedrawRequested(now)) => {
1005 if let Interaction::AutoScrolling {
1006 origin,
1007 current,
1008 last_frame,
1009 } = state.interaction
1010 {
1011 if last_frame == Some(*now) {
1012 shell.request_redraw();
1013 return;
1014 }
1015
1016 state.interaction = Interaction::AutoScrolling {
1017 origin,
1018 current,
1019 last_frame: None,
1020 };
1021
1022 let mut delta = current - origin;
1023
1024 if delta.x.abs() < AUTOSCROLL_DEADZONE {
1025 delta.x = 0.0;
1026 }
1027
1028 if delta.y.abs() < AUTOSCROLL_DEADZONE {
1029 delta.y = 0.0;
1030 }
1031
1032 if delta.x != 0.0 || delta.y != 0.0 {
1033 let time_delta =
1034 if let Some(last_frame) = last_frame {
1035 *now - last_frame
1036 } else {
1037 Duration::ZERO
1038 };
1039
1040 let scroll_factor = time_delta.as_secs_f32();
1041
1042 state.scroll(
1043 self.direction.align(Vector::new(
1044 delta.x.signum()
1045 * delta
1046 .x
1047 .abs()
1048 .powf(AUTOSCROLL_SMOOTHNESS)
1049 * scroll_factor,
1050 delta.y.signum()
1051 * delta
1052 .y
1053 .abs()
1054 .powf(AUTOSCROLL_SMOOTHNESS)
1055 * scroll_factor,
1056 )),
1057 bounds,
1058 content_bounds,
1059 );
1060
1061 let has_scrolled = notify_scroll(
1062 state,
1063 &self.on_scroll,
1064 bounds,
1065 content_bounds,
1066 shell,
1067 );
1068
1069 if has_scrolled || time_delta.is_zero() {
1070 state.interaction =
1071 Interaction::AutoScrolling {
1072 origin,
1073 current,
1074 last_frame: Some(*now),
1075 };
1076
1077 shell.request_redraw();
1078 }
1079
1080 return;
1081 }
1082 }
1083
1084 let _ = notify_viewport(
1085 state,
1086 &self.on_scroll,
1087 bounds,
1088 content_bounds,
1089 shell,
1090 );
1091 }
1092 _ => {}
1093 }
1094 };
1095
1096 update();
1097
1098 let status = if state.scrollers_grabbed() {
1099 Status::Dragged {
1100 is_horizontal_scrollbar_dragged: state
1101 .x_scroller_grabbed_at()
1102 .is_some(),
1103 is_vertical_scrollbar_dragged: state
1104 .y_scroller_grabbed_at()
1105 .is_some(),
1106 is_horizontal_scrollbar_disabled: scrollbars.is_x_disabled(),
1107 is_vertical_scrollbar_disabled: scrollbars.is_y_disabled(),
1108 }
1109 } else if cursor_over_scrollable.is_some() {
1110 Status::Hovered {
1111 is_horizontal_scrollbar_hovered: mouse_over_x_scrollbar,
1112 is_vertical_scrollbar_hovered: mouse_over_y_scrollbar,
1113 is_horizontal_scrollbar_disabled: scrollbars.is_x_disabled(),
1114 is_vertical_scrollbar_disabled: scrollbars.is_y_disabled(),
1115 }
1116 } else {
1117 Status::Active {
1118 is_horizontal_scrollbar_disabled: scrollbars.is_x_disabled(),
1119 is_vertical_scrollbar_disabled: scrollbars.is_y_disabled(),
1120 }
1121 };
1122
1123 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
1124 self.last_status = Some(status);
1125 }
1126
1127 if last_offsets != (state.offset_x, state.offset_y)
1128 || self
1129 .last_status
1130 .is_some_and(|last_status| last_status != status)
1131 {
1132 shell.request_redraw();
1133 }
1134 }
1135
1136 fn draw(
1137 &self,
1138 tree: &Tree,
1139 renderer: &mut Renderer,
1140 theme: &Theme,
1141 defaults: &renderer::Style,
1142 layout: Layout<'_>,
1143 cursor: mouse::Cursor,
1144 viewport: &Rectangle,
1145 ) {
1146 let state = tree.state.downcast_ref::<State>();
1147
1148 let bounds = layout.bounds();
1149 let content_layout = layout.children().next().unwrap();
1150 let content_bounds = content_layout.bounds();
1151
1152 let Some(visible_bounds) = bounds.intersection(viewport) else {
1153 return;
1154 };
1155
1156 let scrollbars =
1157 Scrollbars::new(state, self.direction, bounds, content_bounds);
1158
1159 let cursor_over_scrollable = cursor.position_over(bounds);
1160 let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
1161 scrollbars.is_mouse_over(cursor);
1162
1163 let translation =
1164 state.translation(self.direction, bounds, content_bounds);
1165
1166 let cursor = match cursor_over_scrollable {
1167 Some(cursor_position)
1168 if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) =>
1169 {
1170 mouse::Cursor::Available(cursor_position + translation)
1171 }
1172 _ => mouse::Cursor::Unavailable,
1173 };
1174
1175 let style = theme.style(
1176 &self.class,
1177 self.last_status.unwrap_or(Status::Active {
1178 is_horizontal_scrollbar_disabled: false,
1179 is_vertical_scrollbar_disabled: false,
1180 }),
1181 );
1182
1183 container::draw_background(renderer, &style.container, layout.bounds());
1184
1185 if scrollbars.active() {
1187 renderer.with_layer(visible_bounds, |renderer| {
1188 renderer.with_translation(
1189 Vector::new(-translation.x, -translation.y),
1190 |renderer| {
1191 self.content.as_widget().draw(
1192 &tree.children[0],
1193 renderer,
1194 theme,
1195 defaults,
1196 content_layout,
1197 cursor,
1198 &Rectangle {
1199 y: visible_bounds.y + translation.y,
1200 x: visible_bounds.x + translation.x,
1201 ..visible_bounds
1202 },
1203 );
1204 },
1205 );
1206 });
1207
1208 let draw_scrollbar =
1209 |renderer: &mut Renderer,
1210 style: Rail,
1211 scrollbar: &internals::Scrollbar| {
1212 if scrollbar.bounds.width > 0.0
1213 && scrollbar.bounds.height > 0.0
1214 && (style.background.is_some()
1215 || (style.border.color != Color::TRANSPARENT
1216 && style.border.width > 0.0))
1217 {
1218 renderer.fill_quad(
1219 renderer::Quad {
1220 bounds: scrollbar.bounds,
1221 border: style.border,
1222 ..renderer::Quad::default()
1223 },
1224 style.background.unwrap_or(Background::Color(
1225 Color::TRANSPARENT,
1226 )),
1227 );
1228 }
1229
1230 if let Some(scroller) = scrollbar.scroller
1231 && scroller.bounds.width > 0.0
1232 && scroller.bounds.height > 0.0
1233 && (style.scroller.background
1234 != Background::Color(Color::TRANSPARENT)
1235 || (style.scroller.border.color
1236 != Color::TRANSPARENT
1237 && style.scroller.border.width > 0.0))
1238 {
1239 renderer.fill_quad(
1240 renderer::Quad {
1241 bounds: scroller.bounds,
1242 border: style.scroller.border,
1243 ..renderer::Quad::default()
1244 },
1245 style.scroller.background,
1246 );
1247 }
1248 };
1249
1250 renderer.with_layer(
1251 Rectangle {
1252 width: (visible_bounds.width + 2.0).min(viewport.width),
1253 height: (visible_bounds.height + 2.0).min(viewport.height),
1254 ..visible_bounds
1255 },
1256 |renderer| {
1257 if let Some(scrollbar) = scrollbars.y {
1258 draw_scrollbar(
1259 renderer,
1260 style.vertical_rail,
1261 &scrollbar,
1262 );
1263 }
1264
1265 if let Some(scrollbar) = scrollbars.x {
1266 draw_scrollbar(
1267 renderer,
1268 style.horizontal_rail,
1269 &scrollbar,
1270 );
1271 }
1272
1273 if let (Some(x), Some(y)) = (scrollbars.x, scrollbars.y) {
1274 let background =
1275 style.gap.or(style.container.background);
1276
1277 if let Some(background) = background {
1278 renderer.fill_quad(
1279 renderer::Quad {
1280 bounds: Rectangle {
1281 x: y.bounds.x,
1282 y: x.bounds.y,
1283 width: y.bounds.width,
1284 height: x.bounds.height,
1285 },
1286 ..renderer::Quad::default()
1287 },
1288 background,
1289 );
1290 }
1291 }
1292 },
1293 );
1294 } else {
1295 self.content.as_widget().draw(
1296 &tree.children[0],
1297 renderer,
1298 theme,
1299 defaults,
1300 content_layout,
1301 cursor,
1302 &Rectangle {
1303 x: visible_bounds.x + translation.x,
1304 y: visible_bounds.y + translation.y,
1305 ..visible_bounds
1306 },
1307 );
1308 }
1309 }
1310
1311 fn mouse_interaction(
1312 &self,
1313 tree: &Tree,
1314 layout: Layout<'_>,
1315 cursor: mouse::Cursor,
1316 _viewport: &Rectangle,
1317 renderer: &Renderer,
1318 ) -> mouse::Interaction {
1319 let state = tree.state.downcast_ref::<State>();
1320 let bounds = layout.bounds();
1321 let cursor_over_scrollable = cursor.position_over(bounds);
1322
1323 let content_layout = layout.children().next().unwrap();
1324 let content_bounds = content_layout.bounds();
1325
1326 let scrollbars =
1327 Scrollbars::new(state, self.direction, bounds, content_bounds);
1328
1329 let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
1330 scrollbars.is_mouse_over(cursor);
1331
1332 if state.scrollers_grabbed() {
1333 return mouse::Interaction::None;
1334 }
1335
1336 let translation =
1337 state.translation(self.direction, bounds, content_bounds);
1338
1339 let cursor = match cursor_over_scrollable {
1340 Some(cursor_position)
1341 if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) =>
1342 {
1343 mouse::Cursor::Available(cursor_position + translation)
1344 }
1345 _ => cursor.levitate() + translation,
1346 };
1347
1348 self.content.as_widget().mouse_interaction(
1349 &tree.children[0],
1350 content_layout,
1351 cursor,
1352 &Rectangle {
1353 y: bounds.y + translation.y,
1354 x: bounds.x + translation.x,
1355 ..bounds
1356 },
1357 renderer,
1358 )
1359 }
1360
1361 fn overlay<'b>(
1362 &'b mut self,
1363 tree: &'b mut Tree,
1364 layout: Layout<'b>,
1365 renderer: &Renderer,
1366 viewport: &Rectangle,
1367 translation: Vector,
1368 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
1369 let state = tree.state.downcast_ref::<State>();
1370 let bounds = layout.bounds();
1371 let content_layout = layout.children().next().unwrap();
1372 let content_bounds = content_layout.bounds();
1373 let visible_bounds = bounds.intersection(viewport).unwrap_or(*viewport);
1374 let offset = state.translation(self.direction, bounds, content_bounds);
1375
1376 let overlay = self.content.as_widget_mut().overlay(
1377 &mut tree.children[0],
1378 layout.children().next().unwrap(),
1379 renderer,
1380 &visible_bounds,
1381 translation - offset,
1382 );
1383
1384 let icon = if let Interaction::AutoScrolling { origin, .. } =
1385 state.interaction
1386 {
1387 let scrollbars =
1388 Scrollbars::new(state, self.direction, bounds, content_bounds);
1389
1390 Some(overlay::Element::new(Box::new(AutoScrollIcon {
1391 origin,
1392 vertical: scrollbars.y.is_some(),
1393 horizontal: scrollbars.x.is_some(),
1394 class: &self.class,
1395 })))
1396 } else {
1397 None
1398 };
1399
1400 match (overlay, icon) {
1401 (None, None) => None,
1402 (None, Some(icon)) => Some(icon),
1403 (Some(overlay), None) => Some(overlay),
1404 (Some(overlay), Some(icon)) => Some(overlay::Element::new(
1405 Box::new(overlay::Group::with_children(vec![overlay, icon])),
1406 )),
1407 }
1408 }
1409}
1410
1411struct AutoScrollIcon<'a, Class> {
1412 origin: Point,
1413 vertical: bool,
1414 horizontal: bool,
1415 class: &'a Class,
1416}
1417
1418impl<Class> AutoScrollIcon<'_, Class> {
1419 const SIZE: f32 = 40.0;
1420 const DOT: f32 = Self::SIZE / 10.0;
1421 const PADDING: f32 = Self::SIZE / 10.0;
1422}
1423
1424impl<Message, Theme, Renderer> core::Overlay<Message, Theme, Renderer>
1425 for AutoScrollIcon<'_, Theme::Class<'_>>
1426where
1427 Renderer: text::Renderer,
1428 Theme: Catalog,
1429{
1430 fn layout(&mut self, _renderer: &Renderer, _bounds: Size) -> layout::Node {
1431 layout::Node::new(Size::new(Self::SIZE, Self::SIZE))
1432 .move_to(self.origin - Vector::new(Self::SIZE, Self::SIZE) / 2.0)
1433 }
1434
1435 fn draw(
1436 &self,
1437 renderer: &mut Renderer,
1438 theme: &Theme,
1439 _style: &renderer::Style,
1440 layout: Layout<'_>,
1441 _cursor: mouse::Cursor,
1442 ) {
1443 let bounds = layout.bounds();
1444 let style = theme
1445 .style(
1446 self.class,
1447 Status::Active {
1448 is_horizontal_scrollbar_disabled: false,
1449 is_vertical_scrollbar_disabled: false,
1450 },
1451 )
1452 .auto_scroll;
1453
1454 renderer.with_layer(Rectangle::INFINITE, |renderer| {
1455 renderer.fill_quad(
1456 renderer::Quad {
1457 bounds,
1458 border: style.border,
1459 shadow: style.shadow,
1460 snap: false,
1461 },
1462 style.background,
1463 );
1464
1465 renderer.fill_quad(
1466 renderer::Quad {
1467 bounds: Rectangle::new(
1468 bounds.center()
1469 - Vector::new(Self::DOT, Self::DOT) / 2.0,
1470 Size::new(Self::DOT, Self::DOT),
1471 ),
1472 border: border::rounded(bounds.width),
1473 snap: false,
1474 ..renderer::Quad::default()
1475 },
1476 style.icon,
1477 );
1478
1479 let arrow = core::Text {
1480 content: String::new(),
1481 bounds: bounds.size(),
1482 size: Pixels::from(12),
1483 line_height: text::LineHeight::Relative(1.0),
1484 font: Renderer::ICON_FONT,
1485 align_x: text::Alignment::Center,
1486 align_y: alignment::Vertical::Center,
1487 shaping: text::Shaping::Basic,
1488 wrapping: text::Wrapping::None,
1489 };
1490
1491 if self.vertical {
1492 renderer.fill_text(
1493 core::Text {
1494 content: Renderer::SCROLL_UP_ICON.to_string(),
1495 align_y: alignment::Vertical::Top,
1496 ..arrow
1497 },
1498 Point::new(bounds.center_x(), bounds.y + Self::PADDING),
1499 style.icon,
1500 bounds,
1501 );
1502
1503 renderer.fill_text(
1504 core::Text {
1505 content: Renderer::SCROLL_DOWN_ICON.to_string(),
1506 align_y: alignment::Vertical::Bottom,
1507 ..arrow
1508 },
1509 Point::new(
1510 bounds.center_x(),
1511 bounds.y + bounds.height - Self::PADDING - 0.5,
1512 ),
1513 style.icon,
1514 bounds,
1515 );
1516 }
1517
1518 if self.horizontal {
1519 renderer.fill_text(
1520 core::Text {
1521 content: Renderer::SCROLL_LEFT_ICON.to_string(),
1522 align_x: text::Alignment::Left,
1523 ..arrow
1524 },
1525 Point::new(
1526 bounds.x + Self::PADDING + 1.0,
1527 bounds.center_y() + 1.0,
1528 ),
1529 style.icon,
1530 bounds,
1531 );
1532
1533 renderer.fill_text(
1534 core::Text {
1535 content: Renderer::SCROLL_RIGHT_ICON.to_string(),
1536 align_x: text::Alignment::Right,
1537 ..arrow
1538 },
1539 Point::new(
1540 bounds.x + bounds.width - Self::PADDING - 1.0,
1541 bounds.center_y() + 1.0,
1542 ),
1543 style.icon,
1544 bounds,
1545 );
1546 }
1547 });
1548 }
1549
1550 fn index(&self) -> f32 {
1551 f32::MAX
1552 }
1553}
1554
1555impl<'a, Message, Theme, Renderer>
1556 From<Scrollable<'a, Message, Theme, Renderer>>
1557 for Element<'a, Message, Theme, Renderer>
1558where
1559 Message: 'a,
1560 Theme: 'a + Catalog,
1561 Renderer: 'a + text::Renderer,
1562{
1563 fn from(
1564 text_input: Scrollable<'a, Message, Theme, Renderer>,
1565 ) -> Element<'a, Message, Theme, Renderer> {
1566 Element::new(text_input)
1567 }
1568}
1569
1570fn notify_scroll<Message>(
1571 state: &mut State,
1572 on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>,
1573 bounds: Rectangle,
1574 content_bounds: Rectangle,
1575 shell: &mut Shell<'_, Message>,
1576) -> bool {
1577 if notify_viewport(state, on_scroll, bounds, content_bounds, shell) {
1578 state.last_scrolled = Some(Instant::now());
1579
1580 true
1581 } else {
1582 false
1583 }
1584}
1585
1586fn notify_viewport<Message>(
1587 state: &mut State,
1588 on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>,
1589 bounds: Rectangle,
1590 content_bounds: Rectangle,
1591 shell: &mut Shell<'_, Message>,
1592) -> bool {
1593 if content_bounds.width <= bounds.width
1594 && content_bounds.height <= bounds.height
1595 {
1596 return false;
1597 }
1598
1599 let viewport = Viewport {
1600 offset_x: state.offset_x,
1601 offset_y: state.offset_y,
1602 bounds,
1603 content_bounds,
1604 };
1605
1606 if let Some(last_notified) = state.last_notified {
1608 let last_relative_offset = last_notified.relative_offset();
1609 let current_relative_offset = viewport.relative_offset();
1610
1611 let last_absolute_offset = last_notified.absolute_offset();
1612 let current_absolute_offset = viewport.absolute_offset();
1613
1614 let unchanged = |a: f32, b: f32| {
1615 (a - b).abs() <= f32::EPSILON || (a.is_nan() && b.is_nan())
1616 };
1617
1618 if last_notified.bounds == bounds
1619 && last_notified.content_bounds == content_bounds
1620 && unchanged(last_relative_offset.x, current_relative_offset.x)
1621 && unchanged(last_relative_offset.y, current_relative_offset.y)
1622 && unchanged(last_absolute_offset.x, current_absolute_offset.x)
1623 && unchanged(last_absolute_offset.y, current_absolute_offset.y)
1624 {
1625 return false;
1626 }
1627 }
1628
1629 state.last_notified = Some(viewport);
1630
1631 if let Some(on_scroll) = on_scroll {
1632 shell.publish(on_scroll(viewport));
1633 }
1634
1635 true
1636}
1637
1638#[derive(Debug, Clone, Copy)]
1639struct State {
1640 offset_y: Offset,
1641 offset_x: Offset,
1642 interaction: Interaction,
1643 keyboard_modifiers: keyboard::Modifiers,
1644 last_notified: Option<Viewport>,
1645 last_scrolled: Option<Instant>,
1646 is_scrollbar_visible: bool,
1647}
1648
1649#[derive(Debug, Clone, Copy)]
1650enum Interaction {
1651 None,
1652 YScrollerGrabbed(f32),
1653 XScrollerGrabbed(f32),
1654 TouchScrolling(Point),
1655 AutoScrolling {
1656 origin: Point,
1657 current: Point,
1658 last_frame: Option<Instant>,
1659 },
1660}
1661
1662impl Default for State {
1663 fn default() -> Self {
1664 Self {
1665 offset_y: Offset::Absolute(0.0),
1666 offset_x: Offset::Absolute(0.0),
1667 interaction: Interaction::None,
1668 keyboard_modifiers: keyboard::Modifiers::default(),
1669 last_notified: None,
1670 last_scrolled: None,
1671 is_scrollbar_visible: true,
1672 }
1673 }
1674}
1675
1676impl operation::Scrollable for State {
1677 fn snap_to(&mut self, offset: RelativeOffset<Option<f32>>) {
1678 State::snap_to(self, offset);
1679 }
1680
1681 fn scroll_to(&mut self, offset: AbsoluteOffset<Option<f32>>) {
1682 State::scroll_to(self, offset);
1683 }
1684
1685 fn scroll_by(
1686 &mut self,
1687 offset: AbsoluteOffset,
1688 bounds: Rectangle,
1689 content_bounds: Rectangle,
1690 ) {
1691 State::scroll_by(self, offset, bounds, content_bounds);
1692 }
1693}
1694
1695#[derive(Debug, Clone, Copy, PartialEq)]
1696enum Offset {
1697 Absolute(f32),
1698 Relative(f32),
1699}
1700
1701impl Offset {
1702 fn absolute(self, viewport: f32, content: f32) -> f32 {
1703 match self {
1704 Offset::Absolute(absolute) => {
1705 absolute.min((content - viewport).max(0.0))
1706 }
1707 Offset::Relative(percentage) => {
1708 ((content - viewport) * percentage).max(0.0)
1709 }
1710 }
1711 }
1712
1713 fn translation(
1714 self,
1715 viewport: f32,
1716 content: f32,
1717 alignment: Anchor,
1718 ) -> f32 {
1719 let offset = self.absolute(viewport, content);
1720
1721 match alignment {
1722 Anchor::Start => offset,
1723 Anchor::End => ((content - viewport).max(0.0) - offset).max(0.0),
1724 }
1725 }
1726}
1727
1728#[derive(Debug, Clone, Copy)]
1730pub struct Viewport {
1731 offset_x: Offset,
1732 offset_y: Offset,
1733 bounds: Rectangle,
1734 content_bounds: Rectangle,
1735}
1736
1737impl Viewport {
1738 pub fn absolute_offset(&self) -> AbsoluteOffset {
1740 let x = self
1741 .offset_x
1742 .absolute(self.bounds.width, self.content_bounds.width);
1743 let y = self
1744 .offset_y
1745 .absolute(self.bounds.height, self.content_bounds.height);
1746
1747 AbsoluteOffset { x, y }
1748 }
1749
1750 pub fn absolute_offset_reversed(&self) -> AbsoluteOffset {
1756 let AbsoluteOffset { x, y } = self.absolute_offset();
1757
1758 AbsoluteOffset {
1759 x: (self.content_bounds.width - self.bounds.width).max(0.0) - x,
1760 y: (self.content_bounds.height - self.bounds.height).max(0.0) - y,
1761 }
1762 }
1763
1764 pub fn relative_offset(&self) -> RelativeOffset {
1766 let AbsoluteOffset { x, y } = self.absolute_offset();
1767
1768 let x = x / (self.content_bounds.width - self.bounds.width);
1769 let y = y / (self.content_bounds.height - self.bounds.height);
1770
1771 RelativeOffset { x, y }
1772 }
1773
1774 pub fn bounds(&self) -> Rectangle {
1776 self.bounds
1777 }
1778
1779 pub fn content_bounds(&self) -> Rectangle {
1781 self.content_bounds
1782 }
1783}
1784
1785impl State {
1786 fn new() -> Self {
1787 State::default()
1788 }
1789
1790 fn scroll(
1791 &mut self,
1792 delta: Vector<f32>,
1793 bounds: Rectangle,
1794 content_bounds: Rectangle,
1795 ) {
1796 if bounds.height < content_bounds.height {
1797 self.offset_y = Offset::Absolute(
1798 (self.offset_y.absolute(bounds.height, content_bounds.height)
1799 + delta.y)
1800 .clamp(0.0, content_bounds.height - bounds.height),
1801 );
1802 }
1803
1804 if bounds.width < content_bounds.width {
1805 self.offset_x = Offset::Absolute(
1806 (self.offset_x.absolute(bounds.width, content_bounds.width)
1807 + delta.x)
1808 .clamp(0.0, content_bounds.width - bounds.width),
1809 );
1810 }
1811 }
1812
1813 fn scroll_y_to(
1814 &mut self,
1815 percentage: f32,
1816 bounds: Rectangle,
1817 content_bounds: Rectangle,
1818 ) {
1819 self.offset_y = Offset::Relative(percentage.clamp(0.0, 1.0));
1820 self.unsnap(bounds, content_bounds);
1821 }
1822
1823 fn scroll_x_to(
1824 &mut self,
1825 percentage: f32,
1826 bounds: Rectangle,
1827 content_bounds: Rectangle,
1828 ) {
1829 self.offset_x = Offset::Relative(percentage.clamp(0.0, 1.0));
1830 self.unsnap(bounds, content_bounds);
1831 }
1832
1833 fn snap_to(&mut self, offset: RelativeOffset<Option<f32>>) {
1834 if let Some(x) = offset.x {
1835 self.offset_x = Offset::Relative(x.clamp(0.0, 1.0));
1836 }
1837
1838 if let Some(y) = offset.y {
1839 self.offset_y = Offset::Relative(y.clamp(0.0, 1.0));
1840 }
1841 }
1842
1843 fn scroll_to(&mut self, offset: AbsoluteOffset<Option<f32>>) {
1844 if let Some(x) = offset.x {
1845 self.offset_x = Offset::Absolute(x.max(0.0));
1846 }
1847
1848 if let Some(y) = offset.y {
1849 self.offset_y = Offset::Absolute(y.max(0.0));
1850 }
1851 }
1852
1853 fn scroll_by(
1855 &mut self,
1856 offset: AbsoluteOffset,
1857 bounds: Rectangle,
1858 content_bounds: Rectangle,
1859 ) {
1860 self.scroll(Vector::new(offset.x, offset.y), bounds, content_bounds);
1861 }
1862
1863 fn unsnap(&mut self, bounds: Rectangle, content_bounds: Rectangle) {
1866 self.offset_x = Offset::Absolute(
1867 self.offset_x.absolute(bounds.width, content_bounds.width),
1868 );
1869 self.offset_y = Offset::Absolute(
1870 self.offset_y.absolute(bounds.height, content_bounds.height),
1871 );
1872 }
1873
1874 fn translation(
1877 &self,
1878 direction: Direction,
1879 bounds: Rectangle,
1880 content_bounds: Rectangle,
1881 ) -> Vector {
1882 Vector::new(
1883 if let Some(horizontal) = direction.horizontal() {
1884 self.offset_x
1885 .translation(
1886 bounds.width,
1887 content_bounds.width,
1888 horizontal.alignment,
1889 )
1890 .round()
1891 } else {
1892 0.0
1893 },
1894 if let Some(vertical) = direction.vertical() {
1895 self.offset_y
1896 .translation(
1897 bounds.height,
1898 content_bounds.height,
1899 vertical.alignment,
1900 )
1901 .round()
1902 } else {
1903 0.0
1904 },
1905 )
1906 }
1907
1908 fn scrollers_grabbed(&self) -> bool {
1909 matches!(
1910 self.interaction,
1911 Interaction::YScrollerGrabbed(_) | Interaction::XScrollerGrabbed(_),
1912 )
1913 }
1914
1915 pub fn y_scroller_grabbed_at(&self) -> Option<f32> {
1916 let Interaction::YScrollerGrabbed(at) = self.interaction else {
1917 return None;
1918 };
1919
1920 Some(at)
1921 }
1922
1923 pub fn x_scroller_grabbed_at(&self) -> Option<f32> {
1924 let Interaction::XScrollerGrabbed(at) = self.interaction else {
1925 return None;
1926 };
1927
1928 Some(at)
1929 }
1930}
1931
1932#[derive(Debug)]
1933struct Scrollbars {
1935 y: Option<internals::Scrollbar>,
1936 x: Option<internals::Scrollbar>,
1937}
1938
1939impl Scrollbars {
1940 fn new(
1942 state: &State,
1943 direction: Direction,
1944 bounds: Rectangle,
1945 content_bounds: Rectangle,
1946 ) -> Self {
1947 let translation = state.translation(direction, bounds, content_bounds);
1948
1949 let show_scrollbar_x = direction
1950 .horizontal()
1951 .filter(|_scrollbar| content_bounds.width > bounds.width);
1952
1953 let show_scrollbar_y = direction
1954 .vertical()
1955 .filter(|_scrollbar| content_bounds.height > bounds.height);
1956
1957 let y_scrollbar = if let Some(vertical) = show_scrollbar_y {
1958 let Scrollbar {
1959 width,
1960 margin,
1961 scroller_width,
1962 ..
1963 } = *vertical;
1964
1965 let x_scrollbar_height = show_scrollbar_x
1968 .map_or(0.0, |h| h.width.max(h.scroller_width) + h.margin);
1969
1970 let total_scrollbar_width =
1971 width.max(scroller_width) + 2.0 * margin;
1972
1973 let total_scrollbar_bounds = Rectangle {
1975 x: bounds.x + bounds.width - total_scrollbar_width,
1976 y: bounds.y,
1977 width: total_scrollbar_width,
1978 height: (bounds.height - x_scrollbar_height).max(0.0),
1979 };
1980
1981 let scrollbar_bounds = Rectangle {
1983 x: bounds.x + bounds.width
1984 - total_scrollbar_width / 2.0
1985 - width / 2.0,
1986 y: bounds.y,
1987 width,
1988 height: (bounds.height - x_scrollbar_height).max(0.0),
1989 };
1990
1991 let ratio = bounds.height / content_bounds.height;
1992
1993 let scroller = if ratio >= 1.0 {
1994 None
1995 } else {
1996 let scroller_height =
1998 (scrollbar_bounds.height * ratio).max(2.0);
1999 let scroller_offset =
2000 translation.y * ratio * scrollbar_bounds.height
2001 / bounds.height;
2002
2003 let scroller_bounds = Rectangle {
2004 x: bounds.x + bounds.width
2005 - total_scrollbar_width / 2.0
2006 - scroller_width / 2.0,
2007 y: (scrollbar_bounds.y + scroller_offset).max(0.0),
2008 width: scroller_width,
2009 height: scroller_height,
2010 };
2011
2012 Some(internals::Scroller {
2013 bounds: scroller_bounds,
2014 })
2015 };
2016
2017 Some(internals::Scrollbar {
2018 total_bounds: total_scrollbar_bounds,
2019 bounds: scrollbar_bounds,
2020 scroller,
2021 alignment: vertical.alignment,
2022 disabled: content_bounds.height <= bounds.height,
2023 })
2024 } else {
2025 None
2026 };
2027
2028 let x_scrollbar = if let Some(horizontal) = show_scrollbar_x {
2029 let Scrollbar {
2030 width,
2031 margin,
2032 scroller_width,
2033 ..
2034 } = *horizontal;
2035
2036 let scrollbar_y_width = y_scrollbar
2039 .map_or(0.0, |scrollbar| scrollbar.total_bounds.width);
2040
2041 let total_scrollbar_height =
2042 width.max(scroller_width) + 2.0 * margin;
2043
2044 let total_scrollbar_bounds = Rectangle {
2046 x: bounds.x,
2047 y: bounds.y + bounds.height - total_scrollbar_height,
2048 width: (bounds.width - scrollbar_y_width).max(0.0),
2049 height: total_scrollbar_height,
2050 };
2051
2052 let scrollbar_bounds = Rectangle {
2054 x: bounds.x,
2055 y: bounds.y + bounds.height
2056 - total_scrollbar_height / 2.0
2057 - width / 2.0,
2058 width: (bounds.width - scrollbar_y_width).max(0.0),
2059 height: width,
2060 };
2061
2062 let ratio = bounds.width / content_bounds.width;
2063
2064 let scroller = if ratio >= 1.0 {
2065 None
2066 } else {
2067 let scroller_length = (scrollbar_bounds.width * ratio).max(2.0);
2069 let scroller_offset =
2070 translation.x * ratio * scrollbar_bounds.width
2071 / bounds.width;
2072
2073 let scroller_bounds = Rectangle {
2074 x: (scrollbar_bounds.x + scroller_offset).max(0.0),
2075 y: bounds.y + bounds.height
2076 - total_scrollbar_height / 2.0
2077 - scroller_width / 2.0,
2078 width: scroller_length,
2079 height: scroller_width,
2080 };
2081
2082 Some(internals::Scroller {
2083 bounds: scroller_bounds,
2084 })
2085 };
2086
2087 Some(internals::Scrollbar {
2088 total_bounds: total_scrollbar_bounds,
2089 bounds: scrollbar_bounds,
2090 scroller,
2091 alignment: horizontal.alignment,
2092 disabled: content_bounds.width <= bounds.width,
2093 })
2094 } else {
2095 None
2096 };
2097
2098 Self {
2099 y: y_scrollbar,
2100 x: x_scrollbar,
2101 }
2102 }
2103
2104 fn is_mouse_over(&self, cursor: mouse::Cursor) -> (bool, bool) {
2105 if let Some(cursor_position) = cursor.position() {
2106 (
2107 self.y
2108 .as_ref()
2109 .map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
2110 .unwrap_or(false),
2111 self.x
2112 .as_ref()
2113 .map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
2114 .unwrap_or(false),
2115 )
2116 } else {
2117 (false, false)
2118 }
2119 }
2120
2121 fn is_y_disabled(&self) -> bool {
2122 self.y.map(|y| y.disabled).unwrap_or(false)
2123 }
2124
2125 fn is_x_disabled(&self) -> bool {
2126 self.x.map(|x| x.disabled).unwrap_or(false)
2127 }
2128
2129 fn grab_y_scroller(&self, cursor_position: Point) -> Option<f32> {
2130 let scrollbar = self.y?;
2131 let scroller = scrollbar.scroller?;
2132
2133 if scrollbar.total_bounds.contains(cursor_position) {
2134 Some(if scroller.bounds.contains(cursor_position) {
2135 (cursor_position.y - scroller.bounds.y) / scroller.bounds.height
2136 } else {
2137 0.5
2138 })
2139 } else {
2140 None
2141 }
2142 }
2143
2144 fn grab_x_scroller(&self, cursor_position: Point) -> Option<f32> {
2145 let scrollbar = self.x?;
2146 let scroller = scrollbar.scroller?;
2147
2148 if scrollbar.total_bounds.contains(cursor_position) {
2149 Some(if scroller.bounds.contains(cursor_position) {
2150 (cursor_position.x - scroller.bounds.x) / scroller.bounds.width
2151 } else {
2152 0.5
2153 })
2154 } else {
2155 None
2156 }
2157 }
2158
2159 fn active(&self) -> bool {
2160 self.y.is_some() || self.x.is_some()
2161 }
2162}
2163
2164pub(super) mod internals {
2165 use crate::core::{Point, Rectangle};
2166
2167 use super::Anchor;
2168
2169 #[derive(Debug, Copy, Clone)]
2170 pub struct Scrollbar {
2171 pub total_bounds: Rectangle,
2172 pub bounds: Rectangle,
2173 pub scroller: Option<Scroller>,
2174 pub alignment: Anchor,
2175 pub disabled: bool,
2176 }
2177
2178 impl Scrollbar {
2179 pub fn is_mouse_over(&self, cursor_position: Point) -> bool {
2181 self.total_bounds.contains(cursor_position)
2182 }
2183
2184 pub fn scroll_percentage_y(
2186 &self,
2187 grabbed_at: f32,
2188 cursor_position: Point,
2189 ) -> f32 {
2190 if let Some(scroller) = self.scroller {
2191 let percentage = (cursor_position.y
2192 - self.bounds.y
2193 - scroller.bounds.height * grabbed_at)
2194 / (self.bounds.height - scroller.bounds.height);
2195
2196 match self.alignment {
2197 Anchor::Start => percentage,
2198 Anchor::End => 1.0 - percentage,
2199 }
2200 } else {
2201 0.0
2202 }
2203 }
2204
2205 pub fn scroll_percentage_x(
2207 &self,
2208 grabbed_at: f32,
2209 cursor_position: Point,
2210 ) -> f32 {
2211 if let Some(scroller) = self.scroller {
2212 let percentage = (cursor_position.x
2213 - self.bounds.x
2214 - scroller.bounds.width * grabbed_at)
2215 / (self.bounds.width - scroller.bounds.width);
2216
2217 match self.alignment {
2218 Anchor::Start => percentage,
2219 Anchor::End => 1.0 - percentage,
2220 }
2221 } else {
2222 0.0
2223 }
2224 }
2225 }
2226
2227 #[derive(Debug, Clone, Copy)]
2229 pub struct Scroller {
2230 pub bounds: Rectangle,
2232 }
2233}
2234
2235#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2237pub enum Status {
2238 Active {
2240 is_horizontal_scrollbar_disabled: bool,
2242 is_vertical_scrollbar_disabled: bool,
2244 },
2245 Hovered {
2247 is_horizontal_scrollbar_hovered: bool,
2249 is_vertical_scrollbar_hovered: bool,
2251 is_horizontal_scrollbar_disabled: bool,
2253 is_vertical_scrollbar_disabled: bool,
2255 },
2256 Dragged {
2258 is_horizontal_scrollbar_dragged: bool,
2260 is_vertical_scrollbar_dragged: bool,
2262 is_horizontal_scrollbar_disabled: bool,
2264 is_vertical_scrollbar_disabled: bool,
2266 },
2267}
2268
2269#[derive(Debug, Clone, Copy, PartialEq)]
2271pub struct Style {
2272 pub container: container::Style,
2274 pub vertical_rail: Rail,
2276 pub horizontal_rail: Rail,
2278 pub gap: Option<Background>,
2280 pub auto_scroll: AutoScroll,
2282}
2283
2284#[derive(Debug, Clone, Copy, PartialEq)]
2286pub struct Rail {
2287 pub background: Option<Background>,
2289 pub border: Border,
2291 pub scroller: Scroller,
2293}
2294
2295#[derive(Debug, Clone, Copy, PartialEq)]
2297pub struct Scroller {
2298 pub background: Background,
2300 pub border: Border,
2302}
2303
2304#[derive(Debug, Clone, Copy, PartialEq)]
2306pub struct AutoScroll {
2307 pub background: Background,
2309 pub border: Border,
2311 pub shadow: Shadow,
2313 pub icon: Color,
2315}
2316
2317pub trait Catalog {
2319 type Class<'a>;
2321
2322 fn default<'a>() -> Self::Class<'a>;
2324
2325 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
2327}
2328
2329pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
2331
2332impl Catalog for Theme {
2333 type Class<'a> = StyleFn<'a, Self>;
2334
2335 fn default<'a>() -> Self::Class<'a> {
2336 Box::new(default)
2337 }
2338
2339 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
2340 class(self, status)
2341 }
2342}
2343
2344pub fn default(theme: &Theme, status: Status) -> Style {
2346 let palette = theme.extended_palette();
2347
2348 let scrollbar = Rail {
2349 background: Some(palette.background.weak.color.into()),
2350 border: border::rounded(2),
2351 scroller: Scroller {
2352 background: palette.background.strongest.color.into(),
2353 border: border::rounded(2),
2354 },
2355 };
2356
2357 let auto_scroll = AutoScroll {
2358 background: palette.background.base.color.scale_alpha(0.9).into(),
2359 border: border::rounded(u32::MAX)
2360 .width(1)
2361 .color(palette.background.base.text.scale_alpha(0.8)),
2362 shadow: Shadow {
2363 color: Color::BLACK.scale_alpha(0.7),
2364 offset: Vector::ZERO,
2365 blur_radius: 2.0,
2366 },
2367 icon: palette.background.base.text.scale_alpha(0.8),
2368 };
2369
2370 match status {
2371 Status::Active { .. } => Style {
2372 container: container::Style::default(),
2373 vertical_rail: scrollbar,
2374 horizontal_rail: scrollbar,
2375 gap: None,
2376 auto_scroll,
2377 },
2378 Status::Hovered {
2379 is_horizontal_scrollbar_hovered,
2380 is_vertical_scrollbar_hovered,
2381 ..
2382 } => {
2383 let hovered_scrollbar = Rail {
2384 scroller: Scroller {
2385 background: palette.primary.strong.color.into(),
2386 ..scrollbar.scroller
2387 },
2388 ..scrollbar
2389 };
2390
2391 Style {
2392 container: container::Style::default(),
2393 vertical_rail: if is_vertical_scrollbar_hovered {
2394 hovered_scrollbar
2395 } else {
2396 scrollbar
2397 },
2398 horizontal_rail: if is_horizontal_scrollbar_hovered {
2399 hovered_scrollbar
2400 } else {
2401 scrollbar
2402 },
2403 gap: None,
2404 auto_scroll,
2405 }
2406 }
2407 Status::Dragged {
2408 is_horizontal_scrollbar_dragged,
2409 is_vertical_scrollbar_dragged,
2410 ..
2411 } => {
2412 let dragged_scrollbar = Rail {
2413 scroller: Scroller {
2414 background: palette.primary.base.color.into(),
2415 ..scrollbar.scroller
2416 },
2417 ..scrollbar
2418 };
2419
2420 Style {
2421 container: container::Style::default(),
2422 vertical_rail: if is_vertical_scrollbar_dragged {
2423 dragged_scrollbar
2424 } else {
2425 scrollbar
2426 },
2427 horizontal_rail: if is_horizontal_scrollbar_dragged {
2428 dragged_scrollbar
2429 } else {
2430 scrollbar
2431 },
2432 gap: None,
2433 auto_scroll,
2434 }
2435 }
2436 }
2437}