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:
2026-10-28 13:48:08 +01:00
parent 2b82f052ae
commit e952a149ec
12 changed files with 322 additions and 44 deletions

View File

@@ -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<Apps>,
rows: Vec<AppRow>,
/// 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<Event> {
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() {

View File

@@ -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.

View File

@@ -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 });

View File

@@ -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<E> {
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<E> {
/// What it is for: shown above it, and what it is called.
pub label: String,
pub value: String,
pub placeholder: Option<String>,
/// Shown as `*`s (a passcode).
pub secret: bool,
pub on_change: fn(String) -> E,
/// What Enter sends, if anything.
pub on_submit: Option<E>,
}
pub enum Kind<E> {
Flex(Flex<E>),
Text(Text),
Button(Button<E>),
Item(Item<E>),
Toggle(Toggle<E>),
Input(Input<E>),
}
impl<E> Kind<E> {
@@ -73,13 +88,19 @@ impl<E> Kind<E> {
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<E>(label: impl Into<String>, 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<E>(label: impl Into<String>, value: impl Into<String>, on_change: fn(String) -> E) -> InputBuilder<E> {
InputBuilder(Input { label: label.into(), value: value.into(), placeholder: None, secret: false, on_change, on_submit: None })
}
pub struct InputBuilder<E>(Input<E>);
pub struct ItemBuilder<E>(Item<E>, Size);
pub struct ToggleBuilder<E>(Toggle<E>, Size);
pub struct FlexBuilder<E>(Flex<E>, Size);
@@ -192,6 +220,32 @@ impl<E> ItemBuilder<E> {
}
}
impl<E> InputBuilder<E> {
/// Shown, dimmed, while it is empty.
pub fn placeholder(mut self, s: impl Into<String>) -> 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<E> From<InputBuilder<E>> for Element<E> {
fn from(b: InputBuilder<E>) -> 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<E> From<ItemBuilder<E>> for Element<E> {
fn from(b: ItemBuilder<E>) -> Self {
Element { kind: Kind::Item(b.0), size: b.1 }

View File

@@ -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>(e: &Element<E>, 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;

View File

@@ -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;

View File

@@ -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<usize>,
/// 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<E>(&mut self, i: &crate::element::Input<E>, 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<E>(pm: &mut PixmapMut, root: &Element<E>, rects: &[Rect], fonts: &Fonts, theme: &Theme, focused: Option<usize>) {
/// ringed, and its caret at `caret` if it is a field.
pub fn render<E>(pm: &mut PixmapMut, root: &Element<E>, rects: &[Rect], fonts: &Fonts, theme: &Theme, focused: Option<usize>, 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);
}

View File

@@ -17,10 +17,15 @@ 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::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,
}
}

View File

@@ -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<std::string::String>,
}
#[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<FormEvent> {
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::<Form>::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::<Form>::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");
}

View File

@@ -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,25 +148,80 @@ 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 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) = (0, 0.0);
(self.focus, self.scroll, self.caret) = (0, 0.0, usize::MAX);
} else {
// The view may have fewer pressable things now.
// 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));
}
}
}
true
}
/// Lays the view out over the whole of `pm` and draws it, scrolled so
/// the focused element is in sight.
@@ -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);
}
}

View File

@@ -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)]

View File

@@ -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