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}