From aadfd8cb374ee677c9b38998e3c3310411ee20f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Martin?= Date: Fri, 9 Oct 2026 23:14:37 +0200 Subject: [PATCH] UI: the website's colours, on every screen The theme takes the website's palette, each a colour the RGB332 frame buffer holds exactly: cyan for the accent, a selected row and a chosen button in cyan with dark blue text, a slate Status Bar (the grey it asked for was shown as a dull olive), the website's orange for warnings and its pink for messages. "Good" (a Fix, the quietest channel) keeps a green of its own, which the website doesn't have. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0162FokPdvY2KsS4NBfwyWPk --- docs/milestones/U1.md | 17 +++++++++++++++++ site/content/dev/milestones/u1.md | 17 +++++++++++++++++ src/apps/gemini_app.cpp | 2 +- src/apps/gnss_app.cpp | 14 +++++++------- src/apps/lora_scanner_app.cpp | 2 +- src/apps/ssh_app.cpp | 2 +- src/apps/system_app.cpp | 8 ++++---- src/apps/wifi_tools_app.cpp | 6 +++--- src/ui/theme.h | 19 ++++++++++++------- src/ui/widgets.cpp | 24 ++++++++++++------------ 10 files changed, 75 insertions(+), 36 deletions(-) diff --git a/docs/milestones/U1.md b/docs/milestones/U1.md index 2ee23af..e6638ca 100644 --- a/docs/milestones/U1.md +++ b/docs/milestones/U1.md @@ -59,3 +59,20 @@ A screenshot could only be taken by typing `screenshot` in the Shell, where "now **Checked on the device** (2026-10-07, with `key shot`): in the Launcher, a 33,383-byte PNG appears in `/screenshots` and is the Launcher; with the help panel open, the picture is of the panel (which now lists Fn p) and the panel stays open; on Settings > Debug Console, no file is written; back on the Settings list, one is. The test pictures were removed. **Not checked:** the real Fn+p on the keyboard (the mapper is host-tested); the two Toasts that refuse, which weren't looked at (one of them is on the page that mustn't be photographed); a device with no card. + +## The website's colours, on every screen + +Every screen takes its colours from `src/ui/theme.h`, so the palette changed there, to the website's (`site/static/css/site.css`, dark side). Each is a colour the RGB332 frame buffer holds exactly. + +| | Was | Is | +|---|---|---| +| Accent | teal-blue | cyan `#00dbff` | +| A selected row, a dialog's chosen button | darker teal, white text | cyan, dark blue text (`#000055`) | +| Status Bar | `#202020`, which the frame buffer showed as a dull olive | slate `#494955`; what is idle on it in light grey `#b6b6aa` | +| Muted text | grey, as the frame buffer rounded it | the same grey, exact: `#9292aa` | +| Warning | orange | the website's orange `#ff9200` | +| Someone wrote (unread count, a mention, a message Toast) | green | pink `#ff92ff` | +| Good (a Fix, the quietest channel, a live task) | the same green | green `#49db55`, the one colour that isn't the website's: it has no green | +| Text on a Toast | black | dark blue | + +Looked at on the device, by screenshot: the Launcher, IRC, Wi-Fi Tools (menu, channel occupancy), GNSS (position, sky), Gemini, the LoRa Scanner (Sniffer, Sweep, presets), Storage, Notes, SSH, the Shell, System, Settings and the help panel. Not looked at: dialogs, a warning or a message Toast, the SSH terminal (which keeps its own 16 ANSI colours), the Setup screens. diff --git a/site/content/dev/milestones/u1.md b/site/content/dev/milestones/u1.md index 972878b..af94d64 100644 --- a/site/content/dev/milestones/u1.md +++ b/site/content/dev/milestones/u1.md @@ -67,3 +67,20 @@ A screenshot could only be taken by typing `screenshot` in the Shell, where "now **Checked on the device** (2026-10-07, with `key shot`): in the Launcher, a 33,383-byte PNG appears in `/screenshots` and is the Launcher; with the help panel open, the picture is of the panel (which now lists Fn p) and the panel stays open; on Settings > Debug Console, no file is written; back on the Settings list, one is. The test pictures were removed. **Not checked:** the real Fn+p on the keyboard (the mapper is host-tested); the two Toasts that refuse, which weren't looked at (one of them is on the page that mustn't be photographed); a device with no card. + +## The website's colours, on every screen + +Every screen takes its colours from `src/ui/theme.h`, so the palette changed there, to the website's (`site/static/css/site.css`, dark side). Each is a colour the RGB332 frame buffer holds exactly. + +| | Was | Is | +|---|---|---| +| Accent | teal-blue | cyan `#00dbff` | +| A selected row, a dialog's chosen button | darker teal, white text | cyan, dark blue text (`#000055`) | +| Status Bar | `#202020`, which the frame buffer showed as a dull olive | slate `#494955`; what is idle on it in light grey `#b6b6aa` | +| Muted text | grey, as the frame buffer rounded it | the same grey, exact: `#9292aa` | +| Warning | orange | the website's orange `#ff9200` | +| Someone wrote (unread count, a mention, a message Toast) | green | pink `#ff92ff` | +| Good (a Fix, the quietest channel, a live task) | the same green | green `#49db55`, the one colour that isn't the website's: it has no green | +| Text on a Toast | black | dark blue | + +Looked at on the device, by screenshot: the Launcher, IRC, Wi-Fi Tools (menu, channel occupancy), GNSS (position, sky), Gemini, the LoRa Scanner (Sniffer, Sweep, presets), Storage, Notes, SSH, the Shell, System, Settings and the help panel. Not looked at: dialogs, a warning or a message Toast, the SSH terminal (which keeps its own 16 ANSI colours), the Setup screens. diff --git a/src/apps/gemini_app.cpp b/src/apps/gemini_app.cpp index 553a580..ffd44fc 100644 --- a/src/apps/gemini_app.cpp +++ b/src/apps/gemini_app.cpp @@ -534,7 +534,7 @@ void GeminiApp::drawLine(Canvas& c, size_t i, int firstRow, int y, int rowsLeft) for (int r = firstRow; r < static_cast(rows.size()) && r - firstRow < rowsLeft; r++) { int ry = y + (r - firstRow) * theme::kLineHeight; if (isSelected) c.fillRect(0, ry, theme::kWidth - 3, theme::kLineHeight, theme::kSelection); - c.setTextColor(isSelected ? theme::kText : st.color); + c.setTextColor(isSelected ? theme::kOnAccent : st.color); std::string text = rows[r]; if (g.type == LineType::Preformatted) text = hscroll_ < static_cast(text.size()) ? text.substr(hscroll_) : ""; c.drawString(text.c_str(), kMargin, ry + 1); diff --git a/src/apps/gnss_app.cpp b/src/apps/gnss_app.cpp index 3d280fa..bea063e 100644 --- a/src/apps/gnss_app.cpp +++ b/src/apps/gnss_app.cpp @@ -22,7 +22,7 @@ uint16_t colorOf(Constellation c) { switch (c) { case Constellation::Gps: return theme::kAccent; case Constellation::Glonass: return 0xF8C8; // red - case Constellation::Galileo: return theme::kMessage; + case Constellation::Galileo: return theme::kGood; case Constellation::BeiDou: return 0xFEA0; // yellow case Constellation::Qzss: return 0xC99F; // violet default: return theme::kText; @@ -96,7 +96,7 @@ void GnssApp::drawPosition(Canvas& c) { char line[64]; c.setFont(&fonts::bold); - c.setTextColor(s.fix == FixType::None ? theme::kWarning : theme::kMessage); + c.setTextColor(s.fix == FixType::None ? theme::kWarning : theme::kGood); if (!gnss_.receiving(now)) std::snprintf(line, sizeof line, "No data from the receiver"); else if (s.fix == FixType::None) std::snprintf(line, sizeof line, "Searching: %u in view, %s", (unsigned)s.satellites.size(), @@ -155,10 +155,10 @@ void GnssApp::drawSky(Canvas& c) { // The sky from above: horizon, 30° and 60° elevation rings, north up. c.drawCircle(cx, cy, radius, theme::kMuted); - c.drawCircle(cx, cy, radius * 2 / 3, 0x4208); - c.drawCircle(cx, cy, radius / 3, 0x4208); - c.drawFastHLine(cx - radius, cy, 2 * radius, 0x4208); - c.drawFastVLine(cx, cy - radius, 2 * radius, 0x4208); + c.drawCircle(cx, cy, radius * 2 / 3, theme::kFaint); + c.drawCircle(cx, cy, radius / 3, theme::kFaint); + c.drawFastHLine(cx - radius, cy, 2 * radius, theme::kFaint); + c.drawFastVLine(cx, cy - radius, 2 * radius, theme::kFaint); c.setFont(&fonts::small); c.setTextColor(theme::kMuted); c.setTextDatum(middle_center); @@ -201,7 +201,7 @@ void GnssApp::drawSky(Canvas& c) { c.drawString("No satellites", x, y); y += theme::kLineHeight; } - c.setTextColor(s.fix == FixType::None ? theme::kWarning : theme::kMessage); + c.setTextColor(s.fix == FixType::None ? theme::kWarning : theme::kGood); c.drawString(fixLabel(s.fix), x, y + 4); c.setFont(&fonts::small); c.setTextColor(theme::kMuted); diff --git a/src/apps/lora_scanner_app.cpp b/src/apps/lora_scanner_app.cpp index a622e63..2ba9405 100644 --- a/src/apps/lora_scanner_app.cpp +++ b/src/apps/lora_scanner_app.cpp @@ -327,7 +327,7 @@ void LoraScannerApp::drawSweep(Canvas& c) { uint32_t hz = radio_.config().frequencyHz; if (hz >= f.fromHz && hz <= f.fromHz + (f.steps - 1) * f.stepHz) { int x = x0 + static_cast((hz - f.fromHz) / f.stepHz) * cell + cell / 2; - for (int y = barTop; y < barBottom; y += 3) c.drawPixel(x, y, theme::kMessage); + for (int y = barTop; y < barBottom; y += 3) c.drawPixel(x, y, theme::kGood); } // The waterfall. diff --git a/src/apps/ssh_app.cpp b/src/apps/ssh_app.cpp index 01ed930..9e75494 100644 --- a/src/apps/ssh_app.cpp +++ b/src/apps/ssh_app.cpp @@ -424,7 +424,7 @@ void SshApp::drawTerminal(Canvas& c) { c.setFont(&fonts::small); c.setTextDatum(top_right); c.fillRect(area.x + area.w - 5 * static_cast(where.size()) - 3, area.y, 5 * static_cast(where.size()) + 3, 9, theme::kAccent); - c.setTextColor(0x0000); + c.setTextColor(theme::kOnAccent); c.drawString(where.c_str(), area.x + area.w - 1, area.y + 1); c.setTextDatum(top_left); } diff --git a/src/apps/system_app.cpp b/src/apps/system_app.cpp index 6918512..3828c7e 100644 --- a/src/apps/system_app.cpp +++ b/src/apps/system_app.cpp @@ -232,7 +232,7 @@ void SystemApp::drawOverview(Canvas& c) { const auto& h = loadHistory_[core]; for (size_t i = 1; i < h.size(); i++) { int x0 = gx + 1 + static_cast((i - 1) * gw / kHistory), x1 = gx + 1 + static_cast(i * gw / kHistory); - c.drawLine(x0, gy + gh - h.at(i - 1) * gh / 100, x1, gy + gh - h.at(i) * gh / 100, core ? theme::kMessage : theme::kAccent); + c.drawLine(x0, gy + gh - h.at(i - 1) * gh / 100, x1, gy + gh - h.at(i) * gh / 100, core ? theme::kGood : theme::kAccent); } } } @@ -290,7 +290,7 @@ void SystemApp::drawMemory(Canvas& c) { struct { size_t bytes; uint16_t color; - } floors[] = {{GeminiService::kStartFloor, theme::kMuted}, {GeminiService::kSteadyFloor, theme::kWarning}, {GeminiService::kTransientFloor, theme::kMessage}}; + } floors[] = {{GeminiService::kStartFloor, theme::kMuted}, {GeminiService::kSteadyFloor, theme::kWarning}, {GeminiService::kTransientFloor, theme::kGood}}; for (auto& f : floors) { int kb = static_cast(f.bytes / 1024), y = yOf(kb); for (int x = gx + 1; x < gx + gw - 1; x += 4) c.drawPixel(x, y, f.color); @@ -335,13 +335,13 @@ void SystemApp::drawNetwork(Canvas& c) { for (size_t i = 0; i < kUsers; i++) { bool active = rateIn_[i] || rateOut_[i]; c.setTextDatum(top_left); - c.setTextColor(active ? theme::kMessage : theme::kText); + c.setTextColor(active ? theme::kGood : theme::kText); c.drawString(net::userName(static_cast(i)), 4, y); c.setTextDatum(top_right); c.setTextColor(theme::kText); c.drawString(net::formatTraffic(traffic_[i].in).c_str(), 124, y); c.drawString(net::formatTraffic(traffic_[i].out).c_str(), 168, y); - c.setTextColor(active ? theme::kMessage : theme::kMuted); + c.setTextColor(active ? theme::kGood : theme::kMuted); c.drawString(active ? net::formatTraffic(rateIn_[i] + rateOut_[i]).c_str() : "-", area.w - 4, y); y += theme::kLineHeight; } diff --git a/src/apps/wifi_tools_app.cpp b/src/apps/wifi_tools_app.cpp index d9897f6..d86cdff 100644 --- a/src/apps/wifi_tools_app.cpp +++ b/src/apps/wifi_tools_app.cpp @@ -232,9 +232,9 @@ void WifiToolsApp::drawChannels(Canvas& c) { for (int ch = 1; ch <= 13; ch++) { int x = x0 + (ch - 1) * slot; int h = static_cast(load[ch] / maxLoad * barMaxH); - uint16_t color = ch == best ? theme::kMessage : (ch == 1 || ch == 6 || ch == 11) ? theme::kAccent : theme::kMuted; + uint16_t color = ch == best ? theme::kGood : (ch == 1 || ch == 6 || ch == 11) ? theme::kAccent : theme::kMuted; if (h > 0) c.fillRect(x, baseY - h, slot - 4, h, color); - c.setTextColor(ch == best ? theme::kMessage : theme::kText); + c.setTextColor(ch == best ? theme::kGood : theme::kText); c.setTextDatum(top_center); c.drawString(std::to_string(ch).c_str(), x + (slot - 4) / 2, baseY + 3); c.setTextDatum(top_left); @@ -265,7 +265,7 @@ void WifiToolsApp::drawTracker(Canvas& c) { int strength = lost ? 0 : std::min(100, std::max(0, (tracker_.latest() + 95) * 100 / 60)); int barW = area.w - 8; c.drawRect(4, area.y + 60, barW, 8, theme::kMuted); - c.fillRect(5, area.y + 61, (barW - 2) * strength / 100, 6, theme::kMessage); + c.fillRect(5, area.y + 61, (barW - 2) * strength / 100, 6, theme::kGood); const int gy = area.y + 74, gh = area.h - 76; const auto& h = tracker_.history(); diff --git a/src/ui/theme.h b/src/ui/theme.h index 52a28da..4f0ce7d 100644 --- a/src/ui/theme.h +++ b/src/ui/theme.h @@ -4,15 +4,20 @@ namespace roro::theme { -// RGB565 +// The website's palette (site/static/css/site.css, its dark side), as RGB565. Every one is a colour +// the 8-bit frame buffer (RGB332) holds exactly, so what is asked for is what is shown. constexpr uint16_t kBackground = 0x0000; constexpr uint16_t kText = 0xFFFF; -constexpr uint16_t kMuted = 0x8410; // grey -constexpr uint16_t kAccent = 0x2CBF; // teal-blue -constexpr uint16_t kSelection = 0x1A6F; // darker accent behind selected rows -constexpr uint16_t kBar = 0x2104; // Status Bar background -constexpr uint16_t kWarning = 0xFC60; // orange -constexpr uint16_t kMessage = 0x3666; // green +constexpr uint16_t kAccent = 0x06DF; // cyan, #00dbff: what is chosen, what is live +constexpr uint16_t kOnAccent = 0x000A; // dark blue, #000055: text and pictures on a cyan fill +constexpr uint16_t kSelection = kAccent; // behind a selected row, whose text is kOnAccent +constexpr uint16_t kMuted = 0x9495; // grey, #9292aa: between the website's two +constexpr uint16_t kFaint = 0x4A4A; // slate, #494955: grid lines, what stays behind +constexpr uint16_t kBar = kFaint; // Status Bar background +constexpr uint16_t kBarMuted = 0xB5B5; // light grey, #b6b6aa: what is idle, on the bar +constexpr uint16_t kWarning = 0xFC80; // orange, #ff9200 +constexpr uint16_t kMessage = 0xFC9F; // pink, #ff92ff: someone wrote +constexpr uint16_t kGood = 0x4ECA; // green, #49db55: a Fix, the best of a set (not on the website) constexpr int kWidth = 240; constexpr int kHeight = 135; diff --git a/src/ui/widgets.cpp b/src/ui/widgets.cpp index e8ab98c..d268e4a 100644 --- a/src/ui/widgets.cpp +++ b/src/ui/widgets.cpp @@ -32,43 +32,43 @@ void statusBar(Canvas& c, const StatusInfo& info) { if (info.unread > 0) right("[" + std::to_string(info.unread) + "]", kMessage); 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.sdPresent) right("SD", info.sdLevel >= 80 ? kWarning : kBarMuted); switch (info.wifi) { case StatusInfo::Wifi::Connected: { // Three signal bars, then the label. for (int i = 0; i < 3; i++) { int h = 2 + 2 * i; - c.fillRect(x - 10 + 4 * i, 9 - h, 3, h, i < info.wifiBars ? kText : kMuted); + c.fillRect(x - 10 + 4 * i, 9 - h, 3, h, i < info.wifiBars ? kText : kBackground); } x -= 14; right("W", kText); break; } - case StatusInfo::Wifi::Searching: right("W?", kMuted); break; + case StatusInfo::Wifi::Searching: right("W?", kBarMuted); break; case StatusInfo::Wifi::Monitoring: right("MON", kAccent); break; case StatusInfo::Wifi::None: break; } switch (info.vpn) { // Q252: there while the tunnel is wanted, bright once the peer has answered - case StatusInfo::Vpn::Trying: right("VPN", kMuted); break; + case StatusInfo::Vpn::Trying: right("VPN", kBarMuted); break; case StatusInfo::Vpn::Up: right("VPN", kAccent); break; case StatusInfo::Vpn::Off: break; } if (info.ssh) right("SSH", kAccent); // Q260: a session goes on, whatever App is in front switch (info.debug) { // Q190: there while the console listens, bright with someone connected - case StatusInfo::Debug::On: right("DBG", kMuted); break; + case StatusInfo::Debug::On: right("DBG", kBarMuted); break; case StatusInfo::Debug::Client: right("DBG", kAccent); break; case StatusInfo::Debug::Off: break; } if (info.tracking) right("REC", kWarning); if (info.capturing) right("CAP", kWarning); switch (info.radio) { // Q101: muted while listening, bright for a moment on each packet - case StatusInfo::Radio::Listening: right("L", kMuted); break; + case StatusInfo::Radio::Listening: right("L", kBarMuted); break; case StatusInfo::Radio::Packet: right("L", kAccent); break; - case StatusInfo::Radio::Sweep: right("SW", kMuted); break; // the Sniffer is paused + case StatusInfo::Radio::Sweep: right("SW", kBarMuted); break; // the Sniffer is paused case StatusInfo::Radio::None: break; } switch (info.gnss) { // Q61: muted while searching, normal with a Fix, the count with a 3D Fix - case StatusInfo::Gnss::Searching: right("G", kMuted); break; + case StatusInfo::Gnss::Searching: right("G", kBarMuted); break; case StatusInfo::Gnss::TwoD: right("G", kText); break; case StatusInfo::Gnss::ThreeD: right("G" + std::to_string(info.gnssSatellites), kText); break; case StatusInfo::Gnss::None: break; @@ -88,10 +88,10 @@ void list(Canvas& c, const ListModel& model, Rect area, const std::function kWidth - 8) w = kWidth - 8; int x = (kWidth - w) / 2, y = kHeight - 22; c.fillRect(x, y, w, 18, color); - c.setTextColor(kBackground); + c.setTextColor(kOnAccent); c.setTextDatum(top_left); c.setClipRect(x + 4, y, w - 8, 18); c.drawString(t.text.c_str(), x + 8, y + 3);