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,110 @@
|
||||
#include "demo_app.h"
|
||||
|
||||
#include "ui/widgets.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
namespace {
|
||||
const char* const kMenu[] = {"Text view", "Line editor", "Dialog", "Toast: info", "Toast: warning",
|
||||
"Toast: burst of 5"};
|
||||
const char* const kText =
|
||||
"Bienvenue sur roro9stack ! Ce texte teste le retour \xC3\xA0 la ligne automatique et les "
|
||||
"accents : \xC3\xA9t\xC3\xA9, \xC3\xA0 bient\xC3\xB4t, gar\xC3\xA7on, No\xC3\xABl, "
|
||||
"ma\xC3\xAEtre, o\xC3\xB9.\n\n"
|
||||
"Use Fn+; and Fn+. to scroll, ` to go back. Fn+` always returns to the Launcher.\n\n"
|
||||
"Unbreakablewordthatislongerthanawholelineofthescreen to test hard breaks.";
|
||||
} // namespace
|
||||
|
||||
void DemoApp::onEnter() {
|
||||
page_ = Page::Menu;
|
||||
dialog_.reset();
|
||||
menu_.setCount(sizeof(kMenu) / sizeof(kMenu[0]));
|
||||
}
|
||||
|
||||
void DemoApp::notify(const char* text, NotificationLevel level) {
|
||||
bus_.publish(Event::withText(EventType::Notification, text, static_cast<int32_t>(level)));
|
||||
}
|
||||
|
||||
bool DemoApp::onKey(const KeyEvent& e) {
|
||||
requestRedraw();
|
||||
if (dialog_) {
|
||||
dialog_->onKey(e);
|
||||
int r = dialog_->result();
|
||||
if (r != DialogModel::kPending) {
|
||||
notify(r == DialogModel::kCancelled ? "Dialog cancelled" : ("Chose " + dialog_->buttons()[r]).c_str(),
|
||||
NotificationLevel::Info);
|
||||
dialog_.reset();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
switch (page_) {
|
||||
case Page::Menu: return onMenuKey(e);
|
||||
case Page::Text:
|
||||
if (e.key == Key::Up && textScroll_ > 0) textScroll_--;
|
||||
else if (e.key == Key::Down && textScroll_ < textLineCount_ - 1) textScroll_++;
|
||||
else if (e.key == Key::Back) page_ = Page::Menu;
|
||||
return true;
|
||||
case Page::Editor:
|
||||
switch (e.key) {
|
||||
case Key::Char: editor_.insert(e.ch); break;
|
||||
case Key::Delete: editor_.backspace(); break;
|
||||
case Key::Left: editor_.left(); break;
|
||||
case Key::Right: editor_.right(); break;
|
||||
case Key::Up: editor_.home(); break;
|
||||
case Key::Down: editor_.end(); break;
|
||||
case Key::Select:
|
||||
if (!editor_.text().empty()) notify(editor_.text().c_str(), NotificationLevel::Message);
|
||||
break;
|
||||
case Key::Back: page_ = Page::Menu; break;
|
||||
default: break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DemoApp::onMenuKey(const KeyEvent& e) {
|
||||
switch (e.key) {
|
||||
case Key::Up: menu_.up(); return true;
|
||||
case Key::Down: menu_.down(); return true;
|
||||
case Key::Select: break;
|
||||
default: return false; // Back leaves the App
|
||||
}
|
||||
switch (menu_.selected()) {
|
||||
case 0: page_ = Page::Text; textScroll_ = 0; break;
|
||||
case 1: page_ = Page::Editor; break;
|
||||
case 2: dialog_.reset(new DialogModel({"Cancel", "Maybe", "OK"})); break;
|
||||
case 3: notify("Hello from the demo", NotificationLevel::Info); break;
|
||||
case 4: notify("Something needs attention", NotificationLevel::Warning); break;
|
||||
case 5:
|
||||
for (int i = 1; i <= 5; i++) notify(("Burst " + std::to_string(i)).c_str(), NotificationLevel::Info);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void DemoApp::draw(Canvas& c) {
|
||||
const auto& area = theme::kContent;
|
||||
switch (page_) {
|
||||
case Page::Menu:
|
||||
widgets::list(c, menu_, area, [](int i) { return std::string(kMenu[i]); });
|
||||
break;
|
||||
case Page::Text: {
|
||||
auto lines = wrapText(kText, area.w - 10, widgets::bodyMeasure(c));
|
||||
textLineCount_ = static_cast<int>(lines.size());
|
||||
widgets::textLines(c, lines, textScroll_, area);
|
||||
break;
|
||||
}
|
||||
case Page::Editor:
|
||||
c.setTextColor(theme::kMuted);
|
||||
c.drawString("Type (opt ' e = \xC3\xA9). Enter: toast", 4, area.y + 4);
|
||||
widgets::lineEditor(c, editor_, {4, area.y + 22, area.w - 8, 0});
|
||||
c.setTextColor(theme::kMuted);
|
||||
c.drawString((std::to_string(editor_.text().size()) + "/39 bytes").c_str(), 4, area.y + 44);
|
||||
break;
|
||||
}
|
||||
if (dialog_)
|
||||
widgets::dialog(c, "Dialog", "Left/Right or Tab to choose, Enter to confirm, ` to cancel.", *dialog_);
|
||||
}
|
||||
|
||||
} // namespace roro
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "app.h"
|
||||
#include "dialog_model.h"
|
||||
#include "event_bus.h"
|
||||
#include "line_editor.h"
|
||||
#include "list_model.h"
|
||||
#include "ui/theme.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
// Exercises every widget: text view, line editor, dialog and Toasts.
|
||||
class DemoApp : public App {
|
||||
public:
|
||||
explicit DemoApp(EventBus& bus) : bus_(bus) {}
|
||||
void onEnter() override;
|
||||
bool onKey(const KeyEvent& e) override;
|
||||
void draw(Canvas& c) override;
|
||||
|
||||
private:
|
||||
enum class Page { Menu, Text, Editor };
|
||||
bool onMenuKey(const KeyEvent& e);
|
||||
void notify(const char* text, NotificationLevel level);
|
||||
|
||||
EventBus& bus_;
|
||||
Page page_ = Page::Menu;
|
||||
ListModel menu_{theme::kContent.h / theme::kLineHeight};
|
||||
int textScroll_ = 0;
|
||||
int textLineCount_ = 0;
|
||||
LineEditor editor_{39};
|
||||
std::unique_ptr<DialogModel> dialog_;
|
||||
};
|
||||
|
||||
} // namespace roro
|
||||
@@ -0,0 +1,84 @@
|
||||
#include "diagnostics_app.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#include "ui/fonts.h"
|
||||
#include "ui/widgets.h"
|
||||
#include "version.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
bool DiagnosticsApp::onKey(const KeyEvent& e) {
|
||||
requestRedraw();
|
||||
if (formatDialog_) {
|
||||
formatDialog_->onKey(e);
|
||||
if (formatDialog_->result() == 1) storage_.requestFormat();
|
||||
if (formatDialog_->result() != DialogModel::kPending) formatDialog_.reset();
|
||||
return true;
|
||||
}
|
||||
if (e.key != Key::Char) return false;
|
||||
int brightness = settings_.getInt(Setting::Brightness);
|
||||
switch (e.ch) {
|
||||
case '+':
|
||||
case '=': settings_.setInt(Setting::Brightness, brightness + 10); return true;
|
||||
case '-': settings_.setInt(Setting::Brightness, brightness - 10); return true;
|
||||
case 'd':
|
||||
settings_.setInt(Setting::DimTimeoutS, 5);
|
||||
settings_.setInt(Setting::OffTimeoutS, 10);
|
||||
return true;
|
||||
case 'r': // restore default timeouts
|
||||
settings_.setInt(Setting::OffTimeoutS, 60);
|
||||
settings_.setInt(Setting::DimTimeoutS, 30);
|
||||
return true;
|
||||
case 'f':
|
||||
if (storage_.state().present) formatDialog_.reset(new DialogModel({"Cancel", "Erase"}));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void DiagnosticsApp::update(uint32_t nowMs) {
|
||||
if (nowMs - lastRefreshMs_ >= 1000) {
|
||||
lastRefreshMs_ = nowMs;
|
||||
requestRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
void DiagnosticsApp::draw(Canvas& c) {
|
||||
const auto& area = theme::kContent;
|
||||
const auto& b = battery_.estimator();
|
||||
const auto s = storage_.state();
|
||||
std::vector<std::string> lines;
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "%s %s", kProductName, versionString());
|
||||
lines.push_back(buf);
|
||||
snprintf(buf, sizeof(buf), "Battery %d%% %d mV (raw %d)", b.percent(), b.millivolts(), battery_.lastRawMillivolts());
|
||||
lines.push_back(buf);
|
||||
if (storage_.formatting())
|
||||
lines.push_back("SD: formatting...");
|
||||
else if (s.present) {
|
||||
snprintf(buf, sizeof(buf), "SD %d%% of %u MB logs:%s capt:%s", s.usedPercent,
|
||||
static_cast<unsigned>(s.totalBytes >> 20), s.logsAllowed ? "y" : "n", s.capturesAllowed ? "y" : "n");
|
||||
lines.push_back(buf);
|
||||
} else
|
||||
lines.push_back("SD: no card");
|
||||
snprintf(buf, sizeof(buf), "Uptime %lus clock %s", millis() / 1000, clock_.displayTime().c_str());
|
||||
lines.push_back(buf);
|
||||
snprintf(buf, sizeof(buf), "Heap %u KB free, min %u KB", ESP.getFreeHeap() / 1024, ESP.getMinFreeHeap() / 1024);
|
||||
lines.push_back(buf);
|
||||
snprintf(buf, sizeof(buf), "Bright %d%% dim %ds off %ds", static_cast<int>(settings_.getInt(Setting::Brightness)),
|
||||
static_cast<int>(settings_.getInt(Setting::DimTimeoutS)),
|
||||
static_cast<int>(settings_.getInt(Setting::OffTimeoutS)));
|
||||
lines.push_back(buf);
|
||||
widgets::textLines(c, lines, 0, area);
|
||||
|
||||
c.setFont(&fonts::small);
|
||||
c.setTextColor(theme::kMuted);
|
||||
c.drawString("+/- bright d: 5s/10s r: 30s/60s f: erase SD", 4, area.y + area.h - 10);
|
||||
|
||||
if (formatDialog_)
|
||||
widgets::dialog(c, "Erase SD card?", "Everything on the card is deleted. It becomes one FAT32 partition.",
|
||||
*formatDialog_);
|
||||
}
|
||||
|
||||
} // namespace roro
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "app.h"
|
||||
#include "dialog_model.h"
|
||||
#include "services/battery_service.h"
|
||||
#include "services/clock_service.h"
|
||||
#include "services/power_service.h"
|
||||
#include "services/storage_service.h"
|
||||
#include "settings.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
// Live hardware values for bring-up; becomes the About screen in step 7.
|
||||
class DiagnosticsApp : public App {
|
||||
public:
|
||||
DiagnosticsApp(Settings& settings, BatteryService& battery, StorageService& storage, ClockService& clock)
|
||||
: settings_(settings), battery_(battery), storage_(storage), clock_(clock) {}
|
||||
bool onKey(const KeyEvent& e) override;
|
||||
void update(uint32_t nowMs) override;
|
||||
void draw(Canvas& c) override;
|
||||
|
||||
private:
|
||||
Settings& settings_;
|
||||
BatteryService& battery_;
|
||||
StorageService& storage_;
|
||||
ClockService& clock_;
|
||||
std::unique_ptr<DialogModel> formatDialog_;
|
||||
uint32_t lastRefreshMs_ = 0;
|
||||
};
|
||||
|
||||
} // namespace roro
|
||||
@@ -0,0 +1,27 @@
|
||||
#include "launcher_app.h"
|
||||
|
||||
#include "ui/widgets.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
void LauncherApp::onEnter() { list_.setCount(static_cast<int>(manager_->visibleApps().size())); }
|
||||
|
||||
bool LauncherApp::onKey(const KeyEvent& e) {
|
||||
switch (e.key) {
|
||||
case Key::Up: list_.up(); break;
|
||||
case Key::Down: list_.down(); break;
|
||||
case Key::Select:
|
||||
if (list_.selected() >= 0) manager_->open(manager_->visibleApps()[list_.selected()]->id);
|
||||
return true;
|
||||
default: return false;
|
||||
}
|
||||
requestRedraw();
|
||||
return true;
|
||||
}
|
||||
|
||||
void LauncherApp::draw(Canvas& c) {
|
||||
auto apps = manager_->visibleApps();
|
||||
widgets::list(c, list_, theme::kContent, [&](int i) { return std::string(apps[i]->title); });
|
||||
}
|
||||
|
||||
} // namespace roro
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include "app.h"
|
||||
#include "app_manager.h"
|
||||
#include "list_model.h"
|
||||
#include "ui/theme.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
// Home screen: lists the visible Apps; Select opens one.
|
||||
class LauncherApp : public App {
|
||||
public:
|
||||
void setManager(AppManager& manager) { manager_ = &manager; }
|
||||
void onEnter() override;
|
||||
bool onKey(const KeyEvent& e) override;
|
||||
void draw(Canvas& c) override;
|
||||
|
||||
private:
|
||||
AppManager* manager_ = nullptr;
|
||||
ListModel list_{theme::kContent.h / theme::kLineHeight};
|
||||
};
|
||||
|
||||
} // namespace roro
|
||||
Reference in New Issue
Block a user