Private
Public Access
0
0

style(themes): compact TOML formatting and lift semantic colors

This commit is contained in:
2026-06-05 00:02:46 -04:00
parent 06e305aba6
commit 7ea52cbbe8
16 changed files with 915 additions and 516 deletions
+101 -81
View File
@@ -10,6 +10,7 @@ Scale uses imgui.get_style().font_scale_main.
from imgui_bundle import imgui, hello_imgui
from typing import Any, Optional
import typing
from contextlib import nullcontext
from src import imgui_scopes as imscope
import src.theme_nerv
@@ -22,22 +23,12 @@ from src.theme_models import ThemeFile, load_themes_from_dir, load_themes_from_t
# Each palette maps imgui color enum values to (R, G, B, A) floats [0..1].
# Only keys that differ from the ImGui dark defaults need to be listed.
# ------------------------------------------------------------------ internal helpers
def _c(r: int, g: int, b: int, a: int = 255) -> tuple[float, float, float, float]:
"""
Convert 0-255 RGBA to 0.0-1.0 floats.
[C: src/theme_nerv.py:module]
"""
"""Convert 0-255 RGBA to 0.0-1.0 floats."""
return (r / 255.0, g / 255.0, b / 255.0, a / 255.0)
_BUILTIN_PALETTES: dict[str, dict[int, tuple]] = {
"ImGui Dark": {}, # empty = use imgui dark defaults
"NERV": {},
}
_TOML_PALETTES: dict[str, ThemeFile] = {}
_TOML_COLOUR_CACHE: dict[str, dict[int, tuple[float, float, float, float]]] = {}
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)
@@ -52,19 +43,26 @@ def _build_imgui_colour_dict(theme: ThemeFile) -> dict[int, tuple[float, float,
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
and TOML-loaded themes."""
names = list(_BUILTIN_PALETTES.keys())
names.extend(sorted(_TOML_PALETTES.keys()))
hi_themes = [name for name in dir(hello_imgui.ImGuiTheme_) if not name.startswith('_') and name != 'count']
hi_themes = [n for n in hi_themes if not hasattr(int, n)]
names.extend(sorted(hi_themes))
return names
def _build_semantic_colour_dict(theme: ThemeFile) -> dict[str, tuple[float, float, float, float]]:
out: dict[str, tuple[float, float, float, float]] = {}
palette_dict = theme.palette.to_dict()
from imgui_bundle import imgui
for col_name, rgb in palette_dict.items():
if not hasattr(imgui.Col_, col_name):
out[col_name] = _hex(rgb)
return out
# ------------------------------------------------------------------ state
_BUILTIN_PALETTES: dict[str, dict[int, tuple]] = {
"ImGui Dark": {},
"NERV": {},
}
_TOML_PALETTES: dict[str, ThemeFile] = {}
_TOML_COLOUR_CACHE: dict[str, dict[int, tuple[float, float, float, float]]] = {}
_TOML_SEMANTIC_CACHE: dict[str, dict[str, tuple[float, float, float, float]]] = {}
_current_palette: str = "10x Dark"
_current_font_path: str = "fonts/Inter-Regular.ttf"
_current_font_size: float = 16.0
@@ -141,13 +139,50 @@ def set_child_transparency(val: float) -> None:
_child_transparency = val
apply(_current_palette)
def apply(palette_name: str) -> None:
"""
def get_color(name: str, alpha: float = 1.0) -> imgui.ImVec4:
"""Return a tone-mapped semantic color from the current palette."""
palette_name = _current_palette
if palette_name in _TOML_SEMANTIC_CACHE:
d = _TOML_SEMANTIC_CACHE[palette_name]
if name in d:
rgba = list(d[name])
rgba[3] = alpha
return imgui.ImVec4(*_tone_map(tuple(rgba), palette_name))
Apply a named palette by setting all ImGui style colors and applying global professional styling.
[C: tests/test_theme.py:test_theme_apply_sets_rounding_and_padding]
"""
# Hardcoded fallbacks if not in TOML (matches ThemePalette defaults)
fallbacks = {
"status_success": (80, 255, 80),
"status_warning": (255, 152, 48),
"status_error": (255, 72, 64),
"status_info": (0, 255, 255),
"bubble_user": (30, 45, 75),
"bubble_ai": (35, 65, 45),
"bubble_vendor": (65, 55, 30),
"bubble_system": (25, 25, 25),
"slice_manual": (255, 165, 0),
"slice_auto": (0, 255, 0),
"slice_selection": (100, 100, 255),
"diff_added": (51, 230, 51),
"diff_removed": (230, 51, 51),
"diff_header": (77, 178, 255),
}
if name in fallbacks:
rgb = fallbacks[name]
return imgui.ImVec4(*_tone_map((rgb[0]/255.0, rgb[1]/255.0, rgb[2]/255.0, alpha), palette_name))
return imgui.ImVec4(1, 1, 1, alpha)
def get_role_tint(role: str) -> imgui.ImVec4:
"""Returns a subtle background tint color based on the message role."""
mapping = {
"User": "bubble_user",
"AI": "bubble_ai",
"Vendor API": "bubble_vendor",
}
return get_color(mapping.get(role, "bubble_system"), alpha=0.6)
def apply(palette_name: str) -> None:
"""Apply a named palette by setting all ImGui style colors and professional styling."""
global _current_palette
_current_palette = palette_name
if palette_name == 'NERV':
@@ -175,13 +210,10 @@ def apply(palette_name: str) -> None:
elif hasattr(hello_imgui.ImGuiTheme_, palette_name):
theme_enum = getattr(hello_imgui.ImGuiTheme_, palette_name)
hello_imgui.apply_theme(theme_enum)
# hello_imgui doesn't expose the underlying dict easily to tone-map after-the-fact
# without re-reading every enum. For now, BUILTIN and TOML themes get full TM.
else:
# Fallback
imgui.style_colors_dark()
# 2. Apply our "Subtle Rounding" professional tweaks on top of ANY theme
# 2. Professional tweaks
style = imgui.get_style()
style.window_rounding = 6.0
style.child_rounding = 4.0
@@ -194,44 +226,50 @@ def apply(palette_name: str) -> None:
style.frame_border_size = 1.0
style.popup_border_size = 1.0
# Apply transparency to WindowBg
win_bg = style.color_(imgui.Col_.window_bg)
win_bg.w = _transparency
style.set_color_(imgui.Col_.window_bg, win_bg)
# Apply child/frame transparency
for col_idx in [imgui.Col_.child_bg, imgui.Col_.frame_bg, imgui.Col_.popup_bg]:
c = style.color_(col_idx)
c.w = _child_transparency
style.set_color_(col_idx, c)
# Spacing & Padding
style.window_padding = imgui.ImVec2(8.0, 8.0)
style.frame_padding = imgui.ImVec2(8.0, 4.0)
style.item_spacing = imgui.ImVec2(8.0, 4.0)
style.item_inner_spacing = imgui.ImVec2(4.0, 4.0)
style.scrollbar_size = 14.0
# Rendering anti-aliasing (Shaders/Quality)
style.anti_aliased_lines = True
style.anti_aliased_fill = True
style.anti_aliased_lines_use_tex = True
# 3. Sync syntax palette and clear markdown render cache
apply_syntax_palette(get_syntax_palette_for_theme(palette_name))
import src.markdown_helper
src.markdown_helper.get_renderer().clear_cache()
try:
import src.markdown_helper
src.markdown_helper.get_renderer().clear_cache()
except (ImportError, AttributeError):
pass
def set_scale(factor: float) -> None:
"""Set the global font/UI scale factor."""
global _current_scale
_current_scale = factor
style = imgui.get_style()
style.font_scale_main = factor
def apply_current() -> None:
"""Apply the loaded palette and scale."""
apply(_current_palette)
set_scale(_current_scale)
def get_palette_names() -> list[str]:
"""Returns a list of all available palettes."""
names = list(_BUILTIN_PALETTES.keys())
names.extend(sorted(_TOML_PALETTES.keys()))
hi_themes = [name for name in dir(hello_imgui.ImGuiTheme_) if not name.startswith('_') and name != 'count']
hi_themes = [n for n in hi_themes if not hasattr(int, n)]
names.extend(sorted(hi_themes))
return names
# ------------------------------------------------------------------ persistence
def save_to_config(config: dict) -> None:
"""Persist theme settings into the config dict under [theme]."""
import sys
"""Persist theme settings into the config dict."""
config.setdefault("theme", {})
config["theme"]["palette"] = _current_palette
config["theme"]["font_path"] = _current_font_path
@@ -239,7 +277,6 @@ def save_to_config(config: dict) -> None:
config["theme"]["scale"] = _current_scale
config["theme"]["transparency"] = _transparency
config["theme"]["child_transparency"] = _child_transparency
# Tone mapping
tm = {}
for p in set(list(_brightness.keys()) + list(_contrast.keys()) + list(_gamma.keys())):
tm[p] = {
@@ -248,49 +285,37 @@ def save_to_config(config: dict) -> None:
"gamma": _gamma.get(p, 1.0)
}
config["theme"]["tone_mapping"] = tm
sys.stderr.write(f"[DEBUG theme_2] save_to_config: palette={_current_palette}\n")
sys.stderr.flush()
def load_from_config(config: dict) -> None:
"""Read [theme] from config. Font is handled separately at startup."""
"""Read [theme] from config."""
import sys
global _current_font_path, _current_font_size, _current_scale, _current_palette, _transparency, _child_transparency, _brightness, _contrast, _gamma
t = config.get("theme", {})
_current_palette = t.get("palette", "10x Dark")
if _current_palette in ("", "DPG Default"):
_current_palette = "10x Dark"
_current_font_path = t.get("font_path", "fonts/Inter-Regular.ttf")
_current_font_size = float(t.get("font_size", 16.0))
_current_scale = float(t.get("scale", 1.0))
_transparency = float(t.get("transparency", 1.0))
_child_transparency = float(t.get("child_transparency", 1.0))
# Tone mapping
tm = t.get("tone_mapping", {})
_brightness = {p: float(v.get("brightness", 1.0)) for p, v in tm.items()}
_contrast = {p: float(v.get("contrast", 1.0)) for p, v in tm.items()}
_gamma = {p: float(v.get("gamma", 1.0)) for p, v in tm.items()}
sys.stderr.write(f"[DEBUG theme_2] load_from_config: palette={_current_palette}\n")
sys.stderr.flush()
def apply_current() -> None:
"""Apply the loaded palette and scale. Call after imgui context exists."""
apply(_current_palette)
set_scale(_current_scale)
# ------------------------------------------------------------------ external themes
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
"""Load all themes from the global themes directory."""
global _TOML_PALETTES, _TOML_COLOUR_CACHE, _TOML_SEMANTIC_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()}
_TOML_SEMANTIC_CACHE = {name: _build_semantic_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)
@@ -300,7 +325,6 @@ def get_syntax_palette_for_theme(theme_name: str) -> str:
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."""
@@ -312,46 +336,42 @@ def apply_syntax_palette(palette_name: str) -> None:
return
ed.TextEditor.set_default_palette(palette_id)
# ------------------------------------------------------------------ font & scaling
load_themes_from_disk()
def set_scale(factor: float) -> None:
global _current_scale
_current_scale = factor
imgui.get_style().font_scale_main = factor
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
def get_tweaked_theme() -> hello_imgui.ImGuiTweakedTheme:
"""Returns an ImGuiTweakedTheme object reflecting the current state."""
tt = hello_imgui.ImGuiTweakedTheme()
if hasattr(hello_imgui.ImGuiTheme_, _current_palette):
tt.theme = getattr(hello_imgui.ImGuiTheme_, _current_palette)
else:
tt.theme = hello_imgui.ImGuiTheme_.imgui_colors_dark
# Sync tweaks
tt.tweaks.rounding = 6.0
return tt
# ------------------------------------------------------------------ specialized colors
def ai_text_color() -> imgui.ImVec4:
"""Returns DATA_GREEN if NERV is active, otherwise standard text color."""
if is_nerv_active():
return imgui.ImVec4(*DATA_GREEN)
return imgui.get_style().color_(imgui.Col_.text)
def ai_text_style():
"""Context manager for AI response text styling."""
return imscope.style_color(imgui.Col_.text, ai_text_color())
def get_role_tint(role: str) -> imgui.ImVec4:
"""Returns a subtle background tint color based on the message role."""
# Deep, low-saturation tints for distinct role hierarchy
if role == "User": return imgui.ImVec4(0.12, 0.18, 0.30, 0.6) # Midnight Blue
elif role == "AI": return imgui.ImVec4(0.14, 0.25, 0.18, 0.6) # Forest Green
elif role == "Vendor API": return imgui.ImVec4(0.25, 0.22, 0.12, 0.5) # Bronze
return imgui.ImVec4(0.1, 0.1, 0.1, 0.4) # Neutral System
def render_post_fx(width: float, height: float, ai_status: str, crt_enabled: bool) -> None:
"""Updates and renders the alert and CRT filters."""
_alert_pulsing.update(ai_status)
_alert_pulsing.render(width, height)
_crt_filter.enabled = crt_enabled
_crt_filter.render(width, height)
_crt_filter.render(width, height)
# ------------------------------------------------------------------ init
load_themes_from_disk()