Frame.theme (7 presets in app/theme_tokens.py) drives font family, corner radius, drop shadow, and an accent hue for every modern-style widget's header/accent region. Rich accent colors (not just the 6 flat panel inks) are approximated via denser Bayer stippling confined to just that region (html_render.ordered_dither_regions), so icon/text content elsewhere stays exactly as crisp as it is today -- verified directly against real Chromium renders, both in unit tests and via run-server. "classic" is a byte-identical no-visual-change default: weather's header keeps its original fixed blue gradient, tasks/calendar keep their flat THEME_* ink. Themes are purely stylistic -- battery's charge-level color, calendar/ tasks' per-owner event chips, and text's own per-widget font choice are never touched.
93 lines
4.2 KiB
Python
93 lines
4.2 KiB
Python
"""app.theme_tokens -- resolve_theme()'s fallback logic (does "classic"
|
|
stay a byte-identical no-op vs. each widget kind's pre-theme-system
|
|
look?) and the font table every preset resolves against. No rendering
|
|
here -- see test_html_render.py for ordered_dither_regions and each
|
|
widget's own test_widgets_*.py for dispatch-level "does a theme actually
|
|
change the output" coverage."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from app import panel_style, theme_tokens
|
|
|
|
|
|
def test_classic_weather_matches_historical_fixed_gradient():
|
|
"""Weather's modern style never went through panel_style.THEME --
|
|
its header was always this fixed blue gradient (see html_render.py's
|
|
removed ACCENT_START/ACCENT_END). "classic" must reproduce it
|
|
byte-for-byte so themes are additive, not a silent regression."""
|
|
resolved = theme_tokens.resolve_theme("classic", "weather", None)
|
|
assert resolved["accent_hex"] == "#1c4fd6"
|
|
assert resolved["accent_hex_dark"] == "#6fa8ff"
|
|
|
|
|
|
def test_classic_tasks_and_calendar_use_flat_theme_ink():
|
|
"""Tasks/calendar's classic accent was always a flat single ink (no
|
|
gradient) resolved through panel_style.THEME -- both ends of the
|
|
"gradient" must be that same ink, not two different shades."""
|
|
tasks = theme_tokens.resolve_theme("classic", "tasks", None)
|
|
calendar = theme_tokens.resolve_theme("classic", "calendar", None)
|
|
assert tasks["accent_hex"] == tasks["accent_hex_dark"]
|
|
assert calendar["accent_hex"] == calendar["accent_hex_dark"]
|
|
|
|
from app.image_pipeline import DEFAULT_PALETTE_RGB
|
|
|
|
expected_tasks = "#%02x%02x%02x" % tuple(DEFAULT_PALETTE_RGB[panel_style.THEME_TASKS])
|
|
expected_calendar = "#%02x%02x%02x" % tuple(DEFAULT_PALETTE_RGB[panel_style.THEME_CALENDAR])
|
|
assert tasks["accent_hex"] == expected_tasks
|
|
assert calendar["accent_hex"] == expected_calendar
|
|
|
|
|
|
def test_classic_unmapped_widget_kind_falls_back_to_black():
|
|
resolved = theme_tokens.resolve_theme("classic", "battery", None)
|
|
from app.image_pipeline import DEFAULT_PALETTE_RGB
|
|
|
|
assert resolved["accent_hex"] == "#%02x%02x%02x" % tuple(DEFAULT_PALETTE_RGB[panel_style.BLACK])
|
|
|
|
|
|
def test_unknown_theme_name_falls_back_to_classic():
|
|
assert theme_tokens.resolve_theme("not-a-real-theme", "weather", None) == \
|
|
theme_tokens.resolve_theme("classic", "weather", None)
|
|
assert theme_tokens.resolve_theme(None, "weather", None) == \
|
|
theme_tokens.resolve_theme("classic", "weather", None)
|
|
|
|
|
|
def test_every_preset_resolves_without_error_for_every_widget_kind():
|
|
widget_kinds = ["weather", "tasks", "calendar", "battery", "text", "static", "whiteboard"]
|
|
for theme_name in theme_tokens.THEMES:
|
|
for kind in widget_kinds:
|
|
resolved = theme_tokens.resolve_theme(theme_name, kind, None)
|
|
assert resolved["accent_hex"].startswith("#") and len(resolved["accent_hex"]) == 7
|
|
assert resolved["accent_hex_dark"].startswith("#") and len(resolved["accent_hex_dark"]) == 7
|
|
assert resolved["font_family"] in theme_tokens.FONT_FAMILIES
|
|
assert Path(resolved["font_regular"]).exists()
|
|
assert Path(resolved["font_bold"]).exists()
|
|
|
|
|
|
def test_non_gradient_theme_has_equal_accent_stops():
|
|
""""moss" is configured with gradient=False -- its two CSS gradient
|
|
stops must be identical (a flat fill), unlike a gradient theme's."""
|
|
resolved = theme_tokens.resolve_theme("moss", "tasks", None)
|
|
assert resolved["gradient"] is False
|
|
assert resolved["accent_hex"] == resolved["accent_hex_dark"]
|
|
|
|
|
|
def test_gradient_theme_has_a_darker_second_stop():
|
|
resolved = theme_tokens.resolve_theme("terracotta", "tasks", None)
|
|
assert resolved["gradient"] is True
|
|
assert resolved["accent_hex"] == "#a84b2a"
|
|
assert resolved["accent_hex_dark"] != resolved["accent_hex"]
|
|
|
|
|
|
def test_font_path_covers_every_family_and_style_combination():
|
|
for family in theme_tokens.FONT_FAMILIES:
|
|
for bold in (False, True):
|
|
for italic in (False, True):
|
|
assert theme_tokens.font_path(family, bold, italic).exists()
|
|
|
|
|
|
def test_font_path_falls_back_to_default_family_for_unknown_name():
|
|
assert theme_tokens.font_path("not-a-real-family", False, False) == \
|
|
theme_tokens.font_path(theme_tokens.DEFAULT_FONT_FAMILY, False, False)
|