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 (right_padding, bottom_padding) = match self.direction {
430 Direction::Vertical(Scrollbar {
431 width,
432 margin,
433 spacing: Some(spacing),
434 ..
435 }) => (width + margin * 2.0 + spacing, 0.0),
436 Direction::Horizontal(Scrollbar {
437 width,
438 margin,
439 spacing: Some(spacing),
440 ..
441 }) => (0.0, width + margin * 2.0 + spacing),
442 _ => (0.0, 0.0),
443 };
444
445 layout::padded(
446 limits,
447 self.width,
448 self.height,
449 Padding {
450 right: right_padding,
451 bottom: bottom_padding,
452 ..Padding::ZERO
453 },
454 |limits| {
455 let child_limits = layout::Limits::new(
456 Size::new(limits.min().width, limits.min().height),
457 Size::new(
458 if self.direction.horizontal().is_some() {
459 f32::INFINITY
460 } else {
461 limits.max().width
462 },
463 if self.direction.vertical().is_some() {
464 f32::MAX
465 } else {
466 limits.max().height
467 },
468 ),
469 );
470
471 self.content.as_widget().layout(
472 &mut tree.children[0],
473 renderer,
474 &child_limits,
475 )
476 },
477 )
478 }
479
480 fn operate(
481 &self,
482 tree: &mut Tree,
483 layout: Layout<'_>,
484 renderer: &Renderer,
485 operation: &mut dyn Operation,
486 ) {
487 let state = tree.state.downcast_mut::<State>();
488
489 let bounds = layout.bounds();
490 let content_layout = layout.children().next().unwrap();
491 let content_bounds = content_layout.bounds();
492 let translation =
493 state.translation(self.direction, bounds, content_bounds);
494
495 operation.scrollable(
496 self.id.as_ref().map(|id| &id.0),
497 bounds,
498 content_bounds,
499 translation,
500 state,
501 );
502
503 operation.container(
504 self.id.as_ref().map(|id| &id.0),
505 bounds,
506 &mut |operation| {
507 self.content.as_widget().operate(
508 &mut tree.children[0],
509 layout.children().next().unwrap(),
510 renderer,
511 operation,
512 );
513 },
514 );
515 }
516
517 fn update(
518 &mut self,
519 tree: &mut Tree,
520 event: &Event,
521 layout: Layout<'_>,
522 cursor: mouse::Cursor,
523 renderer: &Renderer,
524 clipboard: &mut dyn Clipboard,
525 shell: &mut Shell<'_, Message>,
526 _viewport: &Rectangle,
527 ) {
528 let state = tree.state.downcast_mut::<State>();
529 let bounds = layout.bounds();
530 let cursor_over_scrollable = cursor.position_over(bounds);
531
532 let content = layout.children().next().unwrap();
533 let content_bounds = content.bounds();
534
535 let scrollbars =
536 Scrollbars::new(state, self.direction, bounds, content_bounds);
537
538 let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
539 scrollbars.is_mouse_over(cursor);
540
541 let last_offsets = (state.offset_x, state.offset_y);
542
543 if let Some(last_scrolled) = state.last_scrolled {
544 let clear_transaction = match event {
545 Event::Mouse(
546 mouse::Event::ButtonPressed(_)
547 | mouse::Event::ButtonReleased(_)
548 | mouse::Event::CursorLeft,
549 ) => true,
550 Event::Mouse(mouse::Event::CursorMoved { .. }) => {
551 last_scrolled.elapsed() > Duration::from_millis(100)
552 }
553 _ => last_scrolled.elapsed() > Duration::from_millis(1500),
554 };
555
556 if clear_transaction {
557 state.last_scrolled = None;
558 }
559 }
560
561 let mut update = || {
562 if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at {
563 match event {
564 Event::Mouse(mouse::Event::CursorMoved { .. })
565 | Event::Touch(touch::Event::FingerMoved { .. }) => {
566 if let Some(scrollbar) = scrollbars.y {
567 let Some(cursor_position) =
568 cursor.land().position()
569 else {
570 return;
571 };
572
573 state.scroll_y_to(
574 scrollbar.scroll_percentage_y(
575 scroller_grabbed_at,
576 cursor_position,
577 ),
578 bounds,
579 content_bounds,
580 );
581
582 let _ = notify_scroll(
583 state,
584 &self.on_scroll,
585 bounds,
586 content_bounds,
587 shell,
588 );
589
590 shell.capture_event();
591 }
592 }
593 _ => {}
594 }
595 } else if mouse_over_y_scrollbar {
596 match event {
597 Event::Mouse(mouse::Event::ButtonPressed(
598 mouse::Button::Left,
599 ))
600 | Event::Touch(touch::Event::FingerPressed { .. }) => {
601 let Some(cursor_position) = cursor.position() else {
602 return;
603 };
604
605 if let (Some(scroller_grabbed_at), Some(scrollbar)) = (
606 scrollbars.grab_y_scroller(cursor_position),
607 scrollbars.y,
608 ) {
609 state.scroll_y_to(
610 scrollbar.scroll_percentage_y(
611 scroller_grabbed_at,
612 cursor_position,
613 ),
614 bounds,
615 content_bounds,
616 );
617
618 state.y_scroller_grabbed_at =
619 Some(scroller_grabbed_at);
620
621 let _ = notify_scroll(
622 state,
623 &self.on_scroll,
624 bounds,
625 content_bounds,
626 shell,
627 );
628 }
629
630 shell.capture_event();
631 }
632 _ => {}
633 }
634 }
635
636 if let Some(scroller_grabbed_at) = state.x_scroller_grabbed_at {
637 match event {
638 Event::Mouse(mouse::Event::CursorMoved { .. })
639 | Event::Touch(touch::Event::FingerMoved { .. }) => {
640 let Some(cursor_position) = cursor.land().position()
641 else {
642 return;
643 };
644
645 if let Some(scrollbar) = scrollbars.x {
646 state.scroll_x_to(
647 scrollbar.scroll_percentage_x(
648 scroller_grabbed_at,
649 cursor_position,
650 ),
651 bounds,
652 content_bounds,
653 );
654
655 let _ = notify_scroll(
656 state,
657 &self.on_scroll,
658 bounds,
659 content_bounds,
660 shell,
661 );
662 }
663
664 shell.capture_event();
665 }
666 _ => {}
667 }
668 } else if mouse_over_x_scrollbar {
669 match event {
670 Event::Mouse(mouse::Event::ButtonPressed(
671 mouse::Button::Left,
672 ))
673 | Event::Touch(touch::Event::FingerPressed { .. }) => {
674 let Some(cursor_position) = cursor.position() else {
675 return;
676 };
677
678 if let (Some(scroller_grabbed_at), Some(scrollbar)) = (
679 scrollbars.grab_x_scroller(cursor_position),
680 scrollbars.x,
681 ) {
682 state.scroll_x_to(
683 scrollbar.scroll_percentage_x(
684 scroller_grabbed_at,
685 cursor_position,
686 ),
687 bounds,
688 content_bounds,
689 );
690
691 state.x_scroller_grabbed_at =
692 Some(scroller_grabbed_at);
693
694 let _ = notify_scroll(
695 state,
696 &self.on_scroll,
697 bounds,
698 content_bounds,
699 shell,
700 );
701
702 shell.capture_event();
703 }
704 }
705 _ => {}
706 }
707 }
708
709 if state.last_scrolled.is_none()
710 || !matches!(
711 event,
712 Event::Mouse(mouse::Event::WheelScrolled { .. })
713 )
714 {
715 let cursor = match cursor_over_scrollable {
716 Some(cursor_position)
717 if !(mouse_over_x_scrollbar
718 || mouse_over_y_scrollbar) =>
719 {
720 mouse::Cursor::Available(
721 cursor_position
722 + state.translation(
723 self.direction,
724 bounds,
725 content_bounds,
726 ),
727 )
728 }
729 _ => mouse::Cursor::Unavailable,
730 };
731
732 let had_input_method = shell.input_method().is_enabled();
733
734 let translation =
735 state.translation(self.direction, bounds, content_bounds);
736
737 self.content.as_widget_mut().update(
738 &mut tree.children[0],
739 event,
740 content,
741 cursor,
742 renderer,
743 clipboard,
744 shell,
745 &Rectangle {
746 y: bounds.y + translation.y,
747 x: bounds.x + translation.x,
748 ..bounds
749 },
750 );
751
752 if !had_input_method {
753 if let InputMethod::Enabled { position, .. } =
754 shell.input_method_mut()
755 {
756 *position = *position - translation;
757 }
758 }
759 };
760
761 if matches!(
762 event,
763 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
764 | Event::Touch(
765 touch::Event::FingerLifted { .. }
766 | touch::Event::FingerLost { .. }
767 )
768 ) {
769 state.scroll_area_touched_at = None;
770 state.x_scroller_grabbed_at = None;
771 state.y_scroller_grabbed_at = None;
772
773 return;
774 }
775
776 if shell.is_event_captured() {
777 return;
778 }
779
780 if let Event::Keyboard(keyboard::Event::ModifiersChanged(
781 modifiers,
782 )) = event
783 {
784 state.keyboard_modifiers = *modifiers;
785
786 return;
787 }
788
789 match event {
790 Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
791 if cursor_over_scrollable.is_none() {
792 return;
793 }
794
795 let delta = match *delta {
796 mouse::ScrollDelta::Lines { x, y } => {
797 let is_shift_pressed =
798 state.keyboard_modifiers.shift();
799
800 let (x, y) = if cfg!(target_os = "macos")
802 && is_shift_pressed
803 {
804 (y, x)
805 } else {
806 (x, y)
807 };
808
809 let movement = if !is_shift_pressed {
810 Vector::new(x, y)
811 } else {
812 Vector::new(y, x)
813 };
814
815 -movement * 60.0
817 }
818 mouse::ScrollDelta::Pixels { x, y } => {
819 -Vector::new(x, y)
820 }
821 };
822
823 state.scroll(
824 self.direction.align(delta),
825 bounds,
826 content_bounds,
827 );
828
829 let has_scrolled = notify_scroll(
830 state,
831 &self.on_scroll,
832 bounds,
833 content_bounds,
834 shell,
835 );
836
837 let in_transaction = state.last_scrolled.is_some();
838
839 if has_scrolled || in_transaction {
840 shell.capture_event();
841 }
842 }
843 Event::Touch(event)
844 if state.scroll_area_touched_at.is_some()
845 || !mouse_over_y_scrollbar
846 && !mouse_over_x_scrollbar =>
847 {
848 match event {
849 touch::Event::FingerPressed { .. } => {
850 let Some(cursor_position) = cursor.position()
851 else {
852 return;
853 };
854
855 state.scroll_area_touched_at =
856 Some(cursor_position);
857 }
858 touch::Event::FingerMoved { .. } => {
859 if let Some(scroll_box_touched_at) =
860 state.scroll_area_touched_at
861 {
862 let Some(cursor_position) = cursor.position()
863 else {
864 return;
865 };
866
867 let delta = Vector::new(
868 scroll_box_touched_at.x - cursor_position.x,
869 scroll_box_touched_at.y - cursor_position.y,
870 );
871
872 state.scroll(
873 self.direction.align(delta),
874 bounds,
875 content_bounds,
876 );
877
878 state.scroll_area_touched_at =
879 Some(cursor_position);
880
881 let _ = notify_scroll(
883 state,
884 &self.on_scroll,
885 bounds,
886 content_bounds,
887 shell,
888 );
889 }
890 }
891 _ => {}
892 }
893
894 shell.capture_event();
895 }
896 Event::Window(window::Event::RedrawRequested(_)) => {
897 let _ = notify_viewport(
898 state,
899 &self.on_scroll,
900 bounds,
901 content_bounds,
902 shell,
903 );
904 }
905 _ => {}
906 }
907 };
908
909 update();
910
911 let status = if state.y_scroller_grabbed_at.is_some()
912 || state.x_scroller_grabbed_at.is_some()
913 {
914 Status::Dragged {
915 is_horizontal_scrollbar_dragged: state
916 .x_scroller_grabbed_at
917 .is_some(),
918 is_vertical_scrollbar_dragged: state
919 .y_scroller_grabbed_at
920 .is_some(),
921 is_horizontal_scrollbar_disabled: scrollbars.is_x_disabled(),
922 is_vertical_scrollbar_disabled: scrollbars.is_y_disabled(),
923 }
924 } else if cursor_over_scrollable.is_some() {
925 Status::Hovered {
926 is_horizontal_scrollbar_hovered: mouse_over_x_scrollbar,
927 is_vertical_scrollbar_hovered: mouse_over_y_scrollbar,
928 is_horizontal_scrollbar_disabled: scrollbars.is_x_disabled(),
929 is_vertical_scrollbar_disabled: scrollbars.is_y_disabled(),
930 }
931 } else {
932 Status::Active {
933 is_horizontal_scrollbar_disabled: scrollbars.is_x_disabled(),
934 is_vertical_scrollbar_disabled: scrollbars.is_y_disabled(),
935 }
936 };
937
938 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
939 self.last_status = Some(status);
940 }
941
942 if last_offsets != (state.offset_x, state.offset_y)
943 || self
944 .last_status
945 .is_some_and(|last_status| last_status != status)
946 {
947 shell.request_redraw();
948 }
949 }
950
951 fn draw(
952 &self,
953 tree: &Tree,
954 renderer: &mut Renderer,
955 theme: &Theme,
956 defaults: &renderer::Style,
957 layout: Layout<'_>,
958 cursor: mouse::Cursor,
959 viewport: &Rectangle,
960 ) {
961 let state = tree.state.downcast_ref::<State>();
962
963 let bounds = layout.bounds();
964 let content_layout = layout.children().next().unwrap();
965 let content_bounds = content_layout.bounds();
966
967 let Some(visible_bounds) = bounds.intersection(viewport) else {
968 return;
969 };
970
971 let scrollbars =
972 Scrollbars::new(state, self.direction, bounds, content_bounds);
973
974 let cursor_over_scrollable = cursor.position_over(bounds);
975 let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
976 scrollbars.is_mouse_over(cursor);
977
978 let translation =
979 state.translation(self.direction, bounds, content_bounds);
980
981 let cursor = match cursor_over_scrollable {
982 Some(cursor_position)
983 if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) =>
984 {
985 mouse::Cursor::Available(cursor_position + translation)
986 }
987 _ => mouse::Cursor::Unavailable,
988 };
989
990 let style = theme.style(
991 &self.class,
992 self.last_status.unwrap_or(Status::Active {
993 is_horizontal_scrollbar_disabled: false,
994 is_vertical_scrollbar_disabled: false,
995 }),
996 );
997
998 container::draw_background(renderer, &style.container, layout.bounds());
999
1000 if scrollbars.active() {
1002 renderer.with_layer(visible_bounds, |renderer| {
1003 renderer.with_translation(
1004 Vector::new(-translation.x, -translation.y),
1005 |renderer| {
1006 self.content.as_widget().draw(
1007 &tree.children[0],
1008 renderer,
1009 theme,
1010 defaults,
1011 content_layout,
1012 cursor,
1013 &Rectangle {
1014 y: visible_bounds.y + translation.y,
1015 x: visible_bounds.x + translation.x,
1016 ..visible_bounds
1017 },
1018 );
1019 },
1020 );
1021 });
1022
1023 let draw_scrollbar =
1024 |renderer: &mut Renderer,
1025 style: Rail,
1026 scrollbar: &internals::Scrollbar| {
1027 if scrollbar.bounds.width > 0.0
1028 && scrollbar.bounds.height > 0.0
1029 && (style.background.is_some()
1030 || (style.border.color != Color::TRANSPARENT
1031 && style.border.width > 0.0))
1032 {
1033 renderer.fill_quad(
1034 renderer::Quad {
1035 bounds: scrollbar.bounds,
1036 border: style.border,
1037 ..renderer::Quad::default()
1038 },
1039 style.background.unwrap_or(Background::Color(
1040 Color::TRANSPARENT,
1041 )),
1042 );
1043 }
1044
1045 if let Some(scroller) = scrollbar.scroller {
1046 if scroller.bounds.width > 0.0
1047 && scroller.bounds.height > 0.0
1048 && (style.scroller.color != Color::TRANSPARENT
1049 || (style.scroller.border.color
1050 != Color::TRANSPARENT
1051 && style.scroller.border.width > 0.0))
1052 {
1053 renderer.fill_quad(
1054 renderer::Quad {
1055 bounds: scroller.bounds,
1056 border: style.scroller.border,
1057 ..renderer::Quad::default()
1058 },
1059 style.scroller.color,
1060 );
1061 }
1062 }
1063 };
1064
1065 renderer.with_layer(
1066 Rectangle {
1067 width: (visible_bounds.width + 2.0).min(viewport.width),
1068 height: (visible_bounds.height + 2.0).min(viewport.height),
1069 ..visible_bounds
1070 },
1071 |renderer| {
1072 if let Some(scrollbar) = scrollbars.y {
1073 draw_scrollbar(
1074 renderer,
1075 style.vertical_rail,
1076 &scrollbar,
1077 );
1078 }
1079
1080 if let Some(scrollbar) = scrollbars.x {
1081 draw_scrollbar(
1082 renderer,
1083 style.horizontal_rail,
1084 &scrollbar,
1085 );
1086 }
1087
1088 if let (Some(x), Some(y)) = (scrollbars.x, scrollbars.y) {
1089 let background =
1090 style.gap.or(style.container.background);
1091
1092 if let Some(background) = background {
1093 renderer.fill_quad(
1094 renderer::Quad {
1095 bounds: Rectangle {
1096 x: y.bounds.x,
1097 y: x.bounds.y,
1098 width: y.bounds.width,
1099 height: x.bounds.height,
1100 },
1101 ..renderer::Quad::default()
1102 },
1103 background,
1104 );
1105 }
1106 }
1107 },
1108 );
1109 } else {
1110 self.content.as_widget().draw(
1111 &tree.children[0],
1112 renderer,
1113 theme,
1114 defaults,
1115 content_layout,
1116 cursor,
1117 &Rectangle {
1118 x: visible_bounds.x + translation.x,
1119 y: visible_bounds.y + translation.y,
1120 ..visible_bounds
1121 },
1122 );
1123 }
1124 }
1125
1126 fn mouse_interaction(
1127 &self,
1128 tree: &Tree,
1129 layout: Layout<'_>,
1130 cursor: mouse::Cursor,
1131 _viewport: &Rectangle,
1132 renderer: &Renderer,
1133 ) -> mouse::Interaction {
1134 let state = tree.state.downcast_ref::<State>();
1135 let bounds = layout.bounds();
1136 let cursor_over_scrollable = cursor.position_over(bounds);
1137
1138 let content_layout = layout.children().next().unwrap();
1139 let content_bounds = content_layout.bounds();
1140
1141 let scrollbars =
1142 Scrollbars::new(state, self.direction, bounds, content_bounds);
1143
1144 let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
1145 scrollbars.is_mouse_over(cursor);
1146
1147 if (mouse_over_x_scrollbar || mouse_over_y_scrollbar)
1148 || state.scrollers_grabbed()
1149 {
1150 mouse::Interaction::None
1151 } else {
1152 let translation =
1153 state.translation(self.direction, bounds, content_bounds);
1154
1155 let cursor = match cursor_over_scrollable {
1156 Some(cursor_position)
1157 if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) =>
1158 {
1159 mouse::Cursor::Available(cursor_position + translation)
1160 }
1161 _ => mouse::Cursor::Unavailable,
1162 };
1163
1164 self.content.as_widget().mouse_interaction(
1165 &tree.children[0],
1166 content_layout,
1167 cursor,
1168 &Rectangle {
1169 y: bounds.y + translation.y,
1170 x: bounds.x + translation.x,
1171 ..bounds
1172 },
1173 renderer,
1174 )
1175 }
1176 }
1177
1178 fn overlay<'b>(
1179 &'b mut self,
1180 tree: &'b mut Tree,
1181 layout: Layout<'_>,
1182 renderer: &Renderer,
1183 translation: Vector,
1184 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
1185 let bounds = layout.bounds();
1186 let content_layout = layout.children().next().unwrap();
1187 let content_bounds = content_layout.bounds();
1188
1189 let offset = tree.state.downcast_ref::<State>().translation(
1190 self.direction,
1191 bounds,
1192 content_bounds,
1193 );
1194
1195 self.content.as_widget_mut().overlay(
1196 &mut tree.children[0],
1197 layout.children().next().unwrap(),
1198 renderer,
1199 translation - offset,
1200 )
1201 }
1202}
1203
1204impl<'a, Message, Theme, Renderer>
1205 From<Scrollable<'a, Message, Theme, Renderer>>
1206 for Element<'a, Message, Theme, Renderer>
1207where
1208 Message: 'a,
1209 Theme: 'a + Catalog,
1210 Renderer: 'a + core::Renderer,
1211{
1212 fn from(
1213 text_input: Scrollable<'a, Message, Theme, Renderer>,
1214 ) -> Element<'a, Message, Theme, Renderer> {
1215 Element::new(text_input)
1216 }
1217}
1218
1219#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1221pub struct Id(widget::Id);
1222
1223impl Id {
1224 pub fn new(id: impl Into<std::borrow::Cow<'static, str>>) -> Self {
1226 Self(widget::Id::new(id))
1227 }
1228
1229 pub fn unique() -> Self {
1233 Self(widget::Id::unique())
1234 }
1235}
1236
1237impl From<Id> for widget::Id {
1238 fn from(id: Id) -> Self {
1239 id.0
1240 }
1241}
1242
1243impl From<&'static str> for Id {
1244 fn from(id: &'static str) -> Self {
1245 Self::new(id)
1246 }
1247}
1248
1249pub fn snap_to<T>(id: impl Into<Id>, offset: RelativeOffset) -> Task<T> {
1252 task::effect(Action::widget(operation::scrollable::snap_to(
1253 id.into().0,
1254 offset,
1255 )))
1256}
1257
1258pub fn scroll_to<T>(id: impl Into<Id>, offset: AbsoluteOffset) -> Task<T> {
1261 task::effect(Action::widget(operation::scrollable::scroll_to(
1262 id.into().0,
1263 offset,
1264 )))
1265}
1266
1267pub fn scroll_by<T>(id: impl Into<Id>, offset: AbsoluteOffset) -> Task<T> {
1270 task::effect(Action::widget(operation::scrollable::scroll_by(
1271 id.into().0,
1272 offset,
1273 )))
1274}
1275
1276fn notify_scroll<Message>(
1277 state: &mut State,
1278 on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>,
1279 bounds: Rectangle,
1280 content_bounds: Rectangle,
1281 shell: &mut Shell<'_, Message>,
1282) -> bool {
1283 if notify_viewport(state, on_scroll, bounds, content_bounds, shell) {
1284 state.last_scrolled = Some(Instant::now());
1285
1286 true
1287 } else {
1288 false
1289 }
1290}
1291
1292fn notify_viewport<Message>(
1293 state: &mut State,
1294 on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>,
1295 bounds: Rectangle,
1296 content_bounds: Rectangle,
1297 shell: &mut Shell<'_, Message>,
1298) -> bool {
1299 if content_bounds.width <= bounds.width
1300 && content_bounds.height <= bounds.height
1301 {
1302 return false;
1303 }
1304
1305 let viewport = Viewport {
1306 offset_x: state.offset_x,
1307 offset_y: state.offset_y,
1308 bounds,
1309 content_bounds,
1310 };
1311
1312 if let Some(last_notified) = state.last_notified {
1314 let last_relative_offset = last_notified.relative_offset();
1315 let current_relative_offset = viewport.relative_offset();
1316
1317 let last_absolute_offset = last_notified.absolute_offset();
1318 let current_absolute_offset = viewport.absolute_offset();
1319
1320 let unchanged = |a: f32, b: f32| {
1321 (a - b).abs() <= f32::EPSILON || (a.is_nan() && b.is_nan())
1322 };
1323
1324 if last_notified.bounds == bounds
1325 && last_notified.content_bounds == content_bounds
1326 && unchanged(last_relative_offset.x, current_relative_offset.x)
1327 && unchanged(last_relative_offset.y, current_relative_offset.y)
1328 && unchanged(last_absolute_offset.x, current_absolute_offset.x)
1329 && unchanged(last_absolute_offset.y, current_absolute_offset.y)
1330 {
1331 return false;
1332 }
1333 }
1334
1335 state.last_notified = Some(viewport);
1336
1337 if let Some(on_scroll) = on_scroll {
1338 shell.publish(on_scroll(viewport));
1339 }
1340
1341 true
1342}
1343
1344#[derive(Debug, Clone, Copy)]
1345struct State {
1346 scroll_area_touched_at: Option<Point>,
1347 offset_y: Offset,
1348 y_scroller_grabbed_at: Option<f32>,
1349 offset_x: Offset,
1350 x_scroller_grabbed_at: Option<f32>,
1351 keyboard_modifiers: keyboard::Modifiers,
1352 last_notified: Option<Viewport>,
1353 last_scrolled: Option<Instant>,
1354}
1355
1356impl Default for State {
1357 fn default() -> Self {
1358 Self {
1359 scroll_area_touched_at: None,
1360 offset_y: Offset::Absolute(0.0),
1361 y_scroller_grabbed_at: None,
1362 offset_x: Offset::Absolute(0.0),
1363 x_scroller_grabbed_at: None,
1364 keyboard_modifiers: keyboard::Modifiers::default(),
1365 last_notified: None,
1366 last_scrolled: None,
1367 }
1368 }
1369}
1370
1371impl operation::Scrollable for State {
1372 fn snap_to(&mut self, offset: RelativeOffset) {
1373 State::snap_to(self, offset);
1374 }
1375
1376 fn scroll_to(&mut self, offset: AbsoluteOffset) {
1377 State::scroll_to(self, offset);
1378 }
1379
1380 fn scroll_by(
1381 &mut self,
1382 offset: AbsoluteOffset,
1383 bounds: Rectangle,
1384 content_bounds: Rectangle,
1385 ) {
1386 State::scroll_by(self, offset, bounds, content_bounds);
1387 }
1388}
1389
1390#[derive(Debug, Clone, Copy, PartialEq)]
1391enum Offset {
1392 Absolute(f32),
1393 Relative(f32),
1394}
1395
1396impl Offset {
1397 fn absolute(self, viewport: f32, content: f32) -> f32 {
1398 match self {
1399 Offset::Absolute(absolute) => {
1400 absolute.min((content - viewport).max(0.0))
1401 }
1402 Offset::Relative(percentage) => {
1403 ((content - viewport) * percentage).max(0.0)
1404 }
1405 }
1406 }
1407
1408 fn translation(
1409 self,
1410 viewport: f32,
1411 content: f32,
1412 alignment: Anchor,
1413 ) -> f32 {
1414 let offset = self.absolute(viewport, content);
1415
1416 match alignment {
1417 Anchor::Start => offset,
1418 Anchor::End => ((content - viewport).max(0.0) - offset).max(0.0),
1419 }
1420 }
1421}
1422
1423#[derive(Debug, Clone, Copy)]
1425pub struct Viewport {
1426 offset_x: Offset,
1427 offset_y: Offset,
1428 bounds: Rectangle,
1429 content_bounds: Rectangle,
1430}
1431
1432impl Viewport {
1433 pub fn absolute_offset(&self) -> AbsoluteOffset {
1435 let x = self
1436 .offset_x
1437 .absolute(self.bounds.width, self.content_bounds.width);
1438 let y = self
1439 .offset_y
1440 .absolute(self.bounds.height, self.content_bounds.height);
1441
1442 AbsoluteOffset { x, y }
1443 }
1444
1445 pub fn absolute_offset_reversed(&self) -> AbsoluteOffset {
1451 let AbsoluteOffset { x, y } = self.absolute_offset();
1452
1453 AbsoluteOffset {
1454 x: (self.content_bounds.width - self.bounds.width).max(0.0) - x,
1455 y: (self.content_bounds.height - self.bounds.height).max(0.0) - y,
1456 }
1457 }
1458
1459 pub fn relative_offset(&self) -> RelativeOffset {
1461 let AbsoluteOffset { x, y } = self.absolute_offset();
1462
1463 let x = x / (self.content_bounds.width - self.bounds.width);
1464 let y = y / (self.content_bounds.height - self.bounds.height);
1465
1466 RelativeOffset { x, y }
1467 }
1468
1469 pub fn bounds(&self) -> Rectangle {
1471 self.bounds
1472 }
1473
1474 pub fn content_bounds(&self) -> Rectangle {
1476 self.content_bounds
1477 }
1478}
1479
1480impl State {
1481 pub fn new() -> Self {
1483 State::default()
1484 }
1485
1486 pub fn scroll(
1489 &mut self,
1490 delta: Vector<f32>,
1491 bounds: Rectangle,
1492 content_bounds: Rectangle,
1493 ) {
1494 if bounds.height < content_bounds.height {
1495 self.offset_y = Offset::Absolute(
1496 (self.offset_y.absolute(bounds.height, content_bounds.height)
1497 + delta.y)
1498 .clamp(0.0, content_bounds.height - bounds.height),
1499 );
1500 }
1501
1502 if bounds.width < content_bounds.width {
1503 self.offset_x = Offset::Absolute(
1504 (self.offset_x.absolute(bounds.width, content_bounds.width)
1505 + delta.x)
1506 .clamp(0.0, content_bounds.width - bounds.width),
1507 );
1508 }
1509 }
1510
1511 pub fn scroll_y_to(
1516 &mut self,
1517 percentage: f32,
1518 bounds: Rectangle,
1519 content_bounds: Rectangle,
1520 ) {
1521 self.offset_y = Offset::Relative(percentage.clamp(0.0, 1.0));
1522 self.unsnap(bounds, content_bounds);
1523 }
1524
1525 pub fn scroll_x_to(
1530 &mut self,
1531 percentage: f32,
1532 bounds: Rectangle,
1533 content_bounds: Rectangle,
1534 ) {
1535 self.offset_x = Offset::Relative(percentage.clamp(0.0, 1.0));
1536 self.unsnap(bounds, content_bounds);
1537 }
1538
1539 pub fn snap_to(&mut self, offset: RelativeOffset) {
1541 self.offset_x = Offset::Relative(offset.x.clamp(0.0, 1.0));
1542 self.offset_y = Offset::Relative(offset.y.clamp(0.0, 1.0));
1543 }
1544
1545 pub fn scroll_to(&mut self, offset: AbsoluteOffset) {
1547 self.offset_x = Offset::Absolute(offset.x.max(0.0));
1548 self.offset_y = Offset::Absolute(offset.y.max(0.0));
1549 }
1550
1551 pub fn scroll_by(
1553 &mut self,
1554 offset: AbsoluteOffset,
1555 bounds: Rectangle,
1556 content_bounds: Rectangle,
1557 ) {
1558 self.scroll(Vector::new(offset.x, offset.y), bounds, content_bounds);
1559 }
1560
1561 pub fn unsnap(&mut self, bounds: Rectangle, content_bounds: Rectangle) {
1564 self.offset_x = Offset::Absolute(
1565 self.offset_x.absolute(bounds.width, content_bounds.width),
1566 );
1567 self.offset_y = Offset::Absolute(
1568 self.offset_y.absolute(bounds.height, content_bounds.height),
1569 );
1570 }
1571
1572 fn translation(
1575 &self,
1576 direction: Direction,
1577 bounds: Rectangle,
1578 content_bounds: Rectangle,
1579 ) -> Vector {
1580 Vector::new(
1581 if let Some(horizontal) = direction.horizontal() {
1582 self.offset_x.translation(
1583 bounds.width,
1584 content_bounds.width,
1585 horizontal.alignment,
1586 )
1587 } else {
1588 0.0
1589 },
1590 if let Some(vertical) = direction.vertical() {
1591 self.offset_y.translation(
1592 bounds.height,
1593 content_bounds.height,
1594 vertical.alignment,
1595 )
1596 } else {
1597 0.0
1598 },
1599 )
1600 }
1601
1602 pub fn scrollers_grabbed(&self) -> bool {
1604 self.x_scroller_grabbed_at.is_some()
1605 || self.y_scroller_grabbed_at.is_some()
1606 }
1607}
1608
1609#[derive(Debug)]
1610struct Scrollbars {
1612 y: Option<internals::Scrollbar>,
1613 x: Option<internals::Scrollbar>,
1614}
1615
1616impl Scrollbars {
1617 fn new(
1619 state: &State,
1620 direction: Direction,
1621 bounds: Rectangle,
1622 content_bounds: Rectangle,
1623 ) -> Self {
1624 let translation = state.translation(direction, bounds, content_bounds);
1625
1626 let show_scrollbar_x = direction.horizontal().filter(|scrollbar| {
1627 scrollbar.spacing.is_some() || content_bounds.width > bounds.width
1628 });
1629
1630 let show_scrollbar_y = direction.vertical().filter(|scrollbar| {
1631 scrollbar.spacing.is_some() || content_bounds.height > bounds.height
1632 });
1633
1634 let y_scrollbar = if let Some(vertical) = show_scrollbar_y {
1635 let Scrollbar {
1636 width,
1637 margin,
1638 scroller_width,
1639 ..
1640 } = *vertical;
1641
1642 let x_scrollbar_height = show_scrollbar_x
1645 .map_or(0.0, |h| h.width.max(h.scroller_width) + h.margin);
1646
1647 let total_scrollbar_width =
1648 width.max(scroller_width) + 2.0 * margin;
1649
1650 let total_scrollbar_bounds = Rectangle {
1652 x: bounds.x + bounds.width - total_scrollbar_width,
1653 y: bounds.y,
1654 width: total_scrollbar_width,
1655 height: (bounds.height - x_scrollbar_height).max(0.0),
1656 };
1657
1658 let scrollbar_bounds = Rectangle {
1660 x: bounds.x + bounds.width
1661 - total_scrollbar_width / 2.0
1662 - width / 2.0,
1663 y: bounds.y,
1664 width,
1665 height: (bounds.height - x_scrollbar_height).max(0.0),
1666 };
1667
1668 let ratio = bounds.height / content_bounds.height;
1669
1670 let scroller = if ratio >= 1.0 {
1671 None
1672 } else {
1673 let scroller_height =
1675 (scrollbar_bounds.height * ratio).max(2.0);
1676 let scroller_offset =
1677 translation.y * ratio * scrollbar_bounds.height
1678 / bounds.height;
1679
1680 let scroller_bounds = Rectangle {
1681 x: bounds.x + bounds.width
1682 - total_scrollbar_width / 2.0
1683 - scroller_width / 2.0,
1684 y: (scrollbar_bounds.y + scroller_offset).max(0.0),
1685 width: scroller_width,
1686 height: scroller_height,
1687 };
1688
1689 Some(internals::Scroller {
1690 bounds: scroller_bounds,
1691 })
1692 };
1693
1694 Some(internals::Scrollbar {
1695 total_bounds: total_scrollbar_bounds,
1696 bounds: scrollbar_bounds,
1697 scroller,
1698 alignment: vertical.alignment,
1699 disabled: content_bounds.height <= bounds.height,
1700 })
1701 } else {
1702 None
1703 };
1704
1705 let x_scrollbar = if let Some(horizontal) = show_scrollbar_x {
1706 let Scrollbar {
1707 width,
1708 margin,
1709 scroller_width,
1710 ..
1711 } = *horizontal;
1712
1713 let scrollbar_y_width = y_scrollbar
1716 .map_or(0.0, |scrollbar| scrollbar.total_bounds.width);
1717
1718 let total_scrollbar_height =
1719 width.max(scroller_width) + 2.0 * margin;
1720
1721 let total_scrollbar_bounds = Rectangle {
1723 x: bounds.x,
1724 y: bounds.y + bounds.height - total_scrollbar_height,
1725 width: (bounds.width - scrollbar_y_width).max(0.0),
1726 height: total_scrollbar_height,
1727 };
1728
1729 let scrollbar_bounds = Rectangle {
1731 x: bounds.x,
1732 y: bounds.y + bounds.height
1733 - total_scrollbar_height / 2.0
1734 - width / 2.0,
1735 width: (bounds.width - scrollbar_y_width).max(0.0),
1736 height: width,
1737 };
1738
1739 let ratio = bounds.width / content_bounds.width;
1740
1741 let scroller = if ratio >= 1.0 {
1742 None
1743 } else {
1744 let scroller_length = (scrollbar_bounds.width * ratio).max(2.0);
1746 let scroller_offset =
1747 translation.x * ratio * scrollbar_bounds.width
1748 / bounds.width;
1749
1750 let scroller_bounds = Rectangle {
1751 x: (scrollbar_bounds.x + scroller_offset).max(0.0),
1752 y: bounds.y + bounds.height
1753 - total_scrollbar_height / 2.0
1754 - scroller_width / 2.0,
1755 width: scroller_length,
1756 height: scroller_width,
1757 };
1758
1759 Some(internals::Scroller {
1760 bounds: scroller_bounds,
1761 })
1762 };
1763
1764 Some(internals::Scrollbar {
1765 total_bounds: total_scrollbar_bounds,
1766 bounds: scrollbar_bounds,
1767 scroller,
1768 alignment: horizontal.alignment,
1769 disabled: content_bounds.width <= bounds.width,
1770 })
1771 } else {
1772 None
1773 };
1774
1775 Self {
1776 y: y_scrollbar,
1777 x: x_scrollbar,
1778 }
1779 }
1780
1781 fn is_mouse_over(&self, cursor: mouse::Cursor) -> (bool, bool) {
1782 if let Some(cursor_position) = cursor.position() {
1783 (
1784 self.y
1785 .as_ref()
1786 .map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
1787 .unwrap_or(false),
1788 self.x
1789 .as_ref()
1790 .map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
1791 .unwrap_or(false),
1792 )
1793 } else {
1794 (false, false)
1795 }
1796 }
1797
1798 fn is_y_disabled(&self) -> bool {
1799 self.y.map(|y| y.disabled).unwrap_or(false)
1800 }
1801
1802 fn is_x_disabled(&self) -> bool {
1803 self.x.map(|x| x.disabled).unwrap_or(false)
1804 }
1805
1806 fn grab_y_scroller(&self, cursor_position: Point) -> Option<f32> {
1807 let scrollbar = self.y?;
1808 let scroller = scrollbar.scroller?;
1809
1810 if scrollbar.total_bounds.contains(cursor_position) {
1811 Some(if scroller.bounds.contains(cursor_position) {
1812 (cursor_position.y - scroller.bounds.y) / scroller.bounds.height
1813 } else {
1814 0.5
1815 })
1816 } else {
1817 None
1818 }
1819 }
1820
1821 fn grab_x_scroller(&self, cursor_position: Point) -> Option<f32> {
1822 let scrollbar = self.x?;
1823 let scroller = scrollbar.scroller?;
1824
1825 if scrollbar.total_bounds.contains(cursor_position) {
1826 Some(if scroller.bounds.contains(cursor_position) {
1827 (cursor_position.x - scroller.bounds.x) / scroller.bounds.width
1828 } else {
1829 0.5
1830 })
1831 } else {
1832 None
1833 }
1834 }
1835
1836 fn active(&self) -> bool {
1837 self.y.is_some() || self.x.is_some()
1838 }
1839}
1840
1841pub(super) mod internals {
1842 use crate::core::{Point, Rectangle};
1843
1844 use super::Anchor;
1845
1846 #[derive(Debug, Copy, Clone)]
1847 pub struct Scrollbar {
1848 pub total_bounds: Rectangle,
1849 pub bounds: Rectangle,
1850 pub scroller: Option<Scroller>,
1851 pub alignment: Anchor,
1852 pub disabled: bool,
1853 }
1854
1855 impl Scrollbar {
1856 pub fn is_mouse_over(&self, cursor_position: Point) -> bool {
1858 self.total_bounds.contains(cursor_position)
1859 }
1860
1861 pub fn scroll_percentage_y(
1863 &self,
1864 grabbed_at: f32,
1865 cursor_position: Point,
1866 ) -> f32 {
1867 if let Some(scroller) = self.scroller {
1868 let percentage = (cursor_position.y
1869 - self.bounds.y
1870 - scroller.bounds.height * grabbed_at)
1871 / (self.bounds.height - scroller.bounds.height);
1872
1873 match self.alignment {
1874 Anchor::Start => percentage,
1875 Anchor::End => 1.0 - percentage,
1876 }
1877 } else {
1878 0.0
1879 }
1880 }
1881
1882 pub fn scroll_percentage_x(
1884 &self,
1885 grabbed_at: f32,
1886 cursor_position: Point,
1887 ) -> f32 {
1888 if let Some(scroller) = self.scroller {
1889 let percentage = (cursor_position.x
1890 - self.bounds.x
1891 - scroller.bounds.width * grabbed_at)
1892 / (self.bounds.width - scroller.bounds.width);
1893
1894 match self.alignment {
1895 Anchor::Start => percentage,
1896 Anchor::End => 1.0 - percentage,
1897 }
1898 } else {
1899 0.0
1900 }
1901 }
1902 }
1903
1904 #[derive(Debug, Clone, Copy)]
1906 pub struct Scroller {
1907 pub bounds: Rectangle,
1909 }
1910}
1911
1912#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1914pub enum Status {
1915 Active {
1917 is_horizontal_scrollbar_disabled: bool,
1919 is_vertical_scrollbar_disabled: bool,
1921 },
1922 Hovered {
1924 is_horizontal_scrollbar_hovered: bool,
1926 is_vertical_scrollbar_hovered: bool,
1928 is_horizontal_scrollbar_disabled: bool,
1930 is_vertical_scrollbar_disabled: bool,
1932 },
1933 Dragged {
1935 is_horizontal_scrollbar_dragged: bool,
1937 is_vertical_scrollbar_dragged: bool,
1939 is_horizontal_scrollbar_disabled: bool,
1941 is_vertical_scrollbar_disabled: bool,
1943 },
1944}
1945
1946#[derive(Debug, Clone, Copy, PartialEq)]
1948pub struct Style {
1949 pub container: container::Style,
1951 pub vertical_rail: Rail,
1953 pub horizontal_rail: Rail,
1955 pub gap: Option<Background>,
1957}
1958
1959#[derive(Debug, Clone, Copy, PartialEq)]
1961pub struct Rail {
1962 pub background: Option<Background>,
1964 pub border: Border,
1966 pub scroller: Scroller,
1968}
1969
1970#[derive(Debug, Clone, Copy, PartialEq)]
1972pub struct Scroller {
1973 pub color: Color,
1975 pub border: Border,
1977}
1978
1979pub trait Catalog {
1981 type Class<'a>;
1983
1984 fn default<'a>() -> Self::Class<'a>;
1986
1987 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
1989}
1990
1991pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
1993
1994impl Catalog for Theme {
1995 type Class<'a> = StyleFn<'a, Self>;
1996
1997 fn default<'a>() -> Self::Class<'a> {
1998 Box::new(default)
1999 }
2000
2001 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
2002 class(self, status)
2003 }
2004}
2005
2006pub fn default(theme: &Theme, status: Status) -> Style {
2008 let palette = theme.extended_palette();
2009
2010 let scrollbar = Rail {
2011 background: Some(palette.background.weak.color.into()),
2012 border: border::rounded(2),
2013 scroller: Scroller {
2014 color: palette.background.strong.color,
2015 border: border::rounded(2),
2016 },
2017 };
2018
2019 match status {
2020 Status::Active { .. } => Style {
2021 container: container::Style::default(),
2022 vertical_rail: scrollbar,
2023 horizontal_rail: scrollbar,
2024 gap: None,
2025 },
2026 Status::Hovered {
2027 is_horizontal_scrollbar_hovered,
2028 is_vertical_scrollbar_hovered,
2029 ..
2030 } => {
2031 let hovered_scrollbar = Rail {
2032 scroller: Scroller {
2033 color: palette.primary.strong.color,
2034 ..scrollbar.scroller
2035 },
2036 ..scrollbar
2037 };
2038
2039 Style {
2040 container: container::Style::default(),
2041 vertical_rail: if is_vertical_scrollbar_hovered {
2042 hovered_scrollbar
2043 } else {
2044 scrollbar
2045 },
2046 horizontal_rail: if is_horizontal_scrollbar_hovered {
2047 hovered_scrollbar
2048 } else {
2049 scrollbar
2050 },
2051 gap: None,
2052 }
2053 }
2054 Status::Dragged {
2055 is_horizontal_scrollbar_dragged,
2056 is_vertical_scrollbar_dragged,
2057 ..
2058 } => {
2059 let dragged_scrollbar = Rail {
2060 scroller: Scroller {
2061 color: palette.primary.base.color,
2062 ..scrollbar.scroller
2063 },
2064 ..scrollbar
2065 };
2066
2067 Style {
2068 container: container::Style::default(),
2069 vertical_rail: if is_vertical_scrollbar_dragged {
2070 dragged_scrollbar
2071 } else {
2072 scrollbar
2073 },
2074 horizontal_rail: if is_horizontal_scrollbar_dragged {
2075 dragged_scrollbar
2076 } else {
2077 scrollbar
2078 },
2079 gap: None,
2080 }
2081 }
2082 }
2083}