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