A fork of pulp-os for the xteink4 adding custom apps
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

style fixes, bugs, dedup

hans 0c0a96f7 6764be66

+343 -386
+4 -4
kernel/src/board/battery.rs
··· 1 - // battery calibration for the XTEink X4. 1 + // battery calibration for the XTEink X4 2 2 // GPIO0 reads through 100K/100K divider (2:1); ADC 11dB attenuation 3 - // gives 0..2500 mV --- multiply by 2 for actual cell voltage. 3 + // gives 0..2500 mV; multiply by 2 for actual cell voltage 4 4 5 - // voltage divider multiplier (100K/100K resistive divider). 5 + // voltage divider multiplier (100K/100K resistive divider) 6 6 pub const DIVIDER_MULT: u32 = 2; 7 7 8 - // piecewise-linear li-ion discharge curve. sorted descending by mV. 8 + // piecewise-linear li-ion discharge curve, sorted descending by mV 9 9 pub const DISCHARGE_CURVE: &[(u32, u8)] = &[ 10 10 (4200, 100), 11 11 (4060, 90),
+4 -4
kernel/src/board/layout.rs
··· 1 - // physical button positions on the XTEink X4 bezel. 2 - // used by button_feedback to render labels at the correct screen edge. 1 + // physical button positions on the XTEink X4 bezel 2 + // used by button_feedback to render labels at the correct screen edge 3 3 4 - // center-x of bottom-edge buttons. 4 + // center-x of bottom-edge buttons 5 5 pub const CX_BACK: u16 = 84; 6 6 pub const CX_CONFIRM: u16 = 194; 7 7 pub const CX_LEFT: u16 = 286; 8 8 pub const CX_RIGHT: u16 = 396; 9 9 10 - // center-y of right-edge buttons. 10 + // center-y of right-edge buttons 11 11 pub const CY_VOL_UP: u16 = 364; 12 12 pub const CY_VOL_DOWN: u16 = 484;
+2 -2
kernel/src/drivers/battery.rs
··· 1 - // battery voltage estimation --- generic over board calibration. 2 - // board-specific divider ratio and discharge curve live in board::battery. 1 + // battery voltage estimation, generic over board calibration 2 + // board-specific divider ratio and discharge curve live in board::battery 3 3 4 4 use crate::board::battery::{DISCHARGE_CURVE, DIVIDER_MULT}; 5 5
+2 -18
kernel/src/drivers/input.rs
··· 71 71 press_since: Instant, 72 72 long_press_fired: bool, 73 73 last_repeat: Instant, 74 - last_event_at: Instant, 75 74 queue: EventQueue, 76 75 } 77 76 ··· 86 85 press_since: now, 87 86 long_press_fired: false, 88 87 last_repeat: now, 89 - last_event_at: now, 90 88 queue: EventQueue::new(), 91 89 } 92 90 } 93 91 94 92 pub fn poll(&mut self) -> Option<Event> { 95 93 if !self.queue.is_empty() { 96 - let ev = self.queue.pop(); 97 - if ev.is_some() { 98 - self.last_event_at = Instant::now(); 99 - } 100 - return ev; 94 + return self.queue.pop(); 101 95 } 102 96 103 97 let raw = self.read_raw(); ··· 125 119 self.last_repeat = now; 126 120 } 127 121 self.stable = debounced; 128 - let ev = self.queue.pop(); 129 - if ev.is_some() { 130 - self.last_event_at = now; 131 - } 132 - return ev; 122 + return self.queue.pop(); 133 123 } 134 124 135 125 if let Some(btn) = self.stable { ··· 138 128 if !self.long_press_fired && held >= Duration::from_millis(LONG_PRESS_MS) { 139 129 self.long_press_fired = true; 140 130 self.last_repeat = now; 141 - self.last_event_at = now; 142 131 return Some(Event::LongPress(btn)); 143 132 } 144 133 145 134 if self.long_press_fired && (now - self.last_repeat) >= Duration::from_millis(REPEAT_MS) 146 135 { 147 136 self.last_repeat = now; 148 - self.last_event_at = now; 149 137 return Some(Event::Repeat(btn)); 150 138 } 151 139 } 152 140 153 141 None 154 - } 155 - 156 - pub fn ms_since_last_event(&self) -> u64 { 157 - (Instant::now() - self.last_event_at).as_millis() 158 142 } 159 143 160 144 fn read_raw(&mut self) -> Option<Button> {
+15 -20
kernel/src/drivers/storage.rs
··· 2 2 // 3 3 // all I/O through embedded-sdmmc AsyncVolumeManager; functions are 4 4 // synchronous, wrapping async ops with poll_once (SPI bus is blocking 5 - // so every .await resolves immediately). 5 + // so every .await resolves immediately) 6 6 // 7 - // Returns the unified `Error` type (re-exported as `StorageError` for 8 - // backward compatibility). Apps receive it directly through KernelHandle. 7 + // returns the unified Error type (re-exported as StorageError for 8 + // backward compat); apps receive it through KernelHandle 9 9 10 10 use core::ops::ControlFlow; 11 11 ··· 18 18 pub const TITLES_FILE: &str = "TITLES.BIN"; 19 19 pub const TITLE_CAP: usize = 48; 20 20 21 - /// Backward-compatible alias — old code that references `StorageError` 22 - /// continues to compile while call-sites are migrated to `Error`. 21 + // backward-compatible alias 23 22 pub type StorageError = Error; 24 23 25 24 #[derive(Clone, Copy)] ··· 98 97 pos as u8 99 98 } 100 99 101 - // ── file-operation macros ───────────────────────────────────────── 102 - // 103 - // each evaluates to Result<T, Error>; none use `?` internally 104 - // so caller cleanup (close_dir etc) is never bypassed 100 + // file-operation macros; each evaluates to Result<T, Error> 101 + // none use ? internally so caller cleanup is never bypassed 105 102 106 103 macro_rules! op_file_size { 107 104 ($inner:expr, $dir:expr, $name:expr) => { ··· 220 217 }}; 221 218 } 222 219 223 - // dir-scoping macros: open subdir, execute body, close handle 220 + // dir-scoping macros; open subdir, execute body, close handle 224 221 225 222 macro_rules! in_dir { 226 223 ($inner:expr, $dirname:expr, |$dir:ident| $body:expr) => { ··· 255 252 }; 256 253 } 257 254 258 - // borrow helper 259 - 260 255 fn borrow(sd: &SdStorage) -> core::result::Result<core::cell::RefMut<'_, SdStorageInner>, Error> { 261 256 sd.borrow_inner() 262 257 .ok_or(Error::new(ErrorKind::NoCard, "storage::borrow")) 263 258 } 264 259 265 - // ── root file operations ────────────────────────────────────────── 260 + // root file operations 266 261 267 262 pub fn file_size(sd: &SdStorage, name: &str) -> crate::error::Result<u32> { 268 263 poll_once(async { ··· 321 316 }) 322 317 } 323 318 324 - // ── directory listing ───────────────────────────────────────────── 319 + // directory listing 325 320 326 321 pub fn list_root_files(sd: &SdStorage, buf: &mut [DirEntry]) -> crate::error::Result<usize> { 327 322 poll_once(async { ··· 379 374 }) 380 375 } 381 376 382 - // ── directory management ────────────────────────────────────────── 377 + // directory management 383 378 384 379 pub fn ensure_dir(sd: &SdStorage, name: &str) -> crate::error::Result<()> { 385 380 // two poll_once calls so the large make_dir future never shares ··· 411 406 }) 412 407 } 413 408 414 - // ── single-directory file operations ────────────────────────────── 409 + // single-directory file operations 415 410 416 411 pub fn write_file_in_dir( 417 412 sd: &SdStorage, ··· 468 463 }) 469 464 } 470 465 471 - // ── async boot path (runs inside the real executor) ─────────────── 466 + // async boot path (runs inside the real executor) 472 467 473 468 pub async fn ensure_pulp_dir_async(sd: &SdStorage) -> crate::error::Result<()> { 474 469 let mut guard = borrow(sd)?; ··· 488 483 } 489 484 } 490 485 491 - // ── _PULP subdirectory operations ───────────────────────────────── 486 + // _PULP subdirectory operations 492 487 493 488 pub fn ensure_pulp_subdir(sd: &SdStorage, name: &str) -> crate::error::Result<()> { 494 489 let exists = poll_once(async { ··· 590 585 }) 591 586 } 592 587 593 - // ── title mapping ───────────────────────────────────────────────── 588 + // title mapping 594 589 595 - /// Append a title mapping line to _PULP/TITLES.BIN 590 + // append a title line to _PULP/TITLES.BIN 596 591 pub fn save_title(sd: &SdStorage, filename: &str, title: &str) -> crate::error::Result<()> { 597 592 let name_bytes = filename.as_bytes(); 598 593 let title_bytes = title.as_bytes();
+4 -1
kernel/src/drivers/strip.rs
··· 218 218 } 219 219 220 220 // buf_y for x=0: physical row offset into window 221 - let base_buf_y = (HEIGHT as i32 - 1 - gx - wy) as usize; 221 + let base_buf_y_i = HEIGHT as i32 - 1 - gx - wy; 222 + debug_assert!(base_buf_y_i >= 0, "blit_1bpp_270: base_buf_y underflow"); 223 + debug_assert!(gy + y0 as i32 >= wx, "blit_1bpp_270: buf_x underflow"); 224 + let base_buf_y = base_buf_y_i as usize; 222 225 223 226 for y in y0..y1 { 224 227 let row = offset + y * stride;
+21 -143
kernel/src/error.rs
··· 1 - // Unified error type for pulp-os 1 + // unified error type for pulp-os 2 2 // 3 - // Replaces the flat `StorageError` enum and ad-hoc `&'static str` 4 - // errors with a single `Copy` type that carries: 5 - // 6 - // ErrorKind — *what* went wrong (storage, parse, resource …) 7 - // source — *where* it happened (`&'static str`, usually 8 - // module_path!() or a short caller-supplied tag) 9 - // 10 - // Every `Result` in the kernel and app layers should use this type. 11 - // The smol-epub trait boundary (`Result<T, &'static str>`) converts 12 - // at the edge via the `From` impls. 3 + // single Copy type carrying ErrorKind (what) and a &'static str 4 + // source tag (where); smol-epub boundary converts via From impls 13 5 14 6 use core::fmt; 15 7 16 - // --------------------------------------------------------------------------- 17 - // ErrorKind — the category of failure 18 - // --------------------------------------------------------------------------- 19 - 20 - /// What went wrong. 21 8 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 22 9 #[non_exhaustive] 23 10 pub enum ErrorKind { 24 - // -- storage / SD card -- 25 - /// SD card not inserted or not responding. 11 + // storage / sd card 26 12 NoCard, 27 - /// Could not open the FAT volume. 28 13 OpenVolume, 29 - /// Could not open a directory. 30 14 OpenDir, 31 - /// Could not open a file. 32 15 OpenFile, 33 - /// Read I/O failed. 34 16 ReadFailed, 35 - /// Write I/O failed. 36 17 WriteFailed, 37 - /// Seek within a file failed. 38 18 SeekFailed, 39 - /// Delete operation failed. 40 19 DeleteFailed, 41 - /// Directory is full (cannot create entry). 42 20 DirFull, 43 - /// File or directory not found. 44 21 NotFound, 45 22 46 - // -- data / parsing -- 47 - /// EPUB, ZIP, or similar structure is invalid. 23 + // data / parsing 48 24 ParseFailed, 49 - /// Data is malformed or unexpected. 50 25 InvalidData, 51 - /// UTF-8 or other text-encoding error. 52 26 BadEncoding, 53 27 54 - // -- resources -- 55 - /// Heap allocation failed. 28 + // resources 56 29 OutOfMemory, 57 - /// Supplied buffer is too small for the operation. 58 30 BufferTooSmall, 59 31 60 - // -- network (upload) -- 61 - /// Network read/write failed. 32 + // network (upload) 62 33 NetworkIo, 63 - /// Protocol-level error (HTTP, multipart, etc.). 64 34 Protocol, 65 35 66 - // -- catch-all -- 67 - /// Unclassified error (carries context in `source`). 36 + // catch-all 68 37 Other, 69 38 } 70 39 71 40 impl ErrorKind { 72 - /// Short human-readable label (suitable for UI and log lines). 73 41 pub const fn as_str(self) -> &'static str { 74 42 match self { 75 43 Self::NoCard => "no sd card", ··· 93 61 } 94 62 } 95 63 96 - /// True for any variant that originates from SD-card storage I/O. 97 64 pub const fn is_storage(self) -> bool { 98 65 matches!( 99 66 self, ··· 117 84 } 118 85 } 119 86 120 - // --------------------------------------------------------------------------- 121 - // Error — the unified error value 122 - // --------------------------------------------------------------------------- 123 - 124 - /// Unified error for the entire pulp-os stack. 125 - /// 126 - /// Cheap to copy (one discriminant byte + one `&'static str` pointer). 127 - /// Carries *what* failed ([`ErrorKind`]) and a compile-time *source* 128 - /// string that identifies the call-site or subsystem. 129 - /// 130 - /// # Constructing 131 - /// 132 - /// ```ignore 133 - /// // Constant shorthand (no source tag): 134 - /// Error::READ_FAILED 135 - /// 136 - /// // With explicit source: 137 - /// Error::new(ErrorKind::OpenFile, "epub_init_zip") 138 - /// 139 - /// // Via the err!() macro (auto-stamps module_path!()): 140 - /// err!(ReadFailed) 141 - /// err!(OpenFile, "epub_init_zip") 142 - /// ``` 87 + // one discriminant byte + one &'static str pointer; cheap to copy 88 + // source is module_path!() or a short caller-supplied tag 143 89 #[derive(Clone, Copy)] 144 90 pub struct Error { 145 91 kind: ErrorKind, 146 - /// Where the error was created — a `module_path!()` or free-form 147 - /// tag. Empty string when no source was attached. 148 92 source: &'static str, 149 93 } 150 94 151 - // -- construction ---------------------------------------------------------- 152 - 153 95 impl Error { 154 - /// Create an error with explicit kind and source tag. 155 96 #[inline] 156 97 pub const fn new(kind: ErrorKind, source: &'static str) -> Self { 157 98 Self { kind, source } 158 99 } 159 100 160 - /// Create an error from a kind alone (no source context). 161 101 #[inline] 162 102 pub const fn from_kind(kind: ErrorKind) -> Self { 163 103 Self { kind, source: "" } 164 104 } 165 105 166 - // Named constants that mirror the old `StorageError` variants so 167 - // existing match-arms keep compiling during migration. 168 - 169 106 pub const NO_CARD: Self = Self::from_kind(ErrorKind::NoCard); 170 107 pub const OPEN_VOLUME: Self = Self::from_kind(ErrorKind::OpenVolume); 171 108 pub const OPEN_DIR: Self = Self::from_kind(ErrorKind::OpenDir); ··· 178 115 pub const NOT_FOUND: Self = Self::from_kind(ErrorKind::NotFound); 179 116 } 180 117 181 - // -- accessors ------------------------------------------------------------- 182 - 183 118 impl Error { 184 - /// The failure category. 185 119 #[inline] 186 120 pub const fn kind(&self) -> ErrorKind { 187 121 self.kind 188 122 } 189 123 190 - /// The compile-time tag identifying where this error was created. 191 - /// Returns `""` when no source was attached. 192 124 #[inline] 193 125 pub const fn source_tag(&self) -> &'static str { 194 126 self.source 195 127 } 196 128 197 - /// Attach (or replace) the source tag. Useful when propagating 198 - /// an error upward and adding the caller's context. 199 129 #[inline] 200 130 pub const fn with_source(self, source: &'static str) -> Self { 201 131 Self { ··· 204 134 } 205 135 } 206 136 207 - /// Change the kind while keeping the source. 208 137 #[inline] 209 138 pub const fn with_kind(self, kind: ErrorKind) -> Self { 210 139 Self { ··· 213 142 } 214 143 } 215 144 216 - /// True when a source tag has been attached. 217 145 #[inline] 218 146 pub const fn has_source(&self) -> bool { 219 147 !self.source.is_empty() 220 148 } 221 149 222 - /// True when the error originates from storage I/O. 223 150 #[inline] 224 151 pub const fn is_storage(&self) -> bool { 225 152 self.kind.is_storage() 226 153 } 227 154 228 - /// Short label for the smol-epub `Result<T, &'static str>` boundary. 229 155 #[inline] 230 156 pub const fn as_str(&self) -> &'static str { 231 157 self.kind.as_str() 232 158 } 233 159 } 234 - 235 - // -- formatting ------------------------------------------------------------ 236 160 237 161 impl fmt::Debug for Error { 238 162 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { ··· 254 178 } 255 179 } 256 180 257 - // -- equality (semantic: kind only, source is diagnostic) ------------------ 258 - 181 + // equality is semantic: kind only, source is diagnostic 259 182 impl PartialEq for Error { 260 183 #[inline] 261 184 fn eq(&self, other: &Self) -> bool { ··· 265 188 266 189 impl Eq for Error {} 267 190 268 - // --------------------------------------------------------------------------- 269 - // Conversions 270 - // --------------------------------------------------------------------------- 271 - 272 - /// Wrap a bare `&'static str` (from smol-epub helpers, etc.) into an 273 - /// [`Error`]. Well-known strings are mapped to the appropriate kind; 274 - /// everything else becomes [`ErrorKind::Other`] with the original 275 - /// string preserved as the source tag. 191 + // wrap &'static str (smol-epub returns) into Error; well-known 192 + // strings map to the appropriate kind, rest becomes Other 276 193 impl From<&'static str> for Error { 277 194 #[inline] 278 195 fn from(msg: &'static str) -> Self { ··· 297 214 } 298 215 } 299 216 300 - /// Project back to `&'static str` for the smol-epub trait boundary. 217 + // project back to &'static str for the smol-epub trait boundary 301 218 impl From<Error> for &'static str { 302 219 #[inline] 303 220 fn from(e: Error) -> &'static str { 304 - // Prefer the source tag if it is a meaningful human string; 305 - // otherwise fall back to the kind label. 306 221 if e.source.is_empty() { 307 222 e.kind.as_str() 308 223 } else { ··· 311 226 } 312 227 } 313 228 314 - // --------------------------------------------------------------------------- 315 - // ResultExt — ergonomic source tagging on Results 316 - // --------------------------------------------------------------------------- 317 - 318 - /// Extension trait for stamping source context onto any 319 - /// `Result<T, Error>`. 320 - /// 321 - /// ```ignore 322 - /// storage::read_file_chunk(sd, name, off, buf) 323 - /// .source("epub_init_zip")?; 324 - /// ``` 229 + // ergonomic source tagging on Result<T, Error> and 230 + // Result<T, &'static str> (smol-epub returns) 325 231 pub trait ResultExt<T> { 326 - /// Attach a source tag to the error (if any). 327 232 fn source(self, src: &'static str) -> Result<T>; 328 - 329 - /// Replace the error kind while adding a source tag. 330 233 fn map_kind(self, kind: ErrorKind, src: &'static str) -> Result<T>; 331 234 } 332 235 ··· 342 245 } 343 246 } 344 247 345 - /// Blanket impl so `Result<T, &'static str>` (smol-epub returns) can 346 - /// be tagged and converted in one step. 347 248 impl<T> ResultExt<T> for core::result::Result<T, &'static str> { 348 249 #[inline] 349 250 fn source(self, src: &'static str) -> Result<T> { ··· 356 257 } 357 258 } 358 259 359 - // --------------------------------------------------------------------------- 360 - // err! macro — stamps module_path!() automatically 361 - // --------------------------------------------------------------------------- 362 - 363 - /// Create an [`Error`] with the caller's module path baked in. 364 - /// 365 - /// ```ignore 366 - /// // Kind only — source is the calling module's path: 367 - /// err!(ReadFailed) 368 - /// 369 - /// // Kind + explicit context string: 370 - /// err!(OpenFile, "epub_init_zip") 371 - /// ``` 260 + // create an Error with module_path!() as source 261 + // err!(ReadFailed) 262 + // err!(OpenFile, "epub_init_zip") 372 263 #[macro_export] 373 264 macro_rules! err { 374 265 ($kind:ident) => { ··· 379 270 }; 380 271 } 381 272 382 - /// Map any `Result<T, _>` error into an [`Error`] of the given kind, 383 - /// stamping the caller's module path. 384 - /// 385 - /// ```ignore 386 - /// mgr.read(file, buf).await.or_err!(ReadFailed)?; 387 - /// ``` 273 + // map any Result<T, _> into an Error of the given kind 274 + // or_err!(mgr.read(file, buf).await, ReadFailed) 388 275 #[macro_export] 389 276 macro_rules! or_err { 390 277 ($result:expr, $kind:ident) => { ··· 396 283 }; 397 284 } 398 285 399 - // --------------------------------------------------------------------------- 400 - // Result alias 401 - // --------------------------------------------------------------------------- 402 - 403 - /// Convenience alias used throughout pulp-os. 404 - /// 405 - /// Intentionally shadows `core::result::Result` only when imported 406 - /// unqualified — callers that need the two-param form can still write 407 - /// `core::result::Result<T, E>`. 408 286 pub type Result<T> = core::result::Result<T, Error>;
+74 -16
kernel/src/kernel/app.rs
··· 1 - // app protocol: trait, context, transitions, redraw types, and coalescing 1 + // app protocol: trait, context, transitions, redraw types, coalescing, 2 + // and loading indicator state 2 3 // 3 4 // these types define the contract between the kernel scheduler and 4 5 // the app layer. concrete apps implement the App trait; the kernel ··· 9 10 // it. the kernel never knows which specific apps exist. 10 11 // 11 12 // QuickAction types also live here -- they are pure data describing 12 - // what actions an app exposes. the renderer (QuickMenu widget) is 13 - // app-side, but the protocol is kernel-side. 13 + // what actions an app exposes; the renderer (QuickMenu widget) is 14 + // app-side, but the protocol is kernel-side 14 15 15 16 use embassy_time::Instant; 16 17 use esp_hal::delay::Delay; ··· 72 73 73 74 pub const RECENT_FILE: &str = "RECENT"; 74 75 75 - // distros define their own AppId enum and implement this trait. 76 + // distros define their own AppId enum and implement this trait 76 77 // the kernel uses HOME to initialise the nav stack and reset on 77 - // Transition::Home. nothing else about the concrete variants is 78 - // known to the kernel. 78 + // Transition::Home; nothing else about the concrete variants is 79 + // known to the kernel 79 80 80 81 pub trait AppIdType: Copy + Eq + core::fmt::Debug { 81 82 const HOME: Self; ··· 103 104 } 104 105 105 106 const MSG_BUF_SIZE: usize = 64; 107 + const LOADING_BUF_SIZE: usize = 32; 106 108 107 109 pub struct AppContext { 108 110 msg_buf: [u8; MSG_BUF_SIZE], ··· 110 112 redraw: Redraw, 111 113 coalesce_until: Option<Instant>, 112 114 immediate: bool, 115 + 116 + // loading indicator; kernel-level so any app can use it. 117 + // drawn by the app manager after app content, before overlays. 118 + // uses the built-in mono font so it works even with no bitmap 119 + // fonts loaded. 120 + loading_buf: [u8; LOADING_BUF_SIZE], 121 + loading_len: u8, 122 + loading_pct: u8, 123 + loading_active: bool, 124 + loading_region: Region, 113 125 } 114 126 115 127 impl Default for AppContext { ··· 126 138 redraw: Redraw::None, 127 139 coalesce_until: None, 128 140 immediate: false, 141 + loading_buf: [0u8; LOADING_BUF_SIZE], 142 + loading_len: 0, 143 + loading_pct: 0, 144 + loading_active: false, 145 + loading_region: Region::new(0, 0, 0, 0), 129 146 } 130 147 } 131 148 ··· 169 186 self.coalesce_until = None; 170 187 } 171 188 172 - // alias; kept so callers in on_event that were already converted 173 - // continue to compile without churn 174 - #[inline] 175 - pub fn mark_dirty_immediate(&mut self, region: Region) { 176 - self.mark_dirty(region); 177 - } 178 - 179 189 // mark dirty with 50ms coalescing window; use only for background 180 190 // batch updates (title scanner) where many rapid dirty marks 181 191 // should coalesce into a single refresh ··· 213 223 self.immediate = false; 214 224 r 215 225 } 226 + 227 + // loading indicator: set text and percentage. 228 + // draws "msg...pct%" using the built-in mono font. 229 + // region defines where it renders; typically just below the 230 + // app header in the content area. 231 + // auto-marks the region dirty so the next render shows it. 232 + pub fn set_loading(&mut self, region: Region, msg: &str, pct: u8) { 233 + let n = msg.len().min(LOADING_BUF_SIZE); 234 + self.loading_buf[..n].copy_from_slice(&msg[..n].as_bytes()[..n]); 235 + self.loading_len = n as u8; 236 + self.loading_pct = pct.min(100); 237 + self.loading_region = region; 238 + 239 + self.loading_active = true; 240 + self.mark_dirty(region); 241 + } 242 + 243 + // clear the loading indicator and mark its region dirty so 244 + // the underlying content repaints 245 + pub fn clear_loading(&mut self) { 246 + if self.loading_active { 247 + let region = self.loading_region; 248 + self.loading_active = false; 249 + self.loading_len = 0; 250 + self.loading_pct = 0; 251 + self.mark_dirty(region); 252 + } 253 + } 254 + 255 + #[inline] 256 + pub fn loading_active(&self) -> bool { 257 + self.loading_active 258 + } 259 + 260 + #[inline] 261 + pub fn loading_msg(&self) -> &str { 262 + core::str::from_utf8(&self.loading_buf[..self.loading_len as usize]).unwrap_or("") 263 + } 264 + 265 + #[inline] 266 + pub fn loading_pct(&self) -> u8 { 267 + self.loading_pct 268 + } 269 + 270 + #[inline] 271 + pub fn loading_region(&self) -> Region { 272 + self.loading_region 273 + } 216 274 } 217 275 218 276 // background is async for epub streaming (stream_strip_entry_async); ··· 351 409 } 352 410 } 353 411 354 - // aggregate interface the kernel scheduler calls on the app layer. 412 + // aggregate interface the kernel scheduler calls on the app layer 355 413 // a distro implements this (typically via an AppManager struct that 356 - // holds concrete app types and a with_app! dispatch macro). the 414 + // holds concrete app types and a with_app! dispatch macro); the 357 415 // scheduler is generic over AppLayer without importing any concrete 358 - // app types. 416 + // app types 359 417 360 418 // run_special_mode is genuinely async (wifi radio); the rest is sync 361 419 #[allow(async_fn_in_trait)]
+1
kernel/src/kernel/bookmarks.rs
··· 295 295 if write_slot >= self.count { 296 296 self.count = write_slot + 1; 297 297 } 298 + debug_assert!(self.count <= SLOTS, "bookmark count exceeds slot limit"); 298 299 299 300 self.dirty = true; 300 301
+2 -2
kernel/src/kernel/console.rs
··· 2 2 // once to EPD before the app layer takes over 3 3 // 4 4 // uses the embedded-graphics built-in FONT_6X13 -- no TTF assets or 5 - // build.rs font pipeline needed. the kernel can show boot progress 6 - // on a bare display with nothing but this mono font. 5 + // build.rs font pipeline needed; the kernel can show boot progress 6 + // on a bare display with nothing but this mono font 7 7 8 8 use embedded_graphics::mono_font::MonoTextStyle; 9 9 use embedded_graphics::mono_font::ascii::FONT_6X13;
+3 -3
kernel/src/kernel/handle.rs
··· 1 1 // kernel handle: synchronous syscall boundary for apps 2 2 // 3 3 // every storage method calls a single storage::* function and returns 4 - // the unified Error result. apps call these directly. 4 + // the unified Error result; apps call these directly 5 5 // 6 - // app-specific logic (bookmarks, title scan, etc.) accesses the 6 + // app-specific logic (bookmarks, title scan, etc) accesses the 7 7 // underlying caches directly via bookmark_cache() / dir_cache_mut() 8 - // rather than through dedicated handle methods. 8 + // rather than through dedicated handle methods 9 9 10 10 use crate::drivers::storage::{self, DirEntry, DirPage}; 11 11 use crate::error::{Error, Result};
+3 -4
kernel/src/kernel/mod.rs
··· 1 1 // kernel: owns hardware resources, caches, and system state 2 2 // 3 3 // constructed once during boot in main(), lives for the lifetime of 4 - // the program. not a separate Embassy task -- a struct held by main. 4 + // the program; not a separate Embassy task -- a struct held by main 5 5 // 6 6 // apps interact exclusively through KernelHandle, which borrows the 7 - // kernel for the duration of an async lifecycle method. 7 + // kernel for the duration of an async lifecycle method 8 8 9 9 pub mod app; 10 10 pub mod bookmarks; ··· 20 20 // Unified error types (primary home: crate::error) 21 21 pub use crate::error::{Error, ErrorKind, Result, ResultExt}; 22 22 23 - // Backward-compatible alias so `kernel::StorageError` keeps working 24 - // during migration. It is now `type StorageError = Error`. 23 + // backward-compatible alias 25 24 pub use crate::drivers::storage::StorageError; 26 25 27 26 pub use app::{
+39 -43
kernel/src/kernel/scheduler.rs
··· 1 1 // scheduler: main event loop, render pipeline, housekeeping, sleep 2 2 // 3 - // EPD and SD share a single SPI bus via CriticalSectionDevice. 3 + // EPD and SD share a single SPI bus via CriticalSectionDevice; 4 4 // during normal operation, all SD I/O completes before render() 5 - // touches the EPD. during the DU/GC waveform (~400ms), the EPD 5 + // touches the EPD; during the DU/GC waveform (~400ms), the EPD 6 6 // charge pump drives pixels with no SPI commands, so the bus is 7 - // free for SD I/O. busy_wait_with_background exploits this window 8 - // to run background caching and housekeeping during the waveform. 7 + // free for SD I/O - busy_wait_with_background exploits this 8 + // window to run background caching and housekeeping 9 9 // 10 10 // handle_input and poll_housekeeping are synchronous; they return 11 11 // a bool flag when the caller should enter_sleep (which is async 12 - // because it renders a sleep screen via the EPD). 12 + // because it renders a sleep screen via the EPD) 13 13 // 14 14 // sd_card_sleep sends cmd0 before deep sleep to reduce sd card 15 - // idle current from ~150 µa to ~10 µa 15 + // idle current from ~150 uA to ~10 uA 16 16 17 17 use embassy_futures::select::{Either, select}; 18 18 use embassy_time::{Duration, Ticker, with_timeout}; ··· 181 181 false 182 182 } 183 183 184 - // returns true if idle sleep is due 185 - fn poll_housekeeping<A: AppLayer>(&mut self, app_mgr: &A) -> bool { 184 + // shared housekeeping body: battery, sd probe, bookmark flush, stats 185 + fn poll_housekeeping_inner<A: AppLayer>(&mut self, app_mgr: &A) { 186 186 if let Some(mv) = tasks::BATTERY_MV.try_take() { 187 187 self.cached_battery_mv = mv; 188 188 } ··· 201 201 tasks::set_idle_timeout(app_mgr.system_settings().sleep_timeout); 202 202 } 203 203 } 204 + } 204 205 206 + // returns true if idle sleep is due 207 + fn poll_housekeeping<A: AppLayer>(&mut self, app_mgr: &A) -> bool { 208 + self.poll_housekeeping_inner(app_mgr); 205 209 tasks::IDLE_SLEEP_DUE.try_take().is_some() 206 210 } 207 211 208 - // housekeeping without idle-sleep check; do not initiate sleep mid-refresh 212 + // housekeeping without idle-sleep check; never sleep mid-refresh 209 213 fn poll_housekeeping_waveform<A: AppLayer>(&mut self, app_mgr: &A) { 210 - if let Some(mv) = tasks::BATTERY_MV.try_take() { 211 - self.cached_battery_mv = mv; 212 - } 213 - 214 - if tasks::SD_CHECK_DUE.try_take().is_some() { 215 - self.sd_ok = self.sd.probe_ok(); 216 - } 217 - 218 - if tasks::BOOKMARK_FLUSH_DUE.try_take().is_some() && self.bm_cache.is_dirty() { 219 - self.bm_cache.flush(&self.sd); 220 - } 221 - 222 - if tasks::STATUS_DUE.try_take().is_some() { 223 - self.log_stats(); 224 - if app_mgr.settings_loaded() { 225 - tasks::set_idle_timeout(app_mgr.system_settings().sleep_timeout); 226 - } 227 - } 228 - // idle sleep not checked here; never sleep during a waveform 214 + self.poll_housekeeping_inner(app_mgr); 229 215 } 230 216 231 217 // partial refreshes use DU waveform (~400 ms); after ghost_clear_every ··· 330 316 // no SPI commands are sent, so the bus is free for SD I/O. 331 317 // is_busy() is a sync GPIO read; no epd borrow is held across 332 318 // any .await point, so self is fully available for handle() etc. 319 + // 320 + // run_background is wrapped in select so input interrupts long 321 + // background work (e.g. chapter caching). when the background 322 + // future is dropped mid-stream, partial cache writes are safe 323 + // because ch_cached stays false until the full write completes. 324 + // the TICK_MS timeout ensures is_busy is re-checked regularly 325 + // even during long background operations. 333 326 async fn busy_wait_with_background<A: AppLayer>( 334 327 &mut self, 335 328 app_mgr: &mut A, ··· 337 330 let mut deferred: Option<Transition<A::Id>> = None; 338 331 339 332 loop { 340 - // sync gpio read; no borrow held after this line 341 333 if !self.epd.is_busy() { 342 334 break; 343 335 } 344 336 345 - // wait up to TICK_MS for input; no epd borrow involved 346 - let input_event = with_timeout( 347 - Duration::from_millis(TICK_MS), 348 - tasks::INPUT_EVENTS.receive(), 349 - ) 350 - .await 351 - .ok(); 337 + // run background, interruptible by input or tick timeout 338 + let ev = { 339 + let mut handle = self.handle(); 340 + match select( 341 + app_mgr.run_background(&mut handle), 342 + with_timeout( 343 + Duration::from_millis(TICK_MS), 344 + tasks::INPUT_EVENTS.receive(), 345 + ), 346 + ) 347 + .await 348 + { 349 + Either::First(()) => None, 350 + Either::Second(Ok(ev)) => Some(ev), 351 + Either::Second(Err(_)) => None, 352 + } 353 + }; 352 354 353 - if let Some(hw_event) = input_event { 355 + if let Some(hw_event) = ev { 354 356 if !app_mgr.suppress_deferred_input() { 355 357 let t = app_mgr.dispatch_event(hw_event, &mut *self.bm_cache); 356 358 if t != Transition::None && deferred.is_none() { 357 359 deferred = Some(t); 358 360 } 359 361 } 360 - continue; 361 362 } 362 363 363 - // timeout elapsed; spi bus is free during waveform 364 - { 365 - let mut handle = self.handle(); 366 - app_mgr.run_background(&mut handle).await; 367 - } 368 364 self.poll_housekeeping_waveform(app_mgr); 369 365 } 370 366
+7 -24
kernel/src/kernel/tasks.rs
··· 14 14 15 15 pub static BATTERY_MV: Signal<CriticalSectionRawMutex, u16> = Signal::new(); 16 16 17 - const POLL_ACTIVE_MS: u64 = 10; // 100 hz: during/after input 18 - const POLL_IDLE_MS: u64 = 50; // 20 hz: no recent input 19 - const POLL_DROWSY_MS: u64 = 200; // 5 hz: approaching sleep timeout 20 - 21 - const IDLE_AFTER_MS: u64 = 2_000; 22 - const DROWSY_AFTER_MS: u64 = 30_000; 23 - 24 - const BATTERY_INTERVAL_MS: u64 = 30_000; 17 + const BATTERY_INTERVAL_TICKS: u32 = 3000; // 3000 x 10 ms = 30 s 25 18 26 19 #[embassy_executor::task] 27 20 pub async fn input_task(mut input: InputDriver) -> ! { 28 - let mut poll_ms = POLL_ACTIVE_MS; 29 - let mut battery_accum_ms: u64 = 0; 21 + let mut ticker = Ticker::every(Duration::from_millis(10)); 22 + let mut battery_counter: u32 = 0; 30 23 31 24 let raw = input.read_battery_mv(); 32 25 BATTERY_MV.signal(battery::adc_to_battery_mv(raw)); 33 26 34 27 loop { 35 - Timer::after(Duration::from_millis(poll_ms)).await; 28 + ticker.next().await; 36 29 37 30 if let Some(ev) = input.poll() { 38 31 let _ = INPUT_EVENTS.try_send(ev); 39 32 IDLE_RESET.signal(()); 40 - poll_ms = POLL_ACTIVE_MS; 41 - } else { 42 - let since = input.ms_since_last_event(); 43 - poll_ms = if since > DROWSY_AFTER_MS { 44 - POLL_DROWSY_MS 45 - } else if since > IDLE_AFTER_MS { 46 - POLL_IDLE_MS 47 - } else { 48 - POLL_ACTIVE_MS 49 - }; 50 33 } 51 34 52 - battery_accum_ms += poll_ms; 53 - if battery_accum_ms >= BATTERY_INTERVAL_MS { 54 - battery_accum_ms = 0; 35 + battery_counter += 1; 36 + if battery_counter >= BATTERY_INTERVAL_TICKS { 37 + battery_counter = 0; 55 38 let raw = input.read_battery_mv(); 56 39 BATTERY_MV.signal(battery::adc_to_battery_mv(raw)); 57 40 }
+3 -3
kernel/src/lib.rs
··· 1 1 // pulp-kernel -- hardware drivers, scheduling, and system core 2 2 // 3 - // generic over AppLayer; never imports concrete apps or fonts. 3 + // generic over AppLayer; never imports concrete apps or fonts 4 4 // ships a built-in mono font (FONT_6X13) for boot console and 5 - // sleep screen. distros bring their own proportional fonts. 5 + // sleep screen; distros bring their own proportional fonts 6 6 7 7 #![no_std] 8 8 ··· 14 14 pub mod kernel; 15 15 pub mod ui; 16 16 17 - // Re-export the core error types at crate root for convenience. 17 + // re-export core error types at crate root 18 18 pub use error::{Error, ErrorKind, Result, ResultExt};
+5 -3
kernel/src/ui/mod.rs
··· 1 1 // widget primitives for 1-bit e-paper displays 2 2 // 3 - // font-independent: Region, Alignment, stack measurement, StackFmt. 3 + // font-independent: Region, Alignment, stack measurement, StackFmt 4 4 // font-dependent widgets (BitmapLabel, QuickMenu, ButtonFeedback) 5 - // live in the distro's apps::widgets module. 5 + // live in the distro's apps::widgets module 6 6 7 7 pub mod stack_fmt; 8 8 pub mod statusbar; ··· 12 12 pub use statusbar::{ 13 13 BAR_HEIGHT, CONTENT_TOP, free_stack_bytes, paint_stack, stack_high_water_mark, 14 14 }; 15 - pub use widget::{Alignment, Region, draw_progress_bar, wrap_next, wrap_prev}; 15 + pub use widget::{ 16 + Alignment, Region, draw_loading_indicator, draw_progress_bar, wrap_next, wrap_prev, 17 + }; 16 18 17 19 pub use crate::board::{SCREEN_H, SCREEN_W};
+40 -5
kernel/src/ui/widget.rs
··· 1 - // region geometry and alignment helpers, progress bar drawing 1 + // region geometry, alignment helpers, progress bar, loading indicator 2 2 3 3 use embedded_graphics::{ 4 - pixelcolor::BinaryColor, prelude::*, primitives::PrimitiveStyle, primitives::Rectangle, 4 + mono_font::MonoTextStyle, mono_font::ascii::FONT_6X13, pixelcolor::BinaryColor, prelude::*, 5 + primitives::PrimitiveStyle, primitives::Rectangle, text::Text, 5 6 }; 6 7 7 8 use crate::drivers::strip::StripBuffer; 9 + use crate::ui::stack_fmt::BorrowedFmt; 8 10 9 11 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] 10 12 pub struct Region { ··· 115 117 if current == 0 { count - 1 } else { current - 1 } 116 118 } 117 119 118 - // horizontal progress bar for 1-bit e-paper. 120 + // horizontal progress bar for 1-bit e-paper 119 121 // draws a 1px black border around the full track and fills 120 - // proportionally from the left. pct is clamped to 0..=100. 121 - // region should be at least 4px wide and 4px tall. 122 + // proportionally from the left; pct is clamped to 0..=100 123 + // region should be at least 4px wide and 4px tall 122 124 pub fn draw_progress_bar(strip: &mut StripBuffer, region: Region, pct: u8) { 123 125 let pct = pct.min(100) as u32; 124 126 ··· 149 151 .unwrap(); 150 152 } 151 153 } 154 + 155 + // loading indicator for 1-bit e-paper 156 + // draws "msg...pct%" centered vertically in the region using the 157 + // built-in FONT_6X13 mono font; works without any custom bitmap 158 + // fonts loaded, usable from any app or the kernel itself 159 + // 160 + // typical usage: 161 + // draw_loading_indicator(strip, region, "Loading", 25) => "Loading...25%" 162 + // draw_loading_indicator(strip, region, "Caching 3/15", 20) => "Caching 3/15...20%" 163 + pub fn draw_loading_indicator(strip: &mut StripBuffer, region: Region, msg: &str, pct: u8) { 164 + use core::fmt::Write; 165 + 166 + // clear region 167 + region 168 + .to_rect() 169 + .into_styled(PrimitiveStyle::with_fill(BinaryColor::Off)) 170 + .draw(strip) 171 + .unwrap(); 172 + 173 + // format "msg...pct%" 174 + let mut buf = [0u8; 48]; 175 + let mut fmt = BorrowedFmt::new(&mut buf); 176 + let _ = write!(fmt, "{}...{}%", msg, pct.min(100)); 177 + let text = fmt.as_str(); 178 + 179 + // FONT_6X13: 6px wide, 13px tall, ~10px ascent 180 + // center vertically; baseline = region.y + (h + 7) / 2 181 + let style = MonoTextStyle::new(&FONT_6X13, BinaryColor::On); 182 + let baseline_y = region.y as i32 + (region.h as i32 + 7) / 2; 183 + Text::new(text, Point::new(region.x as i32 + 2, baseline_y), style) 184 + .draw(strip) 185 + .unwrap(); 186 + }
+16
src/apps/manager.rs
··· 1 1 // app lifecycle manager: nav stack, dispatch, font propagation, draw 2 2 // 3 3 // all dispatch is static (monomorphized via with_app!); no dyn, no vtable 4 + // loading indicator is drawn between app content and overlays so it 5 + // sits on top of page content but under quick menu and button bumps 4 6 5 7 use crate::apps::files::FilesApp; 6 8 use crate::apps::home::HomeApp; ··· 264 266 } 265 267 266 268 self.propagate_fonts(); 269 + self.launcher.ctx.clear_loading(); 267 270 268 271 if nav.to != AppId::Upload { 269 272 if nav.resume { ··· 307 310 pub fn draw(&self, strip: &mut StripBuffer) { 308 311 let active = self.launcher.active(); 309 312 with_app_ref!(active, self, |app| app.draw(strip)); 313 + 314 + // loading indicator: after app content, before overlays 315 + if self.launcher.ctx.loading_active() { 316 + let region = self.launcher.ctx.loading_region(); 317 + if region.intersects(strip.logical_window()) { 318 + crate::ui::draw_loading_indicator( 319 + strip, 320 + region, 321 + self.launcher.ctx.loading_msg(), 322 + self.launcher.ctx.loading_pct(), 323 + ); 324 + } 325 + } 310 326 311 327 if self.quick_menu.open { 312 328 self.quick_menu.draw(strip);
+3 -4
src/apps/mod.rs
··· 1 1 // app modules, AppId definition, and re-exports from kernel::app 2 2 // 3 3 // AppId is defined here (the distro side) -- the kernel is generic 4 - // over AppIdType and never knows which concrete apps exist. 4 + // over AppIdType and never knows which concrete apps exist 5 5 6 6 pub mod files; 7 7 pub mod home; ··· 35 35 36 36 pub use crate::kernel::app::{App, AppContext, PendingSetting, RECENT_FILE, Redraw}; 37 37 38 - // Unified error types — available to all app code as `crate::apps::Error` etc. 38 + // unified error types 39 39 pub use crate::kernel::{Error, ErrorKind, Result, ResultExt}; 40 40 41 - // Backward-compatible alias; old app code referencing `StorageError` 42 - // keeps compiling — it is now `type StorageError = Error`. 41 + // backward-compatible alias 43 42 pub use crate::kernel::StorageError;
+2 -2
src/apps/reader/images.rs
··· 3 3 // scan_chapter_for_image is the shared core: reads chapter data in 4 4 // chunks, finds IMG_REF markers, resolves paths, checks cache, and 5 5 // either decodes inline (large images) or dispatches to the worker 6 - // (small images). both epub_find_and_dispatch_image (background scan) 7 - // and dispatch_one_image_in_chapter (nearby prefetch) call through it. 6 + // (small images); both epub_find_and_dispatch_image (background scan) 7 + // and dispatch_one_image_in_chapter (nearby prefetch) call through it 8 8 9 9 extern crate alloc; 10 10
+83 -65
src/apps/reader/mod.rs
··· 29 29 use crate::kernel::QuickAction; 30 30 use crate::kernel::bookmarks; 31 31 use crate::kernel::work_queue; 32 - use crate::ui::{Alignment, BUTTON_BAR_H, CONTENT_TOP, Region, StackFmt, draw_progress_bar}; 32 + use crate::ui::{Alignment, BUTTON_BAR_H, CONTENT_TOP, Region, StackFmt}; 33 33 use smol_epub::DecodedImage; 34 34 use smol_epub::cache; 35 35 use smol_epub::epub::{self, EpubMeta, EpubSpine, EpubToc, TocSource}; ··· 79 79 POSITION_OVERLAY_H, 80 80 ); 81 81 82 - pub(super) const LOADING_REGION: Region = Region::new(MARGIN, TEXT_Y, 464, 20); 83 - pub(super) const LOADING_BAR_REGION: Region = Region::new(MARGIN, TEXT_Y + 26, 200, 8); 82 + pub(super) const LOADING_REGION: Region = Region::new(MARGIN, TEXT_Y, 464, 18); 84 83 85 84 pub const QA_FONT_SIZE: u8 = 1; 86 85 pub(super) const QA_PREV_CHAPTER: u8 = 3; ··· 101 100 Ready, 102 101 ShowToc, 103 102 Error, 104 - } 105 - 106 - impl State { 107 - pub(super) fn loading_pct(self) -> u8 { 108 - match self { 109 - Self::NeedBookmark => 0, 110 - Self::NeedInit => 5, 111 - Self::NeedOpf => 15, 112 - Self::NeedToc => 30, 113 - Self::NeedCache => 45, 114 - Self::NeedIndex => 70, 115 - Self::NeedPage => 90, 116 - Self::Ready | Self::ShowToc | Self::Error => 100, 117 - } 118 - } 119 103 } 120 104 121 105 // background caching progress, runs independently of the reading ··· 343 327 self.is_epub && self.bg_cache != BgCacheState::Idle 344 328 } 345 329 330 + pub(super) fn cached_chapter_count(&self) -> usize { 331 + let n = self.spine.len().min(cache::MAX_CACHE_CHAPTERS); 332 + self.ch_cached[..n].iter().filter(|&&c| c).count() 333 + } 334 + 335 + // update the kernel loading indicator with current caching progress 336 + fn set_cache_loading(&self, ctx: &mut AppContext) { 337 + let cached = self.cached_chapter_count(); 338 + let total = self.spine.len(); 339 + let mut lbuf = StackFmt::<28>::new(); 340 + if matches!( 341 + self.bg_cache, 342 + BgCacheState::CacheChapter | BgCacheState::WaitNearbyImage 343 + ) && cached < total 344 + { 345 + let _ = write!(lbuf, "Caching {}/{}", cached, total); 346 + } else { 347 + let _ = write!(lbuf, "Caching image(s)"); 348 + } 349 + let pct = if total > 0 { 350 + ((cached * 100) / total).min(100) as u8 351 + } else { 352 + 100 353 + }; 354 + ctx.set_loading(LOADING_REGION, lbuf.as_str(), pct); 355 + } 356 + 346 357 // run one step of image work queue polling while suspended; 347 358 // chapter caching is async and only runs during active background, 348 359 // so this only handles the sync image recv states ··· 604 615 605 616 log::info!("reader: opening {}", self.name()); 606 617 618 + ctx.set_loading(LOADING_REGION, "Opening", 0); 607 619 ctx.mark_dirty(PAGE_REGION); 608 620 } 609 621 ··· 671 683 self.chapters_cached = false; 672 684 self.goto_last_page = false; 673 685 self.state = State::NeedInit; 686 + ctx.set_loading(LOADING_REGION, "Loading", 10); 674 687 } else { 675 688 self.state = State::NeedPage; 689 + ctx.set_loading(LOADING_REGION, "Loading", 50); 676 690 } 677 691 continue; 678 692 } ··· 680 694 State::NeedInit => match self.epub_init_zip(k) { 681 695 Ok(()) => { 682 696 self.state = State::NeedOpf; 683 - ctx.mark_dirty(LOADING_BAR_REGION); 697 + ctx.set_loading(LOADING_REGION, "Loading", 25); 684 698 } 685 699 Err(e) => { 686 700 log::info!("reader: epub init (zip) failed: {}", e); 687 701 self.error = Some(e); 688 702 self.state = State::Error; 703 + ctx.clear_loading(); 689 704 ctx.mark_dirty(PAGE_REGION); 690 705 } 691 706 }, ··· 693 708 State::NeedOpf => match self.epub_init_opf(k) { 694 709 Ok(()) => { 695 710 self.state = State::NeedToc; 696 - ctx.mark_dirty(LOADING_BAR_REGION); 711 + ctx.set_loading(LOADING_REGION, "Loading", 40); 697 712 } 698 713 Err(e) => { 699 714 log::info!("reader: epub init (opf) failed: {}", e); 700 715 self.error = Some(e); 701 716 self.state = State::Error; 717 + ctx.clear_loading(); 702 718 ctx.mark_dirty(PAGE_REGION); 703 719 } 704 720 }, ··· 739 755 } 740 756 self.rebuild_quick_actions(); 741 757 self.state = State::NeedCache; 742 - // break instead of continue so the progress bar 743 - // renders before potentially heavy chapter caching; 744 - // for cached books this mark coalesces with Ready 745 - // because both complete during the initial waveform 746 - ctx.mark_dirty(LOADING_BAR_REGION); 758 + ctx.set_loading(LOADING_REGION, "Caching", 55); 747 759 } 748 760 749 761 State::NeedCache => match self.epub_check_cache(k) { 750 762 Ok(true) => { 751 763 self.state = State::NeedIndex; 752 - continue; 764 + ctx.set_loading(LOADING_REGION, "Indexing", 75); 753 765 } 754 766 Ok(false) => { 755 767 // cache the current chapter; async version yields ··· 771 783 } 772 784 773 785 self.state = State::NeedIndex; 774 - continue; 786 + ctx.set_loading(LOADING_REGION, "Indexing", 75); 775 787 } 776 788 Err(e) => { 777 789 log::info!("reader: cache ch{} failed: {}", ch, e); 778 790 self.error = Some(e); 779 791 self.state = State::Error; 792 + ctx.clear_loading(); 780 793 ctx.mark_dirty(PAGE_REGION); 781 794 } 782 795 } ··· 785 798 log::info!("reader: cache check failed: {}", e); 786 799 self.error = Some(e); 787 800 self.state = State::Error; 801 + ctx.clear_loading(); 788 802 ctx.mark_dirty(PAGE_REGION); 789 803 } 790 804 }, ··· 805 819 { 806 820 self.error = Some(e); 807 821 self.state = State::Error; 822 + ctx.clear_loading(); 808 823 ctx.mark_dirty(PAGE_REGION); 809 824 break; 810 825 } ··· 824 839 Ok(()) => { 825 840 self.defer_image_decode = false; 826 841 self.state = State::Ready; 842 + ctx.clear_loading(); 827 843 ctx.mark_dirty(PAGE_REGION); 828 844 } 829 845 Err(e) => { 830 846 self.error = Some(e); 831 847 self.state = State::Error; 848 + ctx.clear_loading(); 832 849 ctx.mark_dirty(PAGE_REGION); 833 850 } 834 851 } 835 852 } else { 836 853 self.state = State::NeedPage; 837 - continue; 854 + ctx.set_loading(LOADING_REGION, "Loading page", 90); 838 855 } 839 856 } 840 857 ··· 847 864 Err(e) => { 848 865 self.error = Some(e); 849 866 self.state = State::Error; 867 + ctx.clear_loading(); 850 868 ctx.mark_dirty(PAGE_REGION); 851 869 break; 852 870 } ··· 862 880 if self.state != State::Error { 863 881 self.defer_image_decode = false; 864 882 self.state = State::Ready; 883 + ctx.clear_loading(); 865 884 ctx.mark_dirty(PAGE_REGION); 866 885 } 867 886 } else { ··· 869 888 Ok(()) => { 870 889 self.defer_image_decode = false; 871 890 self.state = State::Ready; 891 + ctx.clear_loading(); 872 892 ctx.mark_dirty(PAGE_REGION); 873 893 } 874 894 Err(e) => { 875 895 log::info!("reader: load failed: {}", e); 876 896 self.error = Some(e); 877 897 self.state = State::Error; 898 + ctx.clear_loading(); 878 899 ctx.mark_dirty(PAGE_REGION); 879 900 } 880 901 } ··· 897 918 State::Ready | State::ShowToc | State::NeedIndex | State::NeedPage 898 919 ) && self.bg_cache != BgCacheState::Idle 899 920 { 921 + // ensure caching indicator is visible (covers resume 922 + // and the transition from initial load to bg caching) 923 + if !ctx.loading_active() { 924 + self.set_cache_loading(ctx); 925 + } 926 + let prev_count = self.cached_chapter_count(); 927 + let prev_bg = self.bg_cache; 900 928 self.bg_cache_step(k).await; 929 + if self.bg_cache == BgCacheState::Idle { 930 + ctx.clear_loading(); 931 + } else if self.cached_chapter_count() != prev_count || self.bg_cache != prev_bg { 932 + self.set_cache_loading(ctx); 933 + } 901 934 } 902 935 } 903 936 ··· 906 939 match event { 907 940 ActionEvent::Press(Action::Back) => { 908 941 self.state = State::Ready; 909 - ctx.mark_dirty_immediate(PAGE_REGION); 942 + ctx.mark_dirty(PAGE_REGION); 910 943 return Transition::None; 911 944 } 912 945 ActionEvent::Press(Action::Next) | ActionEvent::Repeat(Action::Next) => { ··· 922 955 if self.toc_selected >= self.toc_scroll + vis { 923 956 self.toc_scroll = self.toc_selected + 1 - vis; 924 957 } 925 - ctx.mark_dirty_immediate(PAGE_REGION); 958 + ctx.mark_dirty(PAGE_REGION); 926 959 } 927 960 return Transition::None; 928 961 } ··· 941 974 if self.toc_selected < self.toc_scroll { 942 975 self.toc_scroll = self.toc_selected; 943 976 } 944 - ctx.mark_dirty_immediate(PAGE_REGION); 977 + ctx.mark_dirty(PAGE_REGION); 945 978 } 946 979 return Transition::None; 947 980 } ··· 957 990 self.page = 0; 958 991 self.goto_last_page = false; 959 992 self.state = State::NeedIndex; 960 - ctx.mark_dirty_immediate(PAGE_REGION); 993 + ctx.mark_dirty(PAGE_REGION); 961 994 } else { 962 995 log::warn!( 963 996 "toc: entry \"{}\" unresolved (spine_idx=0xFFFF), ignoring", 964 997 entry.title_str() 965 998 ); 966 999 self.state = State::Ready; 967 - ctx.mark_dirty_immediate(PAGE_REGION); 1000 + ctx.mark_dirty(PAGE_REGION); 968 1001 } 969 1002 return Transition::None; 970 1003 } ··· 981 1014 self.show_position = true; 982 1015 } 983 1016 if self.page_forward() { 984 - ctx.mark_dirty_immediate(PAGE_REGION); 1017 + ctx.mark_dirty(PAGE_REGION); 985 1018 } 986 1019 Transition::None 987 1020 } ··· 990 1023 self.show_position = true; 991 1024 } 992 1025 if self.page_backward() { 993 - ctx.mark_dirty_immediate(PAGE_REGION); 1026 + ctx.mark_dirty(PAGE_REGION); 994 1027 } 995 1028 Transition::None 996 1029 } ··· 998 1031 ActionEvent::Release(Action::Next) | ActionEvent::Release(Action::Prev) => { 999 1032 if self.show_position { 1000 1033 self.show_position = false; 1001 - ctx.mark_dirty_immediate(POSITION_OVERLAY); 1034 + ctx.mark_dirty(POSITION_OVERLAY); 1002 1035 } 1003 1036 Transition::None 1004 1037 } 1005 1038 1006 1039 ActionEvent::Press(Action::Next) | ActionEvent::Repeat(Action::Next) => { 1007 1040 if self.page_forward() { 1008 - ctx.mark_dirty_immediate(PAGE_REGION); 1041 + ctx.mark_dirty(PAGE_REGION); 1009 1042 } 1010 1043 Transition::None 1011 1044 } 1012 1045 1013 1046 ActionEvent::Press(Action::Prev) | ActionEvent::Repeat(Action::Prev) => { 1014 1047 if self.page_backward() { 1015 - ctx.mark_dirty_immediate(PAGE_REGION); 1048 + ctx.mark_dirty(PAGE_REGION); 1016 1049 } 1017 1050 Transition::None 1018 1051 } 1019 1052 1020 1053 ActionEvent::Press(Action::NextJump) | ActionEvent::Repeat(Action::NextJump) => { 1021 1054 if self.jump_forward() { 1022 - ctx.mark_dirty_immediate(PAGE_REGION); 1055 + ctx.mark_dirty(PAGE_REGION); 1023 1056 } 1024 1057 Transition::None 1025 1058 } 1026 1059 1027 1060 ActionEvent::Press(Action::PrevJump) | ActionEvent::Repeat(Action::PrevJump) => { 1028 1061 if self.jump_backward() { 1029 - ctx.mark_dirty_immediate(PAGE_REGION); 1062 + ctx.mark_dirty(PAGE_REGION); 1030 1063 } 1031 1064 Transition::None 1032 1065 } ··· 1123 1156 if self.state == State::ShowToc { 1124 1157 draw_chrome_text(strip, STATUS_REGION, "Contents", Alignment::CenterRight, cf); 1125 1158 } else if self.is_epub && !self.spine.is_empty() { 1126 - let mut sbuf = StackFmt::<32>::new(); 1159 + let mut sbuf = StackFmt::<40>::new(); 1127 1160 if self.spine.len() > 1 { 1128 1161 if self.fully_indexed { 1129 1162 let _ = write!( ··· 1149 1182 let _ = write!(sbuf, "p{}", self.page + 1); 1150 1183 } 1151 1184 if self.bg_cache != BgCacheState::Idle { 1152 - let _ = write!(sbuf, " *"); 1185 + let cached = self.cached_chapter_count(); 1186 + let total = self.spine.len(); 1187 + if cached < total { 1188 + let _ = write!(sbuf, " [{}/{}]", cached, total); 1189 + } else { 1190 + let _ = write!(sbuf, " [img]"); 1191 + } 1153 1192 } 1154 1193 draw_chrome_text( 1155 1194 strip, ··· 1187 1226 return; 1188 1227 } 1189 1228 1229 + // loading states: the kernel loading indicator (drawn by 1230 + // AppManager) handles feedback text; nothing else to draw 1190 1231 if self.state != State::Ready && self.state != State::Error && self.state != State::ShowToc 1191 1232 { 1192 - let mut lbuf = StackFmt::<48>::new(); 1193 - match self.state { 1194 - State::NeedCache => { 1195 - let _ = write!(lbuf, "Preparing..."); 1196 - } 1197 - State::NeedIndex => { 1198 - let _ = write!(lbuf, "Indexing..."); 1199 - } 1200 - State::NeedPage => { 1201 - let _ = write!(lbuf, "Loading..."); 1202 - } 1203 - _ => { 1204 - let _ = write!(lbuf, "Loading..."); 1205 - } 1206 - } 1207 - draw_chrome_text( 1208 - strip, 1209 - LOADING_REGION, 1210 - lbuf.as_str(), 1211 - Alignment::CenterLeft, 1212 - cf, 1213 - ); 1214 - draw_progress_bar(strip, LOADING_BAR_REGION, self.state.loading_pct()); 1215 1233 return; 1216 1234 } 1217 1235
+6 -6
src/apps/settings.rs
··· 269 269 let old = self.selected; 270 270 self.selected = wrap_next(self.selected, NUM_ITEMS); 271 271 if self.selected != old { 272 - ctx.mark_dirty_immediate(self.row_region(old)); 273 - ctx.mark_dirty_immediate(self.row_region(self.selected)); 272 + ctx.mark_dirty(self.row_region(old)); 273 + ctx.mark_dirty(self.row_region(self.selected)); 274 274 } 275 275 Transition::None 276 276 } ··· 279 279 let old = self.selected; 280 280 self.selected = wrap_prev(self.selected, NUM_ITEMS); 281 281 if self.selected != old { 282 - ctx.mark_dirty_immediate(self.row_region(old)); 283 - ctx.mark_dirty_immediate(self.row_region(self.selected)); 282 + ctx.mark_dirty(self.row_region(old)); 283 + ctx.mark_dirty(self.row_region(self.selected)); 284 284 } 285 285 Transition::None 286 286 } 287 287 288 288 ActionEvent::Press(Action::NextJump) | ActionEvent::Repeat(Action::NextJump) => { 289 289 self.increment(); 290 - ctx.mark_dirty_immediate(self.value_region(self.selected)); 290 + ctx.mark_dirty(self.value_region(self.selected)); 291 291 Transition::None 292 292 } 293 293 294 294 ActionEvent::Press(Action::PrevJump) | ActionEvent::Repeat(Action::PrevJump) => { 295 295 self.decrement(); 296 - ctx.mark_dirty_immediate(self.value_region(self.selected)); 296 + ctx.mark_dirty(self.value_region(self.selected)); 297 297 Transition::None 298 298 } 299 299
+2 -2
src/apps/widgets/mod.rs
··· 1 1 // font-dependent UI widgets (app-side) 2 2 // 3 3 // these widgets depend on BitmapFont from the fontdue pipeline and 4 - // live in the apps layer, not the kernel. the kernel's ui/ module 5 - // holds only font-independent primitives (Region, Alignment, etc.). 4 + // live in the apps layer, not the kernel; the kernel's ui/ module 5 + // holds only font-independent primitives (Region, Alignment, etc) 6 6 7 7 pub mod bitmap_label; 8 8 pub mod button_feedback;
-10
src/fonts/mod.rs
··· 94 94 heading: &'static BitmapFont, 95 95 } 96 96 97 - impl Default for FontSet { 98 - fn default() -> Self { 99 - Self::new() 100 - } 101 - } 102 - 103 97 impl FontSet { 104 98 fn from_fonts( 105 99 regular: &'static BitmapFont, ··· 164 158 &font_data::REGULAR_HEADING_SMALL, 165 159 ), 166 160 } 167 - } 168 - 169 - pub fn new() -> Self { 170 - Self::for_size(1) 171 161 } 172 162 173 163 #[inline]
+2 -2
src/ui/mod.rs
··· 1 1 // ui re-exports: kernel primitives + app-side font-dependent widgets 2 2 // 3 3 // kernel ui (Region, Alignment, StackFmt, statusbar constants) is 4 - // re-exported from pulp-kernel. font-dependent widgets (BitmapLabel, 5 - // QuickMenu, ButtonFeedback) come from apps::widgets. 4 + // re-exported from pulp-kernel; font-dependent widgets (BitmapLabel, 5 + // QuickMenu, ButtonFeedback) come from apps::widgets 6 6 7 7 // kernel-side primitives 8 8 pub use pulp_kernel::ui::stack_fmt;