faststep/scroll_view/
indicator.rs1use embedded_graphics::{
2 Drawable,
3 draw_target::DrawTarget,
4 pixelcolor::Rgb565,
5 prelude::{Primitive, RgbColor, Size},
6 primitives::{PrimitiveStyle, Rectangle, RoundedRectangle},
7};
8
9use crate::FsTheme;
10
11const SCROLLBAR_WIDTH: u32 = 6;
12const SCROLLBAR_DIRTY_WIDTH: u32 = 14;
13const SCROLLBAR_MARGIN_X: u32 = 4;
14const SCROLLBAR_MARGIN_Y: u32 = 8;
15const SCROLLBAR_MIN_HEIGHT: u32 = 28;
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub struct ScrollBar {
20 pub frame: Rectangle,
22 pub alpha: u8,
24}
25
26pub(super) fn scroll_bar_thumb(
27 viewport: Rectangle,
28 content_height: u32,
29 content_offset: f32,
30 alpha: u8,
31) -> Option<ScrollBar> {
32 let track = scrollbar_track(viewport);
33 if track.size.width == 0 || track.size.height == 0 {
34 return None;
35 }
36
37 let max_position = content_height.saturating_sub(viewport.size.height) as f32;
38 let content_offset = content_offset.clamp(0.0, max_position);
39 let track_height = track.size.height;
40 let thumb_height = (((viewport.size.height as u64 * track_height as u64)
41 / content_height.max(1) as u64) as u32)
42 .max(SCROLLBAR_MIN_HEIGHT)
43 .min(track_height);
44 let travel = track_height.saturating_sub(thumb_height);
45 let thumb_y = if max_position <= 0.0 || travel == 0 {
46 track.top_left.y
47 } else {
48 track.top_left.y + (((content_offset / max_position) * travel as f32) + 0.5) as i32
49 };
50 Some(ScrollBar {
51 frame: Rectangle::new(
52 embedded_graphics::prelude::Point::new(track.top_left.x, thumb_y),
53 Size::new(track.size.width, thumb_height),
54 ),
55 alpha,
56 })
57}
58
59pub(super) fn scroll_bar_dirty_rect(viewport: Rectangle) -> Rectangle {
60 let width = SCROLLBAR_DIRTY_WIDTH.min(viewport.size.width);
61 let x = viewport.top_left.x + viewport.size.width as i32 - width as i32;
62 Rectangle::new(
63 embedded_graphics::prelude::Point::new(x, viewport.top_left.y),
64 Size::new(width, viewport.size.height),
65 )
66}
67
68pub(super) fn motion_content_rect(viewport: Rectangle) -> Rectangle {
69 let reserved = scroll_bar_dirty_rect(viewport)
70 .size
71 .width
72 .min(viewport.size.width);
73 Rectangle::new(
74 viewport.top_left,
75 Size::new(
76 viewport.size.width.saturating_sub(reserved),
77 viewport.size.height,
78 ),
79 )
80}
81
82pub(super) fn draw_scrollbar<D>(display: &mut D, indicator: ScrollBar, theme: &FsTheme)
83where
84 D: DrawTarget<Color = Rgb565>,
85{
86 let color = blend(theme.surface_alt, theme.text_primary, indicator.alpha);
87 RoundedRectangle::with_equal_corners(indicator.frame, Size::new(3, 3))
88 .into_styled(PrimitiveStyle::with_fill(color))
89 .draw(display)
90 .ok();
91}
92
93fn scrollbar_track(viewport: Rectangle) -> Rectangle {
94 let width = SCROLLBAR_WIDTH.min(viewport.size.width);
95 let height = viewport
96 .size
97 .height
98 .saturating_sub(SCROLLBAR_MARGIN_Y.saturating_mul(2));
99 let x =
100 viewport.top_left.x + viewport.size.width as i32 - SCROLLBAR_MARGIN_X as i32 - width as i32;
101 let y = viewport.top_left.y + SCROLLBAR_MARGIN_Y as i32;
102 Rectangle::new(
103 embedded_graphics::prelude::Point::new(x, y),
104 Size::new(width, height),
105 )
106}
107
108fn blend(base: Rgb565, tint: Rgb565, alpha: u8) -> Rgb565 {
109 Rgb565::new(
110 mix_channel(base.r(), tint.r(), alpha),
111 mix_channel(base.g(), tint.g(), alpha),
112 mix_channel(base.b(), tint.b(), alpha),
113 )
114}
115
116fn mix_channel(base: u8, tint: u8, alpha: u8) -> u8 {
117 let alpha = u32::from(alpha);
118 let base = u32::from(base);
119 let tint = u32::from(tint);
120 (((base * (255 - alpha)) + (tint * alpha) + 127) / 255) as u8
121}