fix(gui): Final monolithic stabilization and UI polish
- Restore monolithic architecture in gui_2.py to fix test compatibility. - Implement full-width horizontal expansion for Markdown tables in discussion entries. - Re-implement layered role-based tints using draw_list channels. - Standardize Text Viewer docking ID to '###Text_Viewer_Unified'. - Fix MiniMax compression routing and base URL. - Fully restore missing theme_2.py definitions.
This commit is contained in:
+169
-75
@@ -3,13 +3,18 @@
|
||||
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.
|
||||
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 __future__ import annotations
|
||||
|
||||
from imgui_bundle import imgui, hello_imgui
|
||||
from typing import Any, Optional
|
||||
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
|
||||
|
||||
# ------------------------------------------------------------------ palettes
|
||||
|
||||
@@ -17,33 +22,16 @@ from src import imgui_scopes as imscope
|
||||
# Only keys that differ from the ImGui dark defaults need to be listed.
|
||||
|
||||
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."""
|
||||
"""
|
||||
|
||||
Convert 0-255 RGBA to 0.0-1.0 floats.
|
||||
[C: src/theme_nerv.py:module]
|
||||
"""
|
||||
return (r / 255.0, g / 255.0, b / 255.0, a / 255.0)
|
||||
|
||||
# Semantic Colors
|
||||
SUCCESS_GREEN = _c(100, 255, 100)
|
||||
ERROR_RED = _c(255, 100, 100)
|
||||
WARNING_GOLD = _c(255, 220, 100)
|
||||
INFO_BLUE = _c(100, 200, 255)
|
||||
DIM_GRAY = _c(180, 180, 180)
|
||||
DIM_SYSTEM = _c(120, 120, 100)
|
||||
|
||||
C_OUT = _c(100, 200, 255)
|
||||
C_IN = _c(140, 255, 160)
|
||||
C_REQ = _c(255, 220, 100)
|
||||
C_RES = _c(180, 255, 180)
|
||||
C_TC = _c(255, 180, 80)
|
||||
C_TR = _c(180, 220, 255)
|
||||
C_TRS = _c(200, 180, 255)
|
||||
C_LBL = _c(180, 180, 180)
|
||||
C_VAL = _c(220, 220, 220)
|
||||
C_KEY = _c(140, 200, 255)
|
||||
C_NUM = _c(180, 255, 180)
|
||||
C_TRM = _c(160, 160, 150) # Trimmed/Cruft
|
||||
C_SUB = _c(220, 200, 120)
|
||||
|
||||
_PALETTES: dict[str, dict[int, tuple]] = {
|
||||
"ImGui Dark": {}, # empty = use imgui dark defaults
|
||||
"NERV": {},
|
||||
"10x Dark": {
|
||||
imgui.Col_.window_bg: _c( 34, 32, 28),
|
||||
imgui.Col_.child_bg: _c( 30, 28, 24),
|
||||
@@ -196,27 +184,77 @@ _PALETTES: dict[str, dict[int, tuple]] = {
|
||||
imgui.Col_.nav_cursor: _c(166, 226, 46),
|
||||
imgui.Col_.modal_window_dim_bg: _c( 10, 10, 8, 100),
|
||||
},
|
||||
"Binks": {
|
||||
imgui.Col_.text: _c( 0, 0, 0, 255),
|
||||
imgui.Col_.text_disabled: _c(153, 153, 153, 255),
|
||||
imgui.Col_.window_bg: _c(240, 240, 240, 240),
|
||||
imgui.Col_.child_bg: _c( 0, 0, 0, 0),
|
||||
imgui.Col_.popup_bg: _c(255, 255, 255, 240),
|
||||
imgui.Col_.border: _c( 0, 0, 0, 99),
|
||||
imgui.Col_.border_shadow: _c(255, 255, 255, 25),
|
||||
imgui.Col_.frame_bg: _c(255, 255, 255, 240),
|
||||
imgui.Col_.frame_bg_hovered: _c( 66, 150, 250, 102),
|
||||
imgui.Col_.frame_bg_active: _c( 66, 150, 250, 171),
|
||||
imgui.Col_.title_bg: _c(245, 245, 245, 255),
|
||||
imgui.Col_.title_bg_collapsed: _c(255, 255, 255, 130),
|
||||
imgui.Col_.title_bg_active: _c(209, 209, 209, 255),
|
||||
imgui.Col_.menu_bar_bg: _c(219, 219, 219, 255),
|
||||
imgui.Col_.scrollbar_bg: _c(250, 250, 250, 135),
|
||||
imgui.Col_.scrollbar_grab: _c(176, 176, 176, 255),
|
||||
imgui.Col_.scrollbar_grab_hovered: _c(150, 150, 150, 255),
|
||||
imgui.Col_.scrollbar_grab_active: _c(125, 125, 125, 255),
|
||||
imgui.Col_.check_mark: _c( 66, 150, 250, 255),
|
||||
imgui.Col_.slider_grab: _c( 61, 133, 224, 255),
|
||||
imgui.Col_.slider_grab_active: _c( 66, 150, 250, 255),
|
||||
imgui.Col_.button: _c( 66, 150, 250, 102),
|
||||
imgui.Col_.button_hovered: _c( 66, 150, 250, 255),
|
||||
imgui.Col_.button_active: _c( 15, 135, 250, 255),
|
||||
imgui.Col_.header: _c( 66, 150, 250, 79),
|
||||
imgui.Col_.header_hovered: _c( 66, 150, 250, 204),
|
||||
imgui.Col_.header_active: _c( 66, 150, 250, 255),
|
||||
imgui.Col_.separator: _c(100, 100, 100, 255),
|
||||
imgui.Col_.resize_grip: _c(255, 255, 255, 127),
|
||||
imgui.Col_.resize_grip_hovered: _c( 66, 150, 250, 171),
|
||||
imgui.Col_.resize_grip_active: _c( 66, 150, 250, 242),
|
||||
imgui.Col_.plot_lines: _c( 99, 99, 99, 255),
|
||||
imgui.Col_.plot_lines_hovered: _c(255, 110, 89, 255),
|
||||
imgui.Col_.plot_histogram: _c(230, 178, 0, 255),
|
||||
imgui.Col_.plot_histogram_hovered: _c(255, 153, 0, 255),
|
||||
imgui.Col_.text_selected_bg: _c( 66, 150, 250, 89),
|
||||
imgui.Col_.modal_window_dim_bg: _c( 51, 51, 51, 89),
|
||||
},
|
||||
}
|
||||
|
||||
PALETTE_NAMES: list[str] = list(_PALETTES.keys())
|
||||
def get_palette_names() -> list[str]:
|
||||
"""Returns a list of all available palettes, including hello_imgui built-ins."""
|
||||
names = list(_PALETTES.keys())
|
||||
# Add hello_imgui themes
|
||||
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
|
||||
|
||||
# ------------------------------------------------------------------ state
|
||||
|
||||
_current_palette_name: str = "10x Dark"
|
||||
_current_font_path: str = ""
|
||||
_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
|
||||
_custom_font: imgui.ImFont = None # type: ignore
|
||||
|
||||
_crt_filter = CRTFilter()
|
||||
_alert_pulsing = AlertPulsing()
|
||||
_status_flicker = StatusFlicker()
|
||||
|
||||
# ------------------------------------------------------------------ public API
|
||||
|
||||
def get_palette_names() -> list[str]:
|
||||
return list(_PALETTES.keys())
|
||||
|
||||
def get_current_palette() -> str:
|
||||
return _current_palette_name
|
||||
return _current_palette
|
||||
|
||||
def is_nerv_active() -> bool:
|
||||
return _current_palette == "NERV"
|
||||
|
||||
def get_current_font_path() -> str:
|
||||
return _current_font_path
|
||||
@@ -233,7 +271,7 @@ def get_transparency() -> float:
|
||||
def set_transparency(val: float) -> None:
|
||||
global _transparency
|
||||
_transparency = val
|
||||
apply(_current_palette_name)
|
||||
apply(_current_palette)
|
||||
|
||||
def get_child_transparency() -> float:
|
||||
return _child_transparency
|
||||
@@ -241,28 +279,74 @@ def get_child_transparency() -> float:
|
||||
def set_child_transparency(val: float) -> None:
|
||||
global _child_transparency
|
||||
_child_transparency = val
|
||||
apply(_current_palette_name)
|
||||
apply(_current_palette)
|
||||
|
||||
def apply(palette_name: str) -> None:
|
||||
"""
|
||||
Apply a named palette by setting all ImGui style colors.
|
||||
"""
|
||||
global _current_palette_name
|
||||
_current_palette_name = palette_name
|
||||
colours = _PALETTES.get(palette_name, {})
|
||||
if not colours:
|
||||
# Reset to imgui dark defaults
|
||||
imgui.style_colors_dark()
|
||||
|
||||
|
||||
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]
|
||||
"""
|
||||
global _current_palette
|
||||
_current_palette = palette_name
|
||||
if palette_name == 'NERV':
|
||||
src.theme_nerv.apply_nerv()
|
||||
return
|
||||
|
||||
# 1. Apply base colors
|
||||
if palette_name in _PALETTES:
|
||||
colours = _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(*rgba))
|
||||
elif hasattr(hello_imgui.ImGuiTheme_, palette_name):
|
||||
theme_enum = getattr(hello_imgui.ImGuiTheme_, palette_name)
|
||||
hello_imgui.apply_theme(theme_enum)
|
||||
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()
|
||||
|
||||
# 2. Apply our "Subtle Rounding" professional tweaks on top of ANY theme
|
||||
style = imgui.get_style()
|
||||
# Start from dark defaults so unlisted keys have sensible values
|
||||
imgui.style_colors_dark()
|
||||
for col_enum, rgba in colours.items():
|
||||
col = imgui.ImVec4(*rgba)
|
||||
# Apply global transparency overrides
|
||||
if col_enum == imgui.Col_.window_bg: col.w *= _transparency
|
||||
if col_enum == imgui.Col_.child_bg: col.w *= _child_transparency
|
||||
style.set_color_(col_enum, col)
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
def set_scale(factor: float) -> None:
|
||||
"""Set the global font/UI scale factor."""
|
||||
@@ -272,29 +356,40 @@ def set_scale(factor: float) -> None:
|
||||
style.font_scale_main = factor
|
||||
|
||||
def save_to_config(config: dict) -> None:
|
||||
"""Persist theme settings into the config dict."""
|
||||
"""Persist theme settings into the config dict under [theme]."""
|
||||
import sys
|
||||
config.setdefault("theme", {})
|
||||
config["theme"]["palette"] = _current_palette_name
|
||||
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
|
||||
sys.stderr.write(f"[DEBUG theme_2] save_to_config: palette={_current_palette}, transparency={_transparency}\n")
|
||||
sys.stderr.flush()
|
||||
|
||||
def load_from_config(config: dict) -> None:
|
||||
"""Read theme settings from config."""
|
||||
global _current_palette_name, _current_font_path, _current_font_size, _current_scale, _transparency, _child_transparency
|
||||
"""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
|
||||
t = config.get("theme", {})
|
||||
_current_palette_name = t.get("palette", "10x Dark")
|
||||
_current_font_path = t.get("font_path", "")
|
||||
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"
|
||||
|
||||
_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))
|
||||
sys.stderr.write(f"[DEBUG theme_2] load_from_config effective: palette={_current_palette}, transparency={_transparency}\n")
|
||||
sys.stderr.flush()
|
||||
|
||||
def apply_current() -> None:
|
||||
"""Apply the loaded palette and scale. Call after imgui context exists."""
|
||||
apply(_current_palette_name)
|
||||
apply(_current_palette)
|
||||
set_scale(_current_scale)
|
||||
|
||||
def get_font_loading_params() -> tuple[str, float]:
|
||||
@@ -304,14 +399,20 @@ def get_font_loading_params() -> tuple[str, float]:
|
||||
def get_tweaked_theme() -> hello_imgui.ImGuiTweakedTheme:
|
||||
"""Returns an ImGuiTweakedTheme object reflecting the current state."""
|
||||
tt = hello_imgui.ImGuiTweakedTheme()
|
||||
# Since custom palettes like '10x Dark' are not in hello_imgui enum,
|
||||
# we always use dark as base and apply our specific colors in apply()
|
||||
tt.theme = hello_imgui.ImGuiTheme_.imgui_colors_dark
|
||||
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
|
||||
|
||||
def ai_text_color() -> imgui.ImVec4:
|
||||
return imgui.ImVec4(0.8, 0.9, 0.8, 1.0)
|
||||
"""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."""
|
||||
@@ -319,22 +420,15 @@ def ai_text_style():
|
||||
|
||||
def get_role_tint(role: str) -> imgui.ImVec4:
|
||||
"""Returns a subtle background tint color based on the message role."""
|
||||
# Slightly more opaque and distinct tints for role-based structure
|
||||
if role == "User": return imgui.ImVec4(0.12, 0.18, 0.30, 0.6) # Deep Blue
|
||||
elif role == "AI": return imgui.ImVec4(0.14, 0.25, 0.18, 0.6) # Deep Green
|
||||
elif role == "Vendor API": return imgui.ImVec4(0.25, 0.22, 0.12, 0.5) # Earthy Gold
|
||||
return imgui.ImVec4(0.1, 0.1, 0.1, 0.4) # Dim System
|
||||
|
||||
def is_nerv_active() -> bool:
|
||||
return _current_palette_name == "Nerv"
|
||||
|
||||
from src.theme_nerv_fx import AlertPulsing, CRTFilter
|
||||
_alert_pulsing = AlertPulsing()
|
||||
_crt_filter = CRTFilter()
|
||||
# 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)
|
||||
Reference in New Issue
Block a user