1use 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#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
11pub(crate) enum ScrollDirection {
12 Horizontal,
14 Vertical,
16}
17
18impl PixState {
19 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 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 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 if !label.is_empty() {
59 s.text(label)?;
60 }
61
62 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 s.fill(colors.background);
86 s.rect([0, 0, scroll_width, fpad.y()])?; s.rect([0, 0, fpad.x(), scroll_height])?; s.rect([right, 0, fpad.x(), scroll_height])?; s.rect([0, bottom, scroll_width, fpad.y()])?; 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 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 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 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 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 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 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 *value = (*value).clamp(0, max);
232
233 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 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 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 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 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}