Launcher: a grid of icons, with badges; the list stays as a Setting
The Launcher shows the Apps as a 4 x 3 grid of 32 x 32 icons, the selected one's name under it (issue #9). The icons are the website's own, doubled, and two drawn the same way for SSH and the Shell; they are PNGs in assets/icons, turned into arrays by scripts/make_icons.py, which CI checks. The selected tile is the website's: dark blue on cyan, with notched corners. - GridModel (lib/ui, host-tested) holds the selection and the scrolling. - App::badge(): a dot on the tile for unread IRC messages, a Track recording, a Capture running, an SSH session open. - Settings > Launcher: Grid or List. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0162FokPdvY2KsS4NBfwyWPk
@@ -83,6 +83,9 @@ jobs:
|
||||
git checkout -q --detach "${{ github.sha }}"
|
||||
git describe --tags --always
|
||||
|
||||
- name: The icons are what their pictures give
|
||||
run: python3 scripts/make_icons.py --check
|
||||
|
||||
- name: Host tests, and their coverage of lib/
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
run: |
|
||||
|
||||
@@ -43,6 +43,8 @@ scripts/ci.sh
|
||||
|
||||
This runs the host-side unit tests (`test/`, `native` environment), then builds the firmware. The output is `.pio/build/cardputer-adv/firmware.factory.bin`.
|
||||
|
||||
`scripts/make_icons.py` turns the Launcher's icons (`assets/icons/<App id>.png`, 32 × 32, one colour) into the arrays in `src/ui/icons.cpp`; run it after changing a picture. CI fails when the two don't match.
|
||||
|
||||
`scripts/coverage.sh` runs the same tests with coverage counters and writes a line-by-line report to `.pio/coverage/index.html`. The badge above is its figure for `main`: the share of the lines of `lib/` that the host tests run. `lib/` is the logic that compiles on a PC; `lib/SD` (the card's driver) and `src/` (the Apps, the Services, everything that needs the device) have no host tests and aren't in that figure.
|
||||
|
||||
The framework is rebuilt with the TLS settings in `platformio.ini` (`custom_sdkconfig`, ADR 0006), so the first build after a fresh checkout takes about 6 minutes; later builds take about 15 seconds when little has changed. The platform knows the framework is already rebuilt by `sdkconfig.defaults` in the project folder, which it writes and git ignores: delete it and the next build rebuilds the framework. CI keeps that file, a build cache and ccache in its volume (docs/milestones/R1.md).
|
||||
|
||||
|
After Width: | Height: | Size: 108 B |
|
After Width: | Height: | Size: 121 B |
|
After Width: | Height: | Size: 85 B |
|
After Width: | Height: | Size: 99 B |
|
After Width: | Height: | Size: 102 B |
|
After Width: | Height: | Size: 117 B |
|
After Width: | Height: | Size: 97 B |
|
After Width: | Height: | Size: 99 B |
|
After Width: | Height: | Size: 104 B |
|
After Width: | Height: | Size: 96 B |
|
After Width: | Height: | Size: 107 B |
@@ -1,6 +1,6 @@
|
||||
# U1 — Look and feel
|
||||
|
||||
**Status:** in progress. Shipped: the help key (issue #69) and the key tables the website shares with it (#72), both in **v0.13.0**; the screenshot key (#83, **v0.17.0**). Not started: screen recording (#17), a Launcher of tiles (#9), themes (#10).
|
||||
**Status:** in progress. Shipped: the help key (issue #69) and the key tables the website shares with it (#72), both in **v0.13.0**; the screenshot key (#83, **v0.17.0**). The Launcher as a grid of icons (#9) is done, not released yet. Not started: screen recording (#17), themes (#10).
|
||||
|
||||
**Goal:** the interface is consistent and uncrowded: the same thing is done the same way on every screen, and the 135 pixels of height go to content.
|
||||
|
||||
@@ -60,6 +60,24 @@ A screenshot could only be taken by typing `screenshot` in the Shell, where "now
|
||||
|
||||
**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 Launcher as a grid (issue #9)
|
||||
|
||||
The Launcher was a list of names. It is now a grid of icons.
|
||||
|
||||
| Question in the issue | Decided |
|
||||
|---|---|
|
||||
| Layout | **4 × 3** tiles of 60 × 36, so the 11 Apps are on one page. Only the selected App's name is written, on a line under the grid: a name under every tile doesn't fit 60 pixels ("LoRa Scanner"). More than 12 Apps: the rows scroll |
|
||||
| Icons | **The website's own**: its nine 16 × 16 pictures (`site/static/img/icons-dark.svg`), doubled to 32 × 32, and two drawn the same way for SSH and the Shell. 1 bit a pixel (128 bytes each, in flash). The pictures are PNGs in `assets/icons/`, named after the App's id; `scripts/make_icons.py` writes `src/ui/icons.cpp` from them and CI checks the two match |
|
||||
| Colours | **The website's**: cyan (#00dbff) on black, and the selected tile dark blue (#000055) on a cyan fill with notched corners. Both are RGB332 colours. The name under the grid is cyan too |
|
||||
| Live state | **A dot** on the tile, from `App::badge()`: IRC (unread messages), GNSS (a Track recording), the LoRa Scanner (a Capture running), SSH (a session open). The Launcher asks every App on each pass and redraws when an answer changes |
|
||||
| Order | As registered, as before |
|
||||
| Shortcuts | None for now |
|
||||
| The list | Kept: **Settings > Launcher**, Grid or List. Both keep the same selection |
|
||||
|
||||
- `GridModel` (`lib/ui`) holds the selection and the scrolling, host-tested: left and right go through every item and wrap; up and down stay in the column, and land on the last item where the last row is short.
|
||||
- An App registered without an icon shows its first letter in a frame.
|
||||
- No animation: not asked for, and not measured.
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -19,7 +19,7 @@ const RowDef kRows[] = {
|
||||
{Row::LongName, Kind::Text, "Long name"}, {Row::ShortName, Kind::Text, "Short name"},
|
||||
{Row::Region, Kind::Choice, "Region"}, {Row::Timezone, Kind::Choice, "Timezone"},
|
||||
{Row::Brightness, Kind::Slider, "Brightness"}, {Row::DimTimeout, Kind::Choice, "Dim after"},
|
||||
{Row::OffTimeout, Kind::Choice, "Screen off after"}, {Row::Sound, Kind::Toggle, "Sound & LED"},
|
||||
{Row::OffTimeout, Kind::Choice, "Screen off after"}, {Row::Launcher, Kind::Toggle, "Launcher"}, {Row::Sound, Kind::Toggle, "Sound & LED"},
|
||||
{Row::Gnss, Kind::Toggle, "GNSS"}, {Row::GnssQuiet, Kind::Toggle, "Pause GNSS for LoRa"},
|
||||
{Row::Coordinates, Kind::Toggle, "Coordinates"},
|
||||
{Row::ProbeMacs, Kind::Toggle, "Probe MACs"}, {Row::Wifi, Kind::Page, "Wi-Fi"},
|
||||
@@ -80,6 +80,7 @@ std::string SettingsMenu::value(int i) const {
|
||||
case Row::Brightness: return std::to_string(settings_.getInt(Setting::Brightness)) + "%";
|
||||
case Row::DimTimeout: return formatSeconds(settings_.getInt(Setting::DimTimeoutS));
|
||||
case Row::OffTimeout: return formatSeconds(settings_.getInt(Setting::OffTimeoutS));
|
||||
case Row::Launcher: return settings_.getBool(Setting::LauncherList) ? "List" : "Grid";
|
||||
case Row::Sound: return settings_.getBool(Setting::Sound) ? "On" : "Off";
|
||||
case Row::Gnss: return settings_.getBool(Setting::GnssEnabled) ? "On" : "Off";
|
||||
case Row::GnssQuiet: return settings_.getBool(Setting::GnssQuietForLora) ? "On" : "Off";
|
||||
@@ -129,6 +130,7 @@ std::string SettingsMenu::choose(int i, int c) {
|
||||
}
|
||||
|
||||
void SettingsMenu::toggle(int i) {
|
||||
if (row(i) == Row::Launcher) settings_.setBool(Setting::LauncherList, !settings_.getBool(Setting::LauncherList));
|
||||
if (row(i) == Row::Sound) settings_.setBool(Setting::Sound, !settings_.getBool(Setting::Sound));
|
||||
if (row(i) == Row::Gnss) settings_.setBool(Setting::GnssEnabled, !settings_.getBool(Setting::GnssEnabled));
|
||||
if (row(i) == Row::GnssQuiet) settings_.setBool(Setting::GnssQuietForLora, !settings_.getBool(Setting::GnssQuietForLora));
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace roro {
|
||||
// values, choice lists and validation messages. Rendering and navigation live in the App.
|
||||
class SettingsMenu {
|
||||
public:
|
||||
enum class Row { LongName, ShortName, Region, Timezone, Brightness, DimTimeout, OffTimeout, Sound, Gnss, GnssQuiet, Coordinates, ProbeMacs, Wifi, CheckUpdates, Firmware, Vpn, DebugConsole, About };
|
||||
enum class Row { LongName, ShortName, Region, Timezone, Brightness, DimTimeout, OffTimeout, Launcher, Sound, Gnss, GnssQuiet, Coordinates, ProbeMacs, Wifi, CheckUpdates, Firmware, Vpn, DebugConsole, About };
|
||||
enum class Kind { Text, Choice, Toggle, Slider, Page };
|
||||
|
||||
explicit SettingsMenu(Settings& settings) : settings_(settings) {}
|
||||
|
||||
@@ -50,6 +50,10 @@ class App {
|
||||
virtual bool retainsContent() const { return false; }
|
||||
virtual void contentLost() {}
|
||||
|
||||
// True while something goes on in the App that is worth a look, whether it is in front or
|
||||
// not (unread messages, a recording running): the Launcher marks its tile (issue #9).
|
||||
virtual bool badge() const { return false; }
|
||||
|
||||
void requestRedraw() { redraw_ = true; }
|
||||
|
||||
bool consumeRedraw() {
|
||||
|
||||
@@ -48,7 +48,7 @@ inline constexpr KeyHelp kText[] = {
|
||||
|
||||
// launcher: The Launcher
|
||||
inline constexpr KeyHelp kLauncher[] = {
|
||||
{"; .", "up, down"},
|
||||
{"; . , /", "move"},
|
||||
{"Enter", "open the App"},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -12,6 +14,7 @@ struct AppInfo {
|
||||
const char* title;
|
||||
bool hidden; // not listed in the Launcher, but can still be opened
|
||||
App* app;
|
||||
const uint8_t* icon = nullptr; // the Launcher's picture of it (ui/icons.h); none: its first letter
|
||||
};
|
||||
|
||||
// Owns which App is in the foreground and routes keys. Home always returns to the Launcher;
|
||||
|
||||
@@ -53,6 +53,7 @@ const Definition kDefinitions[] = {
|
||||
{"ssh_key", Kind::String, 0, "", 0, 800},
|
||||
{"ssh_public", Kind::String, 0, "", 0, 200},
|
||||
{"ssh_font", Kind::Int, 1, nullptr, 0, 4},
|
||||
{"launcher_list", Kind::Bool, 0, nullptr, 0, 1}, // off: the grid
|
||||
};
|
||||
static_assert(sizeof(kDefinitions) / sizeof(kDefinitions[0]) == static_cast<size_t>(Setting::Count),
|
||||
"every Setting needs a definition");
|
||||
|
||||
@@ -41,6 +41,7 @@ enum class Setting : uint8_t {
|
||||
SshKey, // string: this device's own SSH private key, as OpenSSH writes one: never shown
|
||||
SshPublic, // string: its public half, the line to put in a server's authorized_keys
|
||||
SshFont, // int: the terminal's font, 0 (smallest) to 4
|
||||
LauncherList, // bool: the Launcher is a list of names instead of a grid of icons (issue #9)
|
||||
Count
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
#include "grid_model.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
void GridModel::setCount(int count) {
|
||||
count_ = count < 0 ? 0 : count;
|
||||
if (count_ == 0) {
|
||||
selected_ = -1;
|
||||
first_ = 0;
|
||||
return;
|
||||
}
|
||||
select(selected_ < 0 ? 0 : selected_);
|
||||
}
|
||||
|
||||
void GridModel::select(int index) {
|
||||
if (count_ == 0) return;
|
||||
selected_ = index < 0 ? 0 : index >= count_ ? count_ - 1 : index;
|
||||
int row = selected_ / cols_;
|
||||
if (row < first_) first_ = row;
|
||||
if (row >= first_ + rows_) first_ = row - rows_ + 1;
|
||||
int maxFirst = rows() > rows_ ? rows() - rows_ : 0;
|
||||
if (first_ > maxFirst) first_ = maxFirst;
|
||||
}
|
||||
|
||||
void GridModel::left() {
|
||||
if (count_) select(selected_ == 0 ? count_ - 1 : selected_ - 1);
|
||||
}
|
||||
|
||||
void GridModel::right() {
|
||||
if (count_) select(selected_ == count_ - 1 ? 0 : selected_ + 1);
|
||||
}
|
||||
|
||||
void GridModel::up() {
|
||||
if (!count_) return;
|
||||
int col = selected_ % cols_, row = selected_ / cols_;
|
||||
row = row == 0 ? rows() - 1 : row - 1;
|
||||
select(row * cols_ + col); // past the end of a short last row: the last item
|
||||
}
|
||||
|
||||
void GridModel::down() {
|
||||
if (!count_) return;
|
||||
int col = selected_ % cols_, row = selected_ / cols_;
|
||||
row = row == rows() - 1 ? 0 : row + 1;
|
||||
select(row * cols_ + col);
|
||||
}
|
||||
|
||||
} // namespace roro
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
namespace roro {
|
||||
|
||||
// Selection and scrolling for a grid of `cols` columns showing `visibleRows` rows at a time, filled
|
||||
// row by row. Left and Right go through every item and wrap around; Up and Down stay in the
|
||||
// column and wrap around, landing on the last item where the last row has no such column.
|
||||
class GridModel {
|
||||
public:
|
||||
GridModel(int cols, int visibleRows) : cols_(cols), rows_(visibleRows) {}
|
||||
|
||||
void setCount(int count);
|
||||
int count() const { return count_; }
|
||||
int selected() const { return selected_; } // -1 when empty
|
||||
int cols() const { return cols_; }
|
||||
int rows() const { return count_ ? (count_ - 1) / cols_ + 1 : 0; }
|
||||
int firstVisibleRow() const { return first_; }
|
||||
int visibleRows() const { return rows_; }
|
||||
|
||||
void select(int index);
|
||||
void left();
|
||||
void right();
|
||||
void up();
|
||||
void down();
|
||||
|
||||
private:
|
||||
int cols_, rows_;
|
||||
int count_ = 0;
|
||||
int selected_ = -1;
|
||||
int first_ = 0;
|
||||
};
|
||||
|
||||
} // namespace roro
|
||||
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Turns the Launcher's icons (assets/icons/<App id>.png) into C arrays: src/ui/icons.h and icons.cpp.
|
||||
|
||||
An icon is 32 x 32 and one colour: a pixel is drawn where the picture is light and opaque, and left
|
||||
alone where it is dark or transparent. Any image editor's PNG will do (grey, RGB, palette, with or
|
||||
without alpha, 1 or 8 bits a channel, not interlaced). Run this after changing a picture:
|
||||
|
||||
python3 scripts/make_icons.py
|
||||
|
||||
CI runs it with --check, which fails when the arrays are not what the pictures give.
|
||||
"""
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import zlib
|
||||
|
||||
ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
|
||||
SOURCE = os.path.join(ROOT, "assets", "icons")
|
||||
SIZE = 32
|
||||
|
||||
|
||||
def read_png(path):
|
||||
"""The picture as rows of booleans: True where a pixel is drawn."""
|
||||
data = open(path, "rb").read()
|
||||
if data[:8] != b"\x89PNG\r\n\x1a\n":
|
||||
raise ValueError("not a PNG")
|
||||
at, idat, palette, alpha = 8, b"", None, None
|
||||
while at < len(data):
|
||||
(length,), kind = struct.unpack(">I", data[at:at + 4]), data[at + 4:at + 8]
|
||||
body = data[at + 8:at + 8 + length]
|
||||
at += 12 + length
|
||||
if kind == b"IHDR":
|
||||
width, height, depth, colour, _, _, interlace = struct.unpack(">IIBBBBB", body)
|
||||
elif kind == b"PLTE":
|
||||
palette = [body[i:i + 3] for i in range(0, len(body), 3)]
|
||||
elif kind == b"tRNS":
|
||||
alpha = body
|
||||
elif kind == b"IDAT":
|
||||
idat += body
|
||||
if interlace or depth not in (1, 8) or (depth == 1 and colour not in (0, 3)):
|
||||
raise ValueError("save it as a plain 8-bit or 1-bit PNG, not interlaced")
|
||||
channels = {0: 1, 2: 3, 3: 1, 4: 2, 6: 4}[colour]
|
||||
step = channels if depth == 8 else 1 # bytes a pixel, for the filters
|
||||
stride = (width * channels * depth + 7) // 8
|
||||
raw, rows, before = zlib.decompress(idat), [], bytes(stride)
|
||||
for y in range(height):
|
||||
kind, line = raw[y * (stride + 1)], bytearray(raw[y * (stride + 1) + 1:(y + 1) * (stride + 1)])
|
||||
for i in range(stride):
|
||||
a = line[i - step] if i >= step else 0
|
||||
b, c = before[i], before[i - step] if i >= step else 0
|
||||
if kind == 1:
|
||||
line[i] = (line[i] + a) & 255
|
||||
elif kind == 2:
|
||||
line[i] = (line[i] + b) & 255
|
||||
elif kind == 3:
|
||||
line[i] = (line[i] + (a + b) // 2) & 255
|
||||
elif kind == 4:
|
||||
p = a + b - c
|
||||
pa, pb, pc = abs(p - a), abs(p - b), abs(p - c)
|
||||
line[i] = (line[i] + (a if pa <= pb and pa <= pc else b if pb <= pc else c)) & 255
|
||||
before = bytes(line)
|
||||
row = []
|
||||
for x in range(width):
|
||||
if depth == 1:
|
||||
v = (line[x // 8] >> (7 - x % 8)) & 1
|
||||
px = [v * 255] if colour == 0 else [v]
|
||||
else:
|
||||
px = line[x * channels:(x + 1) * channels]
|
||||
if colour == 3:
|
||||
opaque = alpha is None or px[0] >= len(alpha) or alpha[px[0]] > 127
|
||||
light = sum(palette[px[0]]) > 381
|
||||
elif colour in (0, 4):
|
||||
opaque, light = colour == 0 or px[1] > 127, px[0] > 127
|
||||
else:
|
||||
opaque, light = colour == 2 or px[3] > 127, sum(px[:3]) > 381
|
||||
row.append(opaque and light)
|
||||
rows.append(row)
|
||||
return rows
|
||||
|
||||
|
||||
def main():
|
||||
names = sorted(f[:-4] for f in os.listdir(SOURCE) if f.endswith(".png"))
|
||||
header = ["// Made by scripts/make_icons.py from assets/icons/*.png: change the pictures, not this file.", "#pragma once", "",
|
||||
"#include <cstdint>", "", "namespace roro::icons {", "",
|
||||
"// One bit a pixel, the leftmost first, a row after another: what Canvas::drawBitmap takes.",
|
||||
f"constexpr int kSize = {SIZE};", ""]
|
||||
body = ["// Made by scripts/make_icons.py from assets/icons/*.png: change the pictures, not this file.", '#include "icons.h"', "",
|
||||
"namespace roro::icons {", ""]
|
||||
for name in names:
|
||||
rows = read_png(os.path.join(SOURCE, name + ".png"))
|
||||
if len(rows) != SIZE or len(rows[0]) != SIZE:
|
||||
sys.exit(f"{name}.png is {len(rows[0])} x {len(rows)}: an icon is {SIZE} x {SIZE}")
|
||||
ident = name.replace("-", "_")
|
||||
header.append(f"extern const uint8_t {ident}[{SIZE * SIZE // 8}]; // {name}.png")
|
||||
body.append(f"const uint8_t {ident}[{SIZE * SIZE // 8}] = {{")
|
||||
for row in rows:
|
||||
packed = [sum(0x80 >> i for i in range(8) if row[b * 8 + i]) for b in range(SIZE // 8)]
|
||||
body.append(" " + ", ".join(f"0x{v:02x}" for v in packed) + ", // " + "".join("#" if v else "." for v in row))
|
||||
body += ["};", ""]
|
||||
header += ["", "} // namespace roro::icons", ""]
|
||||
body += ["} // namespace roro::icons", ""]
|
||||
out = {os.path.join(ROOT, "src", "ui", "icons.h"): "\n".join(header), os.path.join(ROOT, "src", "ui", "icons.cpp"): "\n".join(body)}
|
||||
if "--check" in sys.argv:
|
||||
stale = [os.path.relpath(p, ROOT) for p, text in out.items() if not os.path.exists(p) or open(p).read() != text]
|
||||
if stale:
|
||||
sys.exit("out of date: " + ", ".join(stale) + " (run scripts/make_icons.py)")
|
||||
print(f"{len(names)} icons, up to date")
|
||||
return
|
||||
for path, text in out.items():
|
||||
open(path, "w").write(text)
|
||||
print(f"{len(names)} icons written")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -20,6 +20,8 @@ scripts/ci.sh
|
||||
|
||||
This runs the host-side unit tests (`test/`, `native` environment), then builds the firmware. The output is `.pio/build/cardputer-adv/firmware.factory.bin`.
|
||||
|
||||
`scripts/make_icons.py` turns the Launcher's icons (`assets/icons/<App id>.png`, 32 × 32, one colour) into the arrays in `src/ui/icons.cpp`; run it after changing a picture. CI fails when the two don't match.
|
||||
|
||||
`scripts/coverage.sh` runs the same tests with coverage counters and writes a line-by-line report to `.pio/coverage/index.html`. The badge above is its figure for `main`: the share of the lines of `lib/` that the host tests run. `lib/` is the logic that compiles on a PC; `lib/SD` (the card's driver) and `src/` (the Apps, the Services, everything that needs the device) have no host tests and aren't in that figure.
|
||||
|
||||
The framework is rebuilt with the TLS settings in `platformio.ini` (`custom_sdkconfig`, ADR 0006), so the first build after a fresh checkout takes about 6 minutes; later builds take about 15 seconds when little has changed. The platform knows the framework is already rebuilt by `sdkconfig.defaults` in the project folder, which it writes and git ignores: delete it and the next build rebuilds the framework. CI keeps that file, a build cache and ccache in its volume (docs/milestones/R1.md).
|
||||
|
||||
@@ -8,7 +8,7 @@ docs = true
|
||||
source = "docs/milestones/U1.md"
|
||||
tag = "U1"
|
||||
+++
|
||||
**Status:** in progress. Shipped: the help key (issue #69) and the key tables the website shares with it (#72), both in **v0.13.0**; the screenshot key (#83, **v0.17.0**). Not started: screen recording (#17), a Launcher of tiles (#9), themes (#10).
|
||||
**Status:** in progress. Shipped: the help key (issue #69) and the key tables the website shares with it (#72), both in **v0.13.0**; the screenshot key (#83, **v0.17.0**). The Launcher as a grid of icons (#9) is done, not released yet. Not started: screen recording (#17), themes (#10).
|
||||
|
||||
**Goal:** the interface is consistent and uncrowded: the same thing is done the same way on every screen, and the 135 pixels of height go to content.
|
||||
|
||||
@@ -68,6 +68,24 @@ A screenshot could only be taken by typing `screenshot` in the Shell, where "now
|
||||
|
||||
**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 Launcher as a grid (issue #9)
|
||||
|
||||
The Launcher was a list of names. It is now a grid of icons.
|
||||
|
||||
| Question in the issue | Decided |
|
||||
|---|---|
|
||||
| Layout | **4 × 3** tiles of 60 × 36, so the 11 Apps are on one page. Only the selected App's name is written, on a line under the grid: a name under every tile doesn't fit 60 pixels ("LoRa Scanner"). More than 12 Apps: the rows scroll |
|
||||
| Icons | **The website's own**: its nine 16 × 16 pictures (`site/static/img/icons-dark.svg`), doubled to 32 × 32, and two drawn the same way for SSH and the Shell. 1 bit a pixel (128 bytes each, in flash). The pictures are PNGs in `assets/icons/`, named after the App's id; `scripts/make_icons.py` writes `src/ui/icons.cpp` from them and CI checks the two match |
|
||||
| Colours | **The website's**: cyan (#00dbff) on black, and the selected tile dark blue (#000055) on a cyan fill with notched corners. Both are RGB332 colours. The name under the grid is cyan too |
|
||||
| Live state | **A dot** on the tile, from `App::badge()`: IRC (unread messages), GNSS (a Track recording), the LoRa Scanner (a Capture running), SSH (a session open). The Launcher asks every App on each pass and redraws when an answer changes |
|
||||
| Order | As registered, as before |
|
||||
| Shortcuts | None for now |
|
||||
| The list | Kept: **Settings > Launcher**, Grid or List. Both keep the same selection |
|
||||
|
||||
- `GridModel` (`lib/ui`) holds the selection and the scrolling, host-tested: left and right go through every item and wrap; up and down stay in the column, and land on the last item where the last row is short.
|
||||
- An App registered without an icon shows its first letter in a frame.
|
||||
- No animation: not asked for, and not measured.
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -36,7 +36,13 @@ While you type text (a note, an IRC line, a setting), `;` `.` `,` `/` type their
|
||||
|
||||
## The Launcher
|
||||
|
||||
The home screen lists the Apps. Move with the arrows, open one with <kbd>Enter</kbd>. Back inside an App returns here.
|
||||
The home screen shows the Apps as a grid of icons, four to a row, with the name of the selected one written under the grid. Move with the four arrows, open an App with <kbd>Enter</kbd>. Back inside an App returns here.
|
||||
|
||||
Left and right go through every App and wrap around; up and down stay in the column.
|
||||
|
||||
**An orange dot on an icon** says something is going on in that App: unread messages in IRC, a Track being recorded in GNSS, a Capture running in the LoRa Scanner, a session open in SSH.
|
||||
|
||||
If you prefer the list of names the Launcher used to be, **Settings → Launcher** switches between **Grid** and **List**. In the list, a `*` on the right stands for the dot.
|
||||
|
||||
## The Status Bar
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ Move with the arrows. On a toggle, a choice or a slider, left and right change t
|
||||
| **Timezone** | For the clock and the dates in file names |
|
||||
| **Brightness** | A slider |
|
||||
| **Dim after**, **Screen off after** | Screen timeouts. Dimming must come before the screen turns off |
|
||||
| **Launcher** | The home screen as a **Grid** of icons or a **List** of names (see [The basics](/guide/basics/#the-launcher)) |
|
||||
| **Sound & LED** | The beep and the flash on a [Toast](/guide/basics/) |
|
||||
| **GNSS** | Switches the receiver on and off (see [GNSS](/guide/gnss/)) |
|
||||
| **Pause GNSS for LoRa** | Puts the receiver on standby while the radio listens (see [LoRa Scanner](/guide/lora-scanner/)) |
|
||||
|
||||
@@ -36,7 +36,7 @@ rows = [
|
||||
id = "launcher"
|
||||
title = "The Launcher"
|
||||
rows = [
|
||||
["; .", "up, down"],
|
||||
["; . , /", "move"],
|
||||
["Enter", "open the App"],
|
||||
]
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ class GnssApp : public App {
|
||||
void update(uint32_t nowMs) override;
|
||||
void draw(Canvas& c) override;
|
||||
void help(std::vector<KeyHelp>& out) const override;
|
||||
bool badge() const override { return gnss_.tracking(); }
|
||||
|
||||
private:
|
||||
void drawPosition(Canvas& c);
|
||||
|
||||
@@ -26,6 +26,7 @@ class IrcApp : public App {
|
||||
void draw(Canvas& c) override;
|
||||
void help(std::vector<KeyHelp>& out) const override;
|
||||
const char* helpTitle() const override;
|
||||
bool badge() const override { return irc_.totalUnread() > 0; }
|
||||
|
||||
private:
|
||||
enum class Page { Chat, Settings };
|
||||
|
||||
@@ -1,27 +1,110 @@
|
||||
#include "launcher_app.h"
|
||||
|
||||
#include "ui/fonts.h"
|
||||
#include "ui/icons.h"
|
||||
#include "ui/widgets.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
void LauncherApp::onEnter() { list_.setCount(static_cast<int>(manager_->visibleApps().size())); }
|
||||
void LauncherApp::onEnter() {
|
||||
int count = static_cast<int>(manager_->visibleApps().size());
|
||||
list_.setCount(count);
|
||||
grid_.setCount(count);
|
||||
badges_ = badges();
|
||||
}
|
||||
|
||||
bool LauncherApp::onKey(const KeyEvent& e) {
|
||||
bool list = asList();
|
||||
switch (e.key) {
|
||||
case Key::Up: list_.up(); break;
|
||||
case Key::Down: list_.down(); break;
|
||||
case Key::Up: list ? list_.up() : grid_.up(); break;
|
||||
case Key::Down: list ? list_.down() : grid_.down(); break;
|
||||
case Key::Left:
|
||||
if (list) return false;
|
||||
grid_.left();
|
||||
break;
|
||||
case Key::Right:
|
||||
if (list) return false;
|
||||
grid_.right();
|
||||
break;
|
||||
case Key::Select:
|
||||
if (list_.selected() >= 0) manager_->open(manager_->visibleApps()[list_.selected()]->id);
|
||||
return true;
|
||||
default: return false;
|
||||
}
|
||||
// The view that didn't move follows.
|
||||
if (list) grid_.select(list_.selected());
|
||||
else list_.select(grid_.selected());
|
||||
requestRedraw();
|
||||
return true;
|
||||
}
|
||||
|
||||
void LauncherApp::draw(Canvas& c) {
|
||||
uint32_t LauncherApp::badges() const {
|
||||
uint32_t bits = 0;
|
||||
auto apps = manager_->visibleApps();
|
||||
widgets::list(c, list_, theme::kContent, [&](int i) { return std::string(apps[i]->title); });
|
||||
for (size_t i = 0; i < apps.size() && i < 32; ++i)
|
||||
if (apps[i]->app->badge()) bits |= 1u << i;
|
||||
return bits;
|
||||
}
|
||||
|
||||
void LauncherApp::update(uint32_t) {
|
||||
uint32_t now = badges();
|
||||
if (now == badges_) return;
|
||||
badges_ = now;
|
||||
requestRedraw();
|
||||
}
|
||||
|
||||
void LauncherApp::draw(Canvas& c) {
|
||||
if (!asList()) return drawGrid(c);
|
||||
auto apps = manager_->visibleApps();
|
||||
widgets::list(c, list_, theme::kContent, [&](int i) { return std::string(apps[i]->title); },
|
||||
[&](int i) { return std::string(badges_ & (1u << i) ? "*" : ""); });
|
||||
}
|
||||
|
||||
void LauncherApp::drawGrid(Canvas& c) {
|
||||
using namespace theme;
|
||||
auto apps = manager_->visibleApps();
|
||||
int selected = grid_.selected();
|
||||
for (int row = 0; row < kRows; ++row) {
|
||||
for (int col = 0; col < kCols; ++col) {
|
||||
int i = (grid_.firstVisibleRow() + row) * kCols + col;
|
||||
if (i >= static_cast<int>(apps.size())) break;
|
||||
int x = kContent.x + col * kTileW, y = kContent.y + row * kTileH;
|
||||
int ix = x + (kTileW - icons::kSize) / 2, iy = y + (kTileH - icons::kSize) / 2;
|
||||
// As on the website: cyan on black, and the chosen one dark on a cyan tile with notched corners.
|
||||
uint16_t ink = i == selected ? kOnAccent : kAccent;
|
||||
if (i == selected) {
|
||||
int tx = ix - 6, tw = icons::kSize + 12;
|
||||
c.fillRect(tx + 4, y, tw - 8, kTileH, kAccent);
|
||||
c.fillRect(tx + 2, y + 2, tw - 4, kTileH - 4, kAccent);
|
||||
c.fillRect(tx, y + 4, tw, kTileH - 8, kAccent);
|
||||
}
|
||||
if (apps[i]->icon) {
|
||||
c.drawBitmap(ix, iy, apps[i]->icon, icons::kSize, icons::kSize, ink);
|
||||
} else { // an App without a picture: its first letter, framed
|
||||
c.drawRect(ix + 2, iy + 2, icons::kSize - 4, icons::kSize - 4, ink);
|
||||
c.setFont(&fonts::large);
|
||||
c.setTextColor(ink);
|
||||
c.setTextDatum(middle_center);
|
||||
c.drawString(std::string(1, apps[i]->title[0]).c_str(), ix + icons::kSize / 2, iy + icons::kSize / 2);
|
||||
}
|
||||
if (badges_ & (1u << i)) { // a square dot on the icon's corner, with a margin to stand out from it
|
||||
c.fillRect(ix + icons::kSize - 6, iy - 1, 10, 10, i == selected ? kAccent : kBackground);
|
||||
c.fillRect(ix + icons::kSize - 4, iy + 1, 6, 6, kWarning);
|
||||
}
|
||||
}
|
||||
}
|
||||
// More rows than fit: where these are among them.
|
||||
if (grid_.rows() > kRows) {
|
||||
int h = kRows * kTileH, barH = h * kRows / grid_.rows();
|
||||
c.fillRect(kContent.x + kContent.w - 2, kContent.y + (h - barH) * grid_.firstVisibleRow() / (grid_.rows() - kRows), 2, barH, kMuted);
|
||||
}
|
||||
if (selected >= 0) {
|
||||
c.setFont(&fonts::bold);
|
||||
c.setTextColor(kAccent);
|
||||
c.setTextDatum(top_center);
|
||||
c.drawString(apps[selected]->title, kContent.x + kContent.w / 2, kContent.y + kRows * kTileH + 1);
|
||||
}
|
||||
c.setTextDatum(top_left);
|
||||
}
|
||||
|
||||
} // namespace roro
|
||||
|
||||
@@ -3,23 +3,40 @@
|
||||
#include "app.h"
|
||||
#include "app_keys.h"
|
||||
#include "app_manager.h"
|
||||
#include "grid_model.h"
|
||||
#include "list_model.h"
|
||||
#include "settings.h"
|
||||
#include "ui/theme.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
// Home screen: lists the visible Apps; Select opens one.
|
||||
// Home screen: the visible Apps as a grid of icons, with the selected one's name under it, or as
|
||||
// a list of names (Settings > Launcher). Select opens one. An App with something going on has a
|
||||
// dot on its tile (App::badge, issue #9).
|
||||
class LauncherApp : public App {
|
||||
public:
|
||||
void setManager(AppManager& manager) { manager_ = &manager; }
|
||||
void setSettings(const Settings& settings) { settings_ = &settings; }
|
||||
void onEnter() override;
|
||||
bool onKey(const KeyEvent& e) override;
|
||||
void update(uint32_t nowMs) override;
|
||||
void draw(Canvas& c) override;
|
||||
void help(std::vector<KeyHelp>& out) const override { keys::add(out, keys::kLauncher); }
|
||||
|
||||
private:
|
||||
static constexpr int kCols = 4, kRows = 3;
|
||||
static constexpr int kTileW = theme::kWidth / kCols, kTileH = 36;
|
||||
|
||||
bool asList() const { return settings_ && settings_->getBool(Setting::LauncherList); }
|
||||
uint32_t badges() const; // one bit a visible App
|
||||
void drawGrid(Canvas& c);
|
||||
|
||||
AppManager* manager_ = nullptr;
|
||||
const Settings* settings_ = nullptr;
|
||||
// One selection, kept in both: the view can change in the Settings.
|
||||
ListModel list_{theme::kContent.h / theme::kLineHeight};
|
||||
GridModel grid_{kCols, kRows};
|
||||
uint32_t badges_ = 0;
|
||||
};
|
||||
|
||||
} // namespace roro
|
||||
|
||||
@@ -28,6 +28,7 @@ class LoraScannerApp : public App {
|
||||
void draw(Canvas& c) override;
|
||||
void help(std::vector<KeyHelp>& out) const override;
|
||||
const char* helpTitle() const override;
|
||||
bool badge() const override { return capture_.capturing(); }
|
||||
|
||||
private:
|
||||
enum class View { Packets, Details, Presets, Sweep };
|
||||
|
||||
@@ -28,6 +28,7 @@ class SshApp : public App {
|
||||
bool textEntryActive() const override { return view_ == View::Entry || (view_ == View::Session && !dialog_); }
|
||||
void help(std::vector<KeyHelp>& out) const override;
|
||||
const char* helpTitle() const override;
|
||||
bool badge() const override { return ssh_.active(); }
|
||||
void update(uint32_t nowMs) override;
|
||||
void draw(Canvas& c) override;
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
#include "safe_mode.h"
|
||||
#include "settings.h"
|
||||
#include "storage_paths.h"
|
||||
#include "ui/icons.h"
|
||||
#include "ui/notifier.h"
|
||||
#include "ui/screen.h"
|
||||
#include "scp_args.h"
|
||||
@@ -230,19 +231,20 @@ void setup() {
|
||||
|
||||
apps = new AppManager(launcher);
|
||||
launcher.setManager(*apps);
|
||||
apps->registerApp({"irc", "IRC", false, new IrcApp(*irc, *clockService, bus)});
|
||||
apps->registerApp({"wifi-tools", "Wi-Fi Tools", false, new WifiToolsApp(*wifi, *storageService, *clockService)});
|
||||
apps->registerApp({"gnss", "GNSS", false, new GnssApp(*gnssService, settings)});
|
||||
apps->registerApp({"gemini", "Gemini", false, new GeminiApp(*geminiService)});
|
||||
apps->registerApp({"lora", "LoRa Scanner", false, new LoraScannerApp(*radioService, *loraCapture, settings, *clockService)});
|
||||
apps->registerApp({"storage", "Storage", false, new StorageApp(*fileOps, *storageService, *clockService, *update, *power, bus, *new WebShare(*storageService, *fileOps, *wifi))});
|
||||
apps->registerApp({"notes", "Notes", false, new NotesApp(*fileOps, *storageService, *clockService, *power)});
|
||||
launcher.setSettings(settings);
|
||||
apps->registerApp({"irc", "IRC", false, new IrcApp(*irc, *clockService, bus), icons::irc});
|
||||
apps->registerApp({"wifi-tools", "Wi-Fi Tools", false, new WifiToolsApp(*wifi, *storageService, *clockService), icons::wifi_tools});
|
||||
apps->registerApp({"gnss", "GNSS", false, new GnssApp(*gnssService, settings), icons::gnss});
|
||||
apps->registerApp({"gemini", "Gemini", false, new GeminiApp(*geminiService), icons::gemini});
|
||||
apps->registerApp({"lora", "LoRa Scanner", false, new LoraScannerApp(*radioService, *loraCapture, settings, *clockService), icons::lora});
|
||||
apps->registerApp({"storage", "Storage", false, new StorageApp(*fileOps, *storageService, *clockService, *update, *power, bus, *new WebShare(*storageService, *fileOps, *wifi)), icons::storage});
|
||||
apps->registerApp({"notes", "Notes", false, new NotesApp(*fileOps, *storageService, *clockService, *power), icons::notes});
|
||||
sshService = new SshService(settings);
|
||||
sshApp = new SshApp(*sshService, settings, *storageService);
|
||||
scpService = new ScpService(settings, *storageService, *fileOps);
|
||||
apps->registerApp({"ssh", "SSH", false, sshApp});
|
||||
apps->registerApp({"ssh", "SSH", false, sshApp, icons::ssh});
|
||||
shellApp = new ShellApp(shellRun, shellProbe, shellList, shellCount, helpText(), *apps);
|
||||
apps->registerApp({"shell", "Shell", false, shellApp});
|
||||
apps->registerApp({"shell", "Shell", false, shellApp, icons::shell});
|
||||
shellApp->asking = [] { return scpService->asking(); };
|
||||
shellApp->answer = [](const std::string& password) { scpService->answer(password); };
|
||||
shellApp->abort = [] { scpService->cancel(); };
|
||||
@@ -252,9 +254,9 @@ void setup() {
|
||||
// A long note being rewritten (issue #47): the editor waits for the card, so it draws from there.
|
||||
NoteEditor::onProgress = [](const std::string& name, int percent) { screen.renderUpdate("Saving", name, percent); };
|
||||
apps->registerApp({"system", "System", false,
|
||||
new SystemApp(*wifi, *battery, *storageService, *radioService, *gnssService, nvs)});
|
||||
new SystemApp(*wifi, *battery, *storageService, *radioService, *gnssService, nvs), icons::system});
|
||||
apps->registerApp({"settings", "Settings", false,
|
||||
new SettingsApp({settings, bus, *apps, *battery, *storageService, *clockService, *wifi, *savedNetworks, *update, *vpnService})});
|
||||
new SettingsApp({settings, bus, *apps, *battery, *storageService, *clockService, *wifi, *savedNetworks, *update, *vpnService}), icons::settings});
|
||||
apps->registerApp({"demo", "Widget demo", true, new DemoApp(bus)});
|
||||
apps->registerApp({"setup", "Setup", true, new SetupApp(settings, *apps)});
|
||||
|
||||
|
||||
@@ -0,0 +1,391 @@
|
||||
// Made by scripts/make_icons.py from assets/icons/*.png: change the pictures, not this file.
|
||||
#include "icons.h"
|
||||
|
||||
namespace roro::icons {
|
||||
|
||||
const uint8_t gemini[128] = {
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x3f, 0xff, 0xff, 0xfc, // ..############################..
|
||||
0x3f, 0xff, 0xff, 0xfc, // ..############################..
|
||||
0x33, 0x30, 0x00, 0x0c, // ..##..##..##................##..
|
||||
0x33, 0x30, 0x00, 0x0c, // ..##..##..##................##..
|
||||
0x30, 0x00, 0x00, 0x0c, // ..##........................##..
|
||||
0x30, 0x00, 0x00, 0x0c, // ..##........................##..
|
||||
0x3f, 0xff, 0xff, 0xfc, // ..############################..
|
||||
0x3f, 0xff, 0xff, 0xfc, // ..############################..
|
||||
0x30, 0x00, 0x00, 0x0c, // ..##........................##..
|
||||
0x30, 0x00, 0x00, 0x0c, // ..##........................##..
|
||||
0x30, 0xc0, 0xc0, 0x0c, // ..##....##......##..........##..
|
||||
0x30, 0xc0, 0xc0, 0x0c, // ..##....##......##..........##..
|
||||
0x30, 0x30, 0x30, 0x0c, // ..##......##......##........##..
|
||||
0x30, 0x30, 0x30, 0x0c, // ..##......##......##........##..
|
||||
0x30, 0x0c, 0x0c, 0x0c, // ..##........##......##......##..
|
||||
0x30, 0x0c, 0x0c, 0x0c, // ..##........##......##......##..
|
||||
0x30, 0x30, 0x30, 0x0c, // ..##......##......##........##..
|
||||
0x30, 0x30, 0x30, 0x0c, // ..##......##......##........##..
|
||||
0x30, 0xc0, 0xc0, 0x0c, // ..##....##......##..........##..
|
||||
0x30, 0xc0, 0xc0, 0x0c, // ..##....##......##..........##..
|
||||
0x30, 0x00, 0x00, 0x0c, // ..##........................##..
|
||||
0x30, 0x00, 0x00, 0x0c, // ..##........................##..
|
||||
0x3f, 0xff, 0xff, 0xfc, // ..############################..
|
||||
0x3f, 0xff, 0xff, 0xfc, // ..############################..
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
};
|
||||
|
||||
const uint8_t gnss[128] = {
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x3f, 0xc0, 0x00, // ..........########..............
|
||||
0x00, 0x3f, 0xc0, 0x00, // ..........########..............
|
||||
0x03, 0xc0, 0x0f, 0x00, // ......####..........####........
|
||||
0x03, 0xc0, 0x0f, 0x00, // ......####..........####........
|
||||
0x0c, 0x00, 0x0f, 0x30, // ....##..............####..##....
|
||||
0x0c, 0x00, 0x0f, 0x30, // ....##..............####..##....
|
||||
0x30, 0x00, 0x00, 0x0c, // ..##........................##..
|
||||
0x30, 0x00, 0x00, 0x0c, // ..##........................##..
|
||||
0x30, 0x0f, 0xf0, 0x0c, // ..##........########........##..
|
||||
0x30, 0x0f, 0xf0, 0x0c, // ..##........########........##..
|
||||
0x00, 0x30, 0x0c, 0x0c, // ..........##........##......##..
|
||||
0x00, 0x30, 0x0c, 0x0c, // ..........##........##......##..
|
||||
0x3c, 0x30, 0x0c, 0x0c, // ..####....##........##......##..
|
||||
0x3c, 0x30, 0x0c, 0x0c, // ..####....##........##......##..
|
||||
0x3c, 0x30, 0x0c, 0x0c, // ..####....##........##......##..
|
||||
0x3c, 0x30, 0x0c, 0x0c, // ..####....##........##......##..
|
||||
0x00, 0x30, 0x0c, 0x0c, // ..........##........##......##..
|
||||
0x00, 0x30, 0x0c, 0x0c, // ..........##........##......##..
|
||||
0x30, 0x0f, 0xf0, 0x0c, // ..##........########........##..
|
||||
0x30, 0x0f, 0xf0, 0x0c, // ..##........########........##..
|
||||
0x30, 0x00, 0x00, 0x0c, // ..##........................##..
|
||||
0x30, 0x00, 0x00, 0x0c, // ..##........................##..
|
||||
0x0c, 0x00, 0x00, 0x30, // ....##....................##....
|
||||
0x0c, 0x00, 0x00, 0x30, // ....##....................##....
|
||||
0x03, 0xc0, 0x03, 0xc0, // ......####............####......
|
||||
0x03, 0xc0, 0x03, 0xc0, // ......####............####......
|
||||
0x00, 0x3f, 0xfc, 0x00, // ..........############..........
|
||||
0x00, 0x3f, 0xfc, 0x00, // ..........############..........
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
};
|
||||
|
||||
const uint8_t irc[128] = {
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x30, 0x0c, 0x00, // ..........##........##..........
|
||||
0x00, 0x30, 0x0c, 0x00, // ..........##........##..........
|
||||
0x00, 0x30, 0x0c, 0x00, // ..........##........##..........
|
||||
0x00, 0x30, 0x0c, 0x00, // ..........##........##..........
|
||||
0x00, 0x30, 0x0c, 0x00, // ..........##........##..........
|
||||
0x00, 0x30, 0x0c, 0x00, // ..........##........##..........
|
||||
0x0f, 0xff, 0xff, 0xf0, // ....########################....
|
||||
0x0f, 0xff, 0xff, 0xf0, // ....########################....
|
||||
0x00, 0x30, 0x0c, 0x00, // ..........##........##..........
|
||||
0x00, 0x30, 0x0c, 0x00, // ..........##........##..........
|
||||
0x00, 0x30, 0x0c, 0x00, // ..........##........##..........
|
||||
0x00, 0x30, 0x0c, 0x00, // ..........##........##..........
|
||||
0x00, 0x30, 0x0c, 0x00, // ..........##........##..........
|
||||
0x00, 0x30, 0x0c, 0x00, // ..........##........##..........
|
||||
0x00, 0x30, 0x0c, 0x00, // ..........##........##..........
|
||||
0x00, 0x30, 0x0c, 0x00, // ..........##........##..........
|
||||
0x0f, 0xff, 0xff, 0xf0, // ....########################....
|
||||
0x0f, 0xff, 0xff, 0xf0, // ....########################....
|
||||
0x00, 0x30, 0x0c, 0x00, // ..........##........##..........
|
||||
0x00, 0x30, 0x0c, 0x00, // ..........##........##..........
|
||||
0x00, 0x30, 0x0c, 0x00, // ..........##........##..........
|
||||
0x00, 0x30, 0x0c, 0x00, // ..........##........##..........
|
||||
0x00, 0x30, 0x0c, 0x00, // ..........##........##..........
|
||||
0x00, 0x30, 0x0c, 0x00, // ..........##........##..........
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
};
|
||||
|
||||
const uint8_t lora[128] = {
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x0c, 0x00, 0x00, // ............##..................
|
||||
0x00, 0x0c, 0x00, 0x00, // ............##..................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x0c, 0x00, 0x00, // ............##..................
|
||||
0x00, 0x0c, 0x00, 0x00, // ............##..................
|
||||
0x00, 0x0c, 0x0c, 0x00, // ............##......##..........
|
||||
0x00, 0x0c, 0x0c, 0x00, // ............##......##..........
|
||||
0x00, 0x0c, 0x00, 0x00, // ............##..................
|
||||
0x00, 0x0c, 0x00, 0x00, // ............##..................
|
||||
0x00, 0x0c, 0x0c, 0x00, // ............##......##..........
|
||||
0x00, 0x0c, 0x0c, 0x00, // ............##......##..........
|
||||
0x00, 0x0c, 0x0c, 0x00, // ............##......##..........
|
||||
0x00, 0x0c, 0x0c, 0x00, // ............##......##..........
|
||||
0x00, 0xcc, 0x0c, 0xc0, // ........##..##......##..##......
|
||||
0x00, 0xcc, 0x0c, 0xc0, // ........##..##......##..##......
|
||||
0x00, 0xcc, 0xcc, 0xc0, // ........##..##..##..##..##......
|
||||
0x00, 0xcc, 0xcc, 0xc0, // ........##..##..##..##..##......
|
||||
0x0c, 0xcc, 0xcc, 0xc0, // ....##..##..##..##..##..##......
|
||||
0x0c, 0xcc, 0xcc, 0xc0, // ....##..##..##..##..##..##......
|
||||
0x0c, 0xcc, 0xcc, 0xc0, // ....##..##..##..##..##..##......
|
||||
0x0c, 0xcc, 0xcc, 0xc0, // ....##..##..##..##..##..##......
|
||||
0x0c, 0xcc, 0xcc, 0xc0, // ....##..##..##..##..##..##......
|
||||
0x0c, 0xcc, 0xcc, 0xc0, // ....##..##..##..##..##..##......
|
||||
0x3f, 0xff, 0xff, 0xfc, // ..############################..
|
||||
0x3f, 0xff, 0xff, 0xfc, // ..############################..
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
};
|
||||
|
||||
const uint8_t notes[128] = {
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0xc3, 0x0c, 0x00, // ........##....##....##..........
|
||||
0x00, 0xc3, 0x0c, 0x00, // ........##....##....##..........
|
||||
0x00, 0xc3, 0x0c, 0x00, // ........##....##....##..........
|
||||
0x00, 0xc3, 0x0c, 0x00, // ........##....##....##..........
|
||||
0x0f, 0xff, 0xff, 0xf0, // ....########################....
|
||||
0x0f, 0xff, 0xff, 0xf0, // ....########################....
|
||||
0x0c, 0xc3, 0x0c, 0x30, // ....##..##....##....##....##....
|
||||
0x0c, 0xc3, 0x0c, 0x30, // ....##..##....##....##....##....
|
||||
0x0c, 0x00, 0x00, 0x30, // ....##....................##....
|
||||
0x0c, 0x00, 0x00, 0x30, // ....##....................##....
|
||||
0x0c, 0x00, 0x00, 0x30, // ....##....................##....
|
||||
0x0c, 0x00, 0x00, 0x30, // ....##....................##....
|
||||
0x0c, 0xff, 0xff, 0x30, // ....##..################..##....
|
||||
0x0c, 0xff, 0xff, 0x30, // ....##..################..##....
|
||||
0x0c, 0x00, 0x00, 0x30, // ....##....................##....
|
||||
0x0c, 0x00, 0x00, 0x30, // ....##....................##....
|
||||
0x0c, 0xff, 0xff, 0x30, // ....##..################..##....
|
||||
0x0c, 0xff, 0xff, 0x30, // ....##..################..##....
|
||||
0x0c, 0x00, 0x00, 0x30, // ....##....................##....
|
||||
0x0c, 0x00, 0x00, 0x30, // ....##....................##....
|
||||
0x0c, 0xff, 0xc0, 0x30, // ....##..##########........##....
|
||||
0x0c, 0xff, 0xc0, 0x30, // ....##..##########........##....
|
||||
0x0c, 0x00, 0x00, 0x30, // ....##....................##....
|
||||
0x0c, 0x00, 0x00, 0x30, // ....##....................##....
|
||||
0x0c, 0x00, 0x00, 0x30, // ....##....................##....
|
||||
0x0c, 0x00, 0x00, 0x30, // ....##....................##....
|
||||
0x0f, 0xff, 0xff, 0xf0, // ....########################....
|
||||
0x0f, 0xff, 0xff, 0xf0, // ....########################....
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
};
|
||||
|
||||
const uint8_t settings[128] = {
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x0f, 0xf0, 0x00, // ............########............
|
||||
0x00, 0x0f, 0xf0, 0x00, // ............########............
|
||||
0x03, 0xcc, 0x33, 0xc0, // ......####..##....##..####......
|
||||
0x03, 0xcc, 0x33, 0xc0, // ......####..##....##..####......
|
||||
0x0c, 0x30, 0x0c, 0x30, // ....##....##........##....##....
|
||||
0x0c, 0x30, 0x0c, 0x30, // ....##....##........##....##....
|
||||
0x0c, 0x00, 0x00, 0x30, // ....##....................##....
|
||||
0x0c, 0x00, 0x00, 0x30, // ....##....................##....
|
||||
0x03, 0x0f, 0xf0, 0xc0, // ......##....########....##......
|
||||
0x03, 0x0f, 0xf0, 0xc0, // ......##....########....##......
|
||||
0x3c, 0x3c, 0x3c, 0x3c, // ..####....####....####....####..
|
||||
0x3c, 0x3c, 0x3c, 0x3c, // ..####....####....####....####..
|
||||
0x30, 0x30, 0x0c, 0x0c, // ..##......##........##......##..
|
||||
0x30, 0x30, 0x0c, 0x0c, // ..##......##........##......##..
|
||||
0x30, 0x30, 0x0c, 0x0c, // ..##......##........##......##..
|
||||
0x30, 0x30, 0x0c, 0x0c, // ..##......##........##......##..
|
||||
0x3c, 0x3c, 0x3c, 0x3c, // ..####....####....####....####..
|
||||
0x3c, 0x3c, 0x3c, 0x3c, // ..####....####....####....####..
|
||||
0x03, 0x0f, 0xf0, 0xc0, // ......##....########....##......
|
||||
0x03, 0x0f, 0xf0, 0xc0, // ......##....########....##......
|
||||
0x0c, 0x00, 0x00, 0x30, // ....##....................##....
|
||||
0x0c, 0x00, 0x00, 0x30, // ....##....................##....
|
||||
0x0c, 0x30, 0x0c, 0x30, // ....##....##........##....##....
|
||||
0x0c, 0x30, 0x0c, 0x30, // ....##....##........##....##....
|
||||
0x03, 0xcc, 0x33, 0xc0, // ......####..##....##..####......
|
||||
0x03, 0xcc, 0x33, 0xc0, // ......####..##....##..####......
|
||||
0x00, 0x0f, 0xf0, 0x00, // ............########............
|
||||
0x00, 0x0f, 0xf0, 0x00, // ............########............
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
};
|
||||
|
||||
const uint8_t shell[128] = {
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x3c, 0x00, 0x00, 0x00, // ..####..........................
|
||||
0x3c, 0x00, 0x00, 0x00, // ..####..........................
|
||||
0x0f, 0x00, 0x00, 0x00, // ....####........................
|
||||
0x0f, 0x00, 0x00, 0x00, // ....####........................
|
||||
0x03, 0xc0, 0x00, 0x00, // ......####......................
|
||||
0x03, 0xc0, 0x00, 0x00, // ......####......................
|
||||
0x00, 0xf0, 0x00, 0x00, // ........####....................
|
||||
0x00, 0xf0, 0x00, 0x00, // ........####....................
|
||||
0x00, 0x3c, 0x00, 0x00, // ..........####..................
|
||||
0x00, 0x3c, 0x00, 0x00, // ..........####..................
|
||||
0x00, 0x0f, 0x00, 0x00, // ............####................
|
||||
0x00, 0x0f, 0x00, 0x00, // ............####................
|
||||
0x00, 0x3c, 0x00, 0x00, // ..........####..................
|
||||
0x00, 0x3c, 0x00, 0x00, // ..........####..................
|
||||
0x00, 0xf0, 0x00, 0x00, // ........####....................
|
||||
0x00, 0xf0, 0x00, 0x00, // ........####....................
|
||||
0x03, 0xc0, 0x00, 0x00, // ......####......................
|
||||
0x03, 0xc0, 0x00, 0x00, // ......####......................
|
||||
0x0f, 0x00, 0x00, 0x00, // ....####........................
|
||||
0x0f, 0x00, 0x00, 0x00, // ....####........................
|
||||
0x3c, 0x00, 0xff, 0xfc, // ..####..........##############..
|
||||
0x3c, 0x00, 0xff, 0xfc, // ..####..........##############..
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
};
|
||||
|
||||
const uint8_t ssh[128] = {
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x0f, 0xf0, 0x00, 0x00, // ....########....................
|
||||
0x0f, 0xf0, 0x00, 0x00, // ....########....................
|
||||
0x30, 0x0c, 0x00, 0x00, // ..##........##..................
|
||||
0x30, 0x0c, 0x00, 0x00, // ..##........##..................
|
||||
0xc0, 0x03, 0x00, 0x00, // ##............##................
|
||||
0xc0, 0x03, 0x00, 0x00, // ##............##................
|
||||
0xc3, 0xc3, 0xff, 0xff, // ##....####....##################
|
||||
0xc3, 0xc3, 0xff, 0xff, // ##....####....##################
|
||||
0xc3, 0xc3, 0x00, 0xcc, // ##....####....##........##..##..
|
||||
0xc3, 0xc3, 0x00, 0xcc, // ##....####....##........##..##..
|
||||
0xc0, 0x03, 0x00, 0xcc, // ##............##........##..##..
|
||||
0xc0, 0x03, 0x00, 0xcc, // ##............##........##..##..
|
||||
0x30, 0x0c, 0x00, 0x00, // ..##........##..................
|
||||
0x30, 0x0c, 0x00, 0x00, // ..##........##..................
|
||||
0x0f, 0xf0, 0x00, 0x00, // ....########....................
|
||||
0x0f, 0xf0, 0x00, 0x00, // ....########....................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
};
|
||||
|
||||
const uint8_t storage[128] = {
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x03, 0xff, 0xf0, 0x00, // ......##############............
|
||||
0x03, 0xff, 0xf0, 0x00, // ......##############............
|
||||
0x03, 0x00, 0x0c, 0x00, // ......##............##..........
|
||||
0x03, 0x00, 0x0c, 0x00, // ......##............##..........
|
||||
0x03, 0x33, 0x03, 0x00, // ......##..##..##......##........
|
||||
0x03, 0x33, 0x03, 0x00, // ......##..##..##......##........
|
||||
0x03, 0x33, 0x00, 0xc0, // ......##..##..##........##......
|
||||
0x03, 0x33, 0x00, 0xc0, // ......##..##..##........##......
|
||||
0x03, 0x33, 0x00, 0xc0, // ......##..##..##........##......
|
||||
0x03, 0x33, 0x00, 0xc0, // ......##..##..##........##......
|
||||
0x03, 0x33, 0x00, 0xc0, // ......##..##..##........##......
|
||||
0x03, 0x33, 0x00, 0xc0, // ......##..##..##........##......
|
||||
0x03, 0x00, 0x00, 0xc0, // ......##................##......
|
||||
0x03, 0x00, 0x00, 0xc0, // ......##................##......
|
||||
0x03, 0x00, 0x00, 0xc0, // ......##................##......
|
||||
0x03, 0x00, 0x00, 0xc0, // ......##................##......
|
||||
0x03, 0x3f, 0xfc, 0xc0, // ......##..############..##......
|
||||
0x03, 0x3f, 0xfc, 0xc0, // ......##..############..##......
|
||||
0x03, 0x30, 0x0c, 0xc0, // ......##..##........##..##......
|
||||
0x03, 0x30, 0x0c, 0xc0, // ......##..##........##..##......
|
||||
0x03, 0x30, 0x0c, 0xc0, // ......##..##........##..##......
|
||||
0x03, 0x30, 0x0c, 0xc0, // ......##..##........##..##......
|
||||
0x03, 0x3f, 0xfc, 0xc0, // ......##..############..##......
|
||||
0x03, 0x3f, 0xfc, 0xc0, // ......##..############..##......
|
||||
0x03, 0x00, 0x00, 0xc0, // ......##................##......
|
||||
0x03, 0x00, 0x00, 0xc0, // ......##................##......
|
||||
0x03, 0xff, 0xff, 0xc0, // ......####################......
|
||||
0x03, 0xff, 0xff, 0xc0, // ......####################......
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
};
|
||||
|
||||
const uint8_t system[128] = {
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x30, 0x0c, 0x00, // ..........##........##..........
|
||||
0x00, 0x30, 0x0c, 0x00, // ..........##........##..........
|
||||
0x00, 0x30, 0x0c, 0x00, // ..........##........##..........
|
||||
0x00, 0x30, 0x0c, 0x00, // ..........##........##..........
|
||||
0x00, 0xff, 0xff, 0x00, // ........################........
|
||||
0x00, 0xff, 0xff, 0x00, // ........################........
|
||||
0x0f, 0xc0, 0x03, 0xf0, // ....######............######....
|
||||
0x0f, 0xc0, 0x03, 0xf0, // ....######............######....
|
||||
0x00, 0xcf, 0xf3, 0x00, // ........##..########..##........
|
||||
0x00, 0xcf, 0xf3, 0x00, // ........##..########..##........
|
||||
0x00, 0xcf, 0xf3, 0x00, // ........##..########..##........
|
||||
0x00, 0xcf, 0xf3, 0x00, // ........##..########..##........
|
||||
0x00, 0xcf, 0xf3, 0x00, // ........##..########..##........
|
||||
0x00, 0xcf, 0xf3, 0x00, // ........##..########..##........
|
||||
0x00, 0xcf, 0xf3, 0x00, // ........##..########..##........
|
||||
0x00, 0xcf, 0xf3, 0x00, // ........##..########..##........
|
||||
0x0f, 0xc0, 0x03, 0xf0, // ....######............######....
|
||||
0x0f, 0xc0, 0x03, 0xf0, // ....######............######....
|
||||
0x00, 0xff, 0xff, 0x00, // ........################........
|
||||
0x00, 0xff, 0xff, 0x00, // ........################........
|
||||
0x00, 0x30, 0x0c, 0x00, // ..........##........##..........
|
||||
0x00, 0x30, 0x0c, 0x00, // ..........##........##..........
|
||||
0x00, 0x30, 0x0c, 0x00, // ..........##........##..........
|
||||
0x00, 0x30, 0x0c, 0x00, // ..........##........##..........
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
};
|
||||
|
||||
const uint8_t wifi_tools[128] = {
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x3f, 0xfc, 0x00, // ..........############..........
|
||||
0x00, 0x3f, 0xfc, 0x00, // ..........############..........
|
||||
0x03, 0xc0, 0x03, 0xc0, // ......####............####......
|
||||
0x03, 0xc0, 0x03, 0xc0, // ......####............####......
|
||||
0x0c, 0x00, 0x00, 0x30, // ....##....................##....
|
||||
0x0c, 0x00, 0x00, 0x30, // ....##....................##....
|
||||
0x30, 0x3f, 0xfc, 0x0c, // ..##......############......##..
|
||||
0x30, 0x3f, 0xfc, 0x0c, // ..##......############......##..
|
||||
0x03, 0xc0, 0x03, 0xc0, // ......####............####......
|
||||
0x03, 0xc0, 0x03, 0xc0, // ......####............####......
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x0f, 0xf0, 0x00, // ............########............
|
||||
0x00, 0x0f, 0xf0, 0x00, // ............########............
|
||||
0x00, 0x30, 0x0c, 0x00, // ..........##........##..........
|
||||
0x00, 0x30, 0x0c, 0x00, // ..........##........##..........
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x03, 0xc0, 0x00, // ..............####..............
|
||||
0x00, 0x03, 0xc0, 0x00, // ..............####..............
|
||||
0x00, 0x03, 0xc0, 0x00, // ..............####..............
|
||||
0x00, 0x03, 0xc0, 0x00, // ..............####..............
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
};
|
||||
|
||||
} // namespace roro::icons
|
||||
@@ -0,0 +1,23 @@
|
||||
// Made by scripts/make_icons.py from assets/icons/*.png: change the pictures, not this file.
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace roro::icons {
|
||||
|
||||
// One bit a pixel, the leftmost first, a row after another: what Canvas::drawBitmap takes.
|
||||
constexpr int kSize = 32;
|
||||
|
||||
extern const uint8_t gemini[128]; // gemini.png
|
||||
extern const uint8_t gnss[128]; // gnss.png
|
||||
extern const uint8_t irc[128]; // irc.png
|
||||
extern const uint8_t lora[128]; // lora.png
|
||||
extern const uint8_t notes[128]; // notes.png
|
||||
extern const uint8_t settings[128]; // settings.png
|
||||
extern const uint8_t shell[128]; // shell.png
|
||||
extern const uint8_t ssh[128]; // ssh.png
|
||||
extern const uint8_t storage[128]; // storage.png
|
||||
extern const uint8_t system[128]; // system.png
|
||||
extern const uint8_t wifi_tools[128]; // wifi-tools.png
|
||||
|
||||
} // namespace roro::icons
|
||||
@@ -0,0 +1,97 @@
|
||||
#include <unity.h>
|
||||
|
||||
#include "grid_model.h"
|
||||
|
||||
using namespace roro;
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
void test_empty_grid_has_no_selection() {
|
||||
GridModel g(4, 3);
|
||||
TEST_ASSERT_EQUAL(-1, g.selected());
|
||||
g.right();
|
||||
g.down();
|
||||
TEST_ASSERT_EQUAL(-1, g.selected());
|
||||
TEST_ASSERT_EQUAL(0, g.rows());
|
||||
}
|
||||
|
||||
void test_left_and_right_go_through_every_item() {
|
||||
GridModel g(4, 3);
|
||||
g.setCount(11);
|
||||
TEST_ASSERT_EQUAL(0, g.selected());
|
||||
g.right();
|
||||
TEST_ASSERT_EQUAL(1, g.selected());
|
||||
g.select(3);
|
||||
g.right(); // onto the next row
|
||||
TEST_ASSERT_EQUAL(4, g.selected());
|
||||
g.left();
|
||||
TEST_ASSERT_EQUAL(3, g.selected());
|
||||
g.select(10);
|
||||
g.right(); // around
|
||||
TEST_ASSERT_EQUAL(0, g.selected());
|
||||
g.left();
|
||||
TEST_ASSERT_EQUAL(10, g.selected());
|
||||
}
|
||||
|
||||
void test_up_and_down_stay_in_the_column() {
|
||||
GridModel g(4, 3);
|
||||
g.setCount(11); // rows of 4, 4 and 3
|
||||
g.select(1);
|
||||
g.down();
|
||||
TEST_ASSERT_EQUAL(5, g.selected());
|
||||
g.down();
|
||||
TEST_ASSERT_EQUAL(9, g.selected());
|
||||
g.down(); // around
|
||||
TEST_ASSERT_EQUAL(1, g.selected());
|
||||
g.up();
|
||||
TEST_ASSERT_EQUAL(9, g.selected());
|
||||
}
|
||||
|
||||
void test_a_short_last_row_gives_its_last_item() {
|
||||
GridModel g(4, 3);
|
||||
g.setCount(11);
|
||||
g.select(7); // the fourth column, which the last row doesn't have
|
||||
g.down();
|
||||
TEST_ASSERT_EQUAL(10, g.selected());
|
||||
g.select(3);
|
||||
g.up();
|
||||
TEST_ASSERT_EQUAL(10, g.selected());
|
||||
}
|
||||
|
||||
void test_rows_scroll_to_keep_the_selection_in_view() {
|
||||
GridModel g(4, 3);
|
||||
g.setCount(18); // 5 rows
|
||||
TEST_ASSERT_EQUAL(5, g.rows());
|
||||
TEST_ASSERT_EQUAL(0, g.firstVisibleRow());
|
||||
g.select(12); // the fourth row
|
||||
TEST_ASSERT_EQUAL(1, g.firstVisibleRow());
|
||||
g.select(17);
|
||||
TEST_ASSERT_EQUAL(2, g.firstVisibleRow());
|
||||
g.select(5);
|
||||
TEST_ASSERT_EQUAL(1, g.firstVisibleRow());
|
||||
g.select(0);
|
||||
TEST_ASSERT_EQUAL(0, g.firstVisibleRow());
|
||||
}
|
||||
|
||||
void test_a_smaller_count_keeps_the_selection_inside() {
|
||||
GridModel g(4, 3);
|
||||
g.setCount(18);
|
||||
g.select(17);
|
||||
g.setCount(6);
|
||||
TEST_ASSERT_EQUAL(5, g.selected());
|
||||
TEST_ASSERT_EQUAL(0, g.firstVisibleRow());
|
||||
g.setCount(0);
|
||||
TEST_ASSERT_EQUAL(-1, g.selected());
|
||||
}
|
||||
|
||||
int main() {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_empty_grid_has_no_selection);
|
||||
RUN_TEST(test_left_and_right_go_through_every_item);
|
||||
RUN_TEST(test_up_and_down_stay_in_the_column);
|
||||
RUN_TEST(test_a_short_last_row_gives_its_last_item);
|
||||
RUN_TEST(test_rows_scroll_to_keep_the_selection_in_view);
|
||||
RUN_TEST(test_a_smaller_count_keeps_the_selection_inside);
|
||||
return UNITY_END();
|
||||
}
|
||||
@@ -46,6 +46,15 @@ void test_toggle_rows_flip_on_select() {
|
||||
TEST_ASSERT_FALSE(f.settings.getBool(Setting::Sound));
|
||||
}
|
||||
|
||||
void test_the_launcher_is_a_grid_or_a_list() {
|
||||
Fixture f;
|
||||
int launcher = f.row(SettingsMenu::Row::Launcher);
|
||||
TEST_ASSERT_EQUAL_STRING("Grid", f.menu.value(launcher).c_str());
|
||||
f.menu.toggle(launcher);
|
||||
TEST_ASSERT_TRUE(f.settings.getBool(Setting::LauncherList));
|
||||
TEST_ASSERT_EQUAL_STRING("List", f.menu.value(launcher).c_str());
|
||||
}
|
||||
|
||||
void test_choice_rows_list_options_and_mark_the_current_one() {
|
||||
Fixture f;
|
||||
int tz = f.row(SettingsMenu::Row::Timezone);
|
||||
@@ -161,6 +170,7 @@ int main() {
|
||||
RUN_TEST(test_every_row_has_a_label);
|
||||
RUN_TEST(test_values_are_human_readable);
|
||||
RUN_TEST(test_toggle_rows_flip_on_select);
|
||||
RUN_TEST(test_the_launcher_is_a_grid_or_a_list);
|
||||
RUN_TEST(test_choice_rows_list_options_and_mark_the_current_one);
|
||||
RUN_TEST(test_choosing_applies_the_setting);
|
||||
RUN_TEST(test_choice_that_breaks_a_rule_is_refused_with_a_reason);
|
||||
|
||||