1use crate::container;
23use crate::core::border::{self, Border};
24use crate::core::keyboard;
25use crate::core::layout;
26use crate::core::mouse;
27use crate::core::overlay;
28use crate::core::renderer;
29use crate::core::time::{Duration, Instant};
30use crate::core::touch;
31use crate::core::widget;
32use crate::core::widget::operation::{self, Operation};
33use crate::core::widget::tree::{self, Tree};
34use crate::core::window;
35use crate::core::{
36 self, Background, Clipboard, Color, Element, Event, InputMethod, Layout,
37 Length, Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector,
38 Widget,
39};
40use crate::runtime::Action;
41use crate::runtime::task::{self, Task};
42
43pub use operation::scrollable::{AbsoluteOffset, RelativeOffset};
44
45#[allow(missing_debug_implementations)]
68pub struct Scrollable<
69 'a,
70 Message,
71 Theme = crate::Theme,
72 Renderer = crate::Renderer,
73> where
74 Theme: Catalog,
75 Renderer: core::Renderer,
76{
77 id: Option<Id>,
78 width: Length,
79 height: Length,
80 direction: Direction,
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: core::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 content: content.into(),
110 on_scroll: None,
111 class: Theme::default(),
112 last_status: None,
113 }
114 .validate()
115 }
116
117 fn validate(mut self) -> Self {
118 let size_hint = self.content.as_widget().size_hint();
119
120 debug_assert!(
121 self.direction.vertical().is_none() || !size_hint.height.is_fill(),
122 "scrollable content must not fill its vertical scrolling axis"
123 );
124
125 debug_assert!(
126 self.direction.horizontal().is_none() || !size_hint.width.is_fill(),
127 "scrollable content must not fill its horizontal scrolling axis"
128 );
129
130 if self.direction.horizontal().is_none() {
131 self.width = self.width.enclose(size_hint.width);
132 }
133
134 if self.direction.vertical().is_none() {
135 self.height = self.height.enclose(size_hint.height);
136 }
137
138 self
139 }
140
141 pub fn horizontal(self) -> Self {
143 self.direction(Direction::Horizontal(Scrollbar::default()))
144 }
145
146 pub fn direction(mut self, direction: impl Into<Direction>) -> Self {
148 self.direction = direction.into();
149 self.validate()
150 }
151
152 pub fn id(mut self, id: impl Into<Id>) -> Self {
154 self.id = Some(id.into());
155 self
156 }
157
158 pub fn width(mut self, width: impl Into<Length>) -> Self {
160 self.width = width.into();
161 self
162 }
163
164 pub fn height(mut self, height: impl Into<Length>) -> Self {
166 self.height = height.into();
167 self
168 }
169
170 pub fn on_scroll(mut self, f: impl Fn(Viewport) -> Message + 'a) -> Self {
174 self.on_scroll = Some(Box::new(f));
175 self
176 }
177
178 pub fn anchor_top(self) -> Self {
180 self.anchor_y(Anchor::Start)
181 }
182
183 pub fn anchor_bottom(self) -> Self {
185 self.anchor_y(Anchor::End)
186 }
187
188 pub fn anchor_left(self) -> Self {
190 self.anchor_x(Anchor::Start)
191 }
192
193 pub fn anchor_right(self) -> Self {
195 self.anchor_x(Anchor::End)
196 }
197
198 pub fn anchor_x(mut self, alignment: Anchor) -> Self {
200 match &mut self.direction {
201 Direction::Horizontal(horizontal)
202 | Direction::Both { horizontal, .. } => {
203 horizontal.alignment = alignment;
204 }
205 Direction::Vertical { .. } => {}
206 }
207
208 self
209 }
210
211 pub fn anchor_y(mut self, alignment: Anchor) -> Self {
213 match &mut self.direction {
214 Direction::Vertical(vertical)
215 | Direction::Both { vertical, .. } => {
216 vertical.alignment = alignment;
217 }
218 Direction::Horizontal { .. } => {}
219 }
220
221 self
222 }
223
224 pub fn spacing(mut self, new_spacing: impl Into<Pixels>) -> Self {
230 match &mut self.direction {
231 Direction::Horizontal(scrollbar)
232 | Direction::Vertical(scrollbar) => {
233 scrollbar.spacing = Some(new_spacing.into().0);
234 }
235 Direction::Both { .. } => {}
236 }
237
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 width(mut self, width: impl Into<Pixels>) -> Self {
350 self.width = width.into().0.max(0.0);
351 self
352 }
353
354 pub fn margin(mut self, margin: impl Into<Pixels>) -> Self {
356 self.margin = margin.into().0;
357 self
358 }
359
360 pub fn scroller_width(mut self, scroller_width: impl Into<Pixels>) -> Self {
362 self.scroller_width = scroller_width.into().0.max(0.0);
363 self
364 }
365
366 pub fn anchor(mut self, alignment: Anchor) -> Self {
368 self.alignment = alignment;
369 self
370 }
371
372 pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
378 self.spacing = Some(spacing.into().0);
379 self
380 }
381}
382
383#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
386pub enum Anchor {
387 #[default]
389 Start,
390 End,
392}
393
394impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
395 for Scrollable<'_, Message, Theme, Renderer>
396where
397 Theme: Catalog,
398 Renderer: core::Renderer,
399{
400 fn tag(&self) -> tree::Tag {
401 tree::Tag::of::<State>()
402 }
403
404 fn state(&self) -> tree::State {
405 tree::State::new(State::new())
406 }
407
408 fn children(&self) -> Vec<Tree> {
409 vec![Tree::new(&self.content)]
410 }
411
412 fn diff(&self, tree: &mut Tree) {
413 tree.diff_children(std::slice::from_ref(&self.content));
414 }
415
416 fn size(&self) -> Size<Length> {
417 Size {
418 width: self.width,
419 height: self.height,
420 }
421 }
422
423 fn layout(
424 &self,
425 tree: &mut Tree,
426 renderer: &Renderer,
427 limits: &layout::Limits,
428 ) -> layout::Node {
429 let mut layout = |right_padding, bottom_padding| {
430 layout::padded(
431 limits,
432 self.width,
433 self.height,
434 Padding {
435 right: right_padding,
436 bottom: bottom_padding,
437 ..Padding::ZERO
438 },
439 |limits| {
440 let child_limits = layout::Limits::new(
441 Size::new(limits.min().width, limits.min().height),
442 Size::new(
443 if self.direction.horizontal().is_some() {
444 f32::INFINITY
445 } else {
446 limits.max().width
447 },
448 if self.direction.vertical().is_some() {
449 f32::INFINITY
450 } else {
451 limits.max().height
452 },
453 ),
454 );
455
456 self.content.as_widget().layout(
457 &mut tree.children[0],
458 renderer,
459 &child_limits,
460 )
461 },
462 )
463 };
464
465 match self.direction {
466 Direction::Vertical(Scrollbar {
467 width,
468 margin,
469 spacing: Some(spacing),
470 ..
471 })
472 | Direction::Horizontal(Scrollbar {
473 width,
474 margin,
475 spacing: Some(spacing),
476 ..
477 }) => {
478 let is_vertical =
479 matches!(self.direction, Direction::Vertical(_));
480
481 let padding = width + margin * 2.0 + spacing;
482 let state = tree.state.downcast_mut::<State>();
483
484 let status_quo = layout(
485 if is_vertical && state.is_scrollbar_visible {
486 padding
487 } else {
488 0.0
489 },
490 if !is_vertical && state.is_scrollbar_visible {
491 padding
492 } else {
493 0.0
494 },
495 );
496
497 let is_scrollbar_visible = if is_vertical {
498 status_quo.children()[0].size().height
499 > status_quo.size().height
500 } else {
501 status_quo.children()[0].size().width
502 > status_quo.size().width
503 };
504
505 if state.is_scrollbar_visible == is_scrollbar_visible {
506 status_quo
507 } else {
508 log::trace!("Scrollbar status quo has changed");
509 state.is_scrollbar_visible = is_scrollbar_visible;
510
511 layout(
512 if is_vertical && state.is_scrollbar_visible {
513 padding
514 } else {
515 0.0
516 },
517 if !is_vertical && state.is_scrollbar_visible {
518 padding
519 } else {
520 0.0
521 },
522 )
523 }
524 }
525 _ => layout(0.0, 0.0),
526 }
527 }
528
529 fn operate(
530 &self,
531 tree: &mut Tree,
532 layout: Layout<'_>,
533 renderer: &Renderer,
534 operation: &mut dyn Operation,
535 ) {
536 let state = tree.state.downcast_mut::<State>();
537
538 let bounds = layout.bounds();
539 let content_layout = layout.children().next().unwrap();
540 let content_bounds = content_layout.bounds();
541 let translation =
542 state.translation(self.direction, bounds, content_bounds);
543
544 operation.scrollable(
545 self.id.as_ref().map(|id| &id.0),
546 bounds,
547 content_bounds,
548 translation,
549 state,
550 );
551
552 operation.container(
553 self.id.as_ref().map(|id| &id.0),
554 bounds,
555 &mut |operation| {
556 self.content.as_widget().operate(
557 &mut tree.children[0],
558 layout.children().next().unwrap(),
559 renderer,
560 operation,
561 );
562 },
563 );
564 }
565
566 fn update(
567 &mut self,
568 tree: &mut Tree,
569 event: &Event,
570 layout: Layout<'_>,
571 cursor: mouse::Cursor,
572 renderer: &Renderer,
573 clipboard: &mut dyn Clipboard,
574 shell: &mut Shell<'_, Message>,
575 _viewport: &Rectangle,
576 ) {
577 let state = tree.state.downcast_mut::<State>();
578 let bounds = layout.bounds();
579 let cursor_over_scrollable = cursor.position_over(bounds);
580
581 let content = layout.children().next().unwrap();
582 let content_bounds = content.bounds();
583
584 let scrollbars =
585 Scrollbars::new(state, self.direction, bounds, content_bounds);
586
587 let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
588 scrollbars.is_mouse_over(cursor);
589
590 let last_offsets = (state.offset_x, state.offset_y);
591
592 if let Some(last_scrolled) = state.last_scrolled {
593 let clear_transaction = match event {
594 Event::Mouse(
595 mouse::Event::ButtonPressed(_)
596 | mouse::Event::ButtonReleased(_)
597 | mouse::Event::CursorLeft,
598 ) => true,
599 Event::Mouse(mouse::Event::CursorMoved { .. }) => {
600 last_scrolled.elapsed() > Duration::from_millis(100)
601 }
602 _ => last_scrolled.elapsed() > Duration::from_millis(1500),
603 };
604
605 if clear_transaction {
606 state.last_scrolled = None;
607 }
608 }
609
610 let mut update = || {
611 if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at {
612 match event {
613 Event::Mouse(mouse::Event::CursorMoved { .. })
614 | Event::Touch(touch::Event::FingerMoved { .. }) => {
615 if let Some(scrollbar) = scrollbars.y {
616 let Some(cursor_position) =
617 cursor.land().position()
618 else {
619 return;
620 };
621
622 state.scroll_y_to(
623 scrollbar.scroll_percentage_y(
624 scroller_grabbed_at,
625 cursor_position,
626 ),
627 bounds,
628 content_bounds,
629 );
630
631 let _ = notify_scroll(
632 state,
633 &self.on_scroll,
634 bounds,
635 content_bounds,
636 shell,
637 );
638
639 shell.capture_event();
640 }
641 }
642 _ => {}
643 }
644 } else if mouse_over_y_scrollbar {
645 match event {
646 Event::Mouse(mouse::Event::ButtonPressed(
647 mouse::Button::Left,
648 ))
649 | Event::Touch(touch::Event::FingerPressed { .. }) => {
650 let Some(cursor_position) = cursor.position() else {
651 return;
652 };
653
654 if let (Some(scroller_grabbed_at), Some(scrollbar)) = (
655 scrollbars.grab_y_scroller(cursor_position),
656 scrollbars.y,
657 ) {
658 state.scroll_y_to(
659 scrollbar.scroll_percentage_y(
660 scroller_grabbed_at,
661 cursor_position,
662 ),
663 bounds,
664 content_bounds,
665 );
666
667 state.y_scroller_grabbed_at =
668 Some(scroller_grabbed_at);
669
670 let _ = notify_scroll(
671 state,
672 &self.on_scroll,
673 bounds,
674 content_bounds,
675 shell,
676 );
677 }
678
679 shell.capture_event();
680 }
681 _ => {}
682 }
683 }
684
685 if let Some(scroller_grabbed_at) = state.x_scroller_grabbed_at {
686 match event {
687 Event::Mouse(mouse::Event::CursorMoved { .. })
688 | Event::Touch(touch::Event::FingerMoved { .. }) => {
689 let Some(cursor_position) = cursor.land().position()
690 else {
691 return;
692 };
693
694 if let Some(scrollbar) = scrollbars.x {
695 state.scroll_x_to(
696 scrollbar.scroll_percentage_x(
697 scroller_grabbed_at,
698 cursor_position,
699 ),
700 bounds,
701 content_bounds,
702 );
703
704 let _ = notify_scroll(
705 state,
706 &self.on_scroll,
707 bounds,
708 content_bounds,
709 shell,
710 );
711 }
712
713 shell.capture_event();
714 }
715 _ => {}
716 }
717 } else if mouse_over_x_scrollbar {
718 match event {
719 Event::Mouse(mouse::Event::ButtonPressed(
720 mouse::Button::Left,
721 ))
722 | Event::Touch(touch::Event::FingerPressed { .. }) => {
723 let Some(cursor_position) = cursor.position() else {
724 return;
725 };
726
727 if let (Some(scroller_grabbed_at), Some(scrollbar)) = (
728 scrollbars.grab_x_scroller(cursor_position),
729 scrollbars.x,
730 ) {
731 state.scroll_x_to(
732 scrollbar.scroll_percentage_x(
733 scroller_grabbed_at,
734 cursor_position,
735 ),
736 bounds,
737 content_bounds,
738 );
739
740 state.x_scroller_grabbed_at =
741 Some(scroller_grabbed_at);
742
743 let _ = notify_scroll(
744 state,
745 &self.on_scroll,
746 bounds,
747 content_bounds,
748 shell,
749 );
750
751 shell.capture_event();
752 }
753 }
754 _ => {}
755 }
756 }
757
758 if state.last_scrolled.is_none()
759 || !matches!(
760 event,
761 Event::Mouse(mouse::Event::WheelScrolled { .. })
762 )
763 {
764 let cursor = match cursor_over_scrollable {
765 Some(cursor_position)
766 if !(mouse_over_x_scrollbar
767 || mouse_over_y_scrollbar) =>
768 {
769 mouse::Cursor::Available(
770 cursor_position
771 + state.translation(
772 self.direction,
773 bounds,
774 content_bounds,
775 ),
776 )
777 }
778 _ => mouse::Cursor::Unavailable,
779 };
780
781 let had_input_method = shell.input_method().is_enabled();
782
783 let translation =
784 state.translation(self.direction, bounds, content_bounds);
785
786 self.content.as_widget_mut().update(
787 &mut tree.children[0],
788 event,
789 content,
790 cursor,
791 renderer,
792 clipboard,
793 shell,
794 &Rectangle {
795 y: bounds.y + translation.y,
796 x: bounds.x + translation.x,
797 ..bounds
798 },
799 );
800
801 if !had_input_method {
802 if let InputMethod::Enabled { position, .. } =
803 shell.input_method_mut()
804 {
805 *position = *position - translation;
806 }
807 }
808 };
809
810 if matches!(
811 event,
812 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
813 | Event::Touch(
814 touch::Event::FingerLifted { .. }
815 | touch::Event::FingerLost { .. }
816 )
817 ) {
818 state.scroll_area_touched_at = None;
819 state.x_scroller_grabbed_at = None;
820 state.y_scroller_grabbed_at = None;
821
822 return;
823 }
824
825 if shell.is_event_captured() {
826 return;
827 }
828
829 if let Event::Keyboard(keyboard::Event::ModifiersChanged(
830 modifiers,
831 )) = event
832 {
833 state.keyboard_modifiers = *modifiers;
834
835 return;
836 }
837
838 match event {
839 Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
840 if cursor_over_scrollable.is_none() {
841 return;
842 }
843
844 let delta = match *delta {
845 mouse::ScrollDelta::Lines { x, y } => {
846 let is_shift_pressed =
847 state.keyboard_modifiers.shift();
848
849 let (x, y) = if cfg!(target_os = "macos")
851 && is_shift_pressed
852 {
853 (y, x)
854 } else {
855 (x, y)
856 };
857
858 let movement = if !is_shift_pressed {
859 Vector::new(x, y)
860 } else {
861 Vector::new(y, x)
862 };
863
864 -movement * 60.0
866 }
867 mouse::ScrollDelta::Pixels { x, y } => {
868 -Vector::new(x, y)
869 }
870 };
871
872 state.scroll(
873 self.direction.align(delta),
874 bounds,
875 content_bounds,
876 );
877
878 let has_scrolled = notify_scroll(
879 state,
880 &self.on_scroll,
881 bounds,
882 content_bounds,
883 shell,
884 );
885
886 let in_transaction = state.last_scrolled.is_some();
887
888 if has_scrolled || in_transaction {
889 shell.capture_event();
890 }
891 }
892 Event::Touch(event)
893 if state.scroll_area_touched_at.is_some()
894 || !mouse_over_y_scrollbar
895 && !mouse_over_x_scrollbar =>
896 {
897 match event {
898 touch::Event::FingerPressed { .. } => {
899 let Some(cursor_position) = cursor.position()
900 else {
901 return;
902 };
903
904 state.scroll_area_touched_at =
905 Some(cursor_position);
906 }
907 touch::Event::FingerMoved { .. } => {
908 if let Some(scroll_box_touched_at) =
909 state.scroll_area_touched_at
910 {
911 let Some(cursor_position) = cursor.position()
912 else {
913 return;
914 };
915
916 let delta = Vector::new(
917 scroll_box_touched_at.x - cursor_position.x,
918 scroll_box_touched_at.y - cursor_position.y,
919 );
920
921 state.scroll(
922 self.direction.align(delta),
923 bounds,
924 content_bounds,
925 );
926
927 state.scroll_area_touched_at =
928 Some(cursor_position);
929
930 let _ = notify_scroll(
932 state,
933 &self.on_scroll,
934 bounds,
935 content_bounds,
936 shell,
937 );
938 }
939 }
940 _ => {}
941 }
942
943 shell.capture_event();
944 }
945 Event::Window(window::Event::RedrawRequested(_)) => {
946 let _ = notify_viewport(
947 state,
948 &self.on_scroll,
949 bounds,
950 content_bounds,
951 shell,
952 );
953 }
954 _ => {}
955 }
956 };
957
958 update();
959
960 let status = if state.y_scroller_grabbed_at.is_some()
961 || state.x_scroller_grabbed_at.is_some()
962 {
963 Status::Dragged {
964 is_horizontal_scrollbar_dragged: state
965 .x_scroller_grabbed_at
966 .is_some(),
967 is_vertical_scrollbar_dragged: state
968 .y_scroller_grabbed_at
969 .is_some(),
970 is_horizontal_scrollbar_disabled: scrollbars.is_x_disabled(),
971 is_vertical_scrollbar_disabled: scrollbars.is_y_disabled(),
972 }
973 } else if cursor_over_scrollable.is_some() {
974 Status::Hovered {
975 is_horizontal_scrollbar_hovered: mouse_over_x_scrollbar,
976 is_vertical_scrollbar_hovered: mouse_over_y_scrollbar,
977 is_horizontal_scrollbar_disabled: scrollbars.is_x_disabled(),
978 is_vertical_scrollbar_disabled: scrollbars.is_y_disabled(),
979 }
980 } else {
981 Status::Active {
982 is_horizontal_scrollbar_disabled: scrollbars.is_x_disabled(),
983 is_vertical_scrollbar_disabled: scrollbars.is_y_disabled(),
984 }
985 };
986
987 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
988 self.last_status = Some(status);
989 }
990
991 if last_offsets != (state.offset_x, state.offset_y)
992 || self
993 .last_status
994 .is_some_and(|last_status| last_status != status)
995 {
996 shell.request_redraw();
997 }
998 }
999
1000 fn draw(
1001 &self,
1002 tree: &Tree,
1003 renderer: &mut Renderer,
1004 theme: &Theme,
1005 defaults: &renderer::Style,
1006 layout: Layout<'_>,
1007 cursor: mouse::Cursor,
1008 viewport: &Rectangle,
1009 ) {
1010 let state = tree.state.downcast_ref::<State>();
1011
1012 let bounds = layout.bounds();
1013 let content_layout = layout.children().next().unwrap();
1014 let content_bounds = content_layout.bounds();
1015
1016 let Some(visible_bounds) = bounds.intersection(viewport) else {
1017 return;
1018 };
1019
1020 let scrollbars =
1021 Scrollbars::new(state, self.direction, bounds, content_bounds);
1022
1023 let cursor_over_scrollable = cursor.position_over(bounds);
1024 let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
1025 scrollbars.is_mouse_over(cursor);
1026
1027 let translation =
1028 state.translation(self.direction, bounds, content_bounds);
1029
1030 let cursor = match cursor_over_scrollable {
1031 Some(cursor_position)
1032 if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) =>
1033 {
1034 mouse::Cursor::Available(cursor_position + translation)
1035 }
1036 _ => mouse::Cursor::Unavailable,
1037 };
1038
1039 let style = theme.style(
1040 &self.class,
1041 self.last_status.unwrap_or(Status::Active {
1042 is_horizontal_scrollbar_disabled: false,
1043 is_vertical_scrollbar_disabled: false,
1044 }),
1045 );
1046
1047 container::draw_background(renderer, &style.container, layout.bounds());
1048
1049 if scrollbars.active() {
1051 renderer.with_layer(visible_bounds, |renderer| {
1052 renderer.with_translation(
1053 Vector::new(-translation.x, -translation.y),
1054 |renderer| {
1055 self.content.as_widget().draw(
1056 &tree.children[0],
1057 renderer,
1058 theme,
1059 defaults,
1060 content_layout,
1061 cursor,
1062 &Rectangle {
1063 y: visible_bounds.y + translation.y,
1064 x: visible_bounds.x + translation.x,
1065 ..visible_bounds
1066 },
1067 );
1068 },
1069 );
1070 });
1071
1072 let draw_scrollbar =
1073 |renderer: &mut Renderer,
1074 style: Rail,
1075 scrollbar: &internals::Scrollbar| {
1076 if scrollbar.bounds.width > 0.0
1077 && scrollbar.bounds.height > 0.0
1078 && (style.background.is_some()
1079 || (style.border.color != Color::TRANSPARENT
1080 && style.border.width > 0.0))
1081 {
1082 renderer.fill_quad(
1083 renderer::Quad {
1084 bounds: scrollbar.bounds,
1085 border: style.border,
1086 ..renderer::Quad::default()
1087 },
1088 style.background.unwrap_or(Background::Color(
1089 Color::TRANSPARENT,
1090 )),
1091 );
1092 }
1093
1094 if let Some(scroller) = scrollbar.scroller {
1095 if scroller.bounds.width > 0.0
1096 && scroller.bounds.height > 0.0
1097 && (style.scroller.color != Color::TRANSPARENT
1098 || (style.scroller.border.color
1099 != Color::TRANSPARENT
1100 && style.scroller.border.width > 0.0))
1101 {
1102 renderer.fill_quad(
1103 renderer::Quad {
1104 bounds: scroller.bounds,
1105 border: style.scroller.border,
1106 ..renderer::Quad::default()
1107 },
1108 style.scroller.color,
1109 );
1110 }
1111 }
1112 };
1113
1114 renderer.with_layer(
1115 Rectangle {
1116 width: (visible_bounds.width + 2.0).min(viewport.width),
1117 height: (visible_bounds.height + 2.0).min(viewport.height),
1118 ..visible_bounds
1119 },
1120 |renderer| {
1121 if let Some(scrollbar) = scrollbars.y {
1122 draw_scrollbar(
1123 renderer,
1124 style.vertical_rail,
1125 &scrollbar,
1126 );
1127 }
1128
1129 if let Some(scrollbar) = scrollbars.x {
1130 draw_scrollbar(
1131 renderer,
1132 style.horizontal_rail,
1133 &scrollbar,
1134 );
1135 }
1136
1137 if let (Some(x), Some(y)) = (scrollbars.x, scrollbars.y) {
1138 let background =
1139 style.gap.or(style.container.background);
1140
1141 if let Some(background) = background {
1142 renderer.fill_quad(
1143 renderer::Quad {
1144 bounds: Rectangle {
1145 x: y.bounds.x,
1146 y: x.bounds.y,
1147 width: y.bounds.width,
1148 height: x.bounds.height,
1149 },
1150 ..renderer::Quad::default()
1151 },
1152 background,
1153 );
1154 }
1155 }
1156 },
1157 );
1158 } else {
1159 self.content.as_widget().draw(
1160 &tree.children[0],
1161 renderer,
1162 theme,
1163 defaults,
1164 content_layout,
1165 cursor,
1166 &Rectangle {
1167 x: visible_bounds.x + translation.x,
1168 y: visible_bounds.y + translation.y,
1169 ..visible_bounds
1170 },
1171 );
1172 }
1173 }
1174
1175 fn mouse_interaction(
1176 &self,
1177 tree: &Tree,
1178 layout: Layout<'_>,
1179 cursor: mouse::Cursor,
1180 _viewport: &Rectangle,
1181 renderer: &Renderer,
1182 ) -> mouse::Interaction {
1183 let state = tree.state.downcast_ref::<State>();
1184 let bounds = layout.bounds();
1185 let cursor_over_scrollable = cursor.position_over(bounds);
1186
1187 let content_layout = layout.children().next().unwrap();
1188 let content_bounds = content_layout.bounds();
1189
1190 let scrollbars =
1191 Scrollbars::new(state, self.direction, bounds, content_bounds);
1192
1193 let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
1194 scrollbars.is_mouse_over(cursor);
1195
1196 if (mouse_over_x_scrollbar || mouse_over_y_scrollbar)
1197 || state.scrollers_grabbed()
1198 {
1199 mouse::Interaction::None
1200 } else {
1201 let translation =
1202 state.translation(self.direction, bounds, content_bounds);
1203
1204 let cursor = match cursor_over_scrollable {
1205 Some(cursor_position)
1206 if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) =>
1207 {
1208 mouse::Cursor::Available(cursor_position + translation)
1209 }
1210 _ => mouse::Cursor::Unavailable,
1211 };
1212
1213 self.content.as_widget().mouse_interaction(
1214 &tree.children[0],
1215 content_layout,
1216 cursor,
1217 &Rectangle {
1218 y: bounds.y + translation.y,
1219 x: bounds.x + translation.x,
1220 ..bounds
1221 },
1222 renderer,
1223 )
1224 }
1225 }
1226
1227 fn overlay<'b>(
1228 &'b mut self,
1229 tree: &'b mut Tree,
1230 layout: Layout<'b>,
1231 renderer: &Renderer,
1232 viewport: &Rectangle,
1233 translation: Vector,
1234 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
1235 let bounds = layout.bounds();
1236 let content_layout = layout.children().next().unwrap();
1237 let content_bounds = content_layout.bounds();
1238 let visible_bounds = bounds.intersection(viewport).unwrap_or(*viewport);
1239
1240 let offset = tree.state.downcast_ref::<State>().translation(
1241 self.direction,
1242 bounds,
1243 content_bounds,
1244 );
1245
1246 self.content.as_widget_mut().overlay(
1247 &mut tree.children[0],
1248 layout.children().next().unwrap(),
1249 renderer,
1250 &visible_bounds,
1251 translation - offset,
1252 )
1253 }
1254}
1255
1256impl<'a, Message, Theme, Renderer>
1257 From<Scrollable<'a, Message, Theme, Renderer>>
1258 for Element<'a, Message, Theme, Renderer>
1259where
1260 Message: 'a,
1261 Theme: 'a + Catalog,
1262 Renderer: 'a + core::Renderer,
1263{
1264 fn from(
1265 text_input: Scrollable<'a, Message, Theme, Renderer>,
1266 ) -> Element<'a, Message, Theme, Renderer> {
1267 Element::new(text_input)
1268 }
1269}
1270
1271#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1273pub struct Id(widget::Id);
1274
1275impl Id {
1276 pub fn new(id: impl Into<std::borrow::Cow<'static, str>>) -> Self {
1278 Self(widget::Id::new(id))
1279 }
1280
1281 pub fn unique() -> Self {
1285 Self(widget::Id::unique())
1286 }
1287}
1288
1289impl From<Id> for widget::Id {
1290 fn from(id: Id) -> Self {
1291 id.0
1292 }
1293}
1294
1295impl From<&'static str> for Id {
1296 fn from(id: &'static str) -> Self {
1297 Self::new(id)
1298 }
1299}
1300
1301pub fn snap_to<T>(id: impl Into<Id>, offset: RelativeOffset) -> Task<T> {
1304 task::effect(Action::widget(operation::scrollable::snap_to(
1305 id.into().0,
1306 offset,
1307 )))
1308}
1309
1310pub fn scroll_to<T>(id: impl Into<Id>, offset: AbsoluteOffset) -> Task<T> {
1313 task::effect(Action::widget(operation::scrollable::scroll_to(
1314 id.into().0,
1315 offset,
1316 )))
1317}
1318
1319pub fn scroll_by<T>(id: impl Into<Id>, offset: AbsoluteOffset) -> Task<T> {
1322 task::effect(Action::widget(operation::scrollable::scroll_by(
1323 id.into().0,
1324 offset,
1325 )))
1326}
1327
1328fn notify_scroll<Message>(
1329 state: &mut State,
1330 on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>,
1331 bounds: Rectangle,
1332 content_bounds: Rectangle,
1333 shell: &mut Shell<'_, Message>,
1334) -> bool {
1335 if notify_viewport(state, on_scroll, bounds, content_bounds, shell) {
1336 state.last_scrolled = Some(Instant::now());
1337
1338 true
1339 } else {
1340 false
1341 }
1342}
1343
1344fn notify_viewport<Message>(
1345 state: &mut State,
1346 on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>,
1347 bounds: Rectangle,
1348 content_bounds: Rectangle,
1349 shell: &mut Shell<'_, Message>,
1350) -> bool {
1351 if content_bounds.width <= bounds.width
1352 && content_bounds.height <= bounds.height
1353 {
1354 return false;
1355 }
1356
1357 let viewport = Viewport {
1358 offset_x: state.offset_x,
1359 offset_y: state.offset_y,
1360 bounds,
1361 content_bounds,
1362 };
1363
1364 if let Some(last_notified) = state.last_notified {
1366 let last_relative_offset = last_notified.relative_offset();
1367 let current_relative_offset = viewport.relative_offset();
1368
1369 let last_absolute_offset = last_notified.absolute_offset();
1370 let current_absolute_offset = viewport.absolute_offset();
1371
1372 let unchanged = |a: f32, b: f32| {
1373 (a - b).abs() <= f32::EPSILON || (a.is_nan() && b.is_nan())
1374 };
1375
1376 if last_notified.bounds == bounds
1377 && last_notified.content_bounds == content_bounds
1378 && unchanged(last_relative_offset.x, current_relative_offset.x)
1379 && unchanged(last_relative_offset.y, current_relative_offset.y)
1380 && unchanged(last_absolute_offset.x, current_absolute_offset.x)
1381 && unchanged(last_absolute_offset.y, current_absolute_offset.y)
1382 {
1383 return false;
1384 }
1385 }
1386
1387 state.last_notified = Some(viewport);
1388
1389 if let Some(on_scroll) = on_scroll {
1390 shell.publish(on_scroll(viewport));
1391 }
1392
1393 true
1394}
1395
1396#[derive(Debug, Clone, Copy)]
1397struct State {
1398 scroll_area_touched_at: Option<Point>,
1399 offset_y: Offset,
1400 y_scroller_grabbed_at: Option<f32>,
1401 offset_x: Offset,
1402 x_scroller_grabbed_at: Option<f32>,
1403 keyboard_modifiers: keyboard::Modifiers,
1404 last_notified: Option<Viewport>,
1405 last_scrolled: Option<Instant>,
1406 is_scrollbar_visible: bool,
1407}
1408
1409impl Default for State {
1410 fn default() -> Self {
1411 Self {
1412 scroll_area_touched_at: None,
1413 offset_y: Offset::Absolute(0.0),
1414 y_scroller_grabbed_at: None,
1415 offset_x: Offset::Absolute(0.0),
1416 x_scroller_grabbed_at: None,
1417 keyboard_modifiers: keyboard::Modifiers::default(),
1418 last_notified: None,
1419 last_scrolled: None,
1420 is_scrollbar_visible: true,
1421 }
1422 }
1423}
1424
1425impl operation::Scrollable for State {
1426 fn snap_to(&mut self, offset: RelativeOffset) {
1427 State::snap_to(self, offset);
1428 }
1429
1430 fn scroll_to(&mut self, offset: AbsoluteOffset) {
1431 State::scroll_to(self, offset);
1432 }
1433
1434 fn scroll_by(
1435 &mut self,
1436 offset: AbsoluteOffset,
1437 bounds: Rectangle,
1438 content_bounds: Rectangle,
1439 ) {
1440 State::scroll_by(self, offset, bounds, content_bounds);
1441 }
1442}
1443
1444#[derive(Debug, Clone, Copy, PartialEq)]
1445enum Offset {
1446 Absolute(f32),
1447 Relative(f32),
1448}
1449
1450impl Offset {
1451 fn absolute(self, viewport: f32, content: f32) -> f32 {
1452 match self {
1453 Offset::Absolute(absolute) => {
1454 absolute.min((content - viewport).max(0.0))
1455 }
1456 Offset::Relative(percentage) => {
1457 ((content - viewport) * percentage).max(0.0)
1458 }
1459 }
1460 }
1461
1462 fn translation(
1463 self,
1464 viewport: f32,
1465 content: f32,
1466 alignment: Anchor,
1467 ) -> f32 {
1468 let offset = self.absolute(viewport, content);
1469
1470 match alignment {
1471 Anchor::Start => offset,
1472 Anchor::End => ((content - viewport).max(0.0) - offset).max(0.0),
1473 }
1474 }
1475}
1476
1477#[derive(Debug, Clone, Copy)]
1479pub struct Viewport {
1480 offset_x: Offset,
1481 offset_y: Offset,
1482 bounds: Rectangle,
1483 content_bounds: Rectangle,
1484}
1485
1486impl Viewport {
1487 pub fn absolute_offset(&self) -> AbsoluteOffset {
1489 let x = self
1490 .offset_x
1491 .absolute(self.bounds.width, self.content_bounds.width);
1492 let y = self
1493 .offset_y
1494 .absolute(self.bounds.height, self.content_bounds.height);
1495
1496 AbsoluteOffset { x, y }
1497 }
1498
1499 pub fn absolute_offset_reversed(&self) -> AbsoluteOffset {
1505 let AbsoluteOffset { x, y } = self.absolute_offset();
1506
1507 AbsoluteOffset {
1508 x: (self.content_bounds.width - self.bounds.width).max(0.0) - x,
1509 y: (self.content_bounds.height - self.bounds.height).max(0.0) - y,
1510 }
1511 }
1512
1513 pub fn relative_offset(&self) -> RelativeOffset {
1515 let AbsoluteOffset { x, y } = self.absolute_offset();
1516
1517 let x = x / (self.content_bounds.width - self.bounds.width);
1518 let y = y / (self.content_bounds.height - self.bounds.height);
1519
1520 RelativeOffset { x, y }
1521 }
1522
1523 pub fn bounds(&self) -> Rectangle {
1525 self.bounds
1526 }
1527
1528 pub fn content_bounds(&self) -> Rectangle {
1530 self.content_bounds
1531 }
1532}
1533
1534impl State {
1535 pub fn new() -> Self {
1537 State::default()
1538 }
1539
1540 pub fn scroll(
1543 &mut self,
1544 delta: Vector<f32>,
1545 bounds: Rectangle,
1546 content_bounds: Rectangle,
1547 ) {
1548 if bounds.height < content_bounds.height {
1549 self.offset_y = Offset::Absolute(
1550 (self.offset_y.absolute(bounds.height, content_bounds.height)
1551 + delta.y)
1552 .clamp(0.0, content_bounds.height - bounds.height),
1553 );
1554 }
1555
1556 if bounds.width < content_bounds.width {
1557 self.offset_x = Offset::Absolute(
1558 (self.offset_x.absolute(bounds.width, content_bounds.width)
1559 + delta.x)
1560 .clamp(0.0, content_bounds.width - bounds.width),
1561 );
1562 }
1563 }
1564
1565 pub fn scroll_y_to(
1570 &mut self,
1571 percentage: f32,
1572 bounds: Rectangle,
1573 content_bounds: Rectangle,
1574 ) {
1575 self.offset_y = Offset::Relative(percentage.clamp(0.0, 1.0));
1576 self.unsnap(bounds, content_bounds);
1577 }
1578
1579 pub fn scroll_x_to(
1584 &mut self,
1585 percentage: f32,
1586 bounds: Rectangle,
1587 content_bounds: Rectangle,
1588 ) {
1589 self.offset_x = Offset::Relative(percentage.clamp(0.0, 1.0));
1590 self.unsnap(bounds, content_bounds);
1591 }
1592
1593 pub fn snap_to(&mut self, offset: RelativeOffset) {
1595 self.offset_x = Offset::Relative(offset.x.clamp(0.0, 1.0));
1596 self.offset_y = Offset::Relative(offset.y.clamp(0.0, 1.0));
1597 }
1598
1599 pub fn scroll_to(&mut self, offset: AbsoluteOffset) {
1601 self.offset_x = Offset::Absolute(offset.x.max(0.0));
1602 self.offset_y = Offset::Absolute(offset.y.max(0.0));
1603 }
1604
1605 pub fn scroll_by(
1607 &mut self,
1608 offset: AbsoluteOffset,
1609 bounds: Rectangle,
1610 content_bounds: Rectangle,
1611 ) {
1612 self.scroll(Vector::new(offset.x, offset.y), bounds, content_bounds);
1613 }
1614
1615 pub fn unsnap(&mut self, bounds: Rectangle, content_bounds: Rectangle) {
1618 self.offset_x = Offset::Absolute(
1619 self.offset_x.absolute(bounds.width, content_bounds.width),
1620 );
1621 self.offset_y = Offset::Absolute(
1622 self.offset_y.absolute(bounds.height, content_bounds.height),
1623 );
1624 }
1625
1626 fn translation(
1629 &self,
1630 direction: Direction,
1631 bounds: Rectangle,
1632 content_bounds: Rectangle,
1633 ) -> Vector {
1634 Vector::new(
1635 if let Some(horizontal) = direction.horizontal() {
1636 self.offset_x.translation(
1637 bounds.width,
1638 content_bounds.width,
1639 horizontal.alignment,
1640 )
1641 } else {
1642 0.0
1643 },
1644 if let Some(vertical) = direction.vertical() {
1645 self.offset_y.translation(
1646 bounds.height,
1647 content_bounds.height,
1648 vertical.alignment,
1649 )
1650 } else {
1651 0.0
1652 },
1653 )
1654 }
1655
1656 pub fn scrollers_grabbed(&self) -> bool {
1658 self.x_scroller_grabbed_at.is_some()
1659 || self.y_scroller_grabbed_at.is_some()
1660 }
1661}
1662
1663#[derive(Debug)]
1664struct Scrollbars {
1666 y: Option<internals::Scrollbar>,
1667 x: Option<internals::Scrollbar>,
1668}
1669
1670impl Scrollbars {
1671 fn new(
1673 state: &State,
1674 direction: Direction,
1675 bounds: Rectangle,
1676 content_bounds: Rectangle,
1677 ) -> Self {
1678 let translation = state.translation(direction, bounds, content_bounds);
1679
1680 let show_scrollbar_x = direction
1681 .horizontal()
1682 .filter(|_scrollbar| content_bounds.width > bounds.width);
1683
1684 let show_scrollbar_y = direction
1685 .vertical()
1686 .filter(|_scrollbar| content_bounds.height > bounds.height);
1687
1688 let y_scrollbar = if let Some(vertical) = show_scrollbar_y {
1689 let Scrollbar {
1690 width,
1691 margin,
1692 scroller_width,
1693 ..
1694 } = *vertical;
1695
1696 let x_scrollbar_height = show_scrollbar_x
1699 .map_or(0.0, |h| h.width.max(h.scroller_width) + h.margin);
1700
1701 let total_scrollbar_width =
1702 width.max(scroller_width) + 2.0 * margin;
1703
1704 let total_scrollbar_bounds = Rectangle {
1706 x: bounds.x + bounds.width - total_scrollbar_width,
1707 y: bounds.y,
1708 width: total_scrollbar_width,
1709 height: (bounds.height - x_scrollbar_height).max(0.0),
1710 };
1711
1712 let scrollbar_bounds = Rectangle {
1714 x: bounds.x + bounds.width
1715 - total_scrollbar_width / 2.0
1716 - width / 2.0,
1717 y: bounds.y,
1718 width,
1719 height: (bounds.height - x_scrollbar_height).max(0.0),
1720 };
1721
1722 let ratio = bounds.height / content_bounds.height;
1723
1724 let scroller = if ratio >= 1.0 {
1725 None
1726 } else {
1727 let scroller_height =
1729 (scrollbar_bounds.height * ratio).max(2.0);
1730 let scroller_offset =
1731 translation.y * ratio * scrollbar_bounds.height
1732 / bounds.height;
1733
1734 let scroller_bounds = Rectangle {
1735 x: bounds.x + bounds.width
1736 - total_scrollbar_width / 2.0
1737 - scroller_width / 2.0,
1738 y: (scrollbar_bounds.y + scroller_offset).max(0.0),
1739 width: scroller_width,
1740 height: scroller_height,
1741 };
1742
1743 Some(internals::Scroller {
1744 bounds: scroller_bounds,
1745 })
1746 };
1747
1748 Some(internals::Scrollbar {
1749 total_bounds: total_scrollbar_bounds,
1750 bounds: scrollbar_bounds,
1751 scroller,
1752 alignment: vertical.alignment,
1753 disabled: content_bounds.height <= bounds.height,
1754 })
1755 } else {
1756 None
1757 };
1758
1759 let x_scrollbar = if let Some(horizontal) = show_scrollbar_x {
1760 let Scrollbar {
1761 width,
1762 margin,
1763 scroller_width,
1764 ..
1765 } = *horizontal;
1766
1767 let scrollbar_y_width = y_scrollbar
1770 .map_or(0.0, |scrollbar| scrollbar.total_bounds.width);
1771
1772 let total_scrollbar_height =
1773 width.max(scroller_width) + 2.0 * margin;
1774
1775 let total_scrollbar_bounds = Rectangle {
1777 x: bounds.x,
1778 y: bounds.y + bounds.height - total_scrollbar_height,
1779 width: (bounds.width - scrollbar_y_width).max(0.0),
1780 height: total_scrollbar_height,
1781 };
1782
1783 let scrollbar_bounds = Rectangle {
1785 x: bounds.x,
1786 y: bounds.y + bounds.height
1787 - total_scrollbar_height / 2.0
1788 - width / 2.0,
1789 width: (bounds.width - scrollbar_y_width).max(0.0),
1790 height: width,
1791 };
1792
1793 let ratio = bounds.width / content_bounds.width;
1794
1795 let scroller = if ratio >= 1.0 {
1796 None
1797 } else {
1798 let scroller_length = (scrollbar_bounds.width * ratio).max(2.0);
1800 let scroller_offset =
1801 translation.x * ratio * scrollbar_bounds.width
1802 / bounds.width;
1803
1804 let scroller_bounds = Rectangle {
1805 x: (scrollbar_bounds.x + scroller_offset).max(0.0),
1806 y: bounds.y + bounds.height
1807 - total_scrollbar_height / 2.0
1808 - scroller_width / 2.0,
1809 width: scroller_length,
1810 height: scroller_width,
1811 };
1812
1813 Some(internals::Scroller {
1814 bounds: scroller_bounds,
1815 })
1816 };
1817
1818 Some(internals::Scrollbar {
1819 total_bounds: total_scrollbar_bounds,
1820 bounds: scrollbar_bounds,
1821 scroller,
1822 alignment: horizontal.alignment,
1823 disabled: content_bounds.width <= bounds.width,
1824 })
1825 } else {
1826 None
1827 };
1828
1829 Self {
1830 y: y_scrollbar,
1831 x: x_scrollbar,
1832 }
1833 }
1834
1835 fn is_mouse_over(&self, cursor: mouse::Cursor) -> (bool, bool) {
1836 if let Some(cursor_position) = cursor.position() {
1837 (
1838 self.y
1839 .as_ref()
1840 .map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
1841 .unwrap_or(false),
1842 self.x
1843 .as_ref()
1844 .map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
1845 .unwrap_or(false),
1846 )
1847 } else {
1848 (false, false)
1849 }
1850 }
1851
1852 fn is_y_disabled(&self) -> bool {
1853 self.y.map(|y| y.disabled).unwrap_or(false)
1854 }
1855
1856 fn is_x_disabled(&self) -> bool {
1857 self.x.map(|x| x.disabled).unwrap_or(false)
1858 }
1859
1860 fn grab_y_scroller(&self, cursor_position: Point) -> Option<f32> {
1861 let scrollbar = self.y?;
1862 let scroller = scrollbar.scroller?;
1863
1864 if scrollbar.total_bounds.contains(cursor_position) {
1865 Some(if scroller.bounds.contains(cursor_position) {
1866 (cursor_position.y - scroller.bounds.y) / scroller.bounds.height
1867 } else {
1868 0.5
1869 })
1870 } else {
1871 None
1872 }
1873 }
1874
1875 fn grab_x_scroller(&self, cursor_position: Point) -> Option<f32> {
1876 let scrollbar = self.x?;
1877 let scroller = scrollbar.scroller?;
1878
1879 if scrollbar.total_bounds.contains(cursor_position) {
1880 Some(if scroller.bounds.contains(cursor_position) {
1881 (cursor_position.x - scroller.bounds.x) / scroller.bounds.width
1882 } else {
1883 0.5
1884 })
1885 } else {
1886 None
1887 }
1888 }
1889
1890 fn active(&self) -> bool {
1891 self.y.is_some() || self.x.is_some()
1892 }
1893}
1894
1895pub(super) mod internals {
1896 use crate::core::{Point, Rectangle};
1897
1898 use super::Anchor;
1899
1900 #[derive(Debug, Copy, Clone)]
1901 pub struct Scrollbar {
1902 pub total_bounds: Rectangle,
1903 pub bounds: Rectangle,
1904 pub scroller: Option<Scroller>,
1905 pub alignment: Anchor,
1906 pub disabled: bool,
1907 }
1908
1909 impl Scrollbar {
1910 pub fn is_mouse_over(&self, cursor_position: Point) -> bool {
1912 self.total_bounds.contains(cursor_position)
1913 }
1914
1915 pub fn scroll_percentage_y(
1917 &self,
1918 grabbed_at: f32,
1919 cursor_position: Point,
1920 ) -> f32 {
1921 if let Some(scroller) = self.scroller {
1922 let percentage = (cursor_position.y
1923 - self.bounds.y
1924 - scroller.bounds.height * grabbed_at)
1925 / (self.bounds.height - scroller.bounds.height);
1926
1927 match self.alignment {
1928 Anchor::Start => percentage,
1929 Anchor::End => 1.0 - percentage,
1930 }
1931 } else {
1932 0.0
1933 }
1934 }
1935
1936 pub fn scroll_percentage_x(
1938 &self,
1939 grabbed_at: f32,
1940 cursor_position: Point,
1941 ) -> f32 {
1942 if let Some(scroller) = self.scroller {
1943 let percentage = (cursor_position.x
1944 - self.bounds.x
1945 - scroller.bounds.width * grabbed_at)
1946 / (self.bounds.width - scroller.bounds.width);
1947
1948 match self.alignment {
1949 Anchor::Start => percentage,
1950 Anchor::End => 1.0 - percentage,
1951 }
1952 } else {
1953 0.0
1954 }
1955 }
1956 }
1957
1958 #[derive(Debug, Clone, Copy)]
1960 pub struct Scroller {
1961 pub bounds: Rectangle,
1963 }
1964}
1965
1966#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1968pub enum Status {
1969 Active {
1971 is_horizontal_scrollbar_disabled: bool,
1973 is_vertical_scrollbar_disabled: bool,
1975 },
1976 Hovered {
1978 is_horizontal_scrollbar_hovered: bool,
1980 is_vertical_scrollbar_hovered: bool,
1982 is_horizontal_scrollbar_disabled: bool,
1984 is_vertical_scrollbar_disabled: bool,
1986 },
1987 Dragged {
1989 is_horizontal_scrollbar_dragged: bool,
1991 is_vertical_scrollbar_dragged: bool,
1993 is_horizontal_scrollbar_disabled: bool,
1995 is_vertical_scrollbar_disabled: bool,
1997 },
1998}
1999
2000#[derive(Debug, Clone, Copy, PartialEq)]
2002pub struct Style {
2003 pub container: container::Style,
2005 pub vertical_rail: Rail,
2007 pub horizontal_rail: Rail,
2009 pub gap: Option<Background>,
2011}
2012
2013#[derive(Debug, Clone, Copy, PartialEq)]
2015pub struct Rail {
2016 pub background: Option<Background>,
2018 pub border: Border,
2020 pub scroller: Scroller,
2022}
2023
2024#[derive(Debug, Clone, Copy, PartialEq)]
2026pub struct Scroller {
2027 pub color: Color,
2029 pub border: Border,
2031}
2032
2033pub trait Catalog {
2035 type Class<'a>;
2037
2038 fn default<'a>() -> Self::Class<'a>;
2040
2041 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
2043}
2044
2045pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
2047
2048impl Catalog for Theme {
2049 type Class<'a> = StyleFn<'a, Self>;
2050
2051 fn default<'a>() -> Self::Class<'a> {
2052 Box::new(default)
2053 }
2054
2055 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
2056 class(self, status)
2057 }
2058}
2059
2060pub fn default(theme: &Theme, status: Status) -> Style {
2062 let palette = theme.extended_palette();
2063
2064 let scrollbar = Rail {
2065 background: Some(palette.background.weak.color.into()),
2066 border: border::rounded(2),
2067 scroller: Scroller {
2068 color: palette.background.strong.color,
2069 border: border::rounded(2),
2070 },
2071 };
2072
2073 match status {
2074 Status::Active { .. } => Style {
2075 container: container::Style::default(),
2076 vertical_rail: scrollbar,
2077 horizontal_rail: scrollbar,
2078 gap: None,
2079 },
2080 Status::Hovered {
2081 is_horizontal_scrollbar_hovered,
2082 is_vertical_scrollbar_hovered,
2083 ..
2084 } => {
2085 let hovered_scrollbar = Rail {
2086 scroller: Scroller {
2087 color: palette.primary.strong.color,
2088 ..scrollbar.scroller
2089 },
2090 ..scrollbar
2091 };
2092
2093 Style {
2094 container: container::Style::default(),
2095 vertical_rail: if is_vertical_scrollbar_hovered {
2096 hovered_scrollbar
2097 } else {
2098 scrollbar
2099 },
2100 horizontal_rail: if is_horizontal_scrollbar_hovered {
2101 hovered_scrollbar
2102 } else {
2103 scrollbar
2104 },
2105 gap: None,
2106 }
2107 }
2108 Status::Dragged {
2109 is_horizontal_scrollbar_dragged,
2110 is_vertical_scrollbar_dragged,
2111 ..
2112 } => {
2113 let dragged_scrollbar = Rail {
2114 scroller: Scroller {
2115 color: palette.primary.base.color,
2116 ..scrollbar.scroller
2117 },
2118 ..scrollbar
2119 };
2120
2121 Style {
2122 container: container::Style::default(),
2123 vertical_rail: if is_vertical_scrollbar_dragged {
2124 dragged_scrollbar
2125 } else {
2126 scrollbar
2127 },
2128 horizontal_rail: if is_horizontal_scrollbar_dragged {
2129 dragged_scrollbar
2130 } else {
2131 scrollbar
2132 },
2133 gap: None,
2134 }
2135 }
2136 }
2137}