# theme_2.py """ Theming support for manual_slop GUI — imgui-bundle port. Replaces theme.py (DearPyGui-specific) with imgui-bundle equivalents. Palettes are applied via imgui.get_style().set_color_() calls or hello_imgui.apply_theme(). Font loading uses hello_imgui.load_font(). 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 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 # 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.""" return (r / 255.0, g / 255.0, b / 255.0, a / 255.0) 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) 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 _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 _current_scale: float = 1.0 _transparency: float = 1.0 _child_transparency: float = 1.0 # Per-palette tone mapping: { "Palette Name": value } _brightness: dict[str, float] = {} _contrast: dict[str, float] = {} _gamma: dict[str, float] = {} def _get_tm(d: dict[str, float], palette: str, default: float) -> float: return d.get(palette, default) def get_brightness(palette: str) -> float: return _get_tm(_brightness, palette, 1.0) def get_contrast(palette: str) -> float: return _get_tm(_contrast, palette, 1.0) def get_gamma(palette: str) -> float: return _get_tm(_gamma, palette, 1.0) def set_brightness(palette: str, val: float) -> None: _brightness[palette] = val; apply(palette) def set_contrast(palette: str, val: float) -> None: _contrast[palette] = val; apply(palette) def set_gamma(palette: str, val: float) -> None: _gamma[palette] = val; apply(palette) def reset_tone_mapping(palette: str) -> None: for d in [_brightness, _contrast, _gamma]: if palette in d: del d[palette] apply(palette) def _tone_map(rgb: tuple[float, float, float, float], palette: str) -> tuple[float, float, float, float]: b, c, g = get_brightness(palette), get_contrast(palette), get_gamma(palette) r, g_val, bl, a = rgb # 1. Brightness r *= b; g_val *= b; bl *= b # 2. Contrast r = (r - 0.5) * c + 0.5; g_val = (g_val - 0.5) * c + 0.5; bl = (bl - 0.5) * c + 0.5 # 3. Gamma r = max(0, r)**(1.0/g); g_val = max(0, g_val)**(1.0/g); bl = max(0, bl)**(1.0/g) return (max(0.0, min(1.0, r)), max(0.0, min(1.0, g_val)), max(0.0, min(1.0, bl)), a) _crt_filter = CRTFilter() _alert_pulsing = AlertPulsing() _status_flicker = StatusFlicker() # ------------------------------------------------------------------ public API def get_current_palette() -> str: return _current_palette def is_nerv_active() -> bool: return _current_palette == "NERV" def get_current_font_path() -> str: return _current_font_path def get_current_font_size() -> float: return _current_font_size def get_current_scale() -> float: return _current_scale def get_transparency() -> float: return _transparency def set_transparency(val: float) -> None: global _transparency _transparency = val apply(_current_palette) def get_child_transparency() -> float: return _child_transparency def set_child_transparency(val: float) -> None: global _child_transparency _child_transparency = val apply(_current_palette) def get_table_color(is_alt: bool = False, alpha: float = 1.0) -> imgui.ImVec4: """Returns a tone-mapped table row background color.""" key = "table_row_bg_alt" if is_alt else "table_row_bg" return get_color(key, alpha=alpha) 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 # 1. Check TOML-loaded themes first if palette_name in _TOML_PALETTES: theme_file = _TOML_PALETTES[palette_name] # A. Check semantic cache (for non-ImGui keys) 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)) # B. Check ThemePalette dataclass fields (for basics like 'text') if hasattr(theme_file.palette, name): rgb = getattr(theme_file.palette, name) if isinstance(rgb, tuple) and len(rgb) == 3: return imgui.ImVec4(*_tone_map((rgb[0]/255.0, rgb[1]/255.0, rgb[2]/255.0, alpha), palette_name)) # 2. Hardcoded fallbacks if not in TOML (matches ThemePalette defaults) fallbacks = { "text": (200, 200, 200), "text_disabled": (130, 130, 130), "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), "table_header_text": (255, 255, 255), "table_row_bg": (0, 0, 0), "table_row_bg_alt": (10, 10, 10), } 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)) # 3. Last resort 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': from src import theme_nerv theme_nerv.apply_nerv() apply_syntax_palette(get_syntax_palette_for_theme(palette_name)) return # 1. Apply base colors if palette_name in _BUILTIN_PALETTES: colours = _BUILTIN_PALETTES[palette_name] imgui.style_colors_dark() style = imgui.get_style() for col_enum, rgba in colours.items(): style.set_color_(col_enum, imgui.ImVec4(*_tone_map(rgba, palette_name))) 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(*_tone_map(rgba, palette_name))) elif hasattr(hello_imgui.ImGuiTheme_, palette_name): theme_enum = getattr(hello_imgui.ImGuiTheme_, palette_name) hello_imgui.apply_theme(theme_enum) else: imgui.style_colors_dark() # 2. Professional tweaks style = imgui.get_style() style.window_rounding = 6.0 style.child_rounding = 4.0 style.frame_rounding = 4.0 style.popup_rounding = 4.0 style.scrollbar_rounding = 12.0 style.grab_rounding = 4.0 style.tab_rounding = 4.0 style.window_border_size = 1.0 style.frame_border_size = 1.0 style.popup_border_size = 1.0 win_bg = style.color_(imgui.Col_.window_bg) win_bg.w = _transparency style.set_color_(imgui.Col_.window_bg, win_bg) 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) 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 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)) try: import src.markdown_helper src.markdown_helper.get_renderer().clear_cache() except (ImportError, AttributeError): pass 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.""" config.setdefault("theme", {}) config["theme"]["palette"] = _current_palette config["theme"]["font_path"] = _current_font_path config["theme"]["font_size"] = _current_font_size config["theme"]["scale"] = _current_scale config["theme"]["transparency"] = _transparency config["theme"]["child_transparency"] = _child_transparency tm = {} for p in set(list(_brightness.keys()) + list(_contrast.keys()) + list(_gamma.keys())): tm[p] = { "brightness": _brightness.get(p, 1.0), "contrast": _contrast.get(p, 1.0), "gamma": _gamma.get(p, 1.0) } config["theme"]["tone_mapping"] = tm def load_from_config(config: dict) -> None: """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)) 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()} # ------------------------------------------------------------------ external themes def load_themes_from_disk() -> None: """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) 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) # ------------------------------------------------------------------ font & scaling 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 _current_font_path, _current_font_size def get_tweaked_theme() -> hello_imgui.ImGuiTweakedTheme: 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 tt.tweaks.rounding = 6.0 return tt # ------------------------------------------------------------------ specialized colors def ai_text_color() -> imgui.ImVec4: if is_nerv_active(): return imgui.ImVec4(*DATA_GREEN) return imgui.get_style().color_(imgui.Col_.text) def ai_text_style(): return imscope.style_color(imgui.Col_.text, ai_text_color()) """Returns a subtle background tint color based on the message role.""" 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) # ------------------------------------------------------------------ init load_themes_from_disk()