feat(k-ui): text_input (label above, app owns text via on_change, caret, erase, submit, secret, placeholder); keys Left/Right/Space/Char/Erase; settings app filter; apps 8-page stack (k-ui overflowed 16K), args page -> 0x1000
This commit is contained in:
@@ -1,13 +1,14 @@
|
||||
//! 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.
|
||||
//! 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, Kind};
|
||||
use crate::element::{Element, Input, Kind};
|
||||
use crate::layout::{self, Rect};
|
||||
use crate::render;
|
||||
use crate::text::{Fonts, Measurer};
|
||||
@@ -37,10 +38,20 @@ pub trait App {
|
||||
/// 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.
|
||||
@@ -59,12 +70,15 @@ pub struct Ui<A: App> {
|
||||
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 pressable element in `e`.
|
||||
/// The pre-order indices of every element focus can rest on in `e`.
|
||||
fn pressables<E>(e: &Element<E>, index: &mut usize, out: &mut Vec<usize>) {
|
||||
if e.kind.event().is_some() {
|
||||
if e.kind.focusable() {
|
||||
out.push(*index);
|
||||
}
|
||||
*index += 1;
|
||||
@@ -86,9 +100,20 @@ fn event_at<E: Clone>(e: &Element<E>, index: &mut usize, target: usize) -> Optio
|
||||
}
|
||||
}
|
||||
|
||||
/// The pre-order `target`th element, if it is a field.
|
||||
fn input_at<'e, E>(e: &'e Element<E>, index: &mut usize, target: usize) -> Option<&'e Input<E>> {
|
||||
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<A: App> Ui<A> {
|
||||
pub fn new(theme: Theme) -> Self {
|
||||
Self { app: A::init(), focus: 0, scroll: 0.0, theme }
|
||||
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.
|
||||
@@ -109,13 +134,13 @@ impl<A: App> Ui<A> {
|
||||
return Outcome::Leave;
|
||||
}
|
||||
// A new screen: from its top.
|
||||
(self.focus, self.scroll) = (0, 0.0);
|
||||
(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 or presses; whether anything changed.
|
||||
/// 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);
|
||||
@@ -123,26 +148,81 @@ impl<A: App> Ui<A> {
|
||||
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::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));
|
||||
}
|
||||
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<A::Event>, key: Key) -> Option<A::Event> {
|
||||
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) {
|
||||
@@ -159,7 +239,7 @@ impl<A: App> Ui<A> {
|
||||
for r in &mut rects {
|
||||
r.y -= self.scroll;
|
||||
}
|
||||
render::render(pm, &view, &rects, fonts, &self.theme, focused);
|
||||
render::render(pm, &view, &rects, fonts, &self.theme, focused, self.caret);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user