Add widget kit, input layer with Compose Key, Launcher and demo apps

Steps 5 and 6 of M0.
- lib/ui (host-tested): ListModel, LineEditor (UTF-8, byte-limited),
  wrapText, ToastQueue, DialogModel
- lib/input (host-tested): KeyMapper turns keyboard state into logical
  keys (Fn arrows, ` Back, Fn+` Home) and composes accents with opt
- src/ui: off-screen 16-bit frame pushed in one transfer (fixes the
  flicker), Status Bar, list, text, line editor, dialog, Toast widgets,
  Notifier (Toast + beep + LED), vendored public-domain Latin-1 fonts
- Apps: minimal Launcher, Widget demo, Diagnostics (SD erase now
  behind a confirmation dialog)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
2026-10-02 10:35:48 +02:00
co-authored by Claude Opus 5.5
parent 36ebd23417
commit 879cb0d0b6
39 changed files with 2119 additions and 92 deletions
+107
View File
@@ -0,0 +1,107 @@
#include "key_mapper.h"
#include <algorithm>
namespace roro {
namespace {
constexpr char kArmed = '*';
bool isAccent(char c) { return c == '\'' || c == '`' || c == '^' || c == '"' || c == ',' || c == '~'; }
// Latin-1 code point for accent + base letter, or 0.
uint32_t composeLatin1(char accent, char base) {
struct Entry {
char accent;
const char* bases;
const uint32_t* codes;
};
static const uint32_t acute[] = {0xE1, 0xE9, 0xED, 0xF3, 0xFA, 0xFD, 0xC1, 0xC9, 0xCD, 0xD3, 0xDA, 0xDD};
static const uint32_t grave[] = {0xE0, 0xE8, 0xEC, 0xF2, 0xF9, 0xC0, 0xC8, 0xCC, 0xD2, 0xD9};
static const uint32_t circ[] = {0xE2, 0xEA, 0xEE, 0xF4, 0xFB, 0xC2, 0xCA, 0xCE, 0xD4, 0xDB};
static const uint32_t diaer[] = {0xE4, 0xEB, 0xEF, 0xF6, 0xFC, 0xFF, 0xC4, 0xCB, 0xCF, 0xD6, 0xDC};
static const uint32_t cedil[] = {0xE7, 0xC7};
static const uint32_t tilde[] = {0xE3, 0xF1, 0xF5, 0xC3, 0xD1, 0xD5};
static const Entry table[] = {
{'\'', "aeiouyAEIOUY", acute}, {'`', "aeiouAEIOU", grave}, {'^', "aeiouAEIOU", circ},
{'"', "aeiouyAEIOU", diaer}, {',', "cC", cedil}, {'~', "anoANO", tilde},
};
for (auto& e : table) {
if (e.accent != accent) continue;
for (int i = 0; e.bases[i]; i++)
if (e.bases[i] == base) return e.codes[i];
}
return 0;
}
KeyEvent charEvent(uint32_t cp, const RawKeys& keys) {
KeyEvent e = KeyEvent::character(cp);
e.shift = keys.shift;
e.ctrl = keys.ctrl;
e.alt = keys.alt;
return e;
}
} // namespace
std::vector<KeyEvent> KeyMapper::update(const RawKeys& keys) {
std::vector<KeyEvent> out;
if (keys.opt && !previous_.opt && keys.chars.empty()) {
compose_ = compose_ ? 0 : kArmed; // a second opt cancels
}
if (keys.enter && !previous_.enter) out.push_back(KeyEvent::of(Key::Select));
if (keys.del && !previous_.del) out.push_back(KeyEvent::of(Key::Delete));
if (keys.tab && !previous_.tab) out.push_back(KeyEvent::of(Key::Tab));
for (char c : keys.chars) {
bool wasHeld = std::find(previous_.chars.begin(), previous_.chars.end(), c) != previous_.chars.end();
if (!wasHeld) onChar(c, keys, out);
}
previous_ = keys;
return out;
}
void KeyMapper::onChar(char c, const RawKeys& keys, std::vector<KeyEvent>& out) {
// Holding opt while typing the accent is the same as pressing opt first.
if (keys.opt && !compose_) compose_ = kArmed;
if (compose_ == kArmed) {
if (isAccent(c)) {
compose_ = c;
return;
}
compose_ = 0; // not an accent: type it normally below
} else if (compose_) {
char accent = compose_;
compose_ = 0;
uint32_t composed = composeLatin1(accent, c);
if (composed) {
out.push_back(charEvent(composed, keys));
} else {
out.push_back(charEvent(static_cast<unsigned char>(accent), keys));
out.push_back(charEvent(static_cast<unsigned char>(c), keys));
}
return;
}
if (keys.fn) {
switch (c) {
case ';': out.push_back(KeyEvent::of(Key::Up)); return;
case '.': out.push_back(KeyEvent::of(Key::Down)); return;
case ',': out.push_back(KeyEvent::of(Key::Left)); return;
case '/': out.push_back(KeyEvent::of(Key::Right)); return;
case '`': out.push_back(KeyEvent::of(Key::Home)); return;
default: return; // other Fn combos are unassigned
}
}
if (c == '`') {
out.push_back(KeyEvent::of(Key::Back));
return;
}
out.push_back(charEvent(static_cast<unsigned char>(c), keys));
}
} // namespace roro