denise-keyboard 0.31.0

An on-screen keyboard for Denise panels: a shelf of keys that emits what a real keyboard emits.
Documentation

denise-keyboard

An on-screen keyboard for Denise panels that have no other one — a shelf of keys that slides up from the bottom and emits exactly what a keyboard plugged into the machine emits.

Nothing downstream can tell the difference. denise splits keyboard input into InputEvent::Key, a physical position, and InputEvent::Text, a character somebody meant to insert; the hardware path emits the first followed by whatever the second turns out to be, and so does this. The application hands them to Ui::handle, which is the call the hardware path's events arrive through as well — so a TextInput inserts them without knowing, a binding on Enter fires as it would, and widgets that are not text fields hear the keyboard too.

use denise::{KeyCode, Rect, Size, theme};
use denise_keyboard::Keyboard;
use denise_ui::{Ui, widgets::TextInput};

#[derive(Clone, Debug, PartialEq)]
enum Msg {
    Key(KeyCode),
}

let mut ui: Ui<Msg> = Ui::new(Size::new(800, 480), theme::DARK);
let root = ui.root();
let field = ui.add(root, TextInput::new(), Rect::new(20, 20, 400, 40)).unwrap();
ui.focus(Some(field));

// Whatever the machine is configured for; US when it says nothing. The scale
// and the style are worth handing it: see "Fitting the panel" below.
let mut keyboard = Keyboard::new(denise_layout::from_system().0);

// Once a frame, beside draining messages: the keyboard arrives when focus
// lands on a text field and leaves when it goes anywhere else.
keyboard.follow_focus(&mut ui, Msg::Key);
assert!(keyboard.is_open());
ui.tick(0);

// What the application's message loop does with a key.
for message in [Msg::Key(KeyCode::H), Msg::Key(KeyCode::I)] {
    match message {
        Msg::Key(code) => {
            let events = keyboard.press(code);
            ui.handle(&events);
        }
    }
}

assert_eq!(ui.widget::<TextInput<Msg>>(field).unwrap().text(), "hi");
// The field never lost the caret, which is the point.
assert_eq!(ui.focused(), Some(field));

follow_focus is the ordinary policy and nothing more: it asks the tree whether the newly focused node is a TextInput and opens or closes accordingly. An application wanting a different rule reads Ui::focus_changed() itself and decides — the toolkit reports that focus moved and takes no view on what it means, because whether a node deserves a keyboard is not a question denise-ui can answer.

Escape is yours to bind. A shelf pushes no scene, so the tree does not claim the key and will not close the keyboard behind your back.

Fitting the panel

Two things the application knows and a widget cannot, both builders on Keyboard:

with_scale turns the grid's logical pixels into the surface's own. KEY_HEIGHT is 48 because that is a fingertip, not because it is a count of device pixels — on a 2× panel a keyboard that ignored the scale would draw half-size keys beside full-size everything else. Same scale the application scales its own layout by.

with_style names the face the legends are drawn in. Given none, a Button falls back to the built-in 8×8 bitmap face, and the result is visible: the one widget somebody is touching, drawn in the one typeface that is not the rest of the application. set_style is the same thing for an application that registers its font after building its tree.

The keys wear roles rather than the toolkit's default: characters are Base100, the modifiers and the layout key are Neutral, and Enter is Primary. Forty Primary buttons at once is not emphasis.

How it stays out of the way

A key is a Button::no_focus(), so pressing one moves no focus; the keys sit on a Ui::push_shelf, which slides in without pushing a scene, so the field keeps focus while the keyboard is up. Neither is a keyboard feature — they are toolkit features this is the first user of, and both are in denise-ui.

Layouts

The tables come from denise-layout, the same ones denise-evdev feeds from the hardware, so a dead key composes here exactly as it does there: ¨ then o is one ö, and ¨ then q is ¨q rather than a swallowed mark.

Keys are laid out by position, not by character. KeyCode::Semicolon carries ø on a Norwegian layout and ; on a US one, and the key does not move — which is why switching layouts relabels the keyboard rather than rebuilding it.

The grid is a compact physical keyboard — a Chromebook, near enough: fourteen columns, Backspace top right, Tab opening the second row, Enter closing the home row, Shift at both ends of the bottom one, Caps in its own slot, and Ctrl, Alt, the layout key, space, the cursor keys and Escape along the last — so a panel with no other keyboard can put this one away and correct the middle of an address.

