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