Skip to main content

kobo_core/rendering/
draw.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Nayeem Bin Ahsan
3use crate::rendering::common::BODY_PX;
4use crate::rendering::layout::word_wrap_bytes;
5use crate::rendering::text_render;
6
7pub fn paint_placeholder_box(
8    buf_bytes: &mut [u8],
9    screen_w: usize,
10    screen_h: usize,
11    x: usize,
12    y: usize,
13    w: usize,
14    h: usize,
15) {
16    for ry in 0..h {
17        for rx in 0..w {
18            let px = x + rx;
19            let py = y + ry;
20            if px >= screen_w || py >= screen_h {
21                continue;
22            }
23            let off = (py * screen_w + px) * 2;
24            if off + 2 > buf_bytes.len() {
25                continue;
26            }
27            let border = rx == 0 || ry == 0 || rx == w - 1 || ry == h - 1;
28            let v = if border { 0x6B4D } else { 0xEF5D };
29            buf_bytes[off] = (v & 0xff) as u8;
30            buf_bytes[off + 1] = (v >> 8) as u8;
31        }
32    }
33}
34
35pub fn fill_rounded_rect(
36    buf_bytes: &mut [u8],
37    screen_w: usize,
38    screen_h: usize,
39    x: usize,
40    y: usize,
41    w: usize,
42    h: usize,
43    fill: u16,
44    border: u16,
45    radius: usize,
46) {
47    let r = radius.min(w / 2).min(h / 2);
48    let put = |buf: &mut [u8], px: usize, py: usize, v: u16| {
49        if px >= screen_w || py >= screen_h {
50            return;
51        }
52        let off = (py * screen_w + px) * 2;
53        if off + 2 > buf.len() {
54            return;
55        }
56        buf[off] = (v & 0xff) as u8;
57        buf[off + 1] = (v >> 8) as u8;
58    };
59    for ry in 0..h {
60        for rx in 0..w {
61            let cx_edge = if rx < r {
62                r - rx
63            } else if rx >= w - r {
64                rx - (w - 1 - r)
65            } else {
66                0
67            };
68            let cy_edge = if ry < r {
69                r - ry
70            } else if ry >= h - r {
71                ry - (h - 1 - r)
72            } else {
73                0
74            };
75            let in_corner = (rx < r || rx >= w - r) && (ry < r || ry >= h - r);
76            let v = if in_corner {
77                let dist2 =
78                    (cx_edge as i32) * (cx_edge as i32) + (cy_edge as i32) * (cy_edge as i32);
79                if dist2 > (r as i32) * (r as i32) {
80                    continue;
81                } else if dist2 >= ((r - 1) as i32) * ((r - 1) as i32) {
82                    border
83                } else {
84                    fill
85                }
86            } else {
87                let on_edge = rx == 0 || ry == 0 || rx == w - 1 || ry == h - 1;
88                if on_edge {
89                    border
90                } else {
91                    fill
92                }
93            };
94            put(buf_bytes, x + rx, y + ry, v);
95        }
96    }
97}
98
99pub fn paint_progress_bar(
100    buf_bytes: &mut [u8],
101    screen_w: usize,
102    screen_h: usize,
103    x: usize,
104    y: usize,
105    w: usize,
106    frac: f32,
107) {
108    let bar_h = 8usize;
109    for ry in 0..bar_h {
110        let py = y + ry;
111        if py >= screen_h {
112            break;
113        }
114        for rx in 0..w {
115            let px = x + rx;
116            if px >= screen_w {
117                break;
118            }
119            let off = (py * screen_w + px) * 2;
120            if off + 2 > buf_bytes.len() {
121                continue;
122            }
123            let filled = (rx as f32 / w as f32) < frac;
124            let v = if filled { 0x1082 } else { 0xD6BA };
125            buf_bytes[off] = (v & 0xff) as u8;
126            buf_bytes[off + 1] = (v >> 8) as u8;
127        }
128    }
129}
130
131pub fn measure_text(text: &str, px: f32) -> usize {
132    let space_w = text_render::word_width(" ", px);
133    let mut total = 0.0f32;
134    for (i, word) in text.split(' ').enumerate() {
135        if i > 0 {
136            total += space_w;
137        }
138        if !word.is_empty() {
139            total += text_render::word_width(word, px);
140        }
141    }
142    total as usize
143}
144
145pub fn truncate_to_width(s: &str, px: f32, max_w: usize) -> String {
146    if measure_text(s, px) <= max_w {
147        return s.to_string();
148    }
149    let mut out: String = s.chars().take(3).collect();
150    for ch in s.chars().skip(3) {
151        out.push(ch);
152        if measure_text(&(out.clone() + "..."), px) > max_w {
153            out.pop();
154            break;
155        }
156    }
157    out.push_str("...");
158    out
159}
160
161pub const ACTION_BTN_FILL: u16 = 0x0349;
162pub const ACTION_BTN_FG: u16 = 0xFFFF;
163pub const ACTION_BTN_RADIUS: usize = 10;
164
165pub fn paint_action_button(
166    buf_bytes: &mut [u8],
167    screen_w: usize,
168    screen_h: usize,
169    x: usize,
170    y: usize,
171    w: usize,
172    h: usize,
173    label: &str,
174    px: f32,
175) {
176    fill_rounded_rect(
177        buf_bytes,
178        screen_w,
179        screen_h,
180        x,
181        y,
182        w,
183        h,
184        ACTION_BTN_FILL,
185        ACTION_BTN_FILL,
186        ACTION_BTN_RADIUS,
187    );
188    let lw = measure_text(label, px);
189    let lh = text_render::line_height(px);
190    let tx = x + (w.saturating_sub(lw)) / 2;
191    let ty = y + (h.saturating_sub(lh)) / 2;
192    text_render::blit_rgb565_color(
193        buf_bytes,
194        screen_w,
195        label,
196        px,
197        tx,
198        ty,
199        ACTION_BTN_FG,
200        screen_w,
201        screen_h,
202    );
203}
204
205pub fn paint_nav_bar(
206    buf_bytes: &mut [u8],
207    screen_w: usize,
208    screen_h: usize,
209    nav_bar_h: usize,
210    y: usize,
211    center: &str,
212    clock: &str,
213    battery: i32,
214) {
215    let h = nav_bar_h;
216    let top = y;
217    for ry in 0..h {
218        let py = top + ry;
219        if py >= screen_h {
220            break;
221        }
222        let is_edge = ry == 0;
223        for rx in 0..screen_w {
224            let off = (py * screen_w + rx) * 2;
225            if off + 2 > buf_bytes.len() {
226                break;
227            }
228            let v = if is_edge { 0x1082 } else { 0xFFFF };
229            buf_bytes[off] = (v & 0xff) as u8;
230            buf_bytes[off + 1] = (v >> 8) as u8;
231        }
232    }
233    let label_px = BODY_PX * 0.8;
234    let small_px = BODY_PX * 0.66;
235    let lh = text_render::line_height(label_px);
236    let sh = text_render::line_height(small_px);
237    let cy = top + (h.saturating_sub(lh)) / 2;
238    let sy = top + (h.saturating_sub(sh)) / 2;
239
240    let exit_w = 150usize;
241    let exit_h = 56usize.min(h - 8);
242    let exit_x = 12usize;
243    let exit_y = top + (h.saturating_sub(exit_h)) / 2;
244    paint_action_button(
245        buf_bytes, screen_w, screen_h, exit_x, exit_y, exit_w, exit_h, "Exit", label_px,
246    );
247
248    let status = if battery > 0 {
249        format!("{}  {}%", clock, battery)
250    } else {
251        clock.to_string()
252    };
253    if !status.is_empty() {
254        let sw = measure_text(&status, small_px);
255        let sx = screen_w.saturating_sub(sw + 24);
256        text_render::blit_rgb565(
257            buf_bytes, screen_w, &status, small_px, sx, sy, screen_w, screen_h,
258        );
259    }
260    if !center.is_empty() {
261        let label_w = measure_text(center, label_px);
262        let cx = (screen_w / 2).saturating_sub(label_w / 2);
263        text_render::blit_rgb565(
264            buf_bytes, screen_w, center, label_px, cx, cy, screen_w, screen_h,
265        );
266    }
267}
268
269pub fn paint_wrapped_text(
270    buf_bytes: &mut [u8],
271    screen_w: usize,
272    screen_h: usize,
273    text: &str,
274    x: usize,
275    y: usize,
276    max_w: usize,
277    px: f32,
278    max_lines: usize,
279) -> usize {
280    let lines = word_wrap_bytes(text, max_w, px);
281    let lh = text_render::line_height(px);
282    let mut cy = y;
283    for (i, line) in lines.iter().enumerate() {
284        if i >= max_lines {
285            break;
286        }
287        text_render::blit_rgb565(
288            buf_bytes, screen_w, &line.text, px, x, cy, screen_w, screen_h,
289        );
290        cy += lh;
291    }
292    cy - y
293}
294
295#[cfg(test)]
296mod tests;