Public Access
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:
@@ -0,0 +1,136 @@
|
||||
#include "widgets.h"
|
||||
|
||||
#include "fonts.h"
|
||||
|
||||
namespace roro::widgets {
|
||||
|
||||
using namespace theme;
|
||||
|
||||
MeasureText bodyMeasure(Canvas& canvas) {
|
||||
return [&canvas](const std::string& s) {
|
||||
canvas.setFont(&fonts::body);
|
||||
return static_cast<int>(canvas.textWidth(s.c_str()));
|
||||
};
|
||||
}
|
||||
|
||||
void statusBar(Canvas& c, const StatusInfo& info) {
|
||||
c.fillRect(0, 0, kWidth, kStatusBarHeight, kBar);
|
||||
c.setFont(&fonts::small);
|
||||
c.setTextDatum(top_left);
|
||||
c.setTextColor(kText);
|
||||
c.drawString(info.title.c_str(), 3, 2);
|
||||
|
||||
// Right side, laid out right to left.
|
||||
int x = kWidth - 3;
|
||||
auto right = [&](const std::string& text, uint16_t color) {
|
||||
c.setTextColor(color);
|
||||
c.setTextDatum(top_right);
|
||||
c.drawString(text.c_str(), x, 2);
|
||||
x -= c.textWidth(text.c_str()) + 6;
|
||||
};
|
||||
right(info.clock, kText);
|
||||
if (info.batteryPercent >= 0)
|
||||
right(std::to_string(info.batteryPercent) + "%", info.batteryPercent <= 15 ? kWarning : kText);
|
||||
if (info.sdPresent) right("SD", info.sdLevel >= 80 ? kWarning : kMuted);
|
||||
if (info.compose) right(std::string("opt ") + (info.compose == '*' ? "" : std::string(1, info.compose)), kAccent);
|
||||
c.setTextDatum(top_left);
|
||||
}
|
||||
|
||||
void list(Canvas& c, const ListModel& model, Rect area, const std::function<std::string(int)>& label) {
|
||||
c.setFont(&fonts::body);
|
||||
c.setTextDatum(top_left);
|
||||
int rows = model.visibleRows();
|
||||
for (int row = 0; row < rows; row++) {
|
||||
int i = model.firstVisible() + row;
|
||||
if (i >= model.count()) break;
|
||||
int y = area.y + row * kLineHeight;
|
||||
bool selected = i == model.selected();
|
||||
if (selected) c.fillRect(area.x, y, area.w - 4, kLineHeight, kSelection);
|
||||
c.setTextColor(kText);
|
||||
c.drawString(label(i).c_str(), area.x + 4, y + 1);
|
||||
}
|
||||
// Scrollbar
|
||||
if (model.count() > rows) {
|
||||
int barH = area.h * rows / model.count();
|
||||
int barY = area.y + (area.h - barH) * model.firstVisible() / (model.count() - rows);
|
||||
c.fillRect(area.x + area.w - 2, barY, 2, barH, kMuted);
|
||||
}
|
||||
}
|
||||
|
||||
void textLines(Canvas& c, const std::vector<std::string>& lines, int firstLine, Rect area) {
|
||||
c.setFont(&fonts::body);
|
||||
c.setTextDatum(top_left);
|
||||
c.setTextColor(kText);
|
||||
int rows = area.h / kLineHeight;
|
||||
for (int row = 0; row < rows && firstLine + row < static_cast<int>(lines.size()); row++)
|
||||
c.drawString(lines[firstLine + row].c_str(), area.x + 4, area.y + row * kLineHeight + 1);
|
||||
int total = static_cast<int>(lines.size());
|
||||
if (total > rows) {
|
||||
int barH = area.h * rows / total;
|
||||
int barY = area.y + (area.h - barH) * firstLine / (total - rows);
|
||||
c.fillRect(area.x + area.w - 2, barY, 2, barH, kMuted);
|
||||
}
|
||||
}
|
||||
|
||||
void lineEditor(Canvas& c, const LineEditor& editor, Rect area) {
|
||||
c.setFont(&fonts::body);
|
||||
c.setTextDatum(top_left);
|
||||
c.drawRect(area.x, area.y, area.w, kLineHeight + 4, kAccent);
|
||||
|
||||
const std::string& text = editor.text();
|
||||
int cursorX = c.textWidth(text.substr(0, editor.cursor()).c_str());
|
||||
int visible = area.w - 8;
|
||||
int scroll = cursorX > visible ? cursorX - visible : 0;
|
||||
|
||||
c.setClipRect(area.x + 2, area.y + 1, area.w - 4, kLineHeight + 2);
|
||||
c.setTextColor(kText);
|
||||
c.drawString(text.c_str(), area.x + 3 - scroll, area.y + 3);
|
||||
c.fillRect(area.x + 3 + cursorX - scroll, area.y + 2, 1, kLineHeight, kAccent);
|
||||
c.clearClipRect();
|
||||
}
|
||||
|
||||
void dialog(Canvas& c, const std::string& title, const std::string& message, const DialogModel& model) {
|
||||
Rect box{16, 26, kWidth - 32, 92};
|
||||
c.fillRect(box.x, box.y, box.w, box.h, kBackground);
|
||||
c.drawRect(box.x, box.y, box.w, box.h, kAccent);
|
||||
c.setTextDatum(top_left);
|
||||
c.setFont(&fonts::bold);
|
||||
c.setTextColor(kText);
|
||||
c.drawString(title.c_str(), box.x + 6, box.y + 4);
|
||||
|
||||
auto lines = wrapText(message, box.w - 12, bodyMeasure(c));
|
||||
textLines(c, lines, 0, {box.x + 2, box.y + 20, box.w - 4, 3 * kLineHeight});
|
||||
|
||||
// Buttons, right-aligned.
|
||||
c.setFont(&fonts::body);
|
||||
int x = box.x + box.w - 6;
|
||||
const auto& buttons = model.buttons();
|
||||
for (int i = static_cast<int>(buttons.size()) - 1; i >= 0; i--) {
|
||||
int w = c.textWidth(buttons[i].c_str()) + 12;
|
||||
x -= w;
|
||||
bool selected = i == model.selected();
|
||||
if (selected) c.fillRect(x, box.y + box.h - 19, w, 15, kAccent);
|
||||
else c.drawRect(x, box.y + box.h - 19, w, 15, kMuted);
|
||||
c.setTextColor(selected ? kBackground : kText);
|
||||
c.drawString(buttons[i].c_str(), x + 6, box.y + box.h - 17);
|
||||
x -= 6;
|
||||
}
|
||||
}
|
||||
|
||||
void toast(Canvas& c, const Toast& t) {
|
||||
uint16_t color = t.level == NotificationLevel::Warning ? kWarning
|
||||
: t.level == NotificationLevel::Message ? kMessage
|
||||
: kAccent;
|
||||
c.setFont(&fonts::body);
|
||||
int w = c.textWidth(t.text.c_str()) + 16;
|
||||
if (w > kWidth - 8) w = kWidth - 8;
|
||||
int x = (kWidth - w) / 2, y = kHeight - 22;
|
||||
c.fillRect(x, y, w, 18, color);
|
||||
c.setTextColor(kBackground);
|
||||
c.setTextDatum(top_left);
|
||||
c.setClipRect(x + 4, y, w - 8, 18);
|
||||
c.drawString(t.text.c_str(), x + 8, y + 3);
|
||||
c.clearClipRect();
|
||||
}
|
||||
|
||||
} // namespace roro::widgets
|
||||
Reference in New Issue
Block a user