Skip to main content

slint_backend_kindle/
lib.rs

1//! Slint platform backend for Kindles.
2//!
3//! # Usage
4//!
5//! ```no_run
6//! slint::include_modules!();
7//!
8//! static DEFAULT_FONT: &[u8] = include_bytes!("../ui/MyFont.ttf");
9//! static SERIF_FONT: &[u8] = include_bytes!("../ui/MySerif.ttf");
10//!
11//! fn main() {
12//!     let backend = slint_backend_kindle::install(DEFAULT_FONT)
13//!         .expect("failed to install Kindle backend");
14//!     let app = AppWindow::new().expect("failed to create window");
15//!     backend.register_font_from_memory(SERIF_FONT).expect("failed to register font");
16//!     app.run().expect("event loop error");
17//! }
18//! ```
19
20mod framebuffer;
21mod platform;
22mod power;
23mod touch;
24mod wakeup;
25
26use platform::KindlePlatform;
27use slint::platform::WindowAdapter;
28use slint::platform::software_renderer::MinimalSoftwareWindow;
29use std::cell::RefCell;
30use std::marker::PhantomData;
31use std::rc::Rc;
32use std::sync::atomic::{AtomicBool, Ordering};
33use std::sync::{Arc, Mutex};
34use std::time::Duration;
35
36pub(crate) type OnWakeCallback = Rc<RefCell<Option<Box<dyn FnMut()>>>>;
37
38/// How often to wake from suspend-to-RAM and how long to stay awake afterwards.
39///
40/// Pass to [`KindleBackend::set_wake_schedule`] to opt in. Without it, the
41/// backend never suspends the SoC — the event loop just blocks in `poll(2)`,
42/// which is fine for plugged-in use but burns battery.
43///
44/// Touch activity during the awake window resets `stay_awake`, exactly like
45/// the device's normal idle timer.
46#[derive(Debug, Clone, Copy)]
47pub struct WakeSchedule {
48    /// Time between scheduled wakes from suspend.
49    pub wake_interval: Duration,
50    /// How long to stay awake after a wake or the last touch.
51    pub stay_awake: Duration,
52}
53
54/// Typestate markers
55pub struct NoSchedule;
56pub struct Scheduled;
57
58/// Returned by [`install`]. Use it to add more fonts and configure power.
59///
60/// A new backend is [`NoSchedule`]. Calling
61/// [`set_wake_schedule`](KindleBackend::set_wake_schedule) turns it into a
62/// [`Scheduled`] one, and only that form has
63/// [`on_wake`](KindleBackend::on_wake) — so you can't set a wake callback
64/// without first setting up a wake schedule.
65pub struct KindleBackend<State = NoSchedule> {
66    window: Rc<MinimalSoftwareWindow>,
67    wake_schedule: Arc<Mutex<Option<WakeSchedule>>>,
68    on_wake: OnWakeCallback,
69    black_and_white: Arc<AtomicBool>,
70    _state: PhantomData<State>,
71}
72
73impl<State> KindleBackend<State> {
74    /// Add an extra font (TTF/OTF) from bytes.
75    ///
76    /// Call this **after** you've created your window (e.g. `AppWindow::new()`).
77    /// Fonts can't be added before then because Slint hasn't set up its font
78    /// system yet.
79    pub fn register_font_from_memory(&self, data: &'static [u8]) -> Result<(), slint::PlatformError> {
80        self.window
81            .renderer()
82            .register_font_from_memory(data)
83            .map_err(|e| slint::PlatformError::Other(format!("{e}")))
84    }
85
86    /// Render in **pure black and white** (bilevel) mode: force every pixel to
87    /// pure black or white, with no grey levels at all. Useful on devices where
88    /// greyscale rendering causes a flicker through black to be displayed.
89    ///
90    /// Off by default. A change takes effect on the next render, so set it
91    /// before your first window draw, toggling it later only affects pixels
92    /// redrawn after that.
93    pub fn set_black_and_white(&self, enabled: bool) {
94        self.black_and_white.store(enabled, Ordering::Relaxed);
95    }
96
97    /// Switch state, keeping the same internals.
98    fn into_state<Next>(self) -> KindleBackend<Next> {
99        KindleBackend {
100            window: self.window,
101            wake_schedule: self.wake_schedule,
102            on_wake: self.on_wake,
103            black_and_white: self.black_and_white,
104            _state: PhantomData,
105        }
106    }
107}
108
109impl KindleBackend<NoSchedule> {
110    /// Turn on the wake-from-suspend cycle.
111    ///
112    /// The device sleeps once your app has been idle for `stay_awake`, then
113    /// wakes every `wake_interval` (or earlier, e.g. on a button press) so it
114    /// can refresh.
115    ///
116    /// Returns a [`Scheduled`] backend that lets you set
117    /// [`on_wake`](KindleBackend::on_wake).
118    pub fn set_wake_schedule(self, schedule: WakeSchedule) -> KindleBackend<Scheduled> {
119        *self.wake_schedule.lock().expect("wake schedule poisoned") = Some(schedule);
120        self.into_state()
121    }
122}
123
124impl KindleBackend<Scheduled> {
125    /// Change the wake schedule.
126    ///
127    /// The new schedule takes effect the next time the device is awake (it
128    /// can't be reached while asleep).
129    pub fn set_wake_schedule(&self, schedule: WakeSchedule) {
130        *self.wake_schedule.lock().expect("wake schedule poisoned") = Some(schedule);
131    }
132
133    /// Turn the wake cycle back off.
134    ///
135    /// Forgets the [`on_wake`](KindleBackend::on_wake) callback, since it can't
136    /// fire anymore. Takes effect the next time the device is awake.
137    pub fn clear_wake_schedule(self) -> KindleBackend<NoSchedule> {
138        *self.wake_schedule.lock().expect("wake schedule poisoned") = None;
139        *self.on_wake.borrow_mut() = None;
140        self.into_state()
141    }
142
143    /// Run `callback` once each time the device wakes from a scheduled suspend.
144    ///
145    /// Fires on the event-loop (UI) thread, after resume but before the next
146    /// render. The right place to refresh state that should be current when
147    /// the screen redraws after waking up, like polling an HTTP API, reading a sensor, etc.
148    /// Don't rely on a `slint::Timer` to align with `wake_interval`; Slint timers
149    /// run on their own schedule and may fire before or after the wake.
150    ///
151    /// Replaces any previously-set callback. Not invoked on the initial start of the app.
152    pub fn on_wake<F: FnMut() + 'static>(&self, callback: F) {
153        *self.on_wake.borrow_mut() = Some(Box::new(callback));
154    }
155}
156
157/// Set up the Kindle backend and use `font_data` as the default font.
158///
159/// You **must** pass a font. The Kindle doesn't ship any usable system fonts,
160/// so without one Slint will crash the first time it tries to draw text.
161/// We write the font to a temp file and point Slint at it through an
162/// environment variable so it gets used everywhere a font is needed.
163///
164/// Call this once at startup, before creating any windows. Use the returned
165/// [`KindleBackend`] to add more fonts later.
166///
167/// # Errors
168///
169/// Fails if the temp file can't be written, or if Slint already has a
170/// platform set up.
171pub fn install(font_data: &[u8]) -> Result<KindleBackend, slint::PlatformError> {
172    let path = std::env::temp_dir().join("slint-kindle-default.ttf");
173    std::fs::write(&path, font_data)
174        .map_err(|e| slint::PlatformError::Other(format!("failed to stage default font: {e}")))?;
175
176    // SAFETY: install() runs once at startup before any threads exist, so nothing else can read this env var at the same time.
177    unsafe { std::env::set_var("SLINT_DEFAULT_FONT", &path); }
178
179    let wake_schedule = Arc::new(Mutex::new(None));
180    let on_wake: OnWakeCallback = Rc::new(RefCell::new(None));
181    let black_and_white = Arc::new(AtomicBool::new(false));
182    let platform = KindlePlatform::new(wake_schedule.clone(), on_wake.clone(), black_and_white.clone())
183        .map_err(|e| slint::PlatformError::Other(format!("failed to init Kindle platform: {e}")))?;
184    let window = platform.window.clone();
185    slint::platform::set_platform(Box::new(platform))
186        .map_err(|e| slint::PlatformError::Other(format!("{e}")))?;
187    Ok(KindleBackend {
188        window,
189        wake_schedule,
190        on_wake,
191        black_and_white,
192        _state: PhantomData,
193    })
194}