feat(theme): load themes from TOML and apply syntax palette mapping
This commit is contained in:
+4166
File diff suppressed because one or more lines are too long
+74
-3
@@ -15,6 +15,8 @@ from src import imgui_scopes as imscope
|
||||
import src.theme_nerv
|
||||
from src.theme_nerv import DATA_GREEN
|
||||
from src.theme_nerv_fx import CRTFilter, AlertPulsing, StatusFlicker
|
||||
from src.paths import get_global_themes_path
|
||||
from src.theme_models import ThemeFile, load_themes_from_dir, load_themes_from_toml
|
||||
|
||||
# ------------------------------------------------------------------ palettes
|
||||
|
||||
@@ -225,12 +227,33 @@ _PALETTES: dict[str, dict[int, tuple]] = {
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _hex(rgb: tuple[int, int, int]) -> tuple[float, float, float, float]:
|
||||
return (rgb[0] / 255.0, rgb[1] / 255.0, rgb[2] / 255.0, 1.0)
|
||||
|
||||
|
||||
_TOML_PALETTES: dict[str, ThemeFile] = {}
|
||||
_TOML_COLOUR_CACHE: dict[str, dict[int, tuple[float, float, float, float]]] = {}
|
||||
|
||||
|
||||
def _build_imgui_colour_dict(theme: ThemeFile) -> dict[int, tuple[float, float, float, float]]:
|
||||
from imgui_bundle import imgui
|
||||
out: dict[int, tuple[float, float, float, float]] = {}
|
||||
palette_dict = theme.palette.to_dict()
|
||||
for col_name, rgb in palette_dict.items():
|
||||
if hasattr(imgui.Col_, col_name):
|
||||
enum_val = getattr(imgui.Col_, col_name)
|
||||
if isinstance(enum_val, int):
|
||||
out[enum_val] = _hex(rgb)
|
||||
return out
|
||||
|
||||
|
||||
def get_palette_names() -> list[str]:
|
||||
"""Returns a list of all available palettes, including hello_imgui built-ins."""
|
||||
"""Returns a list of all available palettes, including hello_imgui built-ins
|
||||
and TOML-loaded themes."""
|
||||
names = list(_PALETTES.keys())
|
||||
# Add hello_imgui themes
|
||||
names.extend(sorted(_TOML_PALETTES.keys()))
|
||||
hi_themes = [name for name in dir(hello_imgui.ImGuiTheme_) if not name.startswith('_') and name != 'count']
|
||||
# Filter out int methods that leaked into dir() if any
|
||||
hi_themes = [n for n in hi_themes if not hasattr(int, n)]
|
||||
names.extend(sorted(hi_themes))
|
||||
return names
|
||||
@@ -292,6 +315,7 @@ def apply(palette_name: str) -> None:
|
||||
_current_palette = palette_name
|
||||
if palette_name == 'NERV':
|
||||
src.theme_nerv.apply_nerv()
|
||||
apply_syntax_palette(get_syntax_palette_for_theme(palette_name))
|
||||
return
|
||||
|
||||
# 1. Apply base colors
|
||||
@@ -301,6 +325,16 @@ def apply(palette_name: str) -> None:
|
||||
style = imgui.get_style()
|
||||
for col_enum, rgba in colours.items():
|
||||
style.set_color_(col_enum, imgui.ImVec4(*rgba))
|
||||
elif palette_name in _TOML_PALETTES:
|
||||
colours = _TOML_COLOUR_CACHE.get(palette_name, {})
|
||||
if not colours:
|
||||
theme = _TOML_PALETTES[palette_name]
|
||||
colours = _build_imgui_colour_dict(theme)
|
||||
_TOML_COLOUR_CACHE[palette_name] = colours
|
||||
imgui.style_colors_dark()
|
||||
style = imgui.get_style()
|
||||
for colenum, rgba in colours.items():
|
||||
style.set_color_(colenum, imgui.ImVec4(*rgba))
|
||||
elif hasattr(hello_imgui.ImGuiTheme_, palette_name):
|
||||
theme_enum = getattr(hello_imgui.ImGuiTheme_, palette_name)
|
||||
hello_imgui.apply_theme(theme_enum)
|
||||
@@ -392,6 +426,43 @@ def apply_current() -> None:
|
||||
apply(_current_palette)
|
||||
set_scale(_current_scale)
|
||||
|
||||
|
||||
def load_themes_from_disk() -> None:
|
||||
"""Load all themes from the global themes directory. Each *.toml file
|
||||
in the directory is one theme. Idempotent - safe to call repeatedly.
|
||||
Broken entries are logged and skipped."""
|
||||
global _TOML_PALETTES, _TOML_COLOUR_CACHE
|
||||
themes_dir = get_global_themes_path()
|
||||
loaded: dict[str, ThemeFile] = {}
|
||||
if themes_dir.exists() and themes_dir.is_dir():
|
||||
loaded.update(load_themes_from_dir(themes_dir, scope="global"))
|
||||
_TOML_PALETTES = loaded
|
||||
_TOML_COLOUR_CACHE = {name: _build_imgui_colour_dict(t) for name, t in loaded.items()}
|
||||
|
||||
|
||||
def get_syntax_palette_for_theme(theme_name: str) -> str:
|
||||
"""Return the syntax palette name (one of dark/light/mariana/retro_blue)
|
||||
associated with the given UI theme. Falls back to 'dark' for unknown
|
||||
themes and for non-TOML built-ins."""
|
||||
if theme_name in _TOML_PALETTES:
|
||||
return _TOML_PALETTES[theme_name].syntax_palette
|
||||
return "dark"
|
||||
|
||||
|
||||
def apply_syntax_palette(palette_name: str) -> None:
|
||||
"""Set the default imgui_color_text_edit palette. palette_name must
|
||||
be one of: dark, light, mariana, retro_blue. No-op for unknown names."""
|
||||
from imgui_bundle import imgui_color_text_edit as ed
|
||||
if not hasattr(ed.TextEditor, "PaletteId"):
|
||||
return
|
||||
palette_id = getattr(ed.TextEditor.PaletteId, palette_name, None)
|
||||
if palette_id is None:
|
||||
return
|
||||
ed.TextEditor.set_default_palette(palette_id)
|
||||
|
||||
|
||||
load_themes_from_disk()
|
||||
|
||||
def get_font_loading_params() -> tuple[str, float]:
|
||||
"""Return (font_path, font_size) for use during hello_imgui font loading callback."""
|
||||
return _current_font_path, _current_font_size
|
||||
|
||||
+43
-5
@@ -1,19 +1,18 @@
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
from src import theme_2 as theme
|
||||
from src import paths as paths_mod
|
||||
|
||||
|
||||
def test_theme_apply_sets_rounding_and_padding(monkeypatch):
|
||||
# Mock imgui
|
||||
mock_style = MagicMock()
|
||||
mock_imgui = MagicMock()
|
||||
mock_imgui.get_style.return_value = mock_style
|
||||
mock_imgui.ImVec2.side_effect = lambda x, y: (x, y)
|
||||
monkeypatch.setattr(theme, "imgui", mock_imgui)
|
||||
|
||||
# Call apply with the default palette
|
||||
theme.apply("ImGui Dark")
|
||||
|
||||
# Verify subtle rounding styles
|
||||
assert mock_style.window_rounding == 6.0
|
||||
assert mock_style.child_rounding == 4.0
|
||||
assert mock_style.frame_rounding == 4.0
|
||||
@@ -22,14 +21,53 @@ def test_theme_apply_sets_rounding_and_padding(monkeypatch):
|
||||
assert mock_style.grab_rounding == 4.0
|
||||
assert mock_style.tab_rounding == 4.0
|
||||
|
||||
# Verify borders
|
||||
assert mock_style.window_border_size == 1.0
|
||||
assert mock_style.frame_border_size == 1.0
|
||||
assert mock_style.popup_border_size == 1.0
|
||||
|
||||
# Verify padding/spacing
|
||||
assert mock_style.window_padding == (8.0, 8.0)
|
||||
assert mock_style.frame_padding == (8.0, 4.0)
|
||||
assert mock_style.item_spacing == (8.0, 4.0)
|
||||
assert mock_style.item_inner_spacing == (4.0, 4.0)
|
||||
assert mock_style.scrollbar_size == 14.0
|
||||
|
||||
|
||||
def test_themes_load_from_toml(tmp_path, monkeypatch):
|
||||
from src import paths as paths_mod
|
||||
themes_dir = tmp_path / "themes"
|
||||
themes_dir.mkdir()
|
||||
(themes_dir / "solarized_dark.toml").write_text(
|
||||
'syntax_palette = "dark"\n'
|
||||
'[colors]\n'
|
||||
'window_bg = [0, 43, 54]\n'
|
||||
'text = [147, 161, 161]\n'
|
||||
)
|
||||
(themes_dir / "broken.toml").write_text('syntax_palette = "dark"\n')
|
||||
|
||||
monkeypatch.setattr(theme, "get_global_themes_path", lambda: themes_dir)
|
||||
theme.load_themes_from_disk()
|
||||
names = theme.get_palette_names()
|
||||
assert "solarized_dark" in names
|
||||
assert "broken" not in names
|
||||
|
||||
|
||||
def test_get_syntax_palette_for_theme(tmp_path, monkeypatch):
|
||||
from src import paths as paths_mod
|
||||
themes_dir = tmp_path / "themes"
|
||||
themes_dir.mkdir()
|
||||
(themes_dir / "solarized_light.toml").write_text(
|
||||
'syntax_palette = "light"\n[colors]\nwindow_bg = [253, 246, 227]\n'
|
||||
)
|
||||
monkeypatch.setattr(theme, "get_global_themes_path", lambda: themes_dir)
|
||||
theme.load_themes_from_disk()
|
||||
assert theme.get_syntax_palette_for_theme("solarized_light") == "light"
|
||||
assert theme.get_syntax_palette_for_theme("ImGui Dark") == "dark"
|
||||
|
||||
|
||||
def test_get_syntax_palette_for_unknown_theme_returns_default(tmp_path, monkeypatch):
|
||||
from src import paths as paths_mod
|
||||
themes_dir = tmp_path / "themes"
|
||||
themes_dir.mkdir()
|
||||
monkeypatch.setattr(theme, "get_global_themes_path", lambda: themes_dir)
|
||||
theme.load_themes_from_disk()
|
||||
assert theme.get_syntax_palette_for_theme("NonExistent") == "dark"
|
||||
|
||||
Reference in New Issue
Block a user