feat(k-ui): first slice: App trait + run loop, builders (column/row/text/button), flex layout, theme tokens (Whimsy Dark), tiny-skia + fontdue render (Noto Sans Latin subset), keyboard focus ring; tally demo; manifest memory key (pm spawns with it)

This commit is contained in:
2026-10-28 03:20:41 +01:00
parent c256f4ecf3
commit fe73b89525
27 changed files with 1428 additions and 6 deletions

107
crates/k-ui/src/ui.rs Normal file
View File

@@ -0,0 +1,107 @@
//! 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
//! draws into.
use alloc::vec::Vec;
use tiny_skia::PixmapMut;
use crate::element::{Element, Kind};
use crate::layout::{self, Rect};
use crate::render;
use crate::text::{Fonts, Measurer};
use crate::theme::Theme;
/// Every K-UI app.
pub trait App {
type Event: Clone;
fn init() -> Self;
/// Changes the state; the view follows.
fn update(&mut self, event: Self::Event);
/// What the state looks like: a pure function of it.
fn view(&self) -> Element<Self::Event>;
}
/// A key, as far as K-UI cares.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Key {
Next,
Previous,
Press,
}
pub struct Ui<A: App> {
pub app: A,
/// Which pressable element has focus: its place among them, in
/// pre-order.
focus: usize,
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(_)) {
out.push(*index);
}
*index += 1;
if let Kind::Flex(f) = &e.kind {
for c in &f.children {
pressables(c, index, out);
}
}
}
/// The event of the pre-order `target`th element, if it is a button.
fn event_at<E: Clone>(e: &Element<E>, index: &mut usize, target: usize) -> Option<E> {
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)),
_ => None,
}
}
impl<A: App> Ui<A> {
pub fn new(theme: &'static Theme) -> Self {
Self { app: A::init(), focus: 0, theme }
}
fn pressables(&self, view: &Element<A::Event>) -> Vec<usize> {
let mut out = Vec::new();
pressables(view, &mut 0, &mut out);
out
}
/// Handles a key; whether anything changed.
pub fn key(&mut self, key: Key) -> bool {
let view = self.app.view();
let targets = self.pressables(&view);
if targets.is_empty() {
return false;
}
let n = targets.len();
match key {
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 };
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));
}
}
true
}
/// Lays the view out over the whole of `pm` and draws it.
pub fn draw(&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 focused = self.pressables(&view).get(self.focus).copied();
render::render(pm, &view, &rects, fonts, self.theme, focused);
}
}