179 lines
5.6 KiB
Rust
179 lines
5.6 KiB
Rust
//! 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<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.
|
|
#[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<A: App> {
|
|
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>(e: &Element<E>, index: &mut usize, out: &mut Vec<usize>) {
|
|
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: Clone>(e: &Element<E>, index: &mut usize, target: usize) -> Option<E> {
|
|
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<A: App> Ui<A> {
|
|
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<A::Event>) -> Vec<usize> {
|
|
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))
|
|
}
|