Skip to main content

kobo_core/rendering/
loader.rs

1//! Loading indicators for splash and loading screens.
2//!
3//! Pure functions operating on raw RGB565 byte buffers. All take explicit
4//! screen dimensions so they are reusable by any crate without global state.
5//!
6//! - [`paint_loading_bar`] - horizontal progress bar at the bottom of the screen
7//! - [`paint_spinner`] - rotating arc spinner centred near the bottom
8
9const BAR_SIDE_PAD: usize = 80;
10const BAR_HEIGHT: usize = 8;
11const BAR_FILL_COLOR: u16 = 0x06A4;
12const BAR_TRACK_COLOR: u16 = 0xD6BA;
13
14const SPINNER_ARC_COLOR: u16 = 0xF148;
15
16const SPINNER_OFFSET_FROM_BOTTOM: f32 = 130.0;
17const SPINNER_BADGE_R: f32 = 52.0;
18const SPINNER_ARC_R: f32 = 36.0;
19const SPINNER_ARC_THICK: f32 = 9.0;
20const SPINNER_ARC_SPAN: u32 = 300;
21const SPINNER_PAD: i32 = 56;
22
23/// A screen rectangle (x, y, width, height) in pixels, for partial-refresh
24/// bounding regions.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct Rect {
27    pub x: i32,
28    pub y: i32,
29    pub w: i32,
30    pub h: i32,
31}
32
33fn put_pixel(buf: &mut [u8], screen_w: usize, px: usize, py: usize, val: u16) {
34    let off = (py * screen_w + px) * 2;
35    if off + 2 > buf.len() {
36        return;
37    }
38    buf[off] = (val & 0xff) as u8;
39    buf[off + 1] = (val >> 8) as u8;
40}
41
42/// Draw a horizontal progress bar at the bottom of the screen.
43///
44/// `frac` ranges from 0.0 (empty) to 1.0 (full). The filled portion uses
45/// [`BAR_FILL_COLOR`]; the remaining track uses [`BAR_TRACK_COLOR`].
46pub fn paint_loading_bar(buf: &mut [u8], screen_w: usize, screen_h: usize, frac: f32) {
47    let bar_w = screen_w.saturating_sub(2 * BAR_SIDE_PAD);
48    let bar_y = screen_h - screen_h / 10;
49    let fill_w = ((bar_w as f32) * frac.clamp(0.0, 1.0)).round() as usize;
50    for ry in 0..BAR_HEIGHT {
51        let py = bar_y + ry;
52        if py >= screen_h {
53            break;
54        }
55        for rx in 0..bar_w {
56            let px = BAR_SIDE_PAD + rx;
57            if px >= screen_w {
58                break;
59            }
60            let v = if rx < fill_w {
61                BAR_FILL_COLOR
62            } else {
63                BAR_TRACK_COLOR
64            };
65            put_pixel(buf, screen_w, px, py, v);
66        }
67    }
68}
69
70/// Bounding rectangle of the loading bar for partial-refresh updates, with a
71/// small margin so the refresh band covers the full bar including
72/// anti-aliasing edges.
73pub fn loading_bar_rect(screen_w: i32, screen_h: i32) -> Rect {
74    let bar_w = screen_w - 2 * BAR_SIDE_PAD as i32;
75    let bar_y = screen_h - screen_h / 10;
76    Rect {
77        x: BAR_SIDE_PAD as i32,
78        y: bar_y - 4,
79        w: bar_w,
80        h: BAR_HEIGHT as i32 + 8,
81    }
82}
83
84/// Draw a rotating-arc spinner centred horizontally near the bottom.
85///
86/// `angle_deg` is the clockwise start angle of the arc (0 = right). The arc
87/// spans [`SPINNER_ARC_SPAN`] degrees. Pixels inside the badge circle but
88/// outside the arc ring are set to white (0xFFFF).
89pub fn paint_spinner(buf: &mut [u8], screen_w: usize, screen_h: usize, angle_deg: u32) {
90    let cx = screen_w as f32 / 2.0;
91    let cy = screen_h as f32 - SPINNER_OFFSET_FROM_BOTTOM;
92    let badge_r = SPINNER_BADGE_R;
93    let arc_r = SPINNER_ARC_R;
94    let arc_thick = SPINNER_ARC_THICK;
95    let span = SPINNER_ARC_SPAN;
96    for py in 0..screen_h {
97        let dy = py as f32 - cy;
98        if dy.abs() > badge_r + 2.0 {
99            continue;
100        }
101        for px in 0..screen_w {
102            let dx = px as f32 - cx;
103            let dist = (dx * dx + dy * dy).sqrt();
104            if dist > badge_r + 2.0 {
105                continue;
106            }
107            let mut val: u16 = 0xFFFF;
108            if (dist - arc_r).abs() <= arc_thick {
109                let a = (-dy).atan2(dx).to_degrees().rem_euclid(360.0);
110                let start = angle_deg as f32;
111                let end = start + span as f32;
112                let in_arc = if end <= 360.0 {
113                    a >= start && a < end
114                } else {
115                    a >= start || a < end - 360.0
116                };
117                if in_arc {
118                    val = SPINNER_ARC_COLOR;
119                }
120            }
121            put_pixel(buf, screen_w, px, py, val);
122        }
123    }
124}
125
126/// Bounding rectangle of the spinner.
127pub fn spinner_rect(screen_w: i32, screen_h: i32) -> Rect {
128    let cx = screen_w / 2;
129    let cy = screen_h - SPINNER_OFFSET_FROM_BOTTOM as i32;
130    let r = SPINNER_PAD;
131    Rect {
132        x: cx - r,
133        y: cy - r,
134        w: r * 2,
135        h: r * 2,
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn put_pixel_writes_little_endian_at_offset() {
145        let mut buf = [0u8; 4];
146        put_pixel(&mut buf, 2, 1, 0, 0x1234);
147        // off = (0 * 2 + 1) * 2 = 2 -> little-endian u16
148        assert_eq!(buf, [0, 0, 0x34, 0x12]);
149    }
150
151    #[test]
152    fn put_pixel_ignores_out_of_bounds() {
153        let mut buf = [0u8; 4];
154        put_pixel(&mut buf, 2, 5, 0, 0xFFFF);
155        put_pixel(&mut buf, 2, 0, 5, 0xFFFF);
156        assert_eq!(buf, [0, 0, 0, 0]);
157    }
158
159    #[test]
160    fn loading_bar_rect_geometry() {
161        let r = loading_bar_rect(800, 600);
162        assert_eq!(r.x, BAR_SIDE_PAD as i32);
163        assert_eq!(r.w, 800 - 2 * BAR_SIDE_PAD as i32);
164        let bar_y = 600 - 600 / 10;
165        assert_eq!(r.y, bar_y - 4);
166        assert_eq!(r.h, BAR_HEIGHT as i32 + 8);
167    }
168
169    #[test]
170    fn spinner_rect_is_centred_square() {
171        let r = spinner_rect(800, 600);
172        let cx = 800 / 2;
173        let cy = 600 - SPINNER_OFFSET_FROM_BOTTOM as i32;
174        let rad = SPINNER_PAD;
175        assert_eq!(
176            r,
177            Rect {
178                x: cx - rad,
179                y: cy - rad,
180                w: 2 * rad,
181                h: 2 * rad,
182            }
183        );
184        assert_eq!(r.w, r.h);
185    }
186
187    #[test]
188    fn paint_loading_bar_fill_vs_track() {
189        let (w, h) = (800usize, 600usize);
190        let mut buf = vec![0u8; w * h * 2];
191        let bar_y = h - h / 10;
192        let off = (bar_y * w + BAR_SIDE_PAD) * 2;
193
194        paint_loading_bar(&mut buf, w, h, 0.0);
195        assert_eq!(&buf[off..off + 2], &BAR_TRACK_COLOR.to_le_bytes());
196
197        paint_loading_bar(&mut buf, w, h, 1.0);
198        assert_eq!(&buf[off..off + 2], &BAR_FILL_COLOR.to_le_bytes());
199    }
200}