From e952a149ec88d4fe899786445a68c77f6425e3da Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Wed, 28 Oct 2026 13:48:08 +0100 Subject: [PATCH] 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 --- crates/apps/settings/src/main.rs | 22 ++++-- crates/init/src/load.rs | 7 +- crates/init/src/spawn.rs | 2 +- crates/k-ui/src/element.rs | 58 +++++++++++++- crates/k-ui/src/layout.rs | 8 ++ crates/k-ui/src/lib.rs | 2 +- crates/k-ui/src/render.rs | 55 +++++++++++++- crates/k-ui/src/run.rs | 11 ++- crates/k-ui/src/tests.rs | 68 +++++++++++++++++ crates/k-ui/src/ui.rs | 126 +++++++++++++++++++++++++------ crates/types/src/start_arg.rs | 2 +- docs/ipc.md | 5 +- 12 files changed, 322 insertions(+), 44 deletions(-) diff --git a/crates/apps/settings/src/main.rs b/crates/apps/settings/src/main.rs index 86169c2..23a8d59 100644 --- a/crates/apps/settings/src/main.rs +++ b/crates/apps/settings/src/main.rs @@ -14,7 +14,7 @@ extern crate libkos; use alloc::format; use alloc::string::{String, ToString}; use alloc::vec::Vec; -use k_ui::{App, ButtonVariant, Element, Spacing, TextColour, TextStyle, button, column, list_item, text, toggle}; +use k_ui::{App, ButtonVariant, Element, Spacing, TextColour, TextStyle, button, column, list_item, text, text_input, toggle}; use libkos::apps::Apps; use libkos::session::Role; use manifest::{Capabilities, Capability}; @@ -34,11 +34,13 @@ enum Screen { About, } -#[derive(Clone, Copy)] +#[derive(Clone)] enum Event { Show(Screen), /// Allow or revoke this capability of the app on screen. Flip(Capability), + /// What the app list is narrowed to. + Filter(String), /// Use the display's `n`th theme. Theme(usize), SignOut, @@ -56,6 +58,8 @@ struct Settings { screen: Screen, apps: Option, rows: Vec, + /// Only apps whose name or id has this in it are listed. + filter: String, /// Who is signed in, and how. who: Option<(String, Role)>, /// The display's themes, and which is in use. @@ -81,7 +85,8 @@ impl Settings { fn reload(&mut self) { self.rows.clear(); let Some(apps) = &self.apps else { return }; - let mut buf = [0u8; LISTING]; + // On the heap: an app's stack is a few pages. + let mut buf = alloc::vec![0u8; LISTING]; let Ok(n) = apps.apps(&mut buf) else { self.trouble = Some("The permission manager did not answer.".to_string()); return; @@ -141,12 +146,15 @@ impl Settings { } fn app_list(&self) -> Element { - let rows = self.rows.iter().enumerate().map(|(i, r)| { + let wanted = self.filter.to_ascii_lowercase(); + let shown = |r: &&AppRow| r.name.to_ascii_lowercase().contains(&wanted) || r.id.contains(&wanted); + let rows = self.rows.iter().enumerate().filter(|(_, r)| shown(r)).map(|(i, r)| { let allowed = r.granted.intersect(r.requested).iter().count(); let asked = r.requested.iter().count(); list_item(r.name.clone(), Event::Show(Screen::App(i))).subtitle(format!("{} ยท {} of {} allowed", r.id, allowed, asked)) }); - let view = Self::hint(Self::heading("Apps"), "Esc goes back").children(rows); + let find = text_input("Find an app", self.filter.clone(), Event::Filter).placeholder("Name or id"); + let view = Self::hint(Self::heading("Apps"), "Esc goes back, Down goes to the list").child(find).children(rows); self.with_trouble(view).into() } @@ -209,7 +217,7 @@ impl App for Settings { .collect() }); let theme = window.and_then(libkos::display::theme).map_or(0, |(i, _)| i); - let mut s = Settings { screen: Screen::Home, apps, rows: Vec::new(), who, themes, theme, trouble: None }; + let mut s = Settings { screen: Screen::Home, apps, rows: Vec::new(), filter: String::new(), who, themes, theme, trouble: None }; s.reload(); s } @@ -219,6 +227,7 @@ impl App for Settings { Event::Show(screen) => { self.trouble = None; if screen == Screen::Apps { + self.filter.clear(); self.reload(); } self.screen = screen; @@ -228,6 +237,7 @@ impl App for Settings { self.flip(i, cap); } } + Event::Filter(f) => self.filter = f, Event::Theme(i) => self.choose_theme(i), Event::SignOut => { if let Some(s) = libkos::start::session() { diff --git a/crates/init/src/load.rs b/crates/init/src/load.rs index 7f5e19b..760f30b 100644 --- a/crates/init/src/load.rs +++ b/crates/init/src/load.rs @@ -10,8 +10,11 @@ const PAGE_SIZE: usize = 4096; const USER_CODE_BASE: usize = 0x10000; /// A program's stack, below its image, unless its manifest says more. pub(crate) const STACK_PAGES: usize = 4; -/// The most there is room for under the image (page 0 stays unmapped). -const MAX_STACK_PAGES: usize = USER_CODE_BASE / PAGE_SIZE - 1; +/// An app's: drawing (tiny-skia, fontdue) takes more than 16 KiB. +pub(crate) const APP_STACK_PAGES: usize = 8; +/// The most there is room for under the image, above the arguments page +/// (page 0 stays unmapped). +const MAX_STACK_PAGES: usize = (USER_CODE_BASE - types::start_arg::ARGS_VA) / PAGE_SIZE - 1; const CREATE_CAP: CapSlot = CapSlot::const_new(0); /// Where a program file is read through on its way into a process, a /// chunk at a time, so a program may be any size. diff --git a/crates/init/src/spawn.rs b/crates/init/src/spawn.rs index 436638e..ab5eac3 100644 --- a/crates/init/src/spawn.rs +++ b/crates/init/src/spawn.rs @@ -137,7 +137,7 @@ impl Launcher<'_> { // The grant is the file: its path inside it is empty. let found = file.open(b"").and_then(|h| file.close(h)).is_ok(); let (_, scratch) = self.storage(); - let loaded = found.then(|| load::load_binary(&mut file, scratch, b"", r.limits, load::STACK_PAGES)).flatten(); + let loaded = found.then(|| load::load_binary(&mut file, scratch, b"", r.limits, load::APP_STACK_PAGES)).flatten(); file.detach(); let Some(loaded) = loaded else { return refused(if found { reply::ERROR } else { reply::NOT_FOUND }); diff --git a/crates/k-ui/src/element.rs b/crates/k-ui/src/element.rs index 02bb582..1e89020 100644 --- a/crates/k-ui/src/element.rs +++ b/crates/k-ui/src/element.rs @@ -1,5 +1,5 @@ //! What a view is made of, built with typed builders: `column()`, `row()`, -//! `text(..)`, `button(..)`. Each only offers what makes sense for it +//! `text(..)`, `button(..)`, `text_input(..)`. Each only offers what makes sense for it //! (a text has a style, a button a variant); appearance comes from the //! theme, never from here. @@ -58,12 +58,27 @@ pub struct Toggle { pub on_toggle: E, } +/// A line the user types into. The text is the app's: each change comes +/// back as `on_change(the new text)`, and the view shows what it is given. +pub struct Input { + /// What it is for: shown above it, and what it is called. + pub label: String, + pub value: String, + pub placeholder: Option, + /// Shown as `*`s (a passcode). + pub secret: bool, + pub on_change: fn(String) -> E, + /// What Enter sends, if anything. + pub on_submit: Option, +} + pub enum Kind { Flex(Flex), Text(Text), Button(Button), Item(Item), Toggle(Toggle), + Input(Input), } impl Kind { @@ -73,13 +88,19 @@ impl Kind { Kind::Button(b) => Some(&b.on_press), Kind::Item(i) => Some(&i.on_press), Kind::Toggle(t) => Some(&t.on_toggle), + Kind::Input(i) => i.on_submit.as_ref(), 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(_)) + matches!(self, Kind::Flex(_) | Kind::Item(_) | Kind::Toggle(_) | Kind::Input(_)) + } + + /// Whether focus can rest on it: what can be pressed, and fields. + pub fn focusable(&self) -> bool { + self.event().is_some() || matches!(self, Kind::Input(_)) } } @@ -118,6 +139,13 @@ pub fn toggle(label: impl Into, on: bool, on_toggle: E) -> ToggleBuil ToggleBuilder(Toggle { label: label.into(), on, on_toggle }, Size::Shrink) } +/// A field labelled `label` showing `value`; typing sends +/// `on_change(new text)`. +pub fn text_input(label: impl Into, value: impl Into, on_change: fn(String) -> E) -> InputBuilder { + InputBuilder(Input { label: label.into(), value: value.into(), placeholder: None, secret: false, on_change, on_submit: None }) +} + +pub struct InputBuilder(Input); pub struct ItemBuilder(Item, Size); pub struct ToggleBuilder(Toggle, Size); pub struct FlexBuilder(Flex, Size); @@ -192,6 +220,32 @@ impl ItemBuilder { } } +impl InputBuilder { + /// Shown, dimmed, while it is empty. + pub fn placeholder(mut self, s: impl Into) -> Self { + self.0.placeholder = Some(s.into()); + self + } + + pub fn secret(mut self) -> Self { + self.0.secret = true; + self + } + + pub fn on_submit(mut self, e: E) -> Self { + self.0.on_submit = Some(e); + self + } +} + +/// Its label above it, then the field. +impl From> for Element { + fn from(b: InputBuilder) -> Self { + let label = text(b.0.label.clone()).style(TextStyle::Caption).colour(TextColour::Secondary); + column().gap(Spacing::Xs).child(label).child(Element { kind: Kind::Input(b.0), size: Size::Shrink }).into() + } +} + impl From> for Element { fn from(b: ItemBuilder) -> Self { Element { kind: Kind::Item(b.0), size: b.1 } diff --git a/crates/k-ui/src/layout.rs b/crates/k-ui/src/layout.rs index 8b28d18..eeb4383 100644 --- a/crates/k-ui/src/layout.rs +++ b/crates/k-ui/src/layout.rs @@ -38,6 +38,9 @@ pub fn row_padding(theme: &Theme) -> f32 { theme.space(Spacing::Md) } +/// The least a field is across, empty or not. +pub const FIELD_WIDTH: f32 = 240.0; + /// A toggle's switch: its track's size. pub const SWITCH: (f32, f32) = (48.0, 28.0); @@ -61,6 +64,11 @@ pub fn natural(e: &Element, m: &impl Measure, t: &Theme) -> (f32, f32) { let pad = 2.0 * row_padding(t); (w + t.space(Spacing::Lg) + SWITCH.0 + pad, (h.max(SWITCH.1) + pad).max(MIN_TARGET)) } + Kind::Input(i) => { + let (w, h) = m.text(i.placeholder.as_deref().unwrap_or(&i.value), TextStyle::Body); + let pad = 2.0 * row_padding(t); + ((w + pad).max(FIELD_WIDTH), (h + 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; diff --git a/crates/k-ui/src/lib.rs b/crates/k-ui/src/lib.rs index 867865a..649cab9 100644 --- a/crates/k-ui/src/lib.rs +++ b/crates/k-ui/src/lib.rs @@ -16,7 +16,7 @@ pub mod text; pub mod theme; pub mod ui; -pub use element::{Element, Size, button, column, list_item, row, text, toggle}; +pub use element::{Element, Size, button, column, list_item, row, text, text_input, toggle}; pub use run::run; pub use theme::{ButtonVariant, Spacing, TextColour, TextStyle}; pub use ui::App; diff --git a/crates/k-ui/src/render.rs b/crates/k-ui/src/render.rs index ce710d5..02e4e95 100644 --- a/crates/k-ui/src/render.rs +++ b/crates/k-ui/src/render.rs @@ -1,6 +1,7 @@ //! Drawing a laid-out tree into a window's pixels (RGBA, premultiplied): //! the theme decides every colour and shape. +use alloc::string::String; use tiny_skia::{Color, FillRule, Paint, PathBuilder, PixmapMut, Stroke, Transform}; use crate::element::{Element, Kind}; @@ -11,6 +12,9 @@ use crate::theme::{Rgba, TextStyle, Theme, Weight}; /// The focus ring's distance outside the button, and its width. const RING_GAP: f32 = 3.0; const RING_WIDTH: f32 = 2.0; +/// A field's outline, and its caret, across. +const OUTLINE: f32 = 1.5; +const CARET: f32 = 2.0; fn paint(c: Rgba) -> Paint<'static> { let mut p = Paint::default(); @@ -44,6 +48,8 @@ struct Painter<'p, 'a> { fonts: &'p Fonts, theme: &'p Theme, focused: Option, + /// Where the caret is in the focused field, in characters. + caret: usize, } impl Painter<'_, '_> { @@ -137,8 +143,9 @@ impl Painter<'_, '_> { 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); } + Kind::Input(i) => self.field(i, r, self.focused == Some(me)), } - if self.focused == Some(me) && e.kind.event().is_some() { + if self.focused == Some(me) && e.kind.focusable() { 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() }; @@ -147,6 +154,46 @@ impl Painter<'_, '_> { } } + /// A field: its text (the end of it, if it is too long), or its + /// placeholder dimmed, and the caret while it has focus. + fn field(&mut self, i: &crate::element::Input, r: Rect, focused: bool) { + let theme = self.theme; + self.fill(r, theme.corner_radius, theme.surface); + if let Some(path) = rounded(r, theme.corner_radius) { + let stroke = Stroke { width: OUTLINE, ..Stroke::default() }; + self.pm.stroke_path(&path, &paint(theme.surface_raised), &stroke, Transform::identity(), None); + } + let inner = self.inset(r); + let shown: String = match i.secret { + true => i.value.chars().map(|_| '*').collect(), + false => i.value.clone(), + }; + let (size, weight) = theme.text(TextStyle::Body); + let width = |s: &str| self.fonts.line(s, size, weight).0; + let caret = self.caret.min(shown.chars().count()); + let upto: String = shown.chars().take(caret).collect(); + // Moved left so the caret stays inside. + let shift = (width(&upto) - inner.w + CARET).max(0.0); + if shown.is_empty() { + if let Some(p) = &i.placeholder { + self.text_in(p, TextStyle::Body, inner, false, theme.text_secondary); + } + } else { + let start = shown.char_indices().find(|(at, _)| width(&shown[..*at]) >= shift).map_or(shown.len(), |(at, _)| at); + let skipped = width(&shown[..start]); + let visible: String = shown[start..].chars().scan(skipped, |x, c| { + *x += width(c.encode_utf8(&mut [0; 4])); + (*x - shift <= inner.w).then_some(c) + }).collect(); + self.text_in(&visible, TextStyle::Body, Rect { x: inner.x + skipped - shift, ..inner }, false, theme.text_primary); + } + if focused { + let (_, h, _) = self.fonts.line("", size, weight); + let x = inner.x + width(&upto) - shift; + self.fill(Rect { x, y: inner.y + (inner.h - h) / 2.0, w: CARET, h }, 0.0, theme.accent); + } + } + /// `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) { @@ -162,9 +209,9 @@ impl Painter<'_, '_> { } /// Draws `root`, laid out as `rects`, with `focused` (a pre-order index) -/// ringed. -pub fn render(pm: &mut PixmapMut, root: &Element, rects: &[Rect], fonts: &Fonts, theme: &Theme, focused: Option) { +/// ringed, and its caret at `caret` if it is a field. +pub fn render(pm: &mut PixmapMut, root: &Element, rects: &[Rect], fonts: &Fonts, theme: &Theme, focused: Option, caret: usize) { let bg = theme.background; pm.fill(Color::from_rgba8(bg.0, bg.1, bg.2, bg.3)); - Painter { pm, fonts, theme, focused }.draw(root, rects, &mut 0); + Painter { pm, fonts, theme, focused, caret }.draw(root, rects, &mut 0); } diff --git a/crates/k-ui/src/run.rs b/crates/k-ui/src/run.rs index 82a26c7..a15ae35 100644 --- a/crates/k-ui/src/run.rs +++ b/crates/k-ui/src/run.rs @@ -17,10 +17,15 @@ use crate::ui::{App, Key, Outcome, Ui}; fn key(k: Pressed) -> Option { 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::Tab | Pressed::Down => Some(Key::Next), + Pressed::Up => Some(Key::Previous), + Pressed::Left => Some(Key::Left), + Pressed::Right => Some(Key::Right), + Pressed::Enter => Some(Key::Press), + Pressed::Space => Some(Key::Space), Pressed::Escape => Some(Key::Back), + Pressed::Backspace => Some(Key::Erase), + Pressed::Char(c @ b' '..=b'~') => Some(Key::Char(c as char)), _ => None, } } diff --git a/crates/k-ui/src/tests.rs b/crates/k-ui/src/tests.rs index 3393117..604cd16 100644 --- a/crates/k-ui/src/tests.rs +++ b/crates/k-ui/src/tests.rs @@ -193,3 +193,71 @@ fn rows_stretch_across_and_the_view_scrolls_to_the_focused_one() { let ringed = (0..200).any(|y| at(&pm, 160, y) == (focus.0, focus.1, focus.2)); assert!(ringed, "the last row is in sight"); } + +/// A field and a button: what is typed, what was sent. +struct Form { + name: std::string::String, + sent: Option, +} + +#[derive(Clone)] +enum FormEvent { + Name(std::string::String), + Send, +} + +impl App for Form { + type Event = FormEvent; + fn init() -> Self { + Form { name: "ab".into(), sent: None } + } + fn update(&mut self, e: FormEvent) { + match e { + FormEvent::Name(n) => self.name = n, + FormEvent::Send => self.sent = Some(self.name.clone()), + } + } + fn view(&self) -> Element { + column() + .child(crate::text_input("Name", self.name.clone(), FormEvent::Name).placeholder("Who?").on_submit(FormEvent::Send)) + .child(button("Send", FormEvent::Send)) + .into() + } +} + +#[test] +fn a_field_takes_typing_its_caret_moves_and_enter_submits() { + let mut ui = Ui::
::new(Theme::WHIMSY_DARK); + // Focus starts on the field, the caret at its end. + ui.key(Key::Char('c')); + assert_eq!(ui.app.name, "abc"); + ui.key(Key::Left); + ui.key(Key::Left); + ui.key(Key::Space); + assert_eq!(ui.app.name, "a bc"); + ui.key(Key::Erase); + ui.key(Key::Erase); + assert_eq!(ui.app.name, "bc", "nothing before the caret is left to erase"); + assert_eq!(ui.key(Key::Erase), Outcome::Nothing); + ui.key(Key::Press); + assert_eq!(ui.app.sent.as_deref(), Some("bc")); + // Down leaves the field: now the keys are the button's. + ui.key(Key::Next); + assert_eq!(ui.key(Key::Char('x')), Outcome::Nothing); + assert_eq!(ui.app.name, "bc"); + ui.app.sent = None; + ui.key(Key::Space); + assert_eq!(ui.app.sent.as_deref(), Some("bc"), "Space presses a button"); +} + +#[test] +fn a_field_is_drawn_with_its_label_text_and_caret() { + let fonts = Fonts::load().unwrap(); + let mut ui = Ui::::new(Theme::WHIMSY_DARK); + ui.app.name = std::string::String::new(); + let mut pm = tiny_skia::Pixmap::new(320, 200).unwrap(); + ui.draw(&mut pm.as_mut(), &fonts); + // Somewhere on the field's left is the accent caret. + let caret = (0..320).flat_map(|x| (0..200).map(move |y| (x, y))).any(|(x, y)| at(&pm, x, y) == (T.accent.0, T.accent.1, T.accent.2)); + assert!(caret, "the focused field shows its caret"); +} diff --git a/crates/k-ui/src/ui.rs b/crates/k-ui/src/ui.rs index fc38c49..f238939 100644 --- a/crates/k-ui/src/ui.rs +++ b/crates/k-ui/src/ui.rs @@ -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 { 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: &Element, index: &mut usize, out: &mut Vec) { - if e.kind.event().is_some() { + if e.kind.focusable() { out.push(*index); } *index += 1; @@ -86,9 +100,20 @@ fn event_at(e: &Element, 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, 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, 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 Ui { 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 Ui { 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, 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) { @@ -159,7 +239,7 @@ impl Ui { 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); } } diff --git a/crates/types/src/start_arg.rs b/crates/types/src/start_arg.rs index 030b01f..e7c3287 100644 --- a/crates/types/src/start_arg.rs +++ b/crates/types/src/start_arg.rs @@ -15,7 +15,7 @@ const SESSION_SHIFT: u32 = 56; /// Where the arguments page sits: the program's arguments, NUL separated, /// the first its name. Below the stack, which ends at the image. -pub const ARGS_VA: usize = 0x8000; +pub const ARGS_VA: usize = 0x1000; pub const ARGS_SIZE: usize = 4096; #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] diff --git a/docs/ipc.md b/docs/ipc.md index 1cc6b48..0c91f04 100644 --- a/docs/ipc.md +++ b/docs/ipc.md @@ -78,7 +78,10 @@ A thread made by `SpawnThread`/`SpawnThreadIn` starts with the caller's the slots of the console, log and storage capabilities it was handed, and whether its parent put an arguments page at `ARGS_VA` (NUL-separated, the program's name first; the parent fills a page and `MapPage`s it over, so -the program owns it and it goes when the program does). +the program owns it and it goes when the program does). It sits at +`0x1000`, above the unmapped page 0; the stack runs from just below the +image (`0x10000`) down toward it: 4 pages for system programs unless init's +table says more, 8 for apps. Boot loads only init and storage, both packed into the kernel image (they are what reads the disk, so they cannot come from it). The disk is one ext4