The width is not decoration. Minus, Equal, Backquote, BracketLeft, BracketRight and Backslash are where a Norwegian layout keeps +, ?, the acute and grave dead keys, å, the diaeresis, and '. The first version of this grid was "the positions ISO and ANSI have in common", which sounds careful and quietly meant it could not type one of the three letters the Norwegian layout exists for. A test now walks every layout and asserts every letter it has is on a key.

Modifiers

Shift is a one-shot: armed by a tap, spent by the next key. There is no clock in the press path, so there is no double-tap window to latch it with — and none is wanted, because Caps Lock has a key of its own where Caps Lock goes.

Caps is a latch and not a held Shift: it applies to letters and spares the digit row, which is the difference between a locked keyboard typing 1 and typing !, and caps plus shift gives lower case the way a hand expects. The Composer modelled that already, so it is latched with a CapsLock key rather than reimplemented here.

Ctrl is a one-shot too, and reaches the events it modifies — so a binding on Ctrl+something fires from this keyboard as it would from a real one.

Every key that changes what the next press means says which state it is in: shift becomes SHIFT, caps becomes CAPS, ctrl becomes CTRL.

Pictures, not glyphs

The keys whose meaning is a name — Backspace, Tab, Enter, the two cursor keys, the layout key and Escape — are drawn rather than lettered. A denise_render::icon::Icon is a short list of filled polygons on a hundred-square box, scaled into the key, so it looks the same on every machine and stays crisp at 2×.

That replaced asking the font, which had a ceiling worth stating because it is not obvious how badly fonts disagree here:

face ⌫ ⇥ ⏎ ◀ ▶ ← → ⎋
DejaVu, what font-dejavu puts on a Pi yes yes yes yes yes no
Arial, on a Mac no no no no yes no
denise-render's built-in face no no no no no no

No font, no symbol — and ⎋ was unreachable everywhere. A drawn shape does not care.

The keys whose legend carries state keep their words, and that is not a gap left to fill. shift becomes SHIFT, caps becomes CAPS, ctrl becomes CTRL: one glyph cannot say which of two states it is in, and a Shift key that looks identical armed and unarmed is worse than one that spells it out.

The layout key is both, and gets both. A globe says what the key is for and cannot say which of three layouts is live — which on a panel with no other keyboard is the question only this key can answer. So it wears the globe and keeps the name, us or no or de, small in its corner. Button draws an icon and a corner legend together, and this is what that is for.

Escape earns a picture here that it would not earn anywhere else. On this keyboard the key's job is to put the keyboard away, and a keyboard going downwards is what every phone draws for that — no one has to be told what it means. ⎋ is the correct symbol for Escape and almost nobody reads it, which is the argument it loses; that no font here has the glyph either is a fair sign of how often it is wanted.

Each key keeps its word as its Button label regardless of what is drawn over it. That is what the key still reports, which is what a test and an accessibility pass read.

Two legends on a key

Numbers and punctuation carry what Shift would give, small in the top-right corner: the ! over the 1, the ? over the +. That is the whole reason a real keyboard prints it — you cannot discover Shift by pressing Shift, since pressing it is what changes the legend.

Letters do not. A capital Q over a q is not news, and forty keys each carrying a second glyph is a keyboard that reads as noise. The corner also goes away while Shift is held, because the main legend has already become the shifted character.

Button::with_corner is where it lives — a small second label in the top-right corner, which is a button idea rather than a keyboard one: a stepper or a shortcut button wants the same thing.

The third level is the layout's own AltGr, not a page of symbols chosen here — @ is AltGr+2 on Norwegian and Shift+2 on US, so a fixed grid would be wrong on one of them.

Layouts, and switching them

Keyboard::from_system() starts from whatever the machine is configured for — the same answer the hardware path starts from — and hands back a LayoutSource saying where it came from. LayoutSource::Unknown means the system asked for a layout there is no table for and got US, which is worth putting in front of somebody rather than leaving them to wonder.

The layout key walks the built-ins: us, no, de. Switching reletters the keys where they stand, because a position does not move when the layout changes. German is the layout that proves it — QWERTZ, so KeyCode::Y types z, and a keyboard lettered from key names would be wrong on two rows.

Switching this keyboard does not switch a physical one attached to the same machine; call InputBackend::set_layout too if you want them in step.

