pix_engine/gui/
scroll.rs

1//! UI scrollbar rendering functions.
2
3use super::state::ElementId;
4use crate::{error::Result, ops::clamp_size, prelude::*};
5
6pub(crate) const THUMB_MIN: i32 = 10;
7pub(crate) const SCROLL_SPEED: i32 = 3;
8
9/// Scroll direction.
10#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
11pub(crate) enum ScrollDirection {
12    /// Horizontal.
13    Horizontal,
14    /// Vertical.
15    Vertical,
16}
17
18impl PixState {
19    /// Draw a scrollable region to the current canvas.
20    ///
21    /// # Errors
22    ///
23    /// If the renderer fails to draw to the current render target, then an error is returned.
24    pub fn scroll_area<S, F>(&mut self, label: S, width: u32, height: u32, f: F) -> Result<()>
25    where
26        S: AsRef<str>,
27        F: FnOnce(&mut PixState) -> Result<()>,
28    {
29        let label = label.as_ref();
30
31        let s = self;
32        let id = s.ui.get_id(&label);
33        let label = s.ui.get_label(label);
34        let pos = s.cursor_pos();
35        let spacing = s.theme.spacing;
36        let colors = s.theme.colors;
37        let fpad = spacing.frame_pad;
38        let ipad = spacing.item_pad;
39
40        // Calculate rect
41        let [x, mut y] = pos.coords();
42        let (label_width, label_height) = s.text_size(label)?;
43        if !label.is_empty() {
44            y += label_height + ipad.y();
45        }
46        let scroll_area = rect![x, y, clamp_size(width), clamp_size(height)];
47
48        // Check hover/active/keyboard focus
49        if s.focused() {
50            s.ui.try_hover(id, &scroll_area);
51            s.ui.try_focus(id);
52        }
53
54        s.push();
55        s.ui.push_cursor();
56
57        // Label
58        if !label.is_empty() {
59            s.text(label)?;
60        }
61
62        // Scroll area
63        s.rect_mode(RectMode::Corner);
64        let [stroke, _, fg] = s.widget_colors(id, ColorType::Background);
65        let scroll = s.ui.scroll(id);
66        let texture_id = s.get_or_create_texture(id, None, scroll_area)?;
67        s.ui.offset_mouse(scroll_area.top_left());
68        s.ui.set_column_offset(-scroll.x());
69
70        let scroll_width = scroll_area.width();
71        let scroll_height = scroll_area.height();
72        let right = scroll_area.width() - fpad.x();
73        let bottom = scroll_area.height() - fpad.y();
74
75        s.set_texture_target(texture_id)?;
76        s.background(colors.background);
77
78        s.set_cursor_pos(s.cursor_pos() - scroll);
79        s.stroke(None);
80        s.fill(fg);
81        f(s)?;
82        let max_cursor_pos = s.cursor_pos() + scroll;
83
84        // Since clip doesn't work texture targets, we fake it
85        s.fill(colors.background);
86        s.rect([0, 0, scroll_width, fpad.y()])?; // Top
87        s.rect([0, 0, fpad.x(), scroll_height])?; // Left
88        s.rect([right, 0, fpad.x(), scroll_height])?; // Right
89        s.rect([0, bottom, scroll_width, fpad.y()])?; // Bottom
90
91        s.stroke(stroke);
92        s.fill(None);
93        s.rect([0, 0, scroll_width, scroll_height])?;
94        s.clear_texture_target();
95
96        s.ui.reset_column_offset();
97        s.ui.clear_mouse_offset();
98
99        s.ui.pop_cursor();
100        s.pop();
101
102        s.ui.handle_focus(id);
103
104        // Scrollbars
105        let total_width = max_cursor_pos.x() + s.ui.last_width() + fpad.x();
106        let total_height = max_cursor_pos.y() + fpad.y();
107        let rect = s.scroll(id, scroll_area, total_width, total_height)?;
108        s.advance_cursor([rect.width().max(label_width), rect.bottom() - pos.y()]);
109
110        Ok(())
111    }
112}
113
114impl PixState {
115    /// Handles mouse wheel scroll for `hovered` elements.
116    pub(crate) fn scroll(
117        &mut self,
118        id: ElementId,
119        rect: Rect<i32>,
120        width: i32,
121        height: i32,
122    ) -> Result<Rect<i32>> {
123        let s = self;
124        let scroll_size = s.theme.spacing.scroll_size;
125
126        let scroll = s.ui.scroll(id);
127        let xmax = width - rect.width();
128        let ymax = height - rect.height();
129        let mut new_scroll = scroll;
130
131        // Vertical scroll
132        if ymax > 0 {
133            if s.ui.is_hovered(id) {
134                new_scroll.set_y((scroll.y() + SCROLL_SPEED * -s.ui.mouse.yrel).clamp(0, ymax));
135            }
136
137            if s.ui.is_focused(id) {
138                if let Some(key) = s.ui.key_entered() {
139                    match key {
140                        Key::Up => {
141                            new_scroll.set_y((scroll.y() - SCROLL_SPEED).clamp(0, ymax));
142                        }
143                        Key::Down => {
144                            new_scroll.set_y((scroll.y() + SCROLL_SPEED).clamp(0, ymax));
145                        }
146                        _ => (),
147                    };
148                }
149            }
150
151            let mut scroll_y = new_scroll.y();
152            s.push_id(1);
153            let scrolled = s.scrollbar(
154                id,
155                rect![rect.right(), rect.top(), scroll_size, rect.height()],
156                ymax,
157                &mut scroll_y,
158                ScrollDirection::Vertical,
159            )?;
160            s.pop_id();
161            if scrolled {
162                new_scroll.set_y(scroll_y);
163            }
164        }
165
166        // Horizontal scroll
167        if xmax > 0 {
168            if s.ui.is_hovered(id) {
169                new_scroll.set_x((scroll.x() + SCROLL_SPEED * s.ui.mouse.xrel).clamp(0, xmax));
170            }
171
172            if s.ui.is_focused(id) {
173                if let Some(key) = s.ui.key_entered() {
174                    match key {
175                        Key::Left => {
176                            new_scroll.set_x((scroll.x() - SCROLL_SPEED).clamp(0, xmax));
177                        }
178                        Key::Right => {
179                            new_scroll.set_x((scroll.x() + SCROLL_SPEED).clamp(0, xmax));
180                        }
181                        _ => (),
182                    };
183                }
184            }
185
186            let mut scroll_x = new_scroll.x();
187            s.push_id(2);
188            let scrolled = s.scrollbar(
189                id,
190                rect![rect.left(), rect.bottom(), rect.width(), scroll_size],
191                xmax,
192                &mut scroll_x,
193                ScrollDirection::Horizontal,
194            )?;
195            s.pop_id();
196            if scrolled {
197                new_scroll.set_x(scroll_x);
198            }
199        }
200
201        if new_scroll != scroll {
202            s.ui.set_scroll(id, new_scroll);
203        }
204
205        Ok(rect.offset_size([scroll_size, scroll_size]))
206    }
207
208    /// Helper to render either a vertical or a horizontal scroll bar.
209    fn scrollbar(
210        &mut self,
211        id: ElementId,
212        rect: Rect<i32>,
213        max: i32,
214        value: &mut i32,
215        dir: ScrollDirection,
216    ) -> Result<bool> {
217        use ScrollDirection::{Horizontal, Vertical};
218
219        let s = self;
220        let id = s.ui.get_id(&id);
221        let colors = s.theme.colors;
222
223        // Check hover/active/keyboard focus
224        let hovered = s.focused() && s.ui.try_hover(id, &rect);
225        let focused = s.focused() && s.ui.try_focus(id);
226        let active = s.ui.is_active(id);
227
228        s.push();
229
230        // Clamp value
231        *value = (*value).clamp(0, max);
232
233        // Scroll region
234        if hovered {
235            s.frame_cursor(&Cursor::hand())?;
236        }
237
238        let [stroke, bg, _] = s.widget_colors(id, ColorType::Secondary);
239        if active || focused {
240            s.stroke(stroke);
241        } else {
242            s.stroke(None);
243        }
244        s.fill(colors.on_secondary);
245        s.rect(rect)?;
246
247        // Scroll thumb
248        let thumb_w = match dir {
249            Horizontal => {
250                let w = rect.width() as f32;
251                let w = ((w / (max as f32 + w)) * w) as i32;
252                w.clamp(THUMB_MIN, w)
253            }
254            Vertical => rect.width(),
255        };
256        let thumb_h = match dir {
257            Horizontal => rect.height(),
258            Vertical => {
259                let h = rect.height() as f32;
260                let h = ((h / (max as f32 + h)) * h) as i32;
261                h.clamp(THUMB_MIN, h)
262            }
263        };
264        s.fill(bg);
265        match dir {
266            Horizontal => {
267                let thumb_x = ((rect.width() - thumb_w) * *value) / max;
268                s.rect([rect.x() + thumb_x, rect.y(), thumb_w, thumb_h])?;
269            }
270            Vertical => {
271                let thumb_y = ((rect.height() - thumb_h) * *value) / max;
272                s.rect([rect.x(), rect.y() + thumb_y, thumb_w, thumb_h])?;
273            }
274        }
275
276        s.pop();
277
278        // Process keyboard input
279        let mut new_value = *value;
280        if focused {
281            if let Some(key) = s.ui.key_entered() {
282                match (key, dir) {
283                    (Key::Up, Vertical) | (Key::Left, Horizontal) => {
284                        new_value = value.saturating_sub(SCROLL_SPEED).max(0);
285                    }
286                    (Key::Down, Vertical) | (Key::Right, Horizontal) => {
287                        new_value = value.saturating_add(SCROLL_SPEED).min(max);
288                    }
289                    _ => (),
290                }
291            }
292        }
293
294        // Process mouse wheel
295        if hovered {
296            let offset = match dir {
297                Horizontal => s.ui.mouse.xrel,
298                Vertical => -s.ui.mouse.yrel,
299            };
300            new_value += SCROLL_SPEED * offset;
301        }
302        // Process mouse input
303        if active {
304            new_value = match dir {
305                Horizontal => {
306                    let mx = (s.mouse_pos().x() - rect.x()).clamp(0, rect.width());
307                    (mx * max) / rect.width()
308                }
309                Vertical => {
310                    let my = (s.mouse_pos().y() - rect.y()).clamp(0, rect.height());
311                    (my * max) / rect.height()
312                }
313            };
314        }
315        s.ui.handle_focus(id);
316
317        if new_value == *value {
318            Ok(false)
319        } else {
320            *value = new_value.clamp(0, max);
321            Ok(true)
322        }
323    }
324}