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:
@@ -1,27 +1,21 @@
|
||||
# Implementation Plan: Command Palette & UI Performance Fixes
|
||||
|
||||
## Phase 1: Offloading Performance Fixes
|
||||
- [ ] Task: Async Context Preview
|
||||
- [ ] Add `self._is_generating_preview = False` to `App.__init__`.
|
||||
- [ ] Modify `_check_auto_refresh_context_preview` in `src/gui_2.py`.
|
||||
- [ ] Implement a `worker()` function inside it that runs `app.controller._do_generate()` and updates `app.context_preview_text`.
|
||||
- [ ] Launch this worker via `threading.Thread` only if `not self._is_generating_preview`.
|
||||
- [ ] Ensure thread-safe state updates using appropriate flags.
|
||||
- [x] Task: Async Context Preview
|
||||
- [x] Add `self._is_generating_preview = False` to `App.__init__`.
|
||||
- [x] Modify `_check_auto_refresh_context_preview` in `src/gui_2.py` to use a background thread.
|
||||
- [ ] Task: Incremental AST Selection (Future/Nuance)
|
||||
- [ ] Investigate if `_do_generate` can accept a partial update flag to avoid full project re-render.
|
||||
|
||||
## Phase 2: Command Palette Implementation
|
||||
- [ ] Task: Define Command Registry
|
||||
- [ ] Create a list of dictionaries in `App` containing `name`, `desc`, and `callback`.
|
||||
- [ ] Include common actions: Generate, MD Only, Save, Reset, Toggle various windows.
|
||||
- [ ] Task: Render Command Palette UI
|
||||
- [ ] Add `self.show_command_palette = False` and `self.ui_command_search = ""` to `App`.
|
||||
- [ ] In `render_main_interface`, check for `Ctrl+P` (or `Cmd+P`) to toggle `self.show_command_palette`.
|
||||
- [ ] Create `render_command_palette(app: App)` function.
|
||||
- [ ] Use `imgui.begin_popup_modal` or a custom `begin` window without title bar for the palette feel.
|
||||
- [ ] Implement the filter logic using the search string.
|
||||
- [ ] Handle `Ctrl+P` (or `Cmd+P`) to toggle `self.show_command_palette`.
|
||||
- [ ] Use `imgui.begin_popup_modal` for the palette feel.
|
||||
- [ ] Task: Keyboard Interactivity
|
||||
- [ ] Ensure the search field has focus when opened.
|
||||
- [ ] Handle `imgui.Key.up_arrow` and `imgui.Key.down_arrow` to navigate the list.
|
||||
- [ ] Execute the selected command on `imgui.Key.enter`.
|
||||
- [ ] Implement fuzzy search and keyboard navigation.
|
||||
|
||||
|
||||
## Phase 3: Verification
|
||||
- [ ] Task: Verification
|
||||
|
||||
@@ -2633,6 +2633,7 @@ def run_subagent_summarization(file_path: str, content: str, is_code: bool, outl
|
||||
return "ERROR: Unsupported provider for sub-agent summarization"
|
||||
|
||||
def run_discussion_compression(discussion_text: str) -> str:
|
||||
# Robustly identify the provider string (handles case and whitespace)
|
||||
p = str(get_provider()).lower().strip()
|
||||
prompt = f"The following is a long conversation history.\n\nPlease provide a highly compact, dense summary of the key facts, decisions, bugs encountered, and outcomes that should be retained for context going forward. Categorize into User intent, Tool outputs, and AI reasoning. Omit pleasantries and redundant thoughts.\n\n[HISTORY]\n{discussion_text}"
|
||||
if p == "gemini":
|
||||
|
||||
+45
-18
@@ -221,6 +221,9 @@ class App:
|
||||
self._pending_save_anyway_click = False
|
||||
self.show_missing_files_modal = False
|
||||
self.show_structural_editor_modal = False
|
||||
self.show_command_palette = False
|
||||
self.ui_command_search = ""
|
||||
self.ui_command_idx = 0
|
||||
self.missing_context_files = []
|
||||
self._new_preset_name = ""
|
||||
self._editing_preset_name = ""
|
||||
@@ -322,6 +325,10 @@ class App:
|
||||
self.ui_crt_filter = False
|
||||
self.ui_tool_filter_category = "All"
|
||||
self.shader_uniforms = {'crt': 1.0, 'scanline': 0.5, 'bloom': 0.8}
|
||||
self._is_generating_preview = False
|
||||
self._pending_preview_refresh = False
|
||||
self._preview_refresh_timer: float = 0.0
|
||||
self._preview_refresh_debounce: float = 0.5
|
||||
self._hot_reload_error: Optional[str] = None
|
||||
|
||||
def _set_context_files(self, paths: list[str]) -> None:
|
||||
@@ -3371,12 +3378,12 @@ def render_discussion_entry(app: App, entry: dict, index: int) -> None:
|
||||
p_min = imgui.get_cursor_screen_pos()
|
||||
full_width = imgui.get_content_region_avail().x
|
||||
|
||||
# Start Background Layer
|
||||
# Start Background Layer (Channel 0: Background, Channel 1: Foreground)
|
||||
draw_list.channels_split(2)
|
||||
draw_list.channels_set_current(1) # Foreground
|
||||
draw_list.channels_set_current(1)
|
||||
|
||||
imgui.begin_group()
|
||||
# Force group to take full width
|
||||
# FORCE GROUP TO FULL WIDTH to prevent Markdown table squashing
|
||||
imgui.dummy(imgui.ImVec2(full_width, 0))
|
||||
|
||||
# Header controls
|
||||
@@ -3417,9 +3424,9 @@ def render_discussion_entry(app: App, entry: dict, index: int) -> None:
|
||||
if len(entry["content"]) > 60: preview += "..."
|
||||
imgui.text_colored(C_SUB, preview)
|
||||
else:
|
||||
# Body content - FORCE START ON NEW LINE
|
||||
# Body content - FORCE START ON NEW LINE to prevent horizontal squashing
|
||||
imgui.new_line()
|
||||
imgui.set_cursor_pos_x(imgui.get_cursor_start_pos().x)
|
||||
imgui.spacing()
|
||||
|
||||
thinking_segments, has_content = entry.get("thinking_segments", []), bool(entry.get("content", "").strip())
|
||||
if thinking_segments:
|
||||
@@ -3430,6 +3437,8 @@ def render_discussion_entry(app: App, entry: dict, index: int) -> None:
|
||||
render_discussion_entry_read_mode(app, entry, index)
|
||||
else:
|
||||
if not (bool(thinking_segments) and not has_content):
|
||||
# Ensure multiline editor uses full width
|
||||
imgui.set_next_item_width(-1)
|
||||
ch, entry["content"] = imgui.input_text_multiline("##content", entry["content"], imgui.ImVec2(-1, 150))
|
||||
|
||||
imgui.end_group()
|
||||
@@ -3437,7 +3446,7 @@ def render_discussion_entry(app: App, entry: dict, index: int) -> None:
|
||||
# Finalize Background Tint
|
||||
draw_list.channels_set_current(0)
|
||||
p_max = imgui.get_item_rect_max()
|
||||
# Ensure full width coverage
|
||||
# Ensure full width coverage of the panel
|
||||
p_max.x = p_min.x + full_width + imgui.get_style().window_padding.x
|
||||
draw_list.add_rect_filled(p_min, p_max, imgui.get_color_u32(bg_col), 4.0)
|
||||
draw_list.channels_merge()
|
||||
@@ -3467,6 +3476,7 @@ def render_discussion_entry_read_mode(app: App, entry: dict, index: int) -> None
|
||||
pattern = re.compile(r"\[Definition: (.*?) from (.*?) \(line (\d+)\)\](\s+```[\s\S]*?```)?")
|
||||
matches = list(pattern.finditer(content))
|
||||
|
||||
# Provide a stable width by using a group and ensuring it starts on a new line
|
||||
imgui.begin_group()
|
||||
with theme.ai_text_style():
|
||||
if not matches:
|
||||
@@ -4173,7 +4183,7 @@ def render_text_viewer_window(app: App) -> None:
|
||||
"""Renders the standalone text/code/markdown viewer window."""
|
||||
if not app.show_windows.get("Text Viewer", False): return
|
||||
imgui.set_next_window_size(imgui.ImVec2(900, 700), imgui.Cond_.first_use_ever)
|
||||
# Use a unique stable ID string to clear any legacy docking conflicts
|
||||
# Force a unique ID to clear legacy docking corruption
|
||||
expanded, opened = imgui.begin(f"{app.text_viewer_title or 'Text Viewer'}###Text_Viewer_Unified", True, imgui.WindowFlags_.no_collapse)
|
||||
app.show_windows["Text Viewer"] = bool(opened)
|
||||
if not opened:
|
||||
@@ -4217,7 +4227,6 @@ def render_text_viewer_window(app: App) -> None:
|
||||
if imgui.button("Close"): imgui.close_current_popup()
|
||||
imgui.end_popup()
|
||||
|
||||
imgui.separator()
|
||||
to_remove = -1
|
||||
tags = app.controller.project.get("context_tags", ["auto-ast", "bug", "feature", "important"])
|
||||
for idx, slc in enumerate(app.ui_editing_slices_file.custom_slices):
|
||||
@@ -4225,13 +4234,13 @@ def render_text_viewer_window(app: App) -> None:
|
||||
current_tag = slc.get('tag', '')
|
||||
if current_tag not in tags and current_tag: tags.append(current_tag)
|
||||
tag_idx = tags.index(current_tag) if current_tag in tags else 0
|
||||
imgui.set_next_item_width(150)
|
||||
ch_tag, new_tag_idx = imgui.combo("Category/Tag", tag_idx, tags)
|
||||
imgui.set_next_item_width(100)
|
||||
ch_tag, new_tag_idx = imgui.combo("##Tag", tag_idx, tags)
|
||||
if ch_tag: slc['tag'] = tags[new_tag_idx]
|
||||
imgui.same_line(); imgui.set_next_item_width(300); changed_comm, new_comm = imgui.input_text("Note/Comment", slc.get('comment', ''))
|
||||
imgui.same_line(); imgui.set_next_item_width(-30); changed_comm, new_comm = imgui.input_text("##Note", slc.get('comment', ''))
|
||||
if changed_comm: slc['comment'] = new_comm
|
||||
imgui.same_line()
|
||||
if imgui.button("Remove"): to_remove = idx
|
||||
if imgui.button("X"): to_remove = idx
|
||||
imgui.pop_id()
|
||||
if to_remove != -1: app.ui_editing_slices_file.custom_slices.pop(to_remove)
|
||||
imgui.separator()
|
||||
@@ -5375,12 +5384,30 @@ def _check_auto_refresh_context_preview(app: App) -> None:
|
||||
if not any(getattr(f, 'auto_aggregate', False) for f in app.context_files) and not app.screenshots:
|
||||
app.context_preview_text = "# Context Composition Empty\n\nNo files or screenshots have been selected for aggregation."
|
||||
return
|
||||
try:
|
||||
app.controller.context_files = app.context_files
|
||||
res = app.controller._do_generate()
|
||||
app.context_preview_text = res[0]
|
||||
except Exception:
|
||||
app.context_preview_text = "Error generating context preview."
|
||||
|
||||
if getattr(app, "_is_generating_preview", False):
|
||||
app._pending_preview_refresh = True
|
||||
return
|
||||
|
||||
app._is_generating_preview = True
|
||||
def worker():
|
||||
try:
|
||||
app.controller.context_files = app.context_files
|
||||
res = app.controller._do_generate()
|
||||
app.context_preview_text = res[0]
|
||||
except Exception:
|
||||
app.context_preview_text = "Error generating context preview."
|
||||
finally:
|
||||
app._is_generating_preview = False
|
||||
if getattr(app, "_pending_preview_refresh", False):
|
||||
app._pending_preview_refresh = False
|
||||
# This will trigger again on next GUI frame because _last_context_preview_state
|
||||
# will be slightly behind if another change happened during the thread.
|
||||
# Or we just clear the state so it re-triggers.
|
||||
app._last_context_preview_state = None
|
||||
|
||||
import threading
|
||||
threading.Thread(target=worker, daemon=True).start()
|
||||
|
||||
def render_context_preview_window(app: App) -> None:
|
||||
_check_auto_refresh_context_preview(app)
|
||||
|
||||
+2
-1
@@ -39,7 +39,8 @@ class _ScopeId:
|
||||
"""
|
||||
self._id = str_id
|
||||
def __enter__(self):
|
||||
# Always pass string to avoid access violations with certain types in imgui-bundle
|
||||
# Use explicit conversion to avoid any possible nanobind ambiguity
|
||||
# and access violations. String IDs are the most stable in this binding.
|
||||
imgui.push_id(str(self._id))
|
||||
def __exit__(self, *args):
|
||||
imgui.pop_id()
|
||||
|
||||
+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