//! An app and what K-UI keeps about it: which thing has focus, where the //! caret is if that is a field, 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 field takes what is typed, Backspace, and Left and //! Right for its caret. A press or an edit 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, Input, 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 { /// Tab, Down. Next, /// Up. Previous, Left, Right, /// Enter. Press, Space, Back, /// A printable character typed. Char(char), /// Backspace. Erase, } /// 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, /// The caret in the focused field, in characters; at most its length /// (more means the end). caret: usize, theme: Theme, } /// The pre-order indices of every element focus can rest on in `e`. fn pressables(e: &Element, index: &mut usize, out: &mut Vec) { if e.kind.focusable() { 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, } } /// The pre-order `target`th element, if it is a field. fn input_at<'e, E>(e: &'e Element, index: &mut usize, target: usize) -> Option<&'e Input> { let me = *index; *index += 1; match &e.kind { Kind::Flex(f) => f.children.iter().find_map(|c| input_at(c, index, target)), Kind::Input(i) if me == target => Some(i), _ => None, } } impl Ui { pub fn new(theme: Theme) -> Self { Self { app: A::init(), focus: 0, scroll: 0.0, caret: usize::MAX, 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, self.caret) = (0, 0.0, usize::MAX); return Outcome::Changed; } if self.press(key) { Outcome::Changed } else { Outcome::Nothing } } /// Moves focus, presses or edits; 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(); let target = targets[self.focus.min(n - 1)]; if let Some(field) = input_at(&view, &mut 0, target) { let caret_only = matches!(key, Key::Left | Key::Right); if let Some(event) = self.edit(field, key) { self.send(event); return true; } if caret_only { return true; } } // Not a field's key: the arrows across move focus too, Space presses. let key = match key { Key::Left => Key::Previous, Key::Right => Key::Next, Key::Space => Key::Press, k => k, }; 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, target) else { return false }; self.send(event); return true; } _ => return false, } self.caret = usize::MAX; true } /// What `key` does to the focused `field`: the event carrying its new /// text, if it changed it. The caret moves either way. fn edit(&mut self, field: &Input, key: Key) -> Option { let len = field.value.chars().count(); let caret = self.caret.min(len); let byte = |at: usize| field.value.char_indices().nth(at).map_or(field.value.len(), |(b, _)| b); let mut text = field.value.clone(); match key { Key::Char(c) => text.insert(byte(caret), c), Key::Space => text.insert(byte(caret), ' '), Key::Erase if caret > 0 => { text.remove(byte(caret - 1)); } Key::Left => { self.caret = caret.saturating_sub(1); return None; } Key::Right => { self.caret = (caret + 1).min(len); return None; } _ => return None, } self.caret = match key { Key::Erase => caret - 1, _ => caret + 1, }; Some((field.on_change)(text)) } /// Hands `event` to the app; a new screen starts from its top. fn send(&mut self, event: A::Event) { let screen = self.app.screen(); self.app.update(event); if self.app.screen() != screen { (self.focus, self.scroll, self.caret) = (0, 0.0, usize::MAX); } else { // The view may have fewer things to focus now. let now = self.pressables(&self.app.view()).len(); self.focus = self.focus.min(now.saturating_sub(1)); } } /// 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, self.caret); } } /// 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)) }