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