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
8pub struct Scroll<'a, T> {
12 content: Option<Node<'a, T>>,
13 scrollbar_h: Node<'a, T>,
14 scrollbar_v: Node<'a, T>,
15}
16
17pub 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 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 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}