pixel_widgets/widget/
scroll.rs

1use crate::draw::*;
2use crate::event::{Event, Key};
3use crate::layout::{Rectangle, Size};
4use crate::node::{GenericNode, IntoNode, Node};
5use crate::style::Stylesheet;
6use crate::widget::{dummy::Dummy, Context, Widget};
7
8/// View a small section of larger widget, with scrollbars.
9/// The scrollbars are only rendered if the content is larger than the view in that direction.
10/// The scrollbars can be styled using the `scrollbar-horizontal` and `scrollbar-vertical` child widgets of this widget.
11pub struct Scroll<'a, T> {
12    content: Option<Node<'a, T>>,
13    scrollbar_h: Node<'a, T>,
14    scrollbar_v: Node<'a, T>,
15}
16
17/// State for [`Scroll`](struct.Scroll.html)
18pub struct State {
19    inner: InnerState,
20    scroll_x: f32,
21    scroll_y: f32,
22    cursor_x: f32,
23    cursor_y: f32,
24}
25
26#[derive(Clone, Copy)]
27enum InnerState {
28    Idle,
29    HoverHorizontalBar,
30    HoverVerticalBar,
31    DragHorizontalBar(f32),
32    DragVerticalBar(f32),
33}
34
35impl<'a, T: 'a> Scroll<'a, T> {
36    /// Construct a new `Scroll`
37    pub fn new(content: impl IntoNode<'a, T>) -> Scroll<'a, T> {
38        Self {
39            content: Some(content.into_node()),
40            scrollbar_h: Dummy::new("scrollbar-horizontal").into_node(),
41            scrollbar_v: Dummy::new("scrollbar-vertical").into_node(),
42        }
43    }
44
45    /// Sets the content widget from the first element of an iterator.
46    pub fn extend<I: IntoIterator<Item = N>, N: IntoNode<'a, T>>(mut self, iter: I) -> Self {
47        if self.content.is_none() {
48            self.content = iter.into_iter().next().map(IntoNode::into_node);
49        }
50        self
51    }
52
53    fn scrollbars(
54        &self,
55        state: &State,
56        layout: Rectangle,
57        content: Rectangle,
58        style: &Stylesheet,
59    ) -> (Rectangle, Rectangle) {
60        let content_rect = style.background.content_rect(layout, style.padding);
61
62        let vertical_rect = {
63            let mut bar = Rectangle {
64                left: content_rect.right,
65                top: layout.top,
66                right: layout.right,
67                bottom: content_rect.bottom,
68            };
69            let handle_range = handle_range(
70                bar.top,
71                state.scroll_y,
72                bar.height(),
73                content.height() - content_rect.height(),
74            );
75            bar.top = handle_range.0;
76            bar.bottom = handle_range.1;
77            bar
78        };
79
80        let horizontal_rect = {
81            let mut bar = Rectangle {
82                left: layout.left,
83                top: content_rect.bottom,
84                right: content_rect.right,
85                bottom: layout.bottom,
86            };
87            let handle_range = handle_range(
88                bar.left,
89                state.scroll_x,
90                bar.width(),
91                content.width() - content_rect.width(),
92            );
93            bar.left = handle_range.0;
94            bar.right = handle_range.1;
95            bar
96        };
97
98        (vertical_rect, horizontal_rect)
99    }
100
101    fn content_layout(&self, state: &State, content_rect: &Rectangle) -> Rectangle {
102        let content_size = self.content().size();
103        Rectangle::from_xywh(
104            content_rect.left - state.scroll_x,
105            content_rect.top - state.scroll_y,
106            content_size
107                .0
108                .resolve(content_rect.width(), content_size.0.parts())
109                .max(content_size.0.min_size()),
110            content_size
111                .1
112                .resolve(content_rect.height(), content_size.1.parts())
113                .max(content_size.1.min_size()),
114        )
115    }
116
117    fn content(&self) -> &Node<'a, T> {
118        self.content.as_ref().expect("content of `Scroll` must be set")
119    }
120
121    fn content_mut(&mut self) -> &mut Node<'a, T> {
122        self.content.as_mut().expect("content of `Scroll` must be set")
123    }
124}
125
126impl<'a, T: 'a> Default for Scroll<'a, T> {
127    fn default() -> Self {
128        Self {
129            content: None,
130            scrollbar_h: Dummy::new("scrollbar-horizontal").into_node(),
131            scrollbar_v: Dummy::new("scrollbar-vertical").into_node(),
132        }
133    }
134}
135
136impl<'a, T: 'a> Widget<'a, T> for Scroll<'a, T> {
137    type State = State;
138
139    fn mount(&self) -> Self::State {
140        State::default()
141    }
142
143    fn widget(&self) -> &'static str {
144        "scroll"
145    }
146
147    fn len(&self) -> usize {
148        3
149    }
150
151    fn visit_children(&mut self, visitor: &mut dyn FnMut(&mut dyn GenericNode<'a, T>)) {
152        visitor(&mut **self.content_mut());
153        visitor(&mut *self.scrollbar_h);
154        visitor(&mut *self.scrollbar_v);
155    }
156
157    fn size(&self, _: &State, style: &Stylesheet) -> (Size, Size) {
158        style
159            .background
160            .resolve_size((style.width, style.height), self.content().size(), style.padding)
161    }
162
163    fn focused(&self, _: &State) -> bool {
164        self.content().focused()
165    }
166
167    fn event(
168        &mut self,
169        state: &mut State,
170        layout: Rectangle,
171        clip: Rectangle,
172        style: &Stylesheet,
173        event: Event,
174        context: &mut Context<T>,
175    ) {
176        let content_rect = style.background.content_rect(layout, style.padding);
177        let content_layout = self.content_layout(&*state, &content_rect);
178        let (vbar, hbar) = self.scrollbars(&*state, layout, content_layout, style);
179
180        if self.content().focused() {
181            self.content_mut().event(content_layout, content_rect, event, context);
182            return;
183        }
184
185        match (event, state.inner) {
186            (Event::Cursor(cx, cy), InnerState::DragHorizontalBar(x)) => {
187                context.redraw();
188                state.cursor_x = cx;
189                state.cursor_y = cy;
190
191                let bar = Rectangle {
192                    left: layout.left,
193                    top: content_rect.bottom,
194                    right: content_rect.right,
195                    bottom: layout.bottom,
196                };
197                state.scroll_x = handle_to_scroll(
198                    bar.left,
199                    cx - x,
200                    bar.width(),
201                    content_layout.width() - content_rect.width(),
202                );
203            }
204            (Event::Cursor(cx, cy), InnerState::DragVerticalBar(y)) => {
205                context.redraw();
206                state.cursor_x = cx;
207                state.cursor_y = cy;
208
209                let bar = Rectangle {
210                    left: content_rect.right,
211                    top: layout.top,
212                    right: layout.right,
213                    bottom: content_rect.bottom,
214                };
215                state.scroll_y = handle_to_scroll(
216                    bar.top,
217                    cy - y,
218                    bar.height(),
219                    content_layout.height() - content_rect.height(),
220                );
221            }
222            (Event::Cursor(x, y), _) => {
223                if let Some(clip) = clip.intersect(&content_rect) {
224                    self.content_mut().event(content_layout, clip, event, context);
225                }
226                state.cursor_x = x;
227                state.cursor_y = y;
228                if hbar.point_inside(x, y) && clip.point_inside(x, y) {
229                    state.inner = InnerState::HoverHorizontalBar;
230                } else if vbar.point_inside(x, y) && clip.point_inside(x, y) {
231                    state.inner = InnerState::HoverVerticalBar;
232                } else {
233                    state.inner = InnerState::Idle;
234                }
235            }
236            (Event::Press(Key::LeftMouseButton), InnerState::HoverHorizontalBar) => {
237                state.inner = InnerState::DragHorizontalBar(state.cursor_x - hbar.left);
238            }
239            (Event::Press(Key::LeftMouseButton), InnerState::HoverVerticalBar) => {
240                state.inner = InnerState::DragVerticalBar(state.cursor_y - vbar.top);
241            }
242            (Event::Release(Key::LeftMouseButton), InnerState::DragHorizontalBar(_))
243            | (Event::Release(Key::LeftMouseButton), InnerState::DragVerticalBar(_)) => {
244                if hbar.point_inside(state.cursor_x, state.cursor_y)
245                    && clip.point_inside(state.cursor_x, state.cursor_y)
246                {
247                    state.inner = InnerState::HoverHorizontalBar;
248                } else if vbar.point_inside(state.cursor_x, state.cursor_y)
249                    && clip.point_inside(state.cursor_x, state.cursor_y)
250                {
251                    state.inner = InnerState::HoverVerticalBar;
252                } else {
253                    state.inner = InnerState::Idle;
254                }
255            }
256            (event, InnerState::Idle) => {
257                if let Some(clip) = clip.intersect(&content_rect) {
258                    self.content_mut().event(content_layout, clip, event, context);
259                }
260            }
261            _ => (),
262        }
263    }
264
265    fn draw(
266        &mut self,
267        state: &mut State,
268        layout: Rectangle,
269        clip: Rectangle,
270        style: &Stylesheet,
271    ) -> Vec<Primitive<'a>> {
272        let content_rect = style.background.content_rect(layout, style.padding);
273        let content_layout = self.content_layout(&*state, &content_rect);
274        let (vbar, hbar) = self.scrollbars(&*state, layout, content_layout, style);
275
276        let mut result = Vec::new();
277        result.extend(style.background.render(layout));
278        if let Some(clip) = clip.intersect(&content_rect) {
279            result.push(Primitive::PushClip(clip));
280            result.extend(self.content_mut().draw(content_layout, content_rect));
281            result.push(Primitive::PopClip);
282        }
283        if content_layout.width() > layout.width() {
284            result.extend(self.scrollbar_h.draw(hbar, clip));
285        }
286        if content_layout.height() > layout.height() {
287            result.extend(self.scrollbar_v.draw(vbar, clip));
288        }
289        result
290    }
291}
292
293impl<'a, T: 'a> IntoNode<'a, T> for Scroll<'a, T> {
294    fn into_node(self) -> Node<'a, T> {
295        Node::from_widget(self)
296    }
297}
298
299impl Default for State {
300    fn default() -> State {
301        State {
302            inner: InnerState::Idle,
303            scroll_x: 0.0,
304            scroll_y: 0.0,
305            cursor_x: 0.0,
306            cursor_y: 0.0,
307        }
308    }
309}
310
311fn handle_to_scroll(offset: f32, x: f32, length: f32, content: f32) -> f32 {
312    if content > 0.0 {
313        let range = handle_range(offset, content, length, content);
314        let pos = (x - offset) / (range.0 - offset);
315        (pos * content).max(0.0).min(content).floor()
316    } else {
317        0.0
318    }
319}
320
321fn handle_range(offset: f32, x: f32, length: f32, content: f32) -> (f32, f32) {
322    if content > 0.0 {
323        let size = length * (length / (length + content));
324        let start = length * (x / (length + content));
325        ((offset + start).floor(), (offset + start + size).floor())
326    } else {
327        (offset.floor(), (offset + length).floor())
328    }
329}