yakui_widgets/widgets/
scrollable.rs

1use std::cell::Cell;
2
3use yakui_core::event::{EventInterest, EventResponse, WidgetEvent};
4use yakui_core::geometry::{Constraints, Vec2};
5use yakui_core::widget::{EventContext, LayoutContext, PaintContext, Widget};
6use yakui_core::Response;
7
8use crate::util::widget_children;
9
10#[derive(Debug)]
11#[non_exhaustive]
12#[must_use = "yakui widgets do nothing if you don't `show` them"]
13pub struct Scrollable {
14    pub direction: Option<ScrollDirection>,
15}
16
17impl Scrollable {
18    pub fn none() -> Self {
19        Scrollable { direction: None }
20    }
21
22    pub fn vertical() -> Self {
23        Scrollable {
24            direction: Some(ScrollDirection::Y),
25        }
26    }
27
28    pub fn show<F: FnOnce()>(self, children: F) -> Response<ScrollableResponse> {
29        widget_children::<ScrollableWidget, F>(children, self)
30    }
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum ScrollDirection {
35    Y,
36}
37
38#[derive(Debug)]
39#[non_exhaustive]
40pub struct ScrollableWidget {
41    props: Scrollable,
42    scroll_position: Cell<Vec2>,
43    canvas_size: Cell<Vec2>,
44}
45
46pub type ScrollableResponse = ();
47
48impl Widget for ScrollableWidget {
49    type Props<'a> = Scrollable;
50    type Response = ScrollableResponse;
51
52    fn new() -> Self {
53        Self {
54            props: Scrollable::none(),
55            scroll_position: Cell::new(Vec2::ZERO),
56            canvas_size: Cell::new(Vec2::ZERO),
57        }
58    }
59
60    fn update(&mut self, props: Self::Props<'_>) -> Self::Response {
61        self.props = props;
62    }
63
64    fn layout(&self, mut ctx: LayoutContext<'_>, constraints: Constraints) -> Vec2 {
65        ctx.layout.enable_clipping(ctx.dom);
66
67        let node = ctx.dom.get_current();
68        let mut canvas_size = Vec2::ZERO;
69
70        let child_constraints = match self.props.direction {
71            None => constraints,
72            Some(ScrollDirection::Y) => Constraints {
73                min: Vec2::new(constraints.min.x, 0.0),
74                max: Vec2::new(constraints.max.x, f32::INFINITY),
75            },
76        };
77
78        for &child in &node.children {
79            let child_size = ctx.calculate_layout(child, child_constraints);
80            canvas_size = canvas_size.max(child_size);
81        }
82        self.canvas_size.set(canvas_size);
83
84        let size = constraints.constrain(canvas_size);
85
86        let max_scroll_position = (canvas_size - size).max(Vec2::ZERO);
87        let mut scroll_position = self
88            .scroll_position
89            .get()
90            .min(max_scroll_position)
91            .max(Vec2::ZERO);
92
93        match self.props.direction {
94            None => scroll_position = Vec2::ZERO,
95            Some(ScrollDirection::Y) => scroll_position.x = 0.0,
96        }
97
98        self.scroll_position.set(scroll_position);
99
100        for &child in &node.children {
101            ctx.layout.set_pos(child, -scroll_position);
102        }
103
104        size
105    }
106
107    fn paint(&self, mut ctx: PaintContext<'_>) {
108        let node = ctx.dom.get_current();
109
110        for &child in &node.children {
111            ctx.paint(child);
112        }
113    }
114
115    fn event_interest(&self) -> EventInterest {
116        EventInterest::MOUSE_INSIDE
117    }
118
119    fn event(&mut self, _ctx: EventContext<'_>, event: &WidgetEvent) -> EventResponse {
120        match *event {
121            WidgetEvent::MouseScroll { delta } => {
122                let pos = self.scroll_position.get();
123                self.scroll_position.set(pos + delta);
124                EventResponse::Sink
125            }
126            _ => EventResponse::Bubble,
127        }
128    }
129}