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