Skip to main content

kobo_core/rendering/
loader.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Nayeem Bin Ahsan
3//! Loading indicators for splash and loading screens.
4//!
5//! Pure functions operating on raw RGB565 byte buffers. All take explicit
6//! screen dimensions so they are reusable by any crate without global state.
7//!
8//! - [`paint_loading_bar`] - horizontal progress bar at the bottom of the screen
9//!
10//! There used to be a rotating-arc spinner here. It was removed: a rotating arc
11//! drives every pixel in its ring from black to white and back, continuously,
12//! for as long as loading takes. Non-monotone and unbounded is the worst case
13//! this panel can be given, and no amount of tuning the rate fixed it. The
14//! splash now reveals its wordmark one piece at a time instead, so each pixel
15//! changes state exactly once (see `rendering::splash` in the app crate).
16//!
17//! A progress bar only ever fills in one direction, so it stays valid.
18
19const BAR_SIDE_PAD: usize = 80;
20const BAR_HEIGHT: usize = 8;
21const BAR_FILL_COLOR: u16 = 0x06A4;
22const BAR_TRACK_COLOR: u16 = 0xD6BA;
23
24/// A screen rectangle (x, y, width, height) in pixels, for partial-refresh
25/// bounding regions.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct Rect {
28    pub x: i32,
29    pub y: i32,
30    pub w: i32,
31    pub h: i32,
32}
33
34fn put_pixel(buf: &mut [u8], screen_w: usize, px: usize, py: usize, val: u16) {
35    let off = (py * screen_w + px) * 2;
36    if off + 2 > buf.len() {
37        return;
38    }
39    buf[off] = (val & 0xff) as u8;
40    buf[off + 1] = (val >> 8) as u8;
41}
42
43/// Draw a horizontal progress bar at the bottom of the screen.
44///
45/// `frac` ranges from 0.0 (empty) to 1.0 (full). The filled portion uses
46/// [`BAR_FILL_COLOR`]; the remaining track uses [`BAR_TRACK_COLOR`].
47pub fn paint_loading_bar(buf: &mut [u8], screen_w: usize, screen_h: usize, frac: f32) {
48    let bar_w = screen_w.saturating_sub(2 * BAR_SIDE_PAD);
49    let bar_y = screen_h - screen_h / 10;
50    let fill_w = ((bar_w as f32) * frac.clamp(0.0, 1.0)).round() as usize;
51    for ry in 0..BAR_HEIGHT {
52        let py = bar_y + ry;
53        if py >= screen_h {
54            break;
55        }
56        for rx in 0..bar_w {
57            let px = BAR_SIDE_PAD + rx;
58            if px >= screen_w {
59                break;
60            }
61            let v = if rx < fill_w {
62                BAR_FILL_COLOR
63            } else {
64                BAR_TRACK_COLOR
65            };
66            put_pixel(buf, screen_w, px, py, v);
67        }
68    }
69}
70
71/// Bounding rectangle of the loading bar for partial-refresh updates, with a
72/// small margin so the refresh band covers the full bar including
73/// anti-aliasing edges.
74pub fn loading_bar_rect(screen_w: i32, screen_h: i32) -> Rect {
75    let bar_w = screen_w - 2 * BAR_SIDE_PAD as i32;
76    let bar_y = screen_h - screen_h / 10;
77    Rect {
78        x: BAR_SIDE_PAD as i32,
79        y: bar_y - 4,
80        w: bar_w,
81        h: BAR_HEIGHT as i32 + 8,
82    }
83}
84
85/// Area-average downscale of an RGB888 buffer.
86///
87/// Averaging every source pixel that lands in a destination cell preserves
88/// thin strokes as continuous grey rather than flickering fragments.
89pub fn box_downscale(src: &[u8], sw: usize, sh: usize, dw: usize, dh: usize) -> Vec<u8> {
90    let mut out = vec![0u8; dw * dh * 3];
91    for dy in 0..dh {
92        let y0 = dy * sh / dh;
93        let y1 = (((dy + 1) * sh).div_ceil(dh)).min(sh).max(y0 + 1);
94        for dx in 0..dw {
95            let x0 = dx * sw / dw;
96            let x1 = (((dx + 1) * sw).div_ceil(dw)).min(sw).max(x0 + 1);
97            let (mut r, mut g, mut b, mut n) = (0u32, 0u32, 0u32, 0u32);
98            for sy in y0..y1 {
99                let row = sy * sw;
100                for sx in x0..x1 {
101                    let o = (row + sx) * 3;
102                    r += src[o] as u32;
103                    g += src[o + 1] as u32;
104                    b += src[o + 2] as u32;
105                    n += 1;
106                }
107            }
108            let o = (dy * dw + dx) * 3;
109            out[o] = (r / n) as u8;
110            out[o + 1] = (g / n) as u8;
111            out[o + 2] = (b / n) as u8;
112        }
113    }
114    out
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn put_pixel_writes_little_endian_at_offset() {
123        let mut buf = [0u8; 4];
124        put_pixel(&mut buf, 2, 1, 0, 0x1234);
125        // off = (0 * 2 + 1) * 2 = 2 -> little-endian u16
126        assert_eq!(buf, [0, 0, 0x34, 0x12]);
127    }
128
129    #[test]
130    fn put_pixel_ignores_out_of_bounds() {
131        let mut buf = [0u8; 4];
132        put_pixel(&mut buf, 2, 5, 0, 0xFFFF);
133        put_pixel(&mut buf, 2, 0, 5, 0xFFFF);
134        assert_eq!(buf, [0, 0, 0, 0]);
135    }
136
137    #[test]
138    fn loading_bar_rect_geometry() {
139        let r = loading_bar_rect(800, 600);
140        assert_eq!(r.x, BAR_SIDE_PAD as i32);
141        assert_eq!(r.w, 800 - 2 * BAR_SIDE_PAD as i32);
142        let bar_y = 600 - 600 / 10;
143        assert_eq!(r.y, bar_y - 4);
144        assert_eq!(r.h, BAR_HEIGHT as i32 + 8);
145    }
146
147    #[test]
148    fn paint_loading_bar_fill_vs_track() {
149        let (w, h) = (800usize, 600usize);
150        let mut buf = vec![0u8; w * h * 2];
151        let bar_y = h - h / 10;
152        let off = (bar_y * w + BAR_SIDE_PAD) * 2;
153
154        paint_loading_bar(&mut buf, w, h, 0.0);
155        assert_eq!(&buf[off..off + 2], &BAR_TRACK_COLOR.to_le_bytes());
156
157        paint_loading_bar(&mut buf, w, h, 1.0);
158        assert_eq!(&buf[off..off + 2], &BAR_FILL_COLOR.to_le_bytes());
159    }
160
161    #[test]
162    fn box_downscale_averages_block() {
163        let src = [100u8, 200, 50, 100, 200, 50, 100, 200, 50, 100, 200, 50];
164        let out = box_downscale(&src, 2, 2, 1, 1);
165        assert_eq!(out.len(), 3);
166        assert_eq!(out[0], 100);
167        assert_eq!(out[1], 200);
168        assert_eq!(out[2], 50);
169    }
170
171    #[test]
172    fn box_downscale_identity_passes_through() {
173        let src = [10u8, 20, 30, 40, 50, 60];
174        let out = box_downscale(&src, 1, 2, 1, 2);
175        assert_eq!(out, src);
176    }
177}