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