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 ;
use Keyboard;
use ;
let mut ui: = new;
let root = ui.root;
let field = ui.add.unwrap;
ui.focus;
// 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 = new;
// 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;
assert!;
ui.tick;
// What the application's message loop does with a key.
for message in
assert_eq!;
// The field never lost the caret, which is the point.
assert_eq!;
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;
ui.handle;
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;
ui.handle;
ui.handle;
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.