1use alloc::vec::Vec;
18use embedded_graphics::{pixelcolor::PixelColor, prelude::*, primitives::Rectangle};
19use zest_core::{
20 GesturePhase, RenderError, Renderer, ScrollDirection, ScrollMsg, ScrollState, ScrollbarMode,
21 SnapMode, TouchPhase,
22};
23use zest_theme::Theme;
24
25pub const SCROLLBAR_W: u32 = 8;
27
28#[must_use]
32pub fn render_offset(state: ScrollState, dir: ScrollDirection) -> Point {
33 let off = if state.phase == GesturePhase::Dragging {
34 state.rubber_band(state.offset)
35 } else {
36 state.offset
40 };
41 Point::new(
42 if dir.scrolls_x() { off.x } else { 0 },
43 if dir.scrolls_y() { off.y } else { 0 },
44 )
45}
46
47#[allow(clippy::too_many_arguments)]
62pub fn route_touch<M, F, S>(
63 state: ScrollState,
64 dir: ScrollDirection,
65 viewport: Rectangle,
66 content: Size,
67 point: Point,
68 phase: TouchPhase,
69 snap_lines: &[i32],
70 on_scroll: Option<&S>,
71 mut forward: F,
72) -> Option<M>
73where
74 F: FnMut(Point, TouchPhase) -> Option<M>,
75 S: Fn(ScrollMsg) -> M + ?Sized,
76{
77 let inside = rect_contains(viewport, point);
78 let emit = |sm: ScrollMsg| -> Option<M> { on_scroll.map(|f| f(sm)) };
79
80 match phase {
81 TouchPhase::Down => {
82 let child_msg = if inside { forward(point, phase) } else { None };
85 if inside {
86 let press = emit(ScrollMsg::Press {
87 point,
88 content,
89 viewport: viewport.size,
90 });
91 child_msg.or(press)
94 } else {
95 child_msg
96 }
97 }
98 TouchPhase::Moved => match state.phase {
99 GesturePhase::Pressing => {
100 let crossed = crossed_threshold(state.press_origin, point, dir);
101 if crossed {
102 emit(ScrollMsg::DragTo {
103 point,
104 content,
105 viewport: viewport.size,
106 })
107 } else {
108 forward(point, phase)
109 }
110 }
111 GesturePhase::Dragging => emit(ScrollMsg::DragTo {
112 point,
113 content,
114 viewport: viewport.size,
115 }),
116 _ => {
117 if inside {
118 forward(point, phase)
119 } else {
120 None
121 }
122 }
123 },
124 TouchPhase::Up => match state.phase {
125 GesturePhase::Pressing => {
126 let child_msg = forward(point, phase);
129 child_msg.or_else(|| {
130 emit(ScrollMsg::Release {
131 point,
132 content,
133 viewport: viewport.size,
134 snap_lines: snap_lines.to_vec(),
135 })
136 })
137 }
138 GesturePhase::Dragging => emit(ScrollMsg::Release {
139 point,
140 content,
141 viewport: viewport.size,
142 snap_lines: snap_lines.to_vec(),
143 }),
144 _ => {
145 if inside {
146 forward(point, phase)
147 } else {
148 None
149 }
150 }
151 },
152 }
153}
154
155#[must_use]
159pub fn crossed_threshold(origin: Point, point: Point, dir: ScrollDirection) -> bool {
160 let dx = if dir.scrolls_x() {
161 (point.x - origin.x).abs()
162 } else {
163 0
164 };
165 let dy = if dir.scrolls_y() {
166 (point.y - origin.y).abs()
167 } else {
168 0
169 };
170 dx + dy >= zest_core::scroll::SCROLL_THRESHOLD
171}
172
173#[must_use]
186pub fn snap_lines(
187 child_rects: &[Rectangle],
188 origin: Point,
189 offset: Point,
190 viewport: Size,
191 dir: ScrollDirection,
192 mode: SnapMode,
193) -> Vec<i32> {
194 if mode == SnapMode::None {
195 return Vec::new();
196 }
197 let vertical = dir.scrolls_y();
198 let mut out: Vec<i32> = Vec::with_capacity(child_rects.len());
199 for r in child_rects {
200 let (lead, extent, vp) = if vertical {
204 (
205 r.top_left.y - origin.y + offset.y,
206 r.size.height as i32,
207 viewport.height as i32,
208 )
209 } else {
210 (
211 r.top_left.x - origin.x + offset.x,
212 r.size.width as i32,
213 viewport.width as i32,
214 )
215 };
216 let line = match mode {
217 SnapMode::Center => lead + extent / 2 - vp / 2,
218 SnapMode::End => lead + extent - vp,
219 SnapMode::Start | SnapMode::None => lead,
222 };
223 out.push(line.max(0));
224 }
225 out
226}
227
228#[allow(clippy::too_many_arguments)]
232pub fn draw_scrollbars<C: PixelColor>(
233 renderer: &mut dyn Renderer<C>,
234 theme: &Theme<'_, C>,
235 state: ScrollState,
236 mode: ScrollbarMode,
237 dir: ScrollDirection,
238 viewport: Rectangle,
239 content: Size,
240) -> Result<(), RenderError> {
241 let want = |overflow: bool| match mode {
242 ScrollbarMode::Off => false,
243 ScrollbarMode::On => true,
244 ScrollbarMode::Auto => overflow,
245 ScrollbarMode::Active => overflow && state.is_active(),
246 };
247
248 let off = render_offset(state, dir);
249
250 if dir.scrolls_y() {
251 let overflow = content.height > viewport.size.height;
252 if want(overflow) && overflow {
253 let track = Rectangle::new(
254 Point::new(
255 viewport.top_left.x + viewport.size.width.saturating_sub(SCROLLBAR_W) as i32,
256 viewport.top_left.y,
257 ),
258 Size::new(SCROLLBAR_W, viewport.size.height),
259 );
260 renderer.fill_rect(track, theme.background.divider)?;
261 let thumb = thumb_rect(track, viewport.size.height, content.height, off.y, true);
262 renderer.fill_rect(thumb, theme.accent.base)?;
263 }
264 }
265
266 if dir.scrolls_x() {
267 let overflow = content.width > viewport.size.width;
268 if want(overflow) && overflow {
269 let track = Rectangle::new(
270 Point::new(
271 viewport.top_left.x,
272 viewport.top_left.y + viewport.size.height.saturating_sub(SCROLLBAR_W) as i32,
273 ),
274 Size::new(viewport.size.width, SCROLLBAR_W),
275 );
276 renderer.fill_rect(track, theme.background.divider)?;
277 let thumb = thumb_rect(track, viewport.size.width, content.width, off.x, false);
278 renderer.fill_rect(thumb, theme.accent.base)?;
279 }
280 }
281
282 Ok(())
283}
284
285#[must_use]
290pub fn thumb_rect(
291 track: Rectangle,
292 vp: u32,
293 content: u32,
294 offset: i32,
295 vertical: bool,
296) -> Rectangle {
297 let track_len = if vertical {
298 track.size.height
299 } else {
300 track.size.width
301 };
302 let max = (content as i32 - vp as i32).max(0);
303 let visible_frac = vp as f32 / content as f32;
304 let thumb_len = ((track_len as f32 * visible_frac).max(8.0)) as u32;
305 let thumb_len = thumb_len.min(track_len);
306 let scroll_frac = if max > 0 {
307 (offset.clamp(0, max) as f32) / max as f32
308 } else {
309 0.0
310 };
311 let along = ((track_len.saturating_sub(thumb_len)) as f32 * scroll_frac) as i32;
312 if vertical {
313 Rectangle::new(
314 Point::new(track.top_left.x + 2, track.top_left.y + along),
315 Size::new(track.size.width.saturating_sub(4), thumb_len),
316 )
317 } else {
318 Rectangle::new(
319 Point::new(track.top_left.x + along, track.top_left.y + 2),
320 Size::new(thumb_len, track.size.height.saturating_sub(4)),
321 )
322 }
323}
324
325#[must_use]
327pub fn rect_contains(rect: Rectangle, point: Point) -> bool {
328 let br = rect.top_left + Point::new(rect.size.width as i32, rect.size.height as i32);
329 point.x >= rect.top_left.x && point.x < br.x && point.y >= rect.top_left.y && point.y < br.y
330}