From 06e305aba615e606ace5dfcd72c1c6becb73c760 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Thu, 4 Jun 2026 23:44:43 -0400 Subject: [PATCH] feat(theme): add tone mapping and fix missing palette colors --- conductor/theme_polish_and_tonemapping.md | 33 ++++++++++ config.toml | 16 ++--- manualslop_layout.ini | 50 +++++++++------- src/gui_2.py | 21 +++++++ src/theme_2.py | 73 +++++++++++++++++++---- src/theme_models.py | 36 ++++++++++- themes/10x_dark.toml | 1 + themes/binks.toml | 1 + themes/monokai.toml | 1 + themes/nord_dark.toml | 1 + 10 files changed, 186 insertions(+), 47 deletions(-) create mode 100644 conductor/theme_polish_and_tonemapping.md diff --git a/conductor/theme_polish_and_tonemapping.md b/conductor/theme_polish_and_tonemapping.md new file mode 100644 index 00000000..8ec2fd19 --- /dev/null +++ b/conductor/theme_polish_and_tonemapping.md @@ -0,0 +1,33 @@ +# Theme Polish & Tone Mapping + +## Problem + +1. **Missing Theme Colors**: The `ThemePalette` dataclass in `src/theme_models.py` only defined a subset of the ~55 ImGui colors. Because `from_dict` strictly matched dataclass fields, colors like `resize_grip` and `tab_dimmed` from the TOML files were being discarded, breaking window resizing handles and inactive tab styling. +2. **Context Preview Syntax Palette**: `theme_2.apply()` failed to apply the syntax palette for non-NERV themes, and `src/markdown_helper.py` cached its `TextEditor` instances without clearing them on theme switch. This caused "Context Preview" to remain stuck on the previous theme's syntax colors. +3. **Light Theme Brightness**: The user requested a way to dim light themes. We will introduce a Tone Mapping system (Brightness, Contrast, Gamma) that mathematical adjusts the RGB colors before applying them to ImGui. The user requested this to be saved per-palette so each theme can have its own exposure profile. + +## Proposed Solution + +### 1. Fix Theme Models +- Ensure `src/theme_models.py`'s `ThemePalette` dataclass has all missing ImGui colors (e.g., `resize_grip`, `resize_grip_active`, `resize_grip_hovered`, `tab_dimmed`, `tab_dimmed_selected`, `docking_preview`, `plot_lines`, `nav_windowing_highlight`, etc.). *(Note: I proactively applied the class definition update during exploration, but will formally commit it)*. + +### 2. Fix Context Preview Syntax Highlight Sync +- Update `src/theme_2.py` to ensure `apply_syntax_palette()` is called for *all* themes during `apply()`. +- Add an `import src.markdown_helper; src.markdown_helper.get_renderer().clear_cache()` call to the end of `theme_2.apply()` to force code blocks to recreate their `TextEditor` instances with the new palette. + +### 3. Per-Palette Tone Mapping +- Add mathematical tone mapping variables to `src/theme_2.py`: `_brightness`, `_contrast`, and `_gamma` (stored as dictionaries keyed by the palette name to allow per-palette saving). +- Implement a math function to adjust RGB floats: + - Brightness: `c * brightness` + - Contrast: `(c - 0.5) * contrast + 0.5` + - Gamma: `pow(c, 1.0 / gamma)` +- Update the palette application loop in `theme_2.apply()` to pass every color float through this tone mapper before calling `style.set_color_()`. +- Update `save_to_config` and `load_from_config` to persist the tone mapping overrides per-palette under `[theme.tone_mapping.]`. +- Add Brightness, Contrast, and Gamma sliders to the Theme panel in `src/gui_2.py`. + +## Implementation Steps +1. **Model & Sync Fixes**: Verify `src/theme_models.py` and update `src/theme_2.py`'s `apply()` function to trigger syntax updates and markdown cache clearing. +2. **Tone Mapping Logic**: Add the dicts and the math `_tone_map(rgb, palette)` function to `theme_2.py`, wrapping all color assignments. +3. **State Persistence**: Update `save_to_config` / `load_from_config` to handle the new per-palette dictionary. +4. **UI Integration**: Add the 3 sliders to `_render_theme_panel` in `src/gui_2.py`, complete with a "Reset to Defaults" button for the current palette. +5. **Testing**: Run the existing test suite and verify no regressions in config saving. \ No newline at end of file diff --git a/config.toml b/config.toml index ab175015..b5194190 100644 --- a/config.toml +++ b/config.toml @@ -1,9 +1,9 @@ [ai] provider = "minimax" model = "MiniMax-M3" -temperature = 0.699999988079071 +temperature = 0.0 top_p = 1.0 -max_tokens = 99999 +max_tokens = 999999 history_trunc_limit = 900000 active_preset = "Basic Do Not" system_prompt = "- **Do not** create shell scripts, README files, or descriptive files unless explicitly instructed.\n- **Do not** do anything beyond what was asked. Suggest extras in text; do not implement them." @@ -16,7 +16,7 @@ paths = [ "C:/projects/manual_slop/manual_slop.toml", "C:/projects/Pikuma/ps1-ai/pikuma_ps1.toml", ] -active = "C:/projects/gencpp/.ai/gencpp_sloppy.toml" +active = "C:/projects/Pikuma/ps1-ai/pikuma_ps1.toml" [gui] separate_message_panel = true @@ -38,7 +38,7 @@ separate_external_tools = false "AI Settings" = true "MMA Dashboard" = false "Task DAG" = false -"Usage Analytics" = false +"Usage Analytics" = true "Tier 1" = false "Tier 2" = false "Tier 3" = false @@ -52,17 +52,17 @@ separate_external_tools = false Message = true Response = true "Tool Calls" = true -"Text Viewer" = true +"Text Viewer" = false Theme = true "Log Management" = true -Diagnostics = true +Diagnostics = false "Context Preview" = false "External Tools" = false "Shader Editor" = false "Undo/Redo History" = false [theme] -palette = "Nord Dark" +palette = "moss" font_path = "C:/projects/manual_slop/assets/fonts/MapleMono-Regular.ttf" font_size = 20.0 scale = 1.0 @@ -81,7 +81,7 @@ logs_dir = "C:\\projects\\manual_slop\\logs" scripts_dir = "C:\\projects\\manual_slop\\scripts" [rag] -enabled = true +enabled = false embedding_provider = "local" chunk_size = 1000 chunk_overlap = 200 diff --git a/manualslop_layout.ini b/manualslop_layout.ini index ddfe6f69..c4bdcbc1 100644 --- a/manualslop_layout.ini +++ b/manualslop_layout.ini @@ -44,20 +44,20 @@ Collapsed=0 DockId=0x00000010,0 [Window][Message] -Pos=1276,28 -Size=1495,1608 +Pos=1196,28 +Size=1362,1548 Collapsed=0 DockId=0x00000006,0 [Window][Response] Pos=0,28 -Size=1274,1608 +Size=1194,1548 Collapsed=0 DockId=0x00000010,5 [Window][Tool Calls] -Pos=1276,28 -Size=1495,1608 +Pos=1196,28 +Size=1362,1548 Collapsed=0 DockId=0x00000006,3 @@ -77,7 +77,7 @@ DockId=0xAFC85805,2 [Window][Theme] Pos=0,28 -Size=1274,1608 +Size=1194,1548 Collapsed=0 DockId=0x00000010,0 @@ -87,8 +87,8 @@ Size=900,700 Collapsed=0 [Window][Diagnostics] -Pos=1210,28 -Size=1514,1470 +Pos=1655,28 +Size=1658,1838 Collapsed=0 DockId=0x00000006,4 @@ -105,26 +105,26 @@ Collapsed=0 DockId=0x0000000D,0 [Window][Discussion Hub] -Pos=1276,28 -Size=1495,1608 +Pos=1196,28 +Size=1362,1548 Collapsed=0 DockId=0x00000006,1 [Window][Operations Hub] Pos=0,28 -Size=1274,1608 +Size=1194,1548 Collapsed=0 DockId=0x00000010,4 [Window][Files & Media] Pos=0,28 -Size=1274,1608 +Size=1194,1548 Collapsed=0 DockId=0x00000010,2 [Window][AI Settings] Pos=0,28 -Size=1274,1608 +Size=1194,1548 Collapsed=0 DockId=0x00000010,3 @@ -140,8 +140,8 @@ Collapsed=0 DockId=0x00000006,2 [Window][Log Management] -Pos=1276,28 -Size=1495,1608 +Pos=1196,28 +Size=1362,1548 Collapsed=0 DockId=0x00000006,2 @@ -410,7 +410,7 @@ DockId=0x00000006,1 [Window][Project Settings] Pos=0,28 -Size=1274,1608 +Size=1194,1548 Collapsed=0 DockId=0x00000010,1 @@ -521,10 +521,9 @@ Size=2176,1441 Collapsed=0 [Window][###Text_Viewer_Unified] -Pos=0,28 -Size=1274,1608 +Pos=182,742 +Size=1163,908 Collapsed=0 -DockId=0x00000010,6 [Window][Command Palette##manual_slop] Pos=1295,781 @@ -536,6 +535,11 @@ Pos=1626,882 Size=638,148 Collapsed=0 +[Window][Project Stale] +Pos=10,50 +Size=169,184 +Collapsed=0 + [Table][0xFB6E3870,4] RefScale=13 Column 0 Width=80 @@ -825,13 +829,13 @@ Column 4 Weight=1.0000 DockNode ID=0x00000008 Pos=3125,170 Size=593,1157 Split=Y DockNode ID=0x00000009 Parent=0x00000008 SizeRef=1029,147 Selected=0x0469CA7A DockNode ID=0x0000000A Parent=0x00000008 SizeRef=1029,145 Selected=0xDF822E02 -DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,28 Size=2771,1608 Split=X +DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,28 Size=2558,1548 Split=X DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=2357,1183 Split=X DockNode ID=0x0000000B Parent=0x00000003 SizeRef=404,1186 Split=X Selected=0xF4139CA2 - DockNode ID=0x00000005 Parent=0x0000000B SizeRef=1274,1681 Split=Y Selected=0x3F1379AF - DockNode ID=0x00000010 Parent=0x00000005 SizeRef=983,1140 CentralNode=1 Selected=0x1DCB2623 + DockNode ID=0x00000005 Parent=0x0000000B SizeRef=1194,1681 Split=Y Selected=0x3F1379AF + DockNode ID=0x00000010 Parent=0x00000005 SizeRef=983,1140 CentralNode=1 Selected=0x418C7449 DockNode ID=0x00000011 Parent=0x00000005 SizeRef=983,184 Selected=0x432BAE4E - DockNode ID=0x00000006 Parent=0x0000000B SizeRef=1495,1681 Selected=0x6F2B5B04 + DockNode ID=0x00000006 Parent=0x0000000B SizeRef=1362,1681 Selected=0x2C0206CE DockNode ID=0x0000000D Parent=0x00000003 SizeRef=435,1186 Selected=0x363E93D6 DockNode ID=0x00000004 Parent=0xAFC85805 SizeRef=488,1183 Selected=0x3AEC3498 diff --git a/src/gui_2.py b/src/gui_2.py index 917137a6..305376f4 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -4567,6 +4567,27 @@ def render_theme_panel(app: App) -> None: app._flush_to_config() models.save_config(app.config) + imgui.separator() + imgui.text("Tone Mapping (Per-Palette)") + curr_p = theme.get_current_palette() + + imgui.text("Brightness") + ch_b, b = imgui.slider_float("##tm_b", theme.get_brightness(curr_p), 0.1, 2.0, "%.2f") + if ch_b: theme.set_brightness(curr_p, b); app._flush_to_config(); models.save_config(app.config) + + imgui.text("Contrast") + ch_c, c = imgui.slider_float("##tm_c", theme.get_contrast(curr_p), 0.1, 2.0, "%.2f") + if ch_c: theme.set_contrast(curr_p, c); app._flush_to_config(); models.save_config(app.config) + + imgui.text("Gamma") + ch_g, g = imgui.slider_float("##tm_g", theme.get_gamma(curr_p), 0.1, 3.0, "%.2f") + if ch_g: theme.set_gamma(curr_p, g); app._flush_to_config(); models.save_config(app.config) + + if imgui.button("Reset Tone Mapping"): + theme.reset_tone_mapping(curr_p) + app._flush_to_config() + models.save_config(app.config) + imgui.end() if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_theme_panel") diff --git a/src/theme_2.py b/src/theme_2.py index 5a71c26b..507547fb 100644 --- a/src/theme_2.py +++ b/src/theme_2.py @@ -72,6 +72,38 @@ _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() @@ -129,7 +161,7 @@ def apply(palette_name: str) -> None: imgui.style_colors_dark() style = imgui.get_style() for col_enum, rgba in colours.items(): - style.set_color_(col_enum, imgui.ImVec4(*rgba)) + 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: @@ -139,17 +171,15 @@ def apply(palette_name: str) -> None: imgui.style_colors_dark() style = imgui.get_style() for colenum, rgba in colours.items(): - style.set_color_(colenum, imgui.ImVec4(*rgba)) + 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) + # 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 to Nord Dark if requested but not found, otherwise ImGui Dark - if palette_name == "Nord Dark": - # This should not happen since it's in _PALETTES, but for safety - imgui.style_colors_dark() - else: - imgui.style_colors_dark() + # Fallback + imgui.style_colors_dark() # 2. Apply our "Subtle Rounding" professional tweaks on top of ANY theme style = imgui.get_style() @@ -187,6 +217,11 @@ def apply(palette_name: str) -> None: 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() + def set_scale(factor: float) -> None: """Set the global font/UI scale factor.""" global _current_scale @@ -204,16 +239,23 @@ def save_to_config(config: dict) -> None: config["theme"]["scale"] = _current_scale config["theme"]["transparency"] = _transparency config["theme"]["child_transparency"] = _child_transparency - sys.stderr.write(f"[DEBUG theme_2] save_to_config: palette={_current_palette}, transparency={_transparency}\n") + # Tone mapping + 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 + 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.""" import sys - global _current_font_path, _current_font_size, _current_scale, _current_palette, _transparency, _child_transparency + global _current_font_path, _current_font_size, _current_scale, _current_palette, _transparency, _child_transparency, _brightness, _contrast, _gamma t = config.get("theme", {}) - sys.stderr.write(f"[DEBUG theme_2] load_from_config raw: {t}\n") - sys.stderr.flush() _current_palette = t.get("palette", "10x Dark") if _current_palette in ("", "DPG Default"): _current_palette = "10x Dark" @@ -223,7 +265,12 @@ def load_from_config(config: dict) -> None: _current_scale = float(t.get("scale", 1.0)) _transparency = float(t.get("transparency", 1.0)) _child_transparency = float(t.get("child_transparency", 1.0)) - sys.stderr.write(f"[DEBUG theme_2] load_from_config effective: palette={_current_palette}, transparency={_transparency}\n") + # 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: diff --git a/src/theme_models.py b/src/theme_models.py index 83133033..70fc159c 100644 --- a/src/theme_models.py +++ b/src/theme_models.py @@ -9,19 +9,20 @@ from typing import Any VALID_SYNTAX_PALETTES: tuple[str, ...] = ("dark", "light", "mariana", "retro_blue") +@dataclass @dataclass class ThemePalette: window_bg: tuple[int, int, int] = (0, 0, 0) - text: tuple[int, int, int] = (200, 200, 200) - text_disabled: tuple[int, int, int] = (130, 130, 130) child_bg: tuple[int, int, int] = (0, 0, 0) popup_bg: tuple[int, int, int] = (0, 0, 0) border: tuple[int, int, int] = (60, 60, 60) + border_shadow: tuple[int, int, int] = (0, 0, 0) frame_bg: tuple[int, int, int] = (45, 45, 45) frame_bg_hovered: tuple[int, int, int] = (60, 60, 60) frame_bg_active: tuple[int, int, int] = (75, 75, 75) title_bg: tuple[int, int, int] = (40, 40, 40) title_bg_active: tuple[int, int, int] = (60, 45, 15) + title_bg_collapsed: tuple[int, int, int] = (30, 30, 30) menu_bar_bg: tuple[int, int, int] = (35, 35, 35) scrollbar_bg: tuple[int, int, int] = (30, 30, 30) scrollbar_grab: tuple[int, int, int] = (80, 80, 80) @@ -39,11 +40,40 @@ class ThemePalette: separator: tuple[int, int, int] = (60, 60, 60) separator_hovered: tuple[int, int, int] = (100, 100, 100) separator_active: tuple[int, int, int] = (200, 200, 200) + resize_grip: tuple[int, int, int] = (60, 60, 60) + resize_grip_hovered: tuple[int, int, int] = (100, 100, 100) + resize_grip_active: tuple[int, int, int] = (200, 200, 200) tab: tuple[int, int, int] = (60, 60, 60) tab_hovered: tuple[int, int, int] = (100, 100, 100) tab_selected: tuple[int, int, int] = (100, 100, 100) + tab_dimmed: tuple[int, int, int] = (60, 60, 60) + tab_dimmed_selected: tuple[int, int, int] = (100, 100, 100) + docking_preview: tuple[int, int, int] = (100, 100, 100) + docking_empty_bg: tuple[int, int, int] = (20, 20, 20) + text: tuple[int, int, int] = (200, 200, 200) + text_disabled: tuple[int, int, int] = (130, 130, 130) text_selected_bg: tuple[int, int, int] = (60, 100, 150) table_header_bg: tuple[int, int, int] = (55, 55, 55) + table_border_strong: tuple[int, int, int] = (60, 60, 60) + table_border_light: tuple[int, int, int] = (40, 40, 40) + table_row_bg: tuple[int, int, int] = (0, 0, 0) + table_row_bg_alt: tuple[int, int, int] = (10, 10, 10) + nav_cursor: tuple[int, int, int] = (100, 100, 100) + nav_windowing_dim_bg: tuple[int, int, int] = (20, 20, 20) + nav_windowing_highlight: tuple[int, int, int] = (200, 200, 200) + modal_window_dim_bg: tuple[int, int, int] = (10, 10, 10) + plot_lines: tuple[int, int, int] = (100, 100, 100) + plot_lines_hovered: tuple[int, int, int] = (200, 100, 100) + plot_histogram: tuple[int, int, int] = (100, 100, 100) + plot_histogram_hovered: tuple[int, int, int] = (200, 100, 100) + drag_drop_target: tuple[int, int, int] = (200, 200, 0) + drag_drop_target_bg: tuple[int, int, int] = (0, 0, 0) + input_text_cursor: tuple[int, int, int] = (200, 200, 200) + tab_dimmed_selected_overline: tuple[int, int, int] = (100, 100, 100) + tab_selected_overline: tuple[int, int, int] = (100, 100, 100) + text_link: tuple[int, int, int] = (60, 100, 150) + tree_lines: tuple[int, int, int] = (60, 60, 60) + unsaved_marker: tuple[int, int, int] = (200, 200, 200) @classmethod def from_dict(cls, data: dict[str, Any]) -> ThemePalette: @@ -122,7 +152,7 @@ def load_theme_file(path: Path, scope: str) -> ThemeFile: raise ValueError(f"failed to parse theme TOML {path}: {e}") from e if not isinstance(data, dict): raise ValueError(f"theme TOML {path} must be a top-level table") - name = path.stem + name = data.get("name", path.stem) theme = ThemeFile.from_dict(name, data, source_path=path, scope=scope) return theme diff --git a/themes/10x_dark.toml b/themes/10x_dark.toml index 8027fd4c..ffe64972 100644 --- a/themes/10x_dark.toml +++ b/themes/10x_dark.toml @@ -1,3 +1,4 @@ +name = "10x Dark" syntax_palette = "dark" description = "10x Dark Theme" diff --git a/themes/binks.toml b/themes/binks.toml index ec26e41c..6ee86a69 100644 --- a/themes/binks.toml +++ b/themes/binks.toml @@ -1,3 +1,4 @@ +name = "Binks" syntax_palette = "light" description = "Binks Theme" diff --git a/themes/monokai.toml b/themes/monokai.toml index 7f1649d0..6af4441a 100644 --- a/themes/monokai.toml +++ b/themes/monokai.toml @@ -1,3 +1,4 @@ +name = "Monokai" syntax_palette = "dark" description = "Monokai Theme" diff --git a/themes/nord_dark.toml b/themes/nord_dark.toml index fa76a36d..e19dbe3c 100644 --- a/themes/nord_dark.toml +++ b/themes/nord_dark.toml @@ -1,3 +1,4 @@ +name = "Nord Dark" syntax_palette = "dark" description = "Nord Dark Theme"