//! 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; 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; /// 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. #[derive(Clone, Copy, Debug, PartialEq, Eq)] 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 { pub app: A, /// 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: Theme, } /// The pre-order indices of every pressable element in `e`. fn pressables(e: &Element, index: &mut usize, out: &mut Vec) { if e.kind.event().is_some() { 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: &Element, index: &mut usize, target: usize) -> Option { let me = *index; *index += 1; match &e.kind { Kind::Flex(f) => f.children.iter().find_map(|c| event_at(c, index, target)), k if me == target => k.event().cloned(), _ => None, } } impl Ui { pub fn new(theme: Theme) -> Self { Self { app: A::init(), focus: 0, scroll: 0.0, theme } } /// A new theme: everything is drawn in it from the next frame. pub fn set_theme(&mut self, theme: Theme) { self.theme = theme; } fn pressables(&self, view: &Element) -> Vec { let mut out = Vec::new(); pressables(view, &mut 0, &mut out); out } /// 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() { return false; } 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); 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, scrolled so /// the focused element is in sight. pub fn draw(&mut self, pm: &mut PixmapMut, fonts: &Fonts) { let view = self.app.view(); 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)) }