Skip to main content

faststep/scroll_view/
indicator.rs

1use 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/// Geometry and opacity for a transient scrollbar thumb.
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub struct ScrollBar {
20    /// Thumb frame inside the viewport gutter.
21    pub frame: Rectangle,
22    /// Thumb opacity in the `0..=255` range.
23    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}