Files
roro9stack/test/test_palette/test_palette.cpp
T
twislaandClaude Opus 5.5 37335b4829 UI: seven themes, each dark and light, chosen in the Settings
Settings > Theme and Settings > Light or dark pick the colours of every
screen (issue #10): roro9stack (the website's, the default), Catppuccin,
Dracula, Nord, ANSI terminal, Gruvbox and Solarized.

- A palette is 14 roles (lib/ui/src/palette.h). theme::kText and the
  others are now references into the one in use, so the Apps are
  unchanged; the main loop applies it when a Setting changes.
- Every colour is one the RGB332 frame buffer holds exactly: the themes'
  own were rounded, and those that fell together or lost contrast were
  picked by hand. test_palette checks exactness and 18 contrast ratios
  for each palette.
- The Sky view's constellation colours come from the theme.
- `theme [0-6] [light|dark]` on the consoles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0162FokPdvY2KsS4NBfwyWPk
2026-10-10 00:11:52 +02:00

102 lines
4.3 KiB
C++

#include <unity.h>
#include <cmath>
#include <cstdio>
#include <string>
#include "palette.h"
using namespace roro::ui;
void setUp() {}
void tearDown() {}
namespace {
double channel(uint32_t v) {
double c = v / 255.0;
return c <= 0.03928 ? c / 12.92 : std::pow((c + 0.055) / 1.055, 2.4);
}
double luminance(uint16_t c) {
uint32_t rgb = rgb888(c);
return 0.2126 * channel(rgb >> 16) + 0.7152 * channel((rgb >> 8) & 255) + 0.0722 * channel(rgb & 255);
}
// The WCAG contrast ratio of two colours as the frame buffer shows them: 1 (the same) to 21.
double contrast(uint16_t a, uint16_t b) {
double la = luminance(a), lb = luminance(b);
return (std::fmax(la, lb) + 0.05) / (std::fmin(la, lb) + 0.05);
}
void atLeast(double wanted, uint16_t a, uint16_t b, const char* what, int theme, bool light) {
char msg[96];
std::snprintf(msg, sizeof msg, "%s %s: %s is %.1f, under %.1f", themeName(theme), light ? "light" : "dark", what, contrast(a, b), wanted);
TEST_ASSERT_TRUE_MESSAGE(contrast(a, b) >= wanted, msg);
}
} // namespace
void test_seven_themes_with_names() {
TEST_ASSERT_EQUAL(7, kThemeCount);
TEST_ASSERT_EQUAL_STRING("roro9stack", themeName(0));
for (int t = 0; t < kThemeCount; ++t) TEST_ASSERT_TRUE(std::string(themeName(t)).size() > 0 && std::string(themeName(t)).size() <= 14);
// Out of range: the first, rather than whatever is next in memory.
TEST_ASSERT_EQUAL_STRING("roro9stack", themeName(7));
TEST_ASSERT_EQUAL_STRING("roro9stack", themeName(-1));
TEST_ASSERT_EQUAL_PTR(&palette(0, true), &palette(99, true));
}
// The frame buffer keeps 3 bits of red and green and 2 of blue: a colour it would round is a
// colour that isn't the one shown.
void test_every_colour_is_one_the_frame_buffer_holds() {
for (int t = 0; t < kThemeCount; ++t)
for (int light = 0; light < 2; ++light) {
const Palette& p = palette(t, light);
const uint16_t* c = &p.background;
for (size_t i = 0; i < sizeof(Palette) / sizeof(uint16_t); ++i) {
uint32_t rgb = rgb888(c[i]);
uint16_t back = static_cast<uint16_t>(((rgb >> 19) & 0x1F) << 11 | ((rgb >> 10) & 0x3F) << 5 | ((rgb >> 3) & 0x1F));
TEST_ASSERT_EQUAL_HEX16(back, c[i]);
}
}
}
void test_every_theme_reads() {
for (int t = 0; t < kThemeCount; ++t)
for (int light = 0; light < 2; ++light) {
const Palette& p = palette(t, light);
atLeast(4.5, p.text, p.background, "text", t, light);
atLeast(3, p.muted, p.background, "muted text", t, light);
atLeast(1.3, p.muted, p.text, "muted against text", t, light);
atLeast(3, p.accent, p.background, "the accent", t, light);
atLeast(3, p.onAccent, p.accent, "text on the accent", t, light);
atLeast(4.5, p.text, p.bar, "text on the bar", t, light);
atLeast(2.5, p.barMuted, p.bar, "idle marks on the bar", t, light);
atLeast(2, p.accent, p.bar, "live marks on the bar", t, light);
atLeast(3, p.warning, p.background, "a warning", t, light);
atLeast(3, p.message, p.background, "a message", t, light);
atLeast(3, p.good, p.background, "good", t, light);
atLeast(3, p.onAccent, p.warning, "text on a warning Toast", t, light);
atLeast(3, p.onAccent, p.message, "text on a message Toast", t, light);
atLeast(2, p.faint, p.accent, "the Launcher's selected icon", t, light);
atLeast(1.15, p.faint, p.background, "grid lines", t, light);
atLeast(2.5, p.red, p.background, "red", t, light);
atLeast(2.5, p.yellow, p.background, "yellow", t, light);
atLeast(2.5, p.violet, p.background, "violet", t, light);
}
}
void test_the_first_is_the_websites() {
const Palette& p = palette(0, false);
TEST_ASSERT_EQUAL_HEX32(0x000000, rgb888(p.background));
TEST_ASSERT_EQUAL_HEX32(0x00dbff, rgb888(p.accent));
TEST_ASSERT_EQUAL_HEX32(0x494955, rgb888(p.bar));
TEST_ASSERT_EQUAL_HEX32(0xffffff, rgb888(palette(0, true).background));
}
int main() {
UNITY_BEGIN();
RUN_TEST(test_seven_themes_with_names);
RUN_TEST(test_every_colour_is_one_the_frame_buffer_holds);
RUN_TEST(test_every_theme_reads);
RUN_TEST(test_the_first_is_the_websites);
return UNITY_END();
}