Seeing what you are typing

Focusing a field scrolls it clear of the keyboard, not merely into its viewport — the tree knows a shelf is in the way. A viewport with nothing to scroll cannot do that, and for those Keyboard::occluded() says exactly what the application has to move something clear of.

What to do about it is the application's, because only it knows what may move, and the two demos in this repository need different answers:

  • The browser grows its page by Keyboard::height() while the keyboard is up, so a field in the last screenful has somewhere to scroll into. Every phone browser does this.
  • The table editor cannot: its form is 300 tall on a 470-tall panel and does not fit above a 276-tall keyboard at any offset. So the whole view scrolls instead: everything hangs off one viewport, and the room the keyboard borrows is added below all of it.

Either way, tell the tree afterwards: Ui::reveal_focused() re-runs the reveal when the geometry around the focus changed rather than the focus itself.

Holding a key

Backspace repeats while it is held, and nothing else does — which is what a phone does, and what stops a slow finger typing aaaaaa. Clearing a URL bar used to be forty taps. A letter held that long offers its alternates instead; see below.

The repeats are the events a real keyboard sends for an auto-repeat: InputEvent::Key with repeat: true, and whatever that types. A TextInput inserts both; anything that must not act twice on one gesture can tell them apart.

Call Keyboard::tick once a frame beside follow_focus and hand the result to Ui::handle. It returns nothing on almost every frame:

let repeats = keyboard.tick(&mut ui, now_ms);
ui.handle(&repeats);

Nothing polls. A repeating key is a Button::with_repeat, which asks the tree to wake it only between a press and its release and answers Wake::Never the moment the finger goes — so a panel nobody is touching schedules nothing, and tick simply finds a tally of nought. That is asserted rather than assumed: ui.animating() is 0 with a keyboard on screen and 1 while a key is held.

A stalled loop does not empty the field when it comes back. Counting repeats from the press is truthful about how much time passed, and would hand over every repeat a ten-second stall covered — so the catch-up is bounded, and the ones a stall swallowed are dropped rather than owed.

Holding a letter

Holding a letter offers its alternates, the way a phone does: é è ê ë over the e, and the finger slides onto one and lifts. The dead keys and the third level already reached those characters, so what this adds is the discoverability — nobody finds ¨ then o by looking at a keyboard.

The characters are the layout's own, in Layout::alternates, because which ones an o should offer is a fact about the language and not about this widget. It follows from that that Norwegian does not offer ø from o: ø has a key, and a slower way to reach a letter you already have is noise. US does offer it, having no such key. German offers ß from s, which is where a writer reaches for it.

The strip is drawn as ordinary nodes at the top of the shelf, not as a popup. That is not a shortcut. A popup pushes a scene, a pushed scene cancels whatever press it covers, and the press it would cancel here is the very one holding the key that opened it.

It wears a frame, in the accent colour, with a margin around the choices and a gap above the row it floats over. Drawn flush and in the keys' own colours it read as another row of the keyboard — so the one thing the gesture exists to say, that these five are the choice right now, was the one thing it did not.

The one call it needs from the application

The choice is made by where the finger lifts, and the press that opened the strip is still down on the key — so the tree goes on routing to the key, quite correctly, and the keyboard has to do its own hit test. Give Keyboard::handle the same events, just before Ui::handle gets them:

let typed = keyboard.handle(&mut ui, &events);
ui.handle(&typed);
ui.handle(&events);

It returns nothing on nearly every call — there is no strip open on nearly every call. What it does return is InputEvent::Text alone, with no InputEvent::Key around it, which is the honest shape: é is not at a position on this keyboard, nothing pressed a key to get it, and a binding watching for keys should not think one was pressed.

Lifting anywhere else ends the gesture and types nothing — except on the key itself, which types what it always types. That is why the strip does not repeat the base character among its choices: the key is still there underneath it, still where the finger already is, so a hold opened by accident is undone by not moving.

Holding a key with nothing to offer does nothing at all, and goes on typing normally when it is released. Only the keys that type a character are asked how long they have been held; a key with a word on it stays free.

What it costs when nobody is holding anything

Nothing, by the same rule the repeat is held to. Button::watching_hold asks the tree to wake it only between a press and its release, so a panel nobody is touching schedules no wake, and Keyboard::tick finds nothing to open.