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:
@@ -44,10 +44,43 @@ pub struct Button<E> {
|
||||
pub on_press: E,
|
||||
}
|
||||
|
||||
/// A row of a list: pressing it sends `on_press`.
|
||||
pub struct Item<E> {
|
||||
pub title: String,
|
||||
pub subtitle: Option<String>,
|
||||
pub on_press: E,
|
||||
}
|
||||
|
||||
/// A labelled switch; pressing it sends `on_toggle` (the app flips `on`).
|
||||
pub struct Toggle<E> {
|
||||
pub label: String,
|
||||
pub on: bool,
|
||||
pub on_toggle: E,
|
||||
}
|
||||
|
||||
pub enum Kind<E> {
|
||||
Flex(Flex<E>),
|
||||
Text(Text),
|
||||
Button(Button<E>),
|
||||
Item(Item<E>),
|
||||
Toggle(Toggle<E>),
|
||||
}
|
||||
|
||||
impl<E> Kind<E> {
|
||||
/// What pressing it sends, if it can be pressed.
|
||||
pub fn event(&self) -> Option<&E> {
|
||||
match self {
|
||||
Kind::Button(b) => Some(&b.on_press),
|
||||
Kind::Item(i) => Some(&i.on_press),
|
||||
Kind::Toggle(t) => Some(&t.on_toggle),
|
||||
Kind::Flex(_) | Kind::Text(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether it fills its column across (rows of a list do).
|
||||
pub fn stretches(&self) -> bool {
|
||||
matches!(self, Kind::Flex(_) | Kind::Item(_) | Kind::Toggle(_))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Element<E> {
|
||||
@@ -74,6 +107,19 @@ pub fn button<E>(label: impl Into<String>, on_press: E) -> ButtonBuilder<E> {
|
||||
ButtonBuilder(Button { label: label.into(), variant: ButtonVariant::Secondary, on_press }, Size::Shrink)
|
||||
}
|
||||
|
||||
/// A row of a list titled `title`, sending `on_press` when pressed.
|
||||
pub fn list_item<E>(title: impl Into<String>, on_press: E) -> ItemBuilder<E> {
|
||||
ItemBuilder(Item { title: title.into(), subtitle: None, on_press }, Size::Shrink)
|
||||
}
|
||||
|
||||
/// A switch labelled `label`, shown `on` or off, sending `on_toggle` when
|
||||
/// pressed.
|
||||
pub fn toggle<E>(label: impl Into<String>, on: bool, on_toggle: E) -> ToggleBuilder<E> {
|
||||
ToggleBuilder(Toggle { label: label.into(), on, on_toggle }, Size::Shrink)
|
||||
}
|
||||
|
||||
pub struct ItemBuilder<E>(Item<E>, Size);
|
||||
pub struct ToggleBuilder<E>(Toggle<E>, Size);
|
||||
pub struct FlexBuilder<E>(Flex<E>, Size);
|
||||
pub struct TextBuilder(Text, Size);
|
||||
pub struct ButtonBuilder<E>(Button<E>, Size);
|
||||
@@ -139,6 +185,25 @@ impl<E> ButtonBuilder<E> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> ItemBuilder<E> {
|
||||
pub fn subtitle(mut self, s: impl Into<String>) -> Self {
|
||||
self.0.subtitle = Some(s.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> From<ItemBuilder<E>> for Element<E> {
|
||||
fn from(b: ItemBuilder<E>) -> Self {
|
||||
Element { kind: Kind::Item(b.0), size: b.1 }
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> From<ToggleBuilder<E>> for Element<E> {
|
||||
fn from(b: ToggleBuilder<E>) -> Self {
|
||||
Element { kind: Kind::Toggle(b.0), size: b.1 }
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> From<FlexBuilder<E>> for Element<E> {
|
||||
fn from(b: FlexBuilder<E>) -> Self {
|
||||
Element { kind: Kind::Flex(b.0), size: b.1 }
|
||||
|
||||
@@ -33,6 +33,14 @@ pub fn button_padding(theme: &Theme) -> (f32, f32) {
|
||||
(theme.space(Spacing::Lg), theme.space(Spacing::Sm))
|
||||
}
|
||||
|
||||
/// A list row's room around its text.
|
||||
pub fn row_padding(theme: &Theme) -> f32 {
|
||||
theme.space(Spacing::Md)
|
||||
}
|
||||
|
||||
/// A toggle's switch: its track's size.
|
||||
pub const SWITCH: (f32, f32) = (48.0, 28.0);
|
||||
|
||||
/// What `e` needs, width and height.
|
||||
pub fn natural<E>(e: &Element<E>, m: &impl Measure, t: &Theme) -> (f32, f32) {
|
||||
match &e.kind {
|
||||
@@ -42,6 +50,17 @@ pub fn natural<E>(e: &Element<E>, m: &impl Measure, t: &Theme) -> (f32, f32) {
|
||||
let (px, py) = button_padding(t);
|
||||
((w + 2.0 * px).max(MIN_TARGET), (h + 2.0 * py).max(MIN_TARGET))
|
||||
}
|
||||
Kind::Item(i) => {
|
||||
let (tw, th) = m.text(&i.title, TextStyle::Label);
|
||||
let (sw, sh) = i.subtitle.as_ref().map_or((0.0, 0.0), |s| m.text(s, TextStyle::Caption));
|
||||
let pad = 2.0 * row_padding(t);
|
||||
(tw.max(sw) + pad, (th + sh + pad).max(MIN_TARGET))
|
||||
}
|
||||
Kind::Toggle(g) => {
|
||||
let (w, h) = m.text(&g.label, TextStyle::Body);
|
||||
let pad = 2.0 * row_padding(t);
|
||||
(w + t.space(Spacing::Lg) + SWITCH.0 + pad, (h.max(SWITCH.1) + pad).max(MIN_TARGET))
|
||||
}
|
||||
Kind::Flex(f) => {
|
||||
let pad = 2.0 * t.space(f.padding);
|
||||
let gaps = t.space(f.gap) * f.children.len().saturating_sub(1) as f32;
|
||||
@@ -109,7 +128,7 @@ fn place<E>(e: &Element<E>, r: Rect, m: &impl Measure, t: &Theme, out: &mut Vec<
|
||||
Size::Fill => share,
|
||||
Size::Shrink => *main,
|
||||
};
|
||||
let cross = if matches!(c.kind, Kind::Flex(_)) { inner_cross } else { cross.min(inner_cross) };
|
||||
let cross = if c.kind.stretches() { inner_cross } else { cross.min(inner_cross) };
|
||||
let (dx, dy) = across(f.axis, at, 0.0);
|
||||
let (w, h) = across(f.axis, main, cross);
|
||||
place(c, Rect { x: inner.x + dx, y: inner.y + dy, w, h }, m, t, out);
|
||||
|
||||
@@ -16,7 +16,7 @@ pub mod text;
|
||||
pub mod theme;
|
||||
pub mod ui;
|
||||
|
||||
pub use element::{Element, Size, button, column, row, text};
|
||||
pub use element::{Element, Size, button, column, list_item, row, text, toggle};
|
||||
pub use run::run;
|
||||
pub use theme::{ButtonVariant, Spacing, TextColour, TextStyle};
|
||||
pub use ui::App;
|
||||
|
||||
@@ -106,19 +106,58 @@ impl Painter<'_, '_> {
|
||||
Kind::Text(t) => self.text_in(&t.content, t.style, r, false, theme.text_colour(t.colour)),
|
||||
Kind::Button(b) => {
|
||||
let (fill, label) = theme.button(b.variant);
|
||||
if let (Some(fill), Some(path)) = (fill, rounded(r, theme.corner_radius)) {
|
||||
self.pm.fill_path(&path, &paint(fill), FillRule::Winding, Transform::identity(), None);
|
||||
if let Some(fill) = fill {
|
||||
self.fill(r, theme.corner_radius, fill);
|
||||
}
|
||||
self.text_in(&b.label, TextStyle::Label, r, true, label);
|
||||
if self.focused == Some(me) {
|
||||
let ring = Rect { x: r.x - RING_GAP, y: r.y - RING_GAP, w: r.w + 2.0 * RING_GAP, h: r.h + 2.0 * RING_GAP };
|
||||
if let Some(path) = rounded(ring, theme.corner_radius + RING_GAP) {
|
||||
let stroke = Stroke { width: RING_WIDTH, ..Stroke::default() };
|
||||
self.pm.stroke_path(&path, &paint(theme.focus), &stroke, Transform::identity(), None);
|
||||
}
|
||||
Kind::Item(i) => {
|
||||
self.fill(r, theme.corner_radius, theme.surface);
|
||||
let inner = self.inset(r);
|
||||
match &i.subtitle {
|
||||
Some(sub) => {
|
||||
let half = Rect { h: inner.h / 2.0, ..inner };
|
||||
self.text_in(&i.title, TextStyle::Label, half, false, theme.text_primary);
|
||||
self.text_in(sub, TextStyle::Caption, Rect { y: inner.y + inner.h / 2.0, ..half }, false, theme.text_secondary);
|
||||
}
|
||||
None => self.text_in(&i.title, TextStyle::Label, inner, false, theme.text_primary),
|
||||
}
|
||||
// A chevron: there is more behind it.
|
||||
let chevron = Rect { x: inner.x + inner.w - 12.0, w: 12.0, ..inner };
|
||||
self.text_in(">", TextStyle::Label, chevron, true, theme.text_secondary);
|
||||
}
|
||||
Kind::Toggle(g) => {
|
||||
self.fill(r, theme.corner_radius, theme.surface);
|
||||
let inner = self.inset(r);
|
||||
self.text_in(&g.label, TextStyle::Body, inner, false, theme.text_primary);
|
||||
let (tw, th) = crate::layout::SWITCH;
|
||||
let track = Rect { x: inner.x + inner.w - tw, y: inner.y + (inner.h - th) / 2.0, w: tw, h: th };
|
||||
self.fill(track, th / 2.0, if g.on { theme.accent } else { theme.surface_raised });
|
||||
let knob = th - 6.0;
|
||||
let kx = if g.on { track.x + tw - knob - 3.0 } else { track.x + 3.0 };
|
||||
self.fill(Rect { x: kx, y: track.y + 3.0, w: knob, h: knob }, knob / 2.0, theme.text_primary);
|
||||
}
|
||||
}
|
||||
if self.focused == Some(me) && e.kind.event().is_some() {
|
||||
let ring = Rect { x: r.x - RING_GAP, y: r.y - RING_GAP, w: r.w + 2.0 * RING_GAP, h: r.h + 2.0 * RING_GAP };
|
||||
if let Some(path) = rounded(ring, theme.corner_radius + RING_GAP) {
|
||||
let stroke = Stroke { width: RING_WIDTH, ..Stroke::default() };
|
||||
self.pm.stroke_path(&path, &paint(theme.focus), &stroke, Transform::identity(), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `r` filled with `c`, its corners rounded by `radius`.
|
||||
fn fill(&mut self, r: Rect, radius: f32, c: Rgba) {
|
||||
if let Some(path) = rounded(r, radius) {
|
||||
self.pm.fill_path(&path, &paint(c), FillRule::Winding, Transform::identity(), None);
|
||||
}
|
||||
}
|
||||
|
||||
/// `r` less a list row's padding all round.
|
||||
fn inset(&self, r: Rect) -> Rect {
|
||||
let p = crate::layout::row_padding(self.theme);
|
||||
Rect { x: r.x + p, y: r.y + p, w: (r.w - 2.0 * p).max(0.0), h: (r.h - 2.0 * p).max(0.0) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! An app's event loop on K-OS: its window (which the permission manager
|
||||
//! opened, named from its manifest), keys from the display while it is in
|
||||
//! front, a redraw after every change. Escape leaves.
|
||||
//! front, a redraw after every change. Escape goes back, and leaves from
|
||||
//! the first screen.
|
||||
|
||||
use libkos::display::{self, Canvas};
|
||||
use libkos::input::{InputEvent, Key as Pressed};
|
||||
@@ -11,18 +12,19 @@ use types::display::ScreenCoord;
|
||||
|
||||
use crate::text::Fonts;
|
||||
use crate::theme::Theme;
|
||||
use crate::ui::{App, Key, Ui};
|
||||
use crate::ui::{App, Key, Outcome, Ui};
|
||||
|
||||
fn key(k: Pressed) -> Option<Key> {
|
||||
match k {
|
||||
Pressed::Tab | Pressed::Down | Pressed::Right => Some(Key::Next),
|
||||
Pressed::Up | Pressed::Left => Some(Key::Previous),
|
||||
Pressed::Enter | Pressed::Space => Some(Key::Press),
|
||||
Pressed::Escape => Some(Key::Back),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs `A` until Escape (or until it is stopped).
|
||||
/// Runs `A` until it is left (or stopped).
|
||||
pub fn run<A: App>() -> ! {
|
||||
let window = libkos::start::display();
|
||||
let Some(mut canvas) = window.and_then(Canvas::attach) else {
|
||||
@@ -41,21 +43,22 @@ pub fn run<A: App>() -> ! {
|
||||
let mut dirty = true;
|
||||
loop {
|
||||
if dirty {
|
||||
draw(&ui, &mut canvas, &fonts);
|
||||
draw(&mut ui, &mut canvas, &fonts);
|
||||
dirty = false;
|
||||
}
|
||||
let _ = syscall::wait(notify);
|
||||
while let Some(event) = display::poll_input(canvas.window()) {
|
||||
match event {
|
||||
InputEvent::KeyDown(Pressed::Escape) => syscall::exit(),
|
||||
InputEvent::KeyDown(k) => dirty |= key(k).is_some_and(|k| ui.key(k)),
|
||||
InputEvent::KeyUp(_) => {}
|
||||
let InputEvent::KeyDown(k) = event else { continue };
|
||||
match key(k).map(|k| ui.key(k)) {
|
||||
Some(Outcome::Leave) => syscall::exit(),
|
||||
Some(Outcome::Changed) => dirty = true,
|
||||
Some(Outcome::Nothing) | None => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw<A: App>(ui: &Ui<A>, canvas: &mut Canvas, fonts: &Fonts) {
|
||||
fn draw<A: App>(ui: &mut Ui<A>, canvas: &mut Canvas, fonts: &Fonts) {
|
||||
let (w, h) = (canvas.width(), canvas.height());
|
||||
if let Some(mut pm) = PixmapMut::from_bytes(canvas.pixels_mut(), w.raw() as u32, h.raw() as u32) {
|
||||
ui.draw(&mut pm, fonts);
|
||||
|
||||
@@ -7,7 +7,8 @@ use crate::element::{Element, button, column, row, text};
|
||||
use crate::layout::{MIN_TARGET, Measure, Rect, layout};
|
||||
use crate::text::Fonts;
|
||||
use crate::theme::{ButtonVariant, Spacing, TextStyle, Theme};
|
||||
use crate::ui::{App, Key, Ui};
|
||||
use crate::element::{list_item, toggle};
|
||||
use crate::ui::{App, Key, Outcome, Ui};
|
||||
|
||||
/// Every character 10 wide, every line 20 high.
|
||||
struct Fixed;
|
||||
@@ -80,17 +81,17 @@ impl App for Counter {
|
||||
#[test]
|
||||
fn keys_move_focus_and_press_the_focused_button() {
|
||||
let mut ui = Ui::<Counter>::new(T);
|
||||
assert!(ui.key(Key::Press), "Add has focus first");
|
||||
assert!(ui.key(Key::Press));
|
||||
assert_eq!(ui.key(Key::Press), Outcome::Changed, "Add has focus first");
|
||||
assert_eq!(ui.key(Key::Press), Outcome::Changed);
|
||||
assert_eq!(ui.app.0, 2);
|
||||
assert!(ui.key(Key::Next));
|
||||
assert!(ui.key(Key::Press), "Reset");
|
||||
assert_eq!(ui.key(Key::Next), Outcome::Changed);
|
||||
assert_eq!(ui.key(Key::Press), Outcome::Changed, "Reset");
|
||||
assert_eq!(ui.app.0, 0);
|
||||
// Reset is gone, so focus is back on Add.
|
||||
assert!(ui.key(Key::Press));
|
||||
assert_eq!(ui.key(Key::Press), Outcome::Changed);
|
||||
assert_eq!(ui.app.0, 1);
|
||||
assert!(ui.key(Key::Previous));
|
||||
assert!(ui.key(Key::Press));
|
||||
assert_eq!(ui.key(Key::Previous), Outcome::Changed);
|
||||
assert_eq!(ui.key(Key::Press), Outcome::Changed);
|
||||
assert_eq!(ui.app.0, 0, "Previous wraps round to Reset");
|
||||
}
|
||||
|
||||
@@ -103,7 +104,7 @@ fn at(pm: &tiny_skia::Pixmap, x: u32, y: u32) -> (u8, u8, u8) {
|
||||
#[test]
|
||||
fn the_view_is_drawn_in_the_theme_with_a_focus_ring() {
|
||||
let fonts = Fonts::load().expect("the bundled fonts parse");
|
||||
let ui = Ui::<Counter>::new(T);
|
||||
let mut ui = Ui::<Counter>::new(T);
|
||||
let mut pm = tiny_skia::Pixmap::new(320, 200).unwrap();
|
||||
ui.draw(&mut pm.as_mut(), &fonts);
|
||||
let bg = T.background;
|
||||
@@ -122,3 +123,73 @@ fn the_view_is_drawn_in_the_theme_with_a_focus_ring() {
|
||||
let lit = (h.x as u32..(h.x + h.w) as u32).any(|x| (h.y as u32..(h.y + h.h) as u32).any(|y| at(&pm, x, y).0 > 150));
|
||||
assert!(lit, "the count is drawn");
|
||||
}
|
||||
|
||||
/// Two screens: a list of rows, and a detail with a toggle.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum Nav {
|
||||
Open(usize),
|
||||
Flip,
|
||||
}
|
||||
|
||||
struct Pages {
|
||||
open: Option<usize>,
|
||||
flag: bool,
|
||||
}
|
||||
|
||||
impl App for Pages {
|
||||
type Event = Nav;
|
||||
fn init() -> Self {
|
||||
Pages { open: None, flag: false }
|
||||
}
|
||||
fn update(&mut self, e: Nav) {
|
||||
match e {
|
||||
Nav::Open(i) => self.open = Some(i),
|
||||
Nav::Flip => self.flag = !self.flag,
|
||||
}
|
||||
}
|
||||
fn view(&self) -> Element<Nav> {
|
||||
match self.open {
|
||||
None => column().gap(Spacing::Sm).children((0..30).map(|i| list_item(format!("Row {i}"), Nav::Open(i)).subtitle("more"))).into(),
|
||||
Some(_) => column().child(text("Detail")).child(toggle("Flag", self.flag, Nav::Flip)).into(),
|
||||
}
|
||||
}
|
||||
fn back(&mut self) -> bool {
|
||||
self.open.take().is_some()
|
||||
}
|
||||
fn screen(&self) -> usize {
|
||||
self.open.map_or(0, |i| i + 1)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rows_open_screens_toggles_flip_and_escape_goes_back_then_leaves() {
|
||||
let mut ui = Ui::<Pages>::new(T);
|
||||
assert_eq!(ui.key(Key::Next), Outcome::Changed);
|
||||
assert_eq!(ui.key(Key::Press), Outcome::Changed);
|
||||
assert_eq!(ui.app.open, Some(1), "the second row");
|
||||
// A new screen: focus is on its first pressable thing, the toggle.
|
||||
assert_eq!(ui.key(Key::Press), Outcome::Changed);
|
||||
assert!(ui.app.flag);
|
||||
assert_eq!(ui.key(Key::Back), Outcome::Changed);
|
||||
assert_eq!(ui.app.open, None);
|
||||
assert_eq!(ui.key(Key::Back), Outcome::Leave, "nowhere left to go back to");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rows_stretch_across_and_the_view_scrolls_to_the_focused_one() {
|
||||
let fonts = Fonts::load().unwrap();
|
||||
let mut ui = Ui::<Pages>::new(T);
|
||||
let view = ui.app.view();
|
||||
let rects = layout(&view, Rect { x: 0.0, y: 0.0, w: 320.0, h: 5000.0 }, &crate::text::Measurer { fonts: &fonts, theme: T }, T);
|
||||
assert_eq!(rects[1].w, 320.0, "a row fills its column");
|
||||
// Thirty rows do not fit in 200 pixels; go to the last one.
|
||||
for _ in 0..29 {
|
||||
ui.key(Key::Next);
|
||||
}
|
||||
let mut pm = tiny_skia::Pixmap::new(320, 200).unwrap();
|
||||
ui.draw(&mut pm.as_mut(), &fonts);
|
||||
let focus = T.focus;
|
||||
// Its ring is drawn somewhere on screen: the view scrolled to it.
|
||||
let ringed = (0..200).any(|y| at(&pm, 160, y) == (focus.0, focus.1, focus.2));
|
||||
assert!(ringed, "the last row is in sight");
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user