feat(settings): settings app: apps list + per-capability allow/revoke, about (who/role), sign out; system-only caps permissions/session (install refused); pm MANAGE_BADGE copy (list/apps/allow/revoke only), APPS op; init WHO + session pass-on; factory decisions seeded for new system apps; k-ui list_item/toggle, screens/back/scroll

This commit is contained in:
2026-10-28 13:14:35 +01:00
parent a2d4a7d085
commit 29becf297c
38 changed files with 947 additions and 85 deletions

View File

@@ -1,6 +1,7 @@
//! An app and what K-UI keeps about it: which pressable thing has focus.
//! Keys move focus (Tab, the arrows) and press (Enter, Space); a press is
//! the button's event, handed to the app. Pure, apart from the pixels it
//! An app and what K-UI keeps about it: which pressable thing has focus,
//! and how far the view is scrolled to keep it in sight. Keys move focus
//! (Tab, the arrows), press (Enter, Space) and go back (Escape); a press is
//! the element's event, handed to the app. Pure, apart from the pixels it
//! draws into.
use alloc::vec::Vec;
@@ -21,6 +22,16 @@ pub trait App {
fn update(&mut self, event: Self::Event);
/// What the state looks like: a pure function of it.
fn view(&self) -> Element<Self::Event>;
/// Escape: back one screen, if there is one to go back to; otherwise
/// the app is left.
fn back(&mut self) -> bool {
false
}
/// Which screen it shows: when this changes, focus starts again from
/// the top.
fn screen(&self) -> usize {
0
}
}
/// A key, as far as K-UI cares.
@@ -29,6 +40,16 @@ pub enum Key {
Next,
Previous,
Press,
Back,
}
/// What a key did.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Outcome {
Nothing,
Changed,
/// Back, with nowhere to go back to: the app is to be left.
Leave,
}
pub struct Ui<A: App> {
@@ -36,12 +57,14 @@ pub struct Ui<A: App> {
/// Which pressable element has focus: its place among them, in
/// pre-order.
focus: usize,
/// How far down the view is scrolled, in pixels.
scroll: f32,
theme: &'static Theme,
}
/// The pre-order indices of every pressable element in `e`.
fn pressables<E>(e: &Element<E>, index: &mut usize, out: &mut Vec<usize>) {
if matches!(e.kind, Kind::Button(_)) {
if e.kind.event().is_some() {
out.push(*index);
}
*index += 1;
@@ -57,15 +80,15 @@ fn event_at<E: Clone>(e: &Element<E>, index: &mut usize, target: usize) -> Optio
let me = *index;
*index += 1;
match &e.kind {
Kind::Button(b) if me == target => Some(b.on_press.clone()),
Kind::Flex(f) => f.children.iter().find_map(|c| event_at(c, index, target)),
k if me == target => k.event().cloned(),
_ => None,
}
}
impl<A: App> Ui<A> {
pub fn new(theme: &'static Theme) -> Self {
Self { app: A::init(), focus: 0, theme }
Self { app: A::init(), focus: 0, scroll: 0.0, theme }
}
fn pressables(&self, view: &Element<A::Event>) -> Vec<usize> {
@@ -74,8 +97,21 @@ impl<A: App> Ui<A> {
out
}
/// Handles a key; whether anything changed.
pub fn key(&mut self, key: Key) -> bool {
/// Handles a key.
pub fn key(&mut self, key: Key) -> Outcome {
if key == Key::Back {
if !self.app.back() {
return Outcome::Leave;
}
// A new screen: from its top.
(self.focus, self.scroll) = (0, 0.0);
return Outcome::Changed;
}
if self.press(key) { Outcome::Changed } else { Outcome::Nothing }
}
/// Moves focus or presses; whether anything changed.
fn press(&mut self, key: Key) -> bool {
let view = self.app.view();
let targets = self.pressables(&view);
if targets.is_empty() {
@@ -83,25 +119,55 @@ impl<A: App> Ui<A> {
}
let n = targets.len();
match key {
Key::Back => return false,
Key::Next => self.focus = (self.focus + 1) % n,
Key::Previous => self.focus = (self.focus + n - 1) % n,
Key::Press => {
let Some(event) = event_at(&view, &mut 0, targets[self.focus.min(n - 1)]) else { return false };
let screen = self.app.screen();
self.app.update(event);
// The view may have fewer pressable things now.
let now = self.pressables(&self.app.view()).len();
self.focus = self.focus.min(now.saturating_sub(1));
if self.app.screen() != screen {
(self.focus, self.scroll) = (0, 0.0);
} else {
// The view may have fewer pressable things now.
let now = self.pressables(&self.app.view()).len();
self.focus = self.focus.min(now.saturating_sub(1));
}
}
}
true
}
/// Lays the view out over the whole of `pm` and draws it.
pub fn draw(&self, pm: &mut PixmapMut, fonts: &Fonts) {
/// Lays the view out over the whole of `pm` and draws it, scrolled so
/// the focused element is in sight.
pub fn draw(&mut self, pm: &mut PixmapMut, fonts: &Fonts) {
let view = self.app.view();
let area = Rect { x: 0.0, y: 0.0, w: pm.width() as f32, h: pm.height() as f32 };
let rects = layout::layout(&view, area, &Measurer { fonts, theme: self.theme }, self.theme);
let (w, h) = (pm.width() as f32, pm.height() as f32);
let measurer = Measurer { fonts, theme: self.theme };
// As tall as it needs: what does not fit is scrolled to.
let tall = layout::natural(&view, &measurer, self.theme).1.max(h);
let mut rects = layout::layout(&view, Rect { x: 0.0, y: 0.0, w, h: tall }, &measurer, self.theme);
let focused = self.pressables(&view).get(self.focus).copied();
if let Some(r) = focused.and_then(|i| rects.get(i)) {
self.scroll = keep_in_sight(self.scroll, r.y, r.h, h, tall);
}
for r in &mut rects {
r.y -= self.scroll;
}
render::render(pm, &view, &rects, fonts, self.theme, focused);
}
}
/// The scroll that shows `top..top + height` within a window `window` high
/// over a view `view` high, moving as little as possible from `scroll`.
fn keep_in_sight(scroll: f32, top: f32, height: f32, window: f32, view: f32) -> f32 {
const MARGIN: f32 = 16.0;
let mut s = scroll;
if top - MARGIN < s {
s = top - MARGIN;
}
if top + height + MARGIN > s + window {
s = top + height + MARGIN - window;
}
s.clamp(0.0, (view - window).max(0.0))
}