Skip to main content

kobo_core/rendering/
density.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Nayeem Bin Ahsan
3//! Physical-size-stable sizing across the Kobo fleet.
4//!
5//! Kobo's `fb_var_screeninfo` reports 0mm for the panel's physical size on most
6//! models, so ppi cannot be derived from millimetres reliably. Instead it is
7//! looked up from the panel resolution (the same approach KOReader takes).
8//!
9//! `dp(n)` returns `n` design units expressed at the reference density, scaled
10//! to the current panel so a control keeps the same real-world size on a 212ppi
11//! Nia and a 300ppi Sage alike. The existing UI was tuned on a 300ppi panel, so
12//! wrapping a former raw-pixel literal in `dp()` leaves 300ppi devices untouched
13//! and only rescales the rest.
14
15use std::sync::atomic::{AtomicUsize, Ordering};
16
17/// Reference density: `dp(n) == n` at 300ppi, the most common modern Kobo panel.
18pub const REF_PPI: usize = 300;
19
20static PPI: AtomicUsize = AtomicUsize::new(REF_PPI);
21
22/// Resolve ppi from a panel resolution. Orientation-agnostic (sorts the pair),
23/// so it works whether the framebuffer reports portrait or landscape.
24///
25/// The 1404x1872 row resolves to the Elipsa / Elipsa 2E (227ppi). It collides
26/// with the long-discontinued Aura ONE (300ppi), which would get a slightly
27/// oversized UI; the trade favours the device people still own.
28pub fn ppi_for(w: usize, h: usize) -> usize {
29    let (lo, hi) = if w <= h { (w, h) } else { (h, w) };
30    match (lo, hi) {
31        (600, 800) => 167,   // Touch, Mini
32        (758, 1024) => 212,  // Glo, Aura, Nia
33        (1072, 1448) => 300, // Glo HD, Clara HD, Clara 2E, Clara Colour
34        (1080, 1430) => 265, // Aura H2O
35        (1264, 1680) => 300, // Libra H2O, Libra 2, Libra Colour
36        (1404, 1872) => 227, // Elipsa, Elipsa 2E
37        (1440, 1920) => 300, // Forma, Sage
38        _ => REF_PPI,
39    }
40}
41
42/// Latch the panel density once at startup. Call from `setup` after the
43/// framebuffer resolution is known.
44pub fn init_ppi(w: usize, h: usize) {
45    PPI.store(ppi_for(w, h), Ordering::Relaxed);
46}
47
48pub fn ppi() -> usize {
49    PPI.load(Ordering::Relaxed)
50}
51
52/// Physical-size-stable pixels: `n` design units at the reference 300ppi,
53/// scaled to the current panel.
54pub fn dp(n: i32) -> i32 {
55    dp_at(n, ppi())
56}
57
58/// Explicit-ppi variant of [`dp`]: the pure, testable core that does not read
59/// the global PPI latch. Call this when the panel density is known locally
60/// (e.g. inside a layout computation that already received `ppi` as a param).
61pub fn dp_at(n: i32, ppi: usize) -> i32 {
62    (n * ppi as i32) / REF_PPI as i32
63}
64
65/// Float variant for font sizes and sub-pixel geometry.
66pub fn dpf(n: f32) -> f32 {
67    n * ppi() as f32 / REF_PPI as f32
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn ppi_table_hits_known_panels() {
76        assert_eq!(ppi_for(758, 1024), 212); // Nia
77        assert_eq!(ppi_for(1072, 1448), 300); // Clara
78        assert_eq!(ppi_for(1264, 1680), 300); // Libra 2
79        assert_eq!(ppi_for(1404, 1872), 227); // Elipsa (not Aura ONE)
80        assert_eq!(ppi_for(1440, 1920), 300); // Sage
81    }
82
83    #[test]
84    fn ppi_is_orientation_agnostic() {
85        assert_eq!(ppi_for(1024, 758), ppi_for(758, 1024));
86    }
87
88    #[test]
89    fn ppi_unknown_panel_falls_back_to_reference() {
90        assert_eq!(ppi_for(999, 999), REF_PPI);
91    }
92
93    #[test]
94    fn dp_is_identity_at_reference_density() {
95        PPI.store(REF_PPI, Ordering::Relaxed);
96        assert_eq!(dp(76), 76);
97        assert_eq!(dp(300), 300);
98    }
99
100    #[test]
101    fn dp_shrinks_on_low_density_panel() {
102        PPI.store(212, Ordering::Relaxed);
103        // 76dp button stays the same physical size: ~54px on a 212ppi panel.
104        assert_eq!(dp(76), 76 * 212 / 300);
105        PPI.store(REF_PPI, Ordering::Relaxed); // restore for other tests
106    }
107
108    #[test]
109    fn dp_at_matches_dp_at_same_ppi() {
110        PPI.store(265, Ordering::Relaxed);
111        assert_eq!(dp_at(100, 265), dp(100));
112        assert_eq!(dp_at(0, 265), 0);
113        PPI.store(REF_PPI, Ordering::Relaxed);
114    }
115
116    #[test]
117    fn dp_at_is_identity_at_reference() {
118        assert_eq!(dp_at(300, REF_PPI), 300);
119    }
120}