···11-// Button feedback: plain text edge labels (no inversion).
22-// Bottom tabs show the mapped action text; side labels hidden.
33-// No visual change on press/release; purely informational.
44-51use embedded_graphics::{pixelcolor::BinaryColor, prelude::*, primitives::PrimitiveStyle};
6273use super::widget::{Alignment, Region};
···1511const TAB_W: u16 = 60;
1612const TAB_H: u16 = 22;
17131818-// total height reserved at bottom of screen for button labels
1914pub const BUTTON_BAR_H: u16 = TAB_H + BOTTOM_INSET;
20152116const RIDGE_W: u16 = 22;
2217const RIDGE_H: u16 = 36;
23182424-// center positions of each button on the screen edge (px)
2519const CX_BACK: u16 = 84;
2620const CX_CONFIRM: u16 = 194;
2721const CX_LEFT: u16 = 286;
···128122 }
129123 }
130124131131- // set chrome font for button label text; call on UI font size change
132125 pub fn set_chrome_font(&mut self, font: &'static BitmapFont) {
133126 self.font = Some(font);
134127 }
135128136136- // draw bottom-edge labels only; no side indicators or inversion
137129 pub fn draw(&self, strip: &mut StripBuffer) {
138130 let font = self.font.unwrap_or(&font_data::REGULAR_BODY_SMALL);
139131140132 for def in BUMPS.iter() {
141141- // skip side-edge indicators (VolUp/VolDown)
142133 if def.edge != Edge::Bottom {
143134 continue;
144135 }
···149140 continue;
150141 }
151142152152- // plain: white background, black text
153143 r.to_rect()
154144 .into_styled(PrimitiveStyle::with_fill(BinaryColor::Off))
155145 .draw(strip)
+1-19
src/ui/quick_menu.rs
···11-// Quick-action overlay: summoned by Power (Menu) from any app.
22-// Core actions (Refresh, Go Home) always present; apps inject up to MAX_APP_ACTIONS items.
33-// Two kinds: Cycle (rotate options) and Trigger (fire on Select). Menu/Back dismisses.
44-// Rendering: plain inverted-text rows, no borders or separators.
55-61use embedded_graphics::{pixelcolor::BinaryColor, prelude::*, primitives::PrimitiveStyle};
7283use super::stack_fmt::StackFmt;
···138use crate::fonts::bitmap::BitmapFont;
149use crate::fonts::font_data;
15101616-// layout constants
1717-1811const OVERLAY_W: u16 = 400;
1912const OVERLAY_X: u16 = (SCREEN_W - OVERLAY_W) / 2;
2020-const OVERLAY_BOTTOM: u16 = 760; // above button widgets (~14px clearance)
1313+const OVERLAY_BOTTOM: u16 = 760;
2114const ITEM_H: u16 = 40;
2215const ITEM_GAP: u16 = 4;
2316const ITEM_STRIDE: u16 = ITEM_H + ITEM_GAP;
···36293730#[derive(Debug, Clone, Copy)]
3831pub enum QuickActionKind {
3939- // rotates through named options; value is the current index
4032 Cycle {
4133 value: u8,
4234 options: &'static [&'static str],
4335 },
4444- // fires immediately on Select; display is the right-column label
4536 Trigger {
4637 display: &'static str,
4738 },
4839}
49405050-// app-provided quick action descriptor; id echoed in AppTrigger
5141#[derive(Debug, Clone, Copy)]
5242pub struct QuickAction {
5343 pub id: u8,
···146136 }
147137 }
148138149149- // set chrome font for menu item text; call on UI font size change
150139 pub fn set_chrome_font(&mut self, font: &'static BitmapFont) {
151140 self.font = Some(font);
152141 }
153142154154- // open overlay with app-provided items; core items appended automatically
155143 pub fn show(&mut self, app_actions: &[QuickAction]) {
156144 let n_app = app_actions.len().min(MAX_APP_ACTIONS);
157145 self.app_count = n_app;
···268256 }
269257 }
270258271271- // cycle: advance value; trigger/core: fire and close
272259 fn activate_selected(&mut self) -> QuickMenuResult {
273260 match &mut self.items[self.selected].kind {
274261 MenuItemKind::AppCycle { value, options, .. } => {
···350337351338 let font = self.font.unwrap_or(&font_data::REGULAR_BODY_SMALL);
352339353353- // clear the overlay background
354340 let outer = self.overlay_region;
355341 if outer.intersects(strip.logical_window()) {
356342 outer
···368354 let label_region = self.item_label_region(i);
369355 let value_region = self.item_value_region(i);
370356371371- // selected row: inverted (white text on black)
372357 if selected {
373358 let row_region = Region::new(OVERLAY_X, self.item_y(i), OVERLAY_W, ITEM_H);
374359 if row_region.intersects(strip.logical_window()) {
···386371 BinaryColor::On
387372 };
388373389389- // draw label text
390374 if label_region.intersects(strip.logical_window()) {
391375 font.draw_aligned(
392376 strip,
···397381 );
398382 }
399383400400- // draw value text
401384 if value_region.intersects(strip.logical_window()) {
402385 self.format_value(i, &mut val_buf);
403386 font.draw_aligned(strip, value_region, val_buf.as_str(), Alignment::Center, fg);
404387 }
405388 }
406389407407- // help text at the bottom
408390 let help = match &self.items[self.selected].kind {
409391 MenuItemKind::AppCycle { .. } => "Up/Down: move Jump: adjust Sel: cycle Menu: close",
410392 _ => "Up/Down: move Sel: activate Menu: close",
+1-4
src/ui/stack_fmt.rs
···11-// No-alloc fmt::Write buffers.
22-// StackFmt<N> owns a [u8; N]; BorrowedFmt wraps &mut [u8].
33-// Both silently truncate on overflow.
11+// No-alloc fmt::Write buffers; silently truncate on overflow.
4253pub struct StackFmt<const N: usize> {
64 buf: [u8; N],
···8583 }
8684}
87858888-// format into a borrowed slice via closure; returns bytes written
8986#[inline]
9087pub fn stack_fmt(buf: &mut [u8], f: impl FnOnce(&mut BorrowedFmt<'_>)) -> usize {
9188 let mut w = BorrowedFmt::new(buf);
+1-10
src/ui/statusbar.rs
···11-// Debug status bar at top of screen (debug builds only).
22-// Shows battery, uptime, heap (current/peak/total), stack, SD state.
33-// In release builds BAR_HEIGHT is 0 and draw/update are no-ops,
44-// so apps reclaim the full screen without any code changes.
11+// Debug status bar; zero height in release builds.
5263#[cfg(debug_assertions)]
74use core::fmt::Write;
···146143 }
147144}
148145149149-// distance from current SP down to _stack_end_cpu0 (bottom of stack)
150146pub fn free_stack_bytes() -> usize {
151147 let sp: usize;
152148 #[cfg(target_arch = "riscv32")]
···158154 sp = 0;
159155 }
160156161161- // lowest address the stack may reach
162157 #[cfg(target_arch = "riscv32")]
163158 {
164159 unsafe extern "C" {
···174169 }
175170}
176171177177-// stack painting: paint_stack() fills unused stack with 0xDEAD_BEEF at boot;
178178-// stack_high_water_mark() scans upward to find peak usage.
179172const STACK_PAINT_WORD: u32 = 0xDEAD_BEEF;
180173181181-// fill unused stack with sentinel; call once early in main before task spawn
182174pub fn paint_stack() {
183175 #[cfg(target_arch = "riscv32")]
184176 {
···213205 }
214206}
215207216216-// scan for first non-sentinel word from stack bottom; return peak usage in bytes
217208pub fn stack_high_water_mark() -> usize {
218209 #[cfg(target_arch = "riscv32")]
219210 {
+1-7
src/ui/widget.rs
···11-// Region geometry and alignment helpers
22-// All coordinates are logical (rotation aware). x/w should be 8-aligned
33-// for partial refresh to avoid byte boundary fixups on the controller.
11+// Region geometry and alignment helpers; x/w should be 8-aligned for partial refresh.
4253use embedded_graphics::{prelude::*, primitives::Rectangle};
64···9795 }
9896}
9997100100-// wrap-around list navigation helpers
101101-102102-// advance index by one, wrapping past count-1 back to 0
10398#[inline]
10499pub fn wrap_next(current: usize, count: usize) -> usize {
105100 if count == 0 {
···108103 if current + 1 >= count { 0 } else { current + 1 }
109104}
110105111111-// retreat index by one, wrapping past 0 to count-1
112106#[inline]
113107pub fn wrap_prev(current: usize, count: usize) -> usize {
114108 if count == 0 {