1use crate::event::{self, Event};
3use crate::keyboard;
4use crate::layout;
5use crate::mouse;
6use crate::overlay;
7use crate::renderer;
8use crate::touch;
9use crate::widget;
10use crate::widget::operation::{self, Operation};
11use crate::widget::tree::{self, Tree};
12use crate::{
13 Background, Clipboard, Color, Command, Element, Layout, Length, Pixels,
14 Point, Rectangle, Shell, Size, Vector, Widget,
15};
16
17pub use iced_style::scrollable::StyleSheet;
18pub use operation::scrollable::RelativeOffset;
19
20pub mod style {
21 pub use iced_style::scrollable::{Scrollbar, Scroller};
25}
26
27#[allow(missing_debug_implementations)]
30pub struct Scrollable<'a, Message, Renderer>
31where
32 Renderer: crate::Renderer,
33 Renderer::Theme: StyleSheet,
34{
35 id: Option<Id>,
36 width: Length,
37 height: Length,
38 vertical: Properties,
39 horizontal: Option<Properties>,
40 content: Element<'a, Message, Renderer>,
41 on_scroll: Option<Box<dyn Fn(RelativeOffset) -> Message + 'a>>,
42 style: <Renderer::Theme as StyleSheet>::Style,
43}
44
45impl<'a, Message, Renderer> Scrollable<'a, Message, Renderer>
46where
47 Renderer: crate::Renderer,
48 Renderer::Theme: StyleSheet,
49{
50 pub fn new(content: impl Into<Element<'a, Message, Renderer>>) -> Self {
52 Scrollable {
53 id: None,
54 width: Length::Shrink,
55 height: Length::Shrink,
56 vertical: Properties::default(),
57 horizontal: None,
58 content: content.into(),
59 on_scroll: None,
60 style: Default::default(),
61 }
62 }
63
64 pub fn id(mut self, id: Id) -> Self {
66 self.id = Some(id);
67 self
68 }
69
70 pub fn width(mut self, width: impl Into<Length>) -> Self {
72 self.width = width.into();
73 self
74 }
75
76 pub fn height(mut self, height: impl Into<Length>) -> Self {
78 self.height = height.into();
79 self
80 }
81
82 pub fn vertical_scroll(mut self, properties: Properties) -> Self {
84 self.vertical = properties;
85 self
86 }
87
88 pub fn horizontal_scroll(mut self, properties: Properties) -> Self {
90 self.horizontal = Some(properties);
91 self
92 }
93
94 pub fn on_scroll(
99 mut self,
100 f: impl Fn(RelativeOffset) -> Message + 'a,
101 ) -> Self {
102 self.on_scroll = Some(Box::new(f));
103 self
104 }
105
106 pub fn style(
108 mut self,
109 style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
110 ) -> Self {
111 self.style = style.into();
112 self
113 }
114}
115
116#[derive(Debug)]
118pub struct Properties {
119 width: f32,
120 margin: f32,
121 scroller_width: f32,
122}
123
124impl Default for Properties {
125 fn default() -> Self {
126 Self {
127 width: 10.0,
128 margin: 0.0,
129 scroller_width: 10.0,
130 }
131 }
132}
133
134impl Properties {
135 pub fn new() -> Self {
137 Self::default()
138 }
139
140 pub fn width(mut self, width: impl Into<Pixels>) -> Self {
143 self.width = width.into().0.max(1.0);
144 self
145 }
146
147 pub fn margin(mut self, margin: impl Into<Pixels>) -> Self {
149 self.margin = margin.into().0;
150 self
151 }
152
153 pub fn scroller_width(mut self, scroller_width: impl Into<Pixels>) -> Self {
156 self.scroller_width = scroller_width.into().0.max(1.0);
157 self
158 }
159}
160
161impl<'a, Message, Renderer> Widget<Message, Renderer>
162 for Scrollable<'a, Message, Renderer>
163where
164 Renderer: crate::Renderer,
165 Renderer::Theme: StyleSheet,
166{
167 fn tag(&self) -> tree::Tag {
168 tree::Tag::of::<State>()
169 }
170
171 fn state(&self) -> tree::State {
172 tree::State::new(State::new())
173 }
174
175 fn children(&self) -> Vec<Tree> {
176 vec![Tree::new(&self.content)]
177 }
178
179 fn diff(&self, tree: &mut Tree) {
180 tree.diff_children(std::slice::from_ref(&self.content))
181 }
182
183 fn width(&self) -> Length {
184 self.width
185 }
186
187 fn height(&self) -> Length {
188 self.height
189 }
190
191 fn layout(
192 &self,
193 renderer: &Renderer,
194 limits: &layout::Limits,
195 ) -> layout::Node {
196 layout(
197 renderer,
198 limits,
199 self.width,
200 self.height,
201 self.horizontal.is_some(),
202 |renderer, limits| {
203 self.content.as_widget().layout(renderer, limits)
204 },
205 )
206 }
207
208 fn operate(
209 &self,
210 tree: &mut Tree,
211 layout: Layout<'_>,
212 renderer: &Renderer,
213 operation: &mut dyn Operation<Message>,
214 ) {
215 let state = tree.state.downcast_mut::<State>();
216
217 operation.scrollable(state, self.id.as_ref().map(|id| &id.0));
218
219 operation.container(
220 self.id.as_ref().map(|id| &id.0),
221 &mut |operation| {
222 self.content.as_widget().operate(
223 &mut tree.children[0],
224 layout.children().next().unwrap(),
225 renderer,
226 operation,
227 );
228 },
229 );
230 }
231
232 fn on_event(
233 &mut self,
234 tree: &mut Tree,
235 event: Event,
236 layout: Layout<'_>,
237 cursor_position: Point,
238 renderer: &Renderer,
239 clipboard: &mut dyn Clipboard,
240 shell: &mut Shell<'_, Message>,
241 ) -> event::Status {
242 update(
243 tree.state.downcast_mut::<State>(),
244 event,
245 layout,
246 cursor_position,
247 clipboard,
248 shell,
249 &self.vertical,
250 self.horizontal.as_ref(),
251 &self.on_scroll,
252 |event, layout, cursor_position, clipboard, shell| {
253 self.content.as_widget_mut().on_event(
254 &mut tree.children[0],
255 event,
256 layout,
257 cursor_position,
258 renderer,
259 clipboard,
260 shell,
261 )
262 },
263 )
264 }
265
266 fn draw(
267 &self,
268 tree: &Tree,
269 renderer: &mut Renderer,
270 theme: &Renderer::Theme,
271 style: &renderer::Style,
272 layout: Layout<'_>,
273 cursor_position: Point,
274 _viewport: &Rectangle,
275 ) {
276 draw(
277 tree.state.downcast_ref::<State>(),
278 renderer,
279 theme,
280 layout,
281 cursor_position,
282 &self.vertical,
283 self.horizontal.as_ref(),
284 &self.style,
285 |renderer, layout, cursor_position, viewport| {
286 self.content.as_widget().draw(
287 &tree.children[0],
288 renderer,
289 theme,
290 style,
291 layout,
292 cursor_position,
293 viewport,
294 )
295 },
296 )
297 }
298
299 fn mouse_interaction(
300 &self,
301 tree: &Tree,
302 layout: Layout<'_>,
303 cursor_position: Point,
304 _viewport: &Rectangle,
305 renderer: &Renderer,
306 ) -> mouse::Interaction {
307 mouse_interaction(
308 tree.state.downcast_ref::<State>(),
309 layout,
310 cursor_position,
311 &self.vertical,
312 self.horizontal.as_ref(),
313 |layout, cursor_position, viewport| {
314 self.content.as_widget().mouse_interaction(
315 &tree.children[0],
316 layout,
317 cursor_position,
318 viewport,
319 renderer,
320 )
321 },
322 )
323 }
324
325 fn overlay<'b>(
326 &'b mut self,
327 tree: &'b mut Tree,
328 layout: Layout<'_>,
329 renderer: &Renderer,
330 ) -> Option<overlay::Element<'b, Message, Renderer>> {
331 self.content
332 .as_widget_mut()
333 .overlay(
334 &mut tree.children[0],
335 layout.children().next().unwrap(),
336 renderer,
337 )
338 .map(|overlay| {
339 let bounds = layout.bounds();
340 let content_layout = layout.children().next().unwrap();
341 let content_bounds = content_layout.bounds();
342 let offset = tree
343 .state
344 .downcast_ref::<State>()
345 .offset(bounds, content_bounds);
346
347 overlay.translate(Vector::new(-offset.x, -offset.y))
348 })
349 }
350}
351
352impl<'a, Message, Renderer> From<Scrollable<'a, Message, Renderer>>
353 for Element<'a, Message, Renderer>
354where
355 Message: 'a,
356 Renderer: 'a + crate::Renderer,
357 Renderer::Theme: StyleSheet,
358{
359 fn from(
360 text_input: Scrollable<'a, Message, Renderer>,
361 ) -> Element<'a, Message, Renderer> {
362 Element::new(text_input)
363 }
364}
365
366#[derive(Debug, Clone, PartialEq, Eq, Hash)]
368pub struct Id(widget::Id);
369
370impl Id {
371 pub fn new(id: impl Into<std::borrow::Cow<'static, str>>) -> Self {
373 Self(widget::Id::new(id))
374 }
375
376 pub fn unique() -> Self {
380 Self(widget::Id::unique())
381 }
382}
383
384impl From<Id> for widget::Id {
385 fn from(id: Id) -> Self {
386 id.0
387 }
388}
389
390pub fn snap_to<Message: 'static>(
393 id: Id,
394 offset: RelativeOffset,
395) -> Command<Message> {
396 Command::widget(operation::scrollable::snap_to(id.0, offset))
397}
398
399pub fn layout<Renderer>(
401 renderer: &Renderer,
402 limits: &layout::Limits,
403 width: Length,
404 height: Length,
405 horizontal_enabled: bool,
406 layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node,
407) -> layout::Node {
408 let limits = limits.width(width).height(height);
409
410 let child_limits = layout::Limits::new(
411 Size::new(limits.min().width, 0.0),
412 Size::new(
413 if horizontal_enabled {
414 f32::INFINITY
415 } else {
416 limits.max().width
417 },
418 f32::MAX,
419 ),
420 );
421
422 let content = layout_content(renderer, &child_limits);
423 let size = limits.resolve(content.size());
424
425 layout::Node::with_children(size, vec![content])
426}
427
428pub fn update<Message>(
431 state: &mut State,
432 event: Event,
433 layout: Layout<'_>,
434 cursor_position: Point,
435 clipboard: &mut dyn Clipboard,
436 shell: &mut Shell<'_, Message>,
437 vertical: &Properties,
438 horizontal: Option<&Properties>,
439 on_scroll: &Option<Box<dyn Fn(RelativeOffset) -> Message + '_>>,
440 update_content: impl FnOnce(
441 Event,
442 Layout<'_>,
443 Point,
444 &mut dyn Clipboard,
445 &mut Shell<'_, Message>,
446 ) -> event::Status,
447) -> event::Status {
448 let bounds = layout.bounds();
449 let mouse_over_scrollable = bounds.contains(cursor_position);
450
451 let content = layout.children().next().unwrap();
452 let content_bounds = content.bounds();
453
454 let scrollbars =
455 Scrollbars::new(state, vertical, horizontal, bounds, content_bounds);
456
457 let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
458 scrollbars.is_mouse_over(cursor_position);
459
460 let event_status = {
461 let cursor_position = if mouse_over_scrollable
462 && !(mouse_over_y_scrollbar || mouse_over_x_scrollbar)
463 {
464 cursor_position + state.offset(bounds, content_bounds)
465 } else {
466 Point::new(-1.0, -1.0)
471 };
472
473 update_content(
474 event.clone(),
475 content,
476 cursor_position,
477 clipboard,
478 shell,
479 )
480 };
481
482 if let event::Status::Captured = event_status {
483 return event::Status::Captured;
484 }
485
486 if let Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) = event
487 {
488 state.keyboard_modifiers = modifiers;
489
490 return event::Status::Ignored;
491 }
492
493 if mouse_over_scrollable {
494 match event {
495 Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
496 let delta = match delta {
497 mouse::ScrollDelta::Lines { x, y } => {
498 let movement = if state.keyboard_modifiers.shift() {
500 Vector::new(y, x)
501 } else {
502 Vector::new(x, y)
503 };
504
505 movement * 60.0
506 }
507 mouse::ScrollDelta::Pixels { x, y } => Vector::new(x, y),
508 };
509
510 state.scroll(delta, bounds, content_bounds);
511
512 notify_on_scroll(
513 state,
514 on_scroll,
515 bounds,
516 content_bounds,
517 shell,
518 );
519
520 return event::Status::Captured;
521 }
522 Event::Touch(event)
523 if state.scroll_area_touched_at.is_some()
524 || !mouse_over_y_scrollbar && !mouse_over_x_scrollbar =>
525 {
526 match event {
527 touch::Event::FingerPressed { .. } => {
528 state.scroll_area_touched_at = Some(cursor_position);
529 }
530 touch::Event::FingerMoved { .. } => {
531 if let Some(scroll_box_touched_at) =
532 state.scroll_area_touched_at
533 {
534 let delta = Vector::new(
535 cursor_position.x - scroll_box_touched_at.x,
536 cursor_position.y - scroll_box_touched_at.y,
537 );
538
539 state.scroll(delta, bounds, content_bounds);
540
541 state.scroll_area_touched_at =
542 Some(cursor_position);
543
544 notify_on_scroll(
545 state,
546 on_scroll,
547 bounds,
548 content_bounds,
549 shell,
550 );
551 }
552 }
553 touch::Event::FingerLifted { .. }
554 | touch::Event::FingerLost { .. } => {
555 state.scroll_area_touched_at = None;
556 }
557 }
558
559 return event::Status::Captured;
560 }
561 _ => {}
562 }
563 }
564
565 if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at {
566 match event {
567 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
568 | Event::Touch(touch::Event::FingerLifted { .. })
569 | Event::Touch(touch::Event::FingerLost { .. }) => {
570 state.y_scroller_grabbed_at = None;
571
572 return event::Status::Captured;
573 }
574 Event::Mouse(mouse::Event::CursorMoved { .. })
575 | Event::Touch(touch::Event::FingerMoved { .. }) => {
576 if let Some(scrollbar) = scrollbars.y {
577 state.scroll_y_to(
578 scrollbar.scroll_percentage_y(
579 scroller_grabbed_at,
580 cursor_position,
581 ),
582 bounds,
583 content_bounds,
584 );
585
586 notify_on_scroll(
587 state,
588 on_scroll,
589 bounds,
590 content_bounds,
591 shell,
592 );
593
594 return event::Status::Captured;
595 }
596 }
597 _ => {}
598 }
599 } else if mouse_over_y_scrollbar {
600 match event {
601 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
602 | Event::Touch(touch::Event::FingerPressed { .. }) => {
603 if let (Some(scroller_grabbed_at), Some(scrollbar)) =
604 (scrollbars.grab_y_scroller(cursor_position), scrollbars.y)
605 {
606 state.scroll_y_to(
607 scrollbar.scroll_percentage_y(
608 scroller_grabbed_at,
609 cursor_position,
610 ),
611 bounds,
612 content_bounds,
613 );
614
615 state.y_scroller_grabbed_at = Some(scroller_grabbed_at);
616
617 notify_on_scroll(
618 state,
619 on_scroll,
620 bounds,
621 content_bounds,
622 shell,
623 );
624 }
625
626 return event::Status::Captured;
627 }
628 _ => {}
629 }
630 }
631
632 if let Some(scroller_grabbed_at) = state.x_scroller_grabbed_at {
633 match event {
634 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
635 | Event::Touch(touch::Event::FingerLifted { .. })
636 | Event::Touch(touch::Event::FingerLost { .. }) => {
637 state.x_scroller_grabbed_at = None;
638
639 return event::Status::Captured;
640 }
641 Event::Mouse(mouse::Event::CursorMoved { .. })
642 | Event::Touch(touch::Event::FingerMoved { .. }) => {
643 if let Some(scrollbar) = scrollbars.x {
644 state.scroll_x_to(
645 scrollbar.scroll_percentage_x(
646 scroller_grabbed_at,
647 cursor_position,
648 ),
649 bounds,
650 content_bounds,
651 );
652
653 notify_on_scroll(
654 state,
655 on_scroll,
656 bounds,
657 content_bounds,
658 shell,
659 );
660 }
661
662 return event::Status::Captured;
663 }
664 _ => {}
665 }
666 } else if mouse_over_x_scrollbar {
667 match event {
668 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
669 | Event::Touch(touch::Event::FingerPressed { .. }) => {
670 if let (Some(scroller_grabbed_at), Some(scrollbar)) =
671 (scrollbars.grab_x_scroller(cursor_position), scrollbars.x)
672 {
673 state.scroll_x_to(
674 scrollbar.scroll_percentage_x(
675 scroller_grabbed_at,
676 cursor_position,
677 ),
678 bounds,
679 content_bounds,
680 );
681
682 state.x_scroller_grabbed_at = Some(scroller_grabbed_at);
683
684 notify_on_scroll(
685 state,
686 on_scroll,
687 bounds,
688 content_bounds,
689 shell,
690 );
691
692 return event::Status::Captured;
693 }
694 }
695 _ => {}
696 }
697 }
698
699 event::Status::Ignored
700}
701
702pub fn mouse_interaction(
704 state: &State,
705 layout: Layout<'_>,
706 cursor_position: Point,
707 vertical: &Properties,
708 horizontal: Option<&Properties>,
709 content_interaction: impl FnOnce(
710 Layout<'_>,
711 Point,
712 &Rectangle,
713 ) -> mouse::Interaction,
714) -> mouse::Interaction {
715 let bounds = layout.bounds();
716 let mouse_over_scrollable = bounds.contains(cursor_position);
717
718 let content_layout = layout.children().next().unwrap();
719 let content_bounds = content_layout.bounds();
720
721 let scrollbars =
722 Scrollbars::new(state, vertical, horizontal, bounds, content_bounds);
723
724 let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
725 scrollbars.is_mouse_over(cursor_position);
726
727 if (mouse_over_x_scrollbar || mouse_over_y_scrollbar)
728 || state.scrollers_grabbed()
729 {
730 mouse::Interaction::Idle
731 } else {
732 let offset = state.offset(bounds, content_bounds);
733
734 let cursor_position = if mouse_over_scrollable
735 && !(mouse_over_y_scrollbar || mouse_over_x_scrollbar)
736 {
737 cursor_position + offset
738 } else {
739 Point::new(-1.0, -1.0)
740 };
741
742 content_interaction(
743 content_layout,
744 cursor_position,
745 &Rectangle {
746 y: bounds.y + offset.y,
747 x: bounds.x + offset.x,
748 ..bounds
749 },
750 )
751 }
752}
753
754pub fn draw<Renderer>(
756 state: &State,
757 renderer: &mut Renderer,
758 theme: &Renderer::Theme,
759 layout: Layout<'_>,
760 cursor_position: Point,
761 vertical: &Properties,
762 horizontal: Option<&Properties>,
763 style: &<Renderer::Theme as StyleSheet>::Style,
764 draw_content: impl FnOnce(&mut Renderer, Layout<'_>, Point, &Rectangle),
765) where
766 Renderer: crate::Renderer,
767 Renderer::Theme: StyleSheet,
768{
769 let bounds = layout.bounds();
770 let content_layout = layout.children().next().unwrap();
771 let content_bounds = content_layout.bounds();
772
773 let scrollbars =
774 Scrollbars::new(state, vertical, horizontal, bounds, content_bounds);
775
776 let mouse_over_scrollable = bounds.contains(cursor_position);
777 let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
778 scrollbars.is_mouse_over(cursor_position);
779
780 let offset = state.offset(bounds, content_bounds);
781
782 let cursor_position = if mouse_over_scrollable
783 && !(mouse_over_x_scrollbar || mouse_over_y_scrollbar)
784 {
785 cursor_position + offset
786 } else {
787 Point::new(-1.0, -1.0)
788 };
789
790 if scrollbars.active() {
792 renderer.with_layer(bounds, |renderer| {
793 renderer.with_translation(
794 Vector::new(-offset.x, -offset.y),
795 |renderer| {
796 draw_content(
797 renderer,
798 content_layout,
799 cursor_position,
800 &Rectangle {
801 y: bounds.y + offset.y,
802 x: bounds.x + offset.x,
803 ..bounds
804 },
805 );
806 },
807 );
808 });
809
810 let draw_scrollbar =
811 |renderer: &mut Renderer,
812 style: style::Scrollbar,
813 scrollbar: &Scrollbar| {
814 if style.background.is_some()
816 || (style.border_color != Color::TRANSPARENT
817 && style.border_width > 0.0)
818 {
819 renderer.fill_quad(
820 renderer::Quad {
821 bounds: scrollbar.bounds,
822 border_radius: style.border_radius.into(),
823 border_width: style.border_width,
824 border_color: style.border_color,
825 },
826 style
827 .background
828 .unwrap_or(Background::Color(Color::TRANSPARENT)),
829 );
830 }
831
832 if style.scroller.color != Color::TRANSPARENT
834 || (style.scroller.border_color != Color::TRANSPARENT
835 && style.scroller.border_width > 0.0)
836 {
837 renderer.fill_quad(
838 renderer::Quad {
839 bounds: scrollbar.scroller.bounds,
840 border_radius: style.scroller.border_radius.into(),
841 border_width: style.scroller.border_width,
842 border_color: style.scroller.border_color,
843 },
844 style.scroller.color,
845 );
846 }
847 };
848
849 renderer.with_layer(
850 Rectangle {
851 width: bounds.width + 2.0,
852 height: bounds.height + 2.0,
853 ..bounds
854 },
855 |renderer| {
856 if let Some(scrollbar) = scrollbars.y {
858 let style = if state.y_scroller_grabbed_at.is_some() {
859 theme.dragging(style)
860 } else if mouse_over_scrollable {
861 theme.hovered(style, mouse_over_y_scrollbar)
862 } else {
863 theme.active(style)
864 };
865
866 draw_scrollbar(renderer, style, &scrollbar);
867 }
868
869 if let Some(scrollbar) = scrollbars.x {
871 let style = if state.x_scroller_grabbed_at.is_some() {
872 theme.dragging_horizontal(style)
873 } else if mouse_over_scrollable {
874 theme.hovered_horizontal(style, mouse_over_x_scrollbar)
875 } else {
876 theme.active_horizontal(style)
877 };
878
879 draw_scrollbar(renderer, style, &scrollbar);
880 }
881 },
882 );
883 } else {
884 draw_content(
885 renderer,
886 content_layout,
887 cursor_position,
888 &Rectangle {
889 x: bounds.x + offset.x,
890 y: bounds.y + offset.y,
891 ..bounds
892 },
893 );
894 }
895}
896
897fn notify_on_scroll<Message>(
898 state: &mut State,
899 on_scroll: &Option<Box<dyn Fn(RelativeOffset) -> Message + '_>>,
900 bounds: Rectangle,
901 content_bounds: Rectangle,
902 shell: &mut Shell<'_, Message>,
903) {
904 if let Some(on_scroll) = on_scroll {
905 if content_bounds.width <= bounds.width
906 && content_bounds.height <= bounds.height
907 {
908 return;
909 }
910
911 let x = state.offset_x.absolute(bounds.width, content_bounds.width)
912 / (content_bounds.width - bounds.width);
913
914 let y = state
915 .offset_y
916 .absolute(bounds.height, content_bounds.height)
917 / (content_bounds.height - bounds.height);
918
919 let new_offset = RelativeOffset { x, y };
920
921 if let Some(prev_offset) = state.last_notified {
923 let unchanged = |a: f32, b: f32| {
924 (a - b).abs() <= f32::EPSILON || (a.is_nan() && b.is_nan())
925 };
926
927 if unchanged(prev_offset.x, new_offset.x)
928 && unchanged(prev_offset.y, new_offset.y)
929 {
930 return;
931 }
932 }
933
934 shell.publish(on_scroll(new_offset));
935 state.last_notified = Some(new_offset);
936 }
937}
938
939#[derive(Debug, Clone, Copy)]
941pub struct State {
942 scroll_area_touched_at: Option<Point>,
943 offset_y: Offset,
944 y_scroller_grabbed_at: Option<f32>,
945 offset_x: Offset,
946 x_scroller_grabbed_at: Option<f32>,
947 keyboard_modifiers: keyboard::Modifiers,
948 last_notified: Option<RelativeOffset>,
949}
950
951impl Default for State {
952 fn default() -> Self {
953 Self {
954 scroll_area_touched_at: None,
955 offset_y: Offset::Absolute(0.0),
956 y_scroller_grabbed_at: None,
957 offset_x: Offset::Absolute(0.0),
958 x_scroller_grabbed_at: None,
959 keyboard_modifiers: keyboard::Modifiers::default(),
960 last_notified: None,
961 }
962 }
963}
964
965impl operation::Scrollable for State {
966 fn snap_to(&mut self, offset: RelativeOffset) {
967 State::snap_to(self, offset);
968 }
969}
970
971#[derive(Debug, Clone, Copy)]
972enum Offset {
973 Absolute(f32),
974 Relative(f32),
975}
976
977impl Offset {
978 fn absolute(self, window: f32, content: f32) -> f32 {
979 match self {
980 Offset::Absolute(absolute) => {
981 absolute.min((content - window).max(0.0))
982 }
983 Offset::Relative(percentage) => {
984 ((content - window) * percentage).max(0.0)
985 }
986 }
987 }
988}
989
990impl State {
991 pub fn new() -> Self {
993 State::default()
994 }
995
996 pub fn scroll(
999 &mut self,
1000 delta: Vector<f32>,
1001 bounds: Rectangle,
1002 content_bounds: Rectangle,
1003 ) {
1004 if bounds.height < content_bounds.height {
1005 self.offset_y = Offset::Absolute(
1006 (self.offset_y.absolute(bounds.height, content_bounds.height)
1007 - delta.y)
1008 .clamp(0.0, content_bounds.height - bounds.height),
1009 )
1010 }
1011
1012 if bounds.width < content_bounds.width {
1013 self.offset_x = Offset::Absolute(
1014 (self.offset_x.absolute(bounds.width, content_bounds.width)
1015 - delta.x)
1016 .clamp(0.0, content_bounds.width - bounds.width),
1017 );
1018 }
1019 }
1020
1021 pub fn scroll_y_to(
1026 &mut self,
1027 percentage: f32,
1028 bounds: Rectangle,
1029 content_bounds: Rectangle,
1030 ) {
1031 self.offset_y = Offset::Relative(percentage.clamp(0.0, 1.0));
1032 self.unsnap(bounds, content_bounds);
1033 }
1034
1035 pub fn scroll_x_to(
1040 &mut self,
1041 percentage: f32,
1042 bounds: Rectangle,
1043 content_bounds: Rectangle,
1044 ) {
1045 self.offset_x = Offset::Relative(percentage.clamp(0.0, 1.0));
1046 self.unsnap(bounds, content_bounds);
1047 }
1048
1049 pub fn snap_to(&mut self, offset: RelativeOffset) {
1051 self.offset_x = Offset::Relative(offset.x.clamp(0.0, 1.0));
1052 self.offset_y = Offset::Relative(offset.y.clamp(0.0, 1.0));
1053 }
1054
1055 pub fn unsnap(&mut self, bounds: Rectangle, content_bounds: Rectangle) {
1058 self.offset_x = Offset::Absolute(
1059 self.offset_x.absolute(bounds.width, content_bounds.width),
1060 );
1061 self.offset_y = Offset::Absolute(
1062 self.offset_y.absolute(bounds.height, content_bounds.height),
1063 );
1064 }
1065
1066 pub fn offset(
1069 &self,
1070 bounds: Rectangle,
1071 content_bounds: Rectangle,
1072 ) -> Vector {
1073 Vector::new(
1074 self.offset_x.absolute(bounds.width, content_bounds.width),
1075 self.offset_y.absolute(bounds.height, content_bounds.height),
1076 )
1077 }
1078
1079 pub fn scrollers_grabbed(&self) -> bool {
1081 self.x_scroller_grabbed_at.is_some()
1082 || self.y_scroller_grabbed_at.is_some()
1083 }
1084}
1085
1086#[derive(Debug)]
1087struct Scrollbars {
1089 y: Option<Scrollbar>,
1090 x: Option<Scrollbar>,
1091}
1092
1093impl Scrollbars {
1094 fn new(
1096 state: &State,
1097 vertical: &Properties,
1098 horizontal: Option<&Properties>,
1099 bounds: Rectangle,
1100 content_bounds: Rectangle,
1101 ) -> Self {
1102 let offset = state.offset(bounds, content_bounds);
1103
1104 let show_scrollbar_x = horizontal.and_then(|h| {
1105 if content_bounds.width > bounds.width {
1106 Some(h)
1107 } else {
1108 None
1109 }
1110 });
1111
1112 let y_scrollbar = if content_bounds.height > bounds.height {
1113 let Properties {
1114 width,
1115 margin,
1116 scroller_width,
1117 } = *vertical;
1118
1119 let x_scrollbar_height = show_scrollbar_x
1122 .map_or(0.0, |h| h.width.max(h.scroller_width) + h.margin);
1123
1124 let total_scrollbar_width =
1125 width.max(scroller_width) + 2.0 * margin;
1126
1127 let total_scrollbar_bounds = Rectangle {
1129 x: bounds.x + bounds.width - total_scrollbar_width,
1130 y: bounds.y,
1131 width: total_scrollbar_width,
1132 height: (bounds.height - x_scrollbar_height).max(0.0),
1133 };
1134
1135 let scrollbar_bounds = Rectangle {
1137 x: bounds.x + bounds.width
1138 - total_scrollbar_width / 2.0
1139 - width / 2.0,
1140 y: bounds.y,
1141 width,
1142 height: (bounds.height - x_scrollbar_height).max(0.0),
1143 };
1144
1145 let ratio = bounds.height / content_bounds.height;
1146 let scroller_height = (bounds.height * ratio).max(2.0);
1148 let scroller_offset = offset.y * ratio;
1149
1150 let scroller_bounds = Rectangle {
1151 x: bounds.x + bounds.width
1152 - total_scrollbar_width / 2.0
1153 - scroller_width / 2.0,
1154 y: (scrollbar_bounds.y + scroller_offset - x_scrollbar_height)
1155 .max(0.0),
1156 width: scroller_width,
1157 height: scroller_height,
1158 };
1159
1160 Some(Scrollbar {
1161 total_bounds: total_scrollbar_bounds,
1162 bounds: scrollbar_bounds,
1163 scroller: Scroller {
1164 bounds: scroller_bounds,
1165 },
1166 })
1167 } else {
1168 None
1169 };
1170
1171 let x_scrollbar = if let Some(horizontal) = show_scrollbar_x {
1172 let Properties {
1173 width,
1174 margin,
1175 scroller_width,
1176 } = *horizontal;
1177
1178 let scrollbar_y_width = y_scrollbar.map_or(0.0, |_| {
1181 vertical.width.max(vertical.scroller_width) + vertical.margin
1182 });
1183
1184 let total_scrollbar_height =
1185 width.max(scroller_width) + 2.0 * margin;
1186
1187 let total_scrollbar_bounds = Rectangle {
1189 x: bounds.x,
1190 y: bounds.y + bounds.height - total_scrollbar_height,
1191 width: (bounds.width - scrollbar_y_width).max(0.0),
1192 height: total_scrollbar_height,
1193 };
1194
1195 let scrollbar_bounds = Rectangle {
1197 x: bounds.x,
1198 y: bounds.y + bounds.height
1199 - total_scrollbar_height / 2.0
1200 - width / 2.0,
1201 width: (bounds.width - scrollbar_y_width).max(0.0),
1202 height: width,
1203 };
1204
1205 let ratio = bounds.width / content_bounds.width;
1206 let scroller_length = (bounds.width * ratio).max(2.0);
1208 let scroller_offset = offset.x * ratio;
1209
1210 let scroller_bounds = Rectangle {
1211 x: (scrollbar_bounds.x + scroller_offset - scrollbar_y_width)
1212 .max(0.0),
1213 y: bounds.y + bounds.height
1214 - total_scrollbar_height / 2.0
1215 - scroller_width / 2.0,
1216 width: scroller_length,
1217 height: scroller_width,
1218 };
1219
1220 Some(Scrollbar {
1221 total_bounds: total_scrollbar_bounds,
1222 bounds: scrollbar_bounds,
1223 scroller: Scroller {
1224 bounds: scroller_bounds,
1225 },
1226 })
1227 } else {
1228 None
1229 };
1230
1231 Self {
1232 y: y_scrollbar,
1233 x: x_scrollbar,
1234 }
1235 }
1236
1237 fn is_mouse_over(&self, cursor_position: Point) -> (bool, bool) {
1238 (
1239 self.y
1240 .as_ref()
1241 .map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
1242 .unwrap_or(false),
1243 self.x
1244 .as_ref()
1245 .map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
1246 .unwrap_or(false),
1247 )
1248 }
1249
1250 fn grab_y_scroller(&self, cursor_position: Point) -> Option<f32> {
1251 self.y.and_then(|scrollbar| {
1252 if scrollbar.total_bounds.contains(cursor_position) {
1253 Some(if scrollbar.scroller.bounds.contains(cursor_position) {
1254 (cursor_position.y - scrollbar.scroller.bounds.y)
1255 / scrollbar.scroller.bounds.height
1256 } else {
1257 0.5
1258 })
1259 } else {
1260 None
1261 }
1262 })
1263 }
1264
1265 fn grab_x_scroller(&self, cursor_position: Point) -> Option<f32> {
1266 self.x.and_then(|scrollbar| {
1267 if scrollbar.total_bounds.contains(cursor_position) {
1268 Some(if scrollbar.scroller.bounds.contains(cursor_position) {
1269 (cursor_position.x - scrollbar.scroller.bounds.x)
1270 / scrollbar.scroller.bounds.width
1271 } else {
1272 0.5
1273 })
1274 } else {
1275 None
1276 }
1277 })
1278 }
1279
1280 fn active(&self) -> bool {
1281 self.y.is_some() || self.x.is_some()
1282 }
1283}
1284
1285#[derive(Debug, Copy, Clone)]
1287struct Scrollbar {
1288 total_bounds: Rectangle,
1291
1292 bounds: Rectangle,
1294
1295 scroller: Scroller,
1297}
1298
1299impl Scrollbar {
1300 fn is_mouse_over(&self, cursor_position: Point) -> bool {
1302 self.total_bounds.contains(cursor_position)
1303 }
1304
1305 fn scroll_percentage_y(
1307 &self,
1308 grabbed_at: f32,
1309 cursor_position: Point,
1310 ) -> f32 {
1311 if cursor_position.x < 0.0 && cursor_position.y < 0.0 {
1312 (self.scroller.bounds.y / self.total_bounds.height).round()
1315 } else {
1316 (cursor_position.y
1317 - self.bounds.y
1318 - self.scroller.bounds.height * grabbed_at)
1319 / (self.bounds.height - self.scroller.bounds.height)
1320 }
1321 }
1322
1323 fn scroll_percentage_x(
1325 &self,
1326 grabbed_at: f32,
1327 cursor_position: Point,
1328 ) -> f32 {
1329 if cursor_position.x < 0.0 && cursor_position.y < 0.0 {
1330 (self.scroller.bounds.x / self.total_bounds.width).round()
1331 } else {
1332 (cursor_position.x
1333 - self.bounds.x
1334 - self.scroller.bounds.width * grabbed_at)
1335 / (self.bounds.width - self.scroller.bounds.width)
1336 }
1337 }
1338}
1339
1340#[derive(Debug, Clone, Copy)]
1342struct Scroller {
1343 bounds: Rectangle,
1345}