Public Access
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
102 lines
4.3 KiB
C++
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();
|
|
}
|