Compare commits
12 Commits
80eaf740da
...
4ae606928e
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ae606928e | |||
| 8d79faa22d | |||
| afcb1bf758 | |||
| d9495f6e23 | |||
| ceb0c7d8a8 | |||
| 4f4fa1015c | |||
| ccf4d3354a | |||
| 9c38ea78f9 | |||
| de0d9f339e | |||
| 4b78e77e2c | |||
| 3fa4f64e53 | |||
| 317f8330de |
@@ -7,7 +7,7 @@
|
|||||||
## UX & UI Principles
|
## UX & UI Principles
|
||||||
|
|
||||||
- **USA Graphics Company Values:** Embrace high information density and tactile interactions.
|
- **USA Graphics Company Values:** Embrace high information density and tactile interactions.
|
||||||
- **Professional Arcade Aesthetics:** Balances high-energy "Arcade" feedback (blinking notifications, tactile updates) with a "Professional" visual discipline. Employs modern typography (Inter/Maple Mono), subtle rounded geometry, and soft shadows to ensure the tool feels like a sophisticated, expert utility rather than a toy.
|
- **Professional Arcade Aesthetics:** Balances high-energy "Arcade" feedback (blinking notifications, tactile updates) with a "Professional" visual discipline. Employs modern typography (Inter/Maple Mono), subtle rounded geometry, and soft shadows to ensure the tool feels like a sophisticated, expert utility. Includes a high-density **NERV Technical Console** theme option for maximum focus and CRT-inspired visual feedback.
|
||||||
- **Rich Text Readability:** Prioritizes legibility of AI communications and technical logs by utilizing GitHub-Flavored Markdown and integrated syntax highlighting. This ensures that complex code fragments and structured data are immediately accessible and professionally presented.
|
- **Rich Text Readability:** Prioritizes legibility of AI communications and technical logs by utilizing GitHub-Flavored Markdown and integrated syntax highlighting. This ensures that complex code fragments and structured data are immediately accessible and professionally presented.
|
||||||
- **Explicit Control & Expert Focus:** The interface should not hold the user's hand. It must prioritize explicit manual confirmation for destructive actions while providing dense, unadulterated access to logs and context.
|
- **Explicit Control & Expert Focus:** The interface should not hold the user's hand. It must prioritize explicit manual confirmation for destructive actions while providing dense, unadulterated access to logs and context.
|
||||||
- **Multi-Viewport Capabilities:** Leverage dockable, floatable panels to allow users to build custom workspaces suitable for multi-monitor setups.
|
- **Multi-Viewport Capabilities:** Leverage dockable, floatable panels to allow users to build custom workspaces suitable for multi-monitor setups.
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ For deep implementation details when planning or implementing tracks, consult `d
|
|||||||
- **Clean Project Root:** Enforces a "Cruft-Free Root" policy by organizing core implementation into a `src/` directory and redirecting all temporary test data, configurations, and AI-generated artifacts to `tests/artifacts/`.
|
- **Clean Project Root:** Enforces a "Cruft-Free Root" policy by organizing core implementation into a `src/` directory and redirecting all temporary test data, configurations, and AI-generated artifacts to `tests/artifacts/`.
|
||||||
- **Performance Diagnostics:** Comprehensive, conditional per-component profiling across the entire application. Features a dedicated **Diagnostics Panel** providing real-time telemetry for FPS, Frame Time, CPU usage, and **Detailed Component Timings** for all GUI panels and background threads, including automated threshold-based latency alerts.
|
- **Performance Diagnostics:** Comprehensive, conditional per-component profiling across the entire application. Features a dedicated **Diagnostics Panel** providing real-time telemetry for FPS, Frame Time, CPU usage, and **Detailed Component Timings** for all GUI panels and background threads, including automated threshold-based latency alerts.
|
||||||
- **Automated UX Verification:** A robust IPC mechanism via API hooks and a modular simulation suite allows for human-like simulation walkthroughs and automated regression testing of the full GUI lifecycle across multiple specialized scenarios.
|
- **Automated UX Verification:** A robust IPC mechanism via API hooks and a modular simulation suite allows for human-like simulation walkthroughs and automated regression testing of the full GUI lifecycle across multiple specialized scenarios.
|
||||||
- **Professional UI Theme & Typography:** Implements a high-fidelity visual system featuring **Inter** and **Maple Mono** fonts for optimal readability. Employs a cohesive "Subtle Rounding" aesthetic across all standard widgets, supported by custom **soft shadow shaders** for modals and popups to provide depth and professional polish.
|
- **Professional UI Theme & Typography:** Implements a high-fidelity visual system featuring **Inter** and **Maple Mono** fonts for optimal readability. Employs a cohesive "Subtle Rounding" aesthetic across all standard widgets, supported by custom **soft shadow shaders** for modals and popups to provide depth and professional polish. Includes a selectable **NERV UI theme** featuring a "Black Void" palette, zero-rounding geometry, and CRT-style visual effects (scanlines, status flickering).
|
||||||
- **Rich Text & Syntax Highlighting:** Provides advanced rendering for messages, logs, and tool outputs using a hybrid Markdown system. Supports GitHub-Flavored Markdown (GFM) via `imgui_markdown` and integrates `ImGuiColorTextEdit` for high-performance syntax highlighting of code blocks (Python, JSON, C++, etc.). Includes automated language detection and clickable URL support.
|
- **Rich Text & Syntax Highlighting:** Provides advanced rendering for messages, logs, and tool outputs using a hybrid Markdown system. Supports GitHub-Flavored Markdown (GFM) via `imgui_markdown` and integrates `ImGuiColorTextEdit` for high-performance syntax highlighting of code blocks (Python, JSON, C++, etc.). Includes automated language detection and clickable URL support.
|
||||||
- **Multi-Viewport & Layout Management:** Full support for ImGui Multi-Viewport, allowing users to detach panels into standalone OS windows for complex multi-monitor workflows. Includes a comprehensive **Layout Presets system**, enabling developers to save, name, and instantly restore custom window arrangements, including their Multi-Viewport state.
|
- **Multi-Viewport & Layout Management:** Full support for ImGui Multi-Viewport, allowing users to detach panels into standalone OS windows for complex multi-monitor workflows. Includes a comprehensive **Layout Presets system**, enabling developers to save, name, and instantly restore custom window arrangements, including their Multi-Viewport state.
|
||||||
- **Headless Backend Service:** Optional headless mode allowing the core AI and tool execution logic to run as a decoupled REST API service (FastAPI), optimized for Docker and server-side environments (e.g., Unraid).
|
- **Headless Backend Service:** Optional headless mode allowing the core AI and tool execution logic to run as a decoupled REST API service (FastAPI), optimized for Docker and server-side environments (e.g., Unraid).
|
||||||
|
|||||||
@@ -54,6 +54,6 @@
|
|||||||
- **Synchronous IPC Approval Flow:** A specialized bridge mechanism that allows headless AI providers (like Gemini CLI) to synchronously request and receive human approval for tool calls via the GUI's REST API hooks.
|
- **Synchronous IPC Approval Flow:** A specialized bridge mechanism that allows headless AI providers (like Gemini CLI) to synchronously request and receive human approval for tool calls via the GUI's REST API hooks.
|
||||||
- **High-Fidelity Selectable Labels:** Implements a pattern for making read-only UI text selectable by wrapping `imgui.input_text` with `imgui.InputTextFlags_.read_only`. Includes a specialized `_render_selectable_label` helper that resets frame backgrounds, borders, and padding to mimic standard labels while enabling OS-level clipboard support (Ctrl+C).
|
- **High-Fidelity Selectable Labels:** Implements a pattern for making read-only UI text selectable by wrapping `imgui.input_text` with `imgui.InputTextFlags_.read_only`. Includes a specialized `_render_selectable_label` helper that resets frame backgrounds, borders, and padding to mimic standard labels while enabling OS-level clipboard support (Ctrl+C).
|
||||||
- **Hybrid Markdown Rendering:** Employs a custom `MarkdownRenderer` that orchestrates `imgui_markdown` for standard text and headers while intercepting code blocks to render them via cached `ImGuiColorTextEdit` instances. This ensures high-performance rich text rendering with robust syntax highlighting and stateful text selection.
|
- **Hybrid Markdown Rendering:** Employs a custom `MarkdownRenderer` that orchestrates `imgui_markdown` for standard text and headers while intercepting code blocks to render them via cached `ImGuiColorTextEdit` instances. This ensures high-performance rich text rendering with robust syntax highlighting and stateful text selection.
|
||||||
- **Faux-Shader Visual Effects:** Utilizes an optimized `ImDrawList`-based batching technique to simulate advanced visual effects such as soft shadows and acrylic glass overlays without the overhead of heavy GPU-resident shaders or external OpenGL dependencies.
|
- **Faux-Shader Visual Effects:** Utilizes an optimized `ImDrawList`-based batching technique to simulate advanced visual effects such as soft shadows, acrylic glass overlays, and **CRT scanline overlays** without the overhead of heavy GPU-resident shaders or external OpenGL dependencies. Includes support for **dynamic status flickering** and **alert pulsing** integrated into the NERV theme.
|
||||||
- **Interface-Driven Development (IDD):** Enforces a "Stub-and-Resolve" pattern where cross-module dependencies are resolved by generating signatures/contracts before implementation.
|
- **Interface-Driven Development (IDD):** Enforces a "Stub-and-Resolve" pattern where cross-module dependencies are resolved by generating signatures/contracts before implementation.
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ This file tracks all major tracks for the project. Each track has its own detail
|
|||||||
*Link: [./tracks/markdown_highlighting_20260308/](./tracks/markdown_highlighting_20260308/)*
|
*Link: [./tracks/markdown_highlighting_20260308/](./tracks/markdown_highlighting_20260308/)*
|
||||||
*Goal: Add rich text rendering with GFM support and syntax highlighting for PowerShell, Python, and JSON/TOML in read-only message and log views.*
|
*Goal: Add rich text rendering with GFM support and syntax highlighting for PowerShell, Python, and JSON/TOML in read-only message and log views.*
|
||||||
|
|
||||||
|
5. [x] **Track: NERV UI Theme Integration**
|
||||||
|
*Link: [./tracks/nerv_ui_theme_20260309/](./tracks/nerv_ui_theme_20260309/)*
|
||||||
|
*Goal: Implement a NERV UI theme for ImGui/Dear PyGui, inspired by technical/military consoles, with CRT effects and a black-void aesthetic.*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### C/C++ Language Support
|
### C/C++ Language Support
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Implementation Plan: NERV UI Theme
|
||||||
|
|
||||||
|
## Phase 1: Research & Theme Infrastructure [checkpoint: 4b78e77]
|
||||||
|
- [x] Task: Research existing theme implementation in src/theme.py and src/theme_2.py. 3fa4f64
|
||||||
|
- [x] Task: Create a new src/theme_nerv.py to house the NERV color constants and theme application logic. 3fa4f64
|
||||||
|
- [x] Task: Conductor - User Manual Verification 'Phase 1: Research & Theme Infrastructure' (Protocol in workflow.md) 4b78e77
|
||||||
|
|
||||||
|
## Phase 2: Base NERV Theme Implementation (Colors & Geometry) [checkpoint: 9c38ea7]
|
||||||
|
- [x] Task: Implement the "Black Void" and "Phosphor" color palette in src/theme_nerv.py. 3fa4f64
|
||||||
|
- [x] Task: Implement "Hard Edges" by setting all rounding parameters to 0.0 in the NERV theme. 3fa4f64
|
||||||
|
- [x] Task: Write unit tests to verify that the NERV theme correctly applies colors and geometry settings. de0d9f3
|
||||||
|
- [x] Task: Conductor - User Manual Verification 'Phase 2: Base NERV Theme Implementation' (Protocol in workflow.md) 9c38ea7
|
||||||
|
|
||||||
|
## Phase 3: Visual Effects (Scanlines & Status Flickering) [checkpoint: 4f4fa10]
|
||||||
|
- [x] Task: Research how to implement a scanline overlay in ImGui (e.g., using a full-screen transparent texture or a custom draw list). 05a2b8e
|
||||||
|
- [x] Task: Implement the subtle scanline overlay (6% opacity). 05a2b8e
|
||||||
|
- [x] Task: Implement "Status Flickering" logic for active system indicators (e.g., a periodic alpha modification for specific text elements). 05a2b8e
|
||||||
|
- [x] Task: Write tests to verify the visual effect triggers (e.g., checking if the scanline overlay is rendered). 4f4fa10
|
||||||
|
- [x] Task: Conductor - User Manual Verification 'Phase 3: Visual Effects' (Protocol in workflow.md) 4f4fa10
|
||||||
|
|
||||||
|
## Phase 4: Alert Pulsing & Error States
|
||||||
|
- [ ] Task: Implement "Alert Pulsing" logic that can be triggered by application error events.
|
||||||
|
- [ ] Task: Integrate Alert Pulsing with the NERV theme (shifting borders/background to Alert Red).
|
||||||
|
- [ ] Task: Write tests to verify that an error state triggers the pulsing effect in the NERV theme.
|
||||||
|
- [ ] Task: Conductor - User Manual Verification 'Phase 4: Alert Pulsing & Error States' (Protocol in workflow.md)
|
||||||
|
|
||||||
|
## Phase 5: Integration & Theme Selector
|
||||||
|
- [ ] Task: Add "NERV" to the theme selection dropdown in src/gui_2.py.
|
||||||
|
- [ ] Task: Ensure that switching to the NERV theme correctly initializes all visual effects (scanlines, etc.).
|
||||||
|
- [ ] Task: Final UX verification and performance check of the NERV theme.
|
||||||
|
- [ ] Task: Conductor - User Manual Verification 'Phase 5: Integration & Theme Selector' (Protocol in workflow.md)
|
||||||
@@ -15,6 +15,7 @@ from src import session_logger
|
|||||||
from src import project_manager
|
from src import project_manager
|
||||||
from src import paths
|
from src import paths
|
||||||
from src import theme_2 as theme
|
from src import theme_2 as theme
|
||||||
|
from src import theme_nerv_fx as theme_fx
|
||||||
from src import api_hooks
|
from src import api_hooks
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from src import log_registry
|
from src import log_registry
|
||||||
@@ -130,6 +131,9 @@ class App:
|
|||||||
self._token_stats: dict[str, Any] = {}
|
self._token_stats: dict[str, Any] = {}
|
||||||
self._token_stats_dirty: bool = True
|
self._token_stats_dirty: bool = True
|
||||||
self.perf_history: dict[str, list] = {"frame_time": [0.0] * 100, "fps": [0.0] * 100}
|
self.perf_history: dict[str, list] = {"frame_time": [0.0] * 100, "fps": [0.0] * 100}
|
||||||
|
self._nerv_scanlines = theme_fx.ScanlineOverlay()
|
||||||
|
self._nerv_alert = theme_fx.AlertPulsing()
|
||||||
|
self._nerv_flicker = theme_fx.StatusFlicker()
|
||||||
|
|
||||||
def _handle_approve_tool(self, user_data=None) -> None:
|
def _handle_approve_tool(self, user_data=None) -> None:
|
||||||
"""UI-level wrapper for approving a pending tool execution ask."""
|
"""UI-level wrapper for approving a pending tool execution ask."""
|
||||||
@@ -294,6 +298,12 @@ class App:
|
|||||||
ws = imgui.get_io().display_size
|
ws = imgui.get_io().display_size
|
||||||
bg.render(ws.x, ws.y)
|
bg.render(ws.x, ws.y)
|
||||||
|
|
||||||
|
if theme.is_nerv_active():
|
||||||
|
ws = imgui.get_io().display_size
|
||||||
|
self._nerv_alert.update(self.ai_status)
|
||||||
|
self._nerv_alert.render(ws.x, ws.y)
|
||||||
|
self._nerv_scanlines.render(ws.x, ws.y)
|
||||||
|
|
||||||
pushed_prior_tint = False
|
pushed_prior_tint = False
|
||||||
if self.perf_profiling_enabled: self.perf_monitor.start_component("_gui_func")
|
if self.perf_profiling_enabled: self.perf_monitor.start_component("_gui_func")
|
||||||
if self.is_viewing_prior_session:
|
if self.is_viewing_prior_session:
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ Scale uses imgui.get_style().font_scale_main.
|
|||||||
|
|
||||||
from imgui_bundle import imgui, hello_imgui
|
from imgui_bundle import imgui, hello_imgui
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
import src.theme_nerv
|
||||||
|
|
||||||
# ------------------------------------------------------------------ palettes
|
# ------------------------------------------------------------------ palettes
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ def _c(r: int, g: int, b: int, a: int = 255) -> tuple[float, float, float, float
|
|||||||
|
|
||||||
_PALETTES: dict[str, dict[int, tuple]] = {
|
_PALETTES: dict[str, dict[int, tuple]] = {
|
||||||
"ImGui Dark": {}, # empty = use imgui dark defaults
|
"ImGui Dark": {}, # empty = use imgui dark defaults
|
||||||
|
"NERV": {},
|
||||||
"10x Dark": {
|
"10x Dark": {
|
||||||
imgui.Col_.window_bg: _c( 34, 32, 28),
|
imgui.Col_.window_bg: _c( 34, 32, 28),
|
||||||
imgui.Col_.child_bg: _c( 30, 28, 24),
|
imgui.Col_.child_bg: _c( 30, 28, 24),
|
||||||
@@ -239,6 +241,9 @@ _child_transparency: float = 1.0
|
|||||||
def get_current_palette() -> str:
|
def get_current_palette() -> str:
|
||||||
return _current_palette
|
return _current_palette
|
||||||
|
|
||||||
|
def is_nerv_active() -> bool:
|
||||||
|
return _current_palette == "NERV"
|
||||||
|
|
||||||
def get_current_font_path() -> str:
|
def get_current_font_path() -> str:
|
||||||
return _current_font_path
|
return _current_font_path
|
||||||
|
|
||||||
@@ -270,6 +275,9 @@ def apply(palette_name: str) -> None:
|
|||||||
"""
|
"""
|
||||||
global _current_palette
|
global _current_palette
|
||||||
_current_palette = palette_name
|
_current_palette = palette_name
|
||||||
|
if palette_name == 'NERV':
|
||||||
|
src.theme_nerv.apply_nerv()
|
||||||
|
return
|
||||||
|
|
||||||
# 1. Apply base colors
|
# 1. Apply base colors
|
||||||
if palette_name in _PALETTES:
|
if palette_name in _PALETTES:
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
from imgui_bundle import imgui
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
NERV_ORANGE = _c(255, 152, 48)
|
||||||
|
DATA_GREEN = _c(80, 255, 80)
|
||||||
|
WIRE_CYAN = _c(32, 240, 255)
|
||||||
|
ALERT_RED = _c(255, 72, 64)
|
||||||
|
STEEL = _c(224, 224, 216)
|
||||||
|
BLACK = _c(0, 0, 0)
|
||||||
|
|
||||||
|
NERV_PALETTE = {
|
||||||
|
imgui.Col_.text: NERV_ORANGE,
|
||||||
|
imgui.Col_.window_bg: BLACK,
|
||||||
|
imgui.Col_.child_bg: BLACK,
|
||||||
|
imgui.Col_.popup_bg: BLACK,
|
||||||
|
imgui.Col_.border: NERV_ORANGE,
|
||||||
|
imgui.Col_.border_shadow: _c(0, 0, 0, 0),
|
||||||
|
imgui.Col_.frame_bg: BLACK,
|
||||||
|
imgui.Col_.frame_bg_hovered: _c(255, 152, 48, 40),
|
||||||
|
imgui.Col_.frame_bg_active: _c(255, 152, 48, 80),
|
||||||
|
imgui.Col_.title_bg: BLACK,
|
||||||
|
imgui.Col_.title_bg_active: NERV_ORANGE,
|
||||||
|
imgui.Col_.title_bg_collapsed: BLACK,
|
||||||
|
imgui.Col_.menu_bar_bg: BLACK,
|
||||||
|
imgui.Col_.scrollbar_bg: BLACK,
|
||||||
|
imgui.Col_.scrollbar_grab: NERV_ORANGE,
|
||||||
|
imgui.Col_.scrollbar_grab_hovered: STEEL,
|
||||||
|
imgui.Col_.scrollbar_grab_active: WIRE_CYAN,
|
||||||
|
imgui.Col_.check_mark: DATA_GREEN,
|
||||||
|
imgui.Col_.slider_grab: WIRE_CYAN,
|
||||||
|
imgui.Col_.slider_grab_active: DATA_GREEN,
|
||||||
|
imgui.Col_.button: BLACK,
|
||||||
|
imgui.Col_.button_hovered: NERV_ORANGE,
|
||||||
|
imgui.Col_.button_active: ALERT_RED,
|
||||||
|
imgui.Col_.header: _c(255, 152, 48, 120),
|
||||||
|
imgui.Col_.header_hovered: NERV_ORANGE,
|
||||||
|
imgui.Col_.header_active: ALERT_RED,
|
||||||
|
imgui.Col_.separator: STEEL,
|
||||||
|
imgui.Col_.separator_hovered: WIRE_CYAN,
|
||||||
|
imgui.Col_.separator_active: DATA_GREEN,
|
||||||
|
imgui.Col_.resize_grip: NERV_ORANGE,
|
||||||
|
imgui.Col_.resize_grip_hovered: STEEL,
|
||||||
|
imgui.Col_.resize_grip_active: WIRE_CYAN,
|
||||||
|
imgui.Col_.tab: BLACK,
|
||||||
|
imgui.Col_.tab_hovered: NERV_ORANGE,
|
||||||
|
imgui.Col_.tab_selected: NERV_ORANGE,
|
||||||
|
imgui.Col_.tab_dimmed: BLACK,
|
||||||
|
imgui.Col_.tab_dimmed_selected: _c(255, 152, 48, 80),
|
||||||
|
imgui.Col_.plot_lines: WIRE_CYAN,
|
||||||
|
imgui.Col_.plot_lines_hovered: DATA_GREEN,
|
||||||
|
imgui.Col_.plot_histogram: DATA_GREEN,
|
||||||
|
imgui.Col_.plot_histogram_hovered: WIRE_CYAN,
|
||||||
|
imgui.Col_.text_selected_bg: _c(255, 152, 48, 100),
|
||||||
|
imgui.Col_.drag_drop_target: DATA_GREEN,
|
||||||
|
imgui.Col_.nav_cursor: NERV_ORANGE,
|
||||||
|
imgui.Col_.nav_windowing_highlight: DATA_GREEN,
|
||||||
|
imgui.Col_.nav_windowing_dim_bg: _c(0, 0, 0, 150),
|
||||||
|
imgui.Col_.modal_window_dim_bg: _c(0, 0, 0, 150),
|
||||||
|
}
|
||||||
|
|
||||||
|
def apply_nerv() -> None:
|
||||||
|
"""Apply NERV theme with hard edges and specific palette."""
|
||||||
|
style = imgui.get_style()
|
||||||
|
for col_enum, rgba in NERV_PALETTE.items():
|
||||||
|
style.set_color_(col_enum, imgui.ImVec4(*rgba))
|
||||||
|
|
||||||
|
# Hard Edges
|
||||||
|
style.window_rounding = 0.0
|
||||||
|
style.child_rounding = 0.0
|
||||||
|
style.frame_rounding = 0.0
|
||||||
|
style.popup_rounding = 0.0
|
||||||
|
style.scrollbar_rounding = 0.0
|
||||||
|
style.grab_rounding = 0.0
|
||||||
|
style.tab_rounding = 0.0
|
||||||
|
|
||||||
|
# Border sizes
|
||||||
|
style.window_border_size = 1.0
|
||||||
|
style.frame_border_size = 1.0
|
||||||
|
style.popup_border_size = 1.0
|
||||||
|
style.child_border_size = 1.0
|
||||||
|
style.tab_border_size = 1.0
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import time
|
||||||
|
import math
|
||||||
|
from imgui_bundle import imgui
|
||||||
|
|
||||||
|
class ScanlineOverlay:
|
||||||
|
def __init__(self):
|
||||||
|
self.enabled = True
|
||||||
|
|
||||||
|
def render(self, width: float, height: float):
|
||||||
|
if not self.enabled:
|
||||||
|
return
|
||||||
|
draw_list = imgui.get_foreground_draw_list()
|
||||||
|
color = imgui.get_color_u32((0.0, 0.0, 0.0, 0.06))
|
||||||
|
for y in range(0, int(height), 2):
|
||||||
|
draw_list.add_line((0.0, float(y)), (float(width), float(y)), color, 1.0)
|
||||||
|
|
||||||
|
class StatusFlicker:
|
||||||
|
def get_alpha(self) -> float:
|
||||||
|
# Modulate between 0.7 and 1.0 using sin wave
|
||||||
|
return 0.85 + 0.15 * math.sin(time.time() * 20.0)
|
||||||
|
|
||||||
|
class AlertPulsing:
|
||||||
|
def __init__(self):
|
||||||
|
self.active = False
|
||||||
|
|
||||||
|
def update(self, status: str):
|
||||||
|
self.active = status.lower().startswith("error")
|
||||||
|
|
||||||
|
def render(self, width: float, height: float):
|
||||||
|
if not self.active:
|
||||||
|
return
|
||||||
|
draw_list = imgui.get_foreground_draw_list()
|
||||||
|
# sin(t) is between -1 and 1
|
||||||
|
# scale to 0 to 1: (sin(t) + 1) / 2
|
||||||
|
# multiply by (0.2 - 0.05) = 0.15 and add 0.05
|
||||||
|
alpha = 0.05 + 0.15 * ((math.sin(time.time() * 4.0) + 1.0) / 2.0)
|
||||||
|
color = imgui.get_color_u32((1.0, 0.0, 0.0, alpha))
|
||||||
|
draw_list.add_rect((0.0, 0.0), (width, height), color, 0.0, 0, 10.0)
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import pytest
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
from imgui_bundle import imgui
|
||||||
|
from src import theme_nerv
|
||||||
|
|
||||||
|
def test_apply_nerv_sets_rounding_and_colors(monkeypatch):
|
||||||
|
# Mock imgui on the module level to intercept calls in apply_nerv
|
||||||
|
mock_style = MagicMock()
|
||||||
|
mock_imgui = MagicMock()
|
||||||
|
mock_imgui.get_style.return_value = mock_style
|
||||||
|
# Mock ImVec4 to return its arguments as a tuple to verify values
|
||||||
|
mock_imgui.ImVec4.side_effect = lambda r, g, b, a=1.0: (r, g, b, a)
|
||||||
|
|
||||||
|
monkeypatch.setattr(theme_nerv, "imgui", mock_imgui)
|
||||||
|
|
||||||
|
# Call apply_nerv
|
||||||
|
theme_nerv.apply_nerv()
|
||||||
|
|
||||||
|
# Verify rounding styles (must be 0.0 for NERV theme)
|
||||||
|
assert mock_style.window_rounding == 0.0
|
||||||
|
assert mock_style.child_rounding == 0.0
|
||||||
|
assert mock_style.frame_rounding == 0.0
|
||||||
|
assert mock_style.popup_rounding == 0.0
|
||||||
|
assert mock_style.scrollbar_rounding == 0.0
|
||||||
|
assert mock_style.grab_rounding == 0.0
|
||||||
|
assert mock_style.tab_rounding == 0.0
|
||||||
|
|
||||||
|
# Verify borders
|
||||||
|
assert mock_style.window_border_size == 1.0
|
||||||
|
assert mock_style.frame_border_size == 1.0
|
||||||
|
assert mock_style.popup_border_size == 1.0
|
||||||
|
assert mock_style.child_border_size == 1.0
|
||||||
|
assert mock_style.tab_border_size == 1.0
|
||||||
|
|
||||||
|
# Verify key colors
|
||||||
|
# window_bg should be BLACK (0, 0, 0, 1.0)
|
||||||
|
# text should be NERV_ORANGE (255/255.0, 152/255.0, 48/255.0, 1.0)
|
||||||
|
|
||||||
|
# Extract calls to set_color_
|
||||||
|
# Using real imgui.Col_ values for keys because they are bound in NERV_PALETTE at import time
|
||||||
|
color_calls = {call[0][0]: call[0][1] for call in mock_style.set_color_.call_args_list}
|
||||||
|
|
||||||
|
assert imgui.Col_.window_bg in color_calls
|
||||||
|
assert color_calls[imgui.Col_.window_bg] == (0.0, 0.0, 0.0, 1.0)
|
||||||
|
|
||||||
|
assert imgui.Col_.text in color_calls
|
||||||
|
assert color_calls[imgui.Col_.text] == (1.0, 152/255.0, 48/255.0, 1.0)
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import pytest
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
from imgui_bundle import imgui
|
||||||
|
from src.theme_nerv_fx import AlertPulsing
|
||||||
|
|
||||||
|
def test_alert_pulsing_update():
|
||||||
|
ap = AlertPulsing()
|
||||||
|
assert ap.active is False
|
||||||
|
|
||||||
|
ap.update("error: something failed")
|
||||||
|
assert ap.active is True
|
||||||
|
|
||||||
|
ap.update("ok: all good")
|
||||||
|
assert ap.active is False
|
||||||
|
|
||||||
|
ap.update("Error: Case Insensitive")
|
||||||
|
assert ap.active is True
|
||||||
|
|
||||||
|
def test_alert_pulsing_render_inactive():
|
||||||
|
ap = AlertPulsing()
|
||||||
|
ap.active = False
|
||||||
|
|
||||||
|
with patch("src.theme_nerv_fx.imgui.get_foreground_draw_list") as mock_get_draw_list:
|
||||||
|
ap.render(100.0, 100.0)
|
||||||
|
mock_get_draw_list.assert_not_called()
|
||||||
|
|
||||||
|
def test_alert_pulsing_render_active():
|
||||||
|
ap = AlertPulsing()
|
||||||
|
ap.active = True
|
||||||
|
|
||||||
|
mock_draw_list = MagicMock()
|
||||||
|
with patch("src.theme_nerv_fx.imgui.get_foreground_draw_list", return_value=mock_draw_list) as mock_get_draw_list, \
|
||||||
|
patch("src.theme_nerv_fx.imgui.get_color_u32", return_value=0xFF0000FF) as mock_get_color, \
|
||||||
|
patch("time.time", return_value=1.0):
|
||||||
|
|
||||||
|
ap.render(800.0, 600.0)
|
||||||
|
|
||||||
|
mock_get_draw_list.assert_called_once()
|
||||||
|
mock_get_color.assert_called_once()
|
||||||
|
mock_draw_list.add_rect.assert_called_once()
|
||||||
|
|
||||||
|
# Check arguments of add_rect
|
||||||
|
# add_rect(p_min, p_max, col, rounding, flags, thickness)
|
||||||
|
args, kwargs = mock_draw_list.add_rect.call_args
|
||||||
|
assert args[0] == (0.0, 0.0)
|
||||||
|
assert args[1] == (800.0, 600.0)
|
||||||
|
assert args[2] == 0xFF0000FF
|
||||||
|
assert args[3] == 0.0
|
||||||
|
assert args[4] == 0
|
||||||
|
assert args[5] == 10.0
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import unittest
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
import math
|
||||||
|
from src.theme_nerv_fx import ScanlineOverlay, StatusFlicker
|
||||||
|
|
||||||
|
class TestThemeNervFx(unittest.TestCase):
|
||||||
|
@patch("src.theme_nerv_fx.imgui")
|
||||||
|
def test_scanline_overlay_render(self, mock_imgui):
|
||||||
|
# Setup
|
||||||
|
mock_draw_list = MagicMock()
|
||||||
|
mock_imgui.get_foreground_draw_list.return_value = mock_draw_list
|
||||||
|
mock_imgui.get_color_u32.return_value = 0x12345678
|
||||||
|
|
||||||
|
overlay = ScanlineOverlay()
|
||||||
|
width, height = 100.0, 10.0
|
||||||
|
|
||||||
|
# Act
|
||||||
|
overlay.render(width, height)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
mock_imgui.get_foreground_draw_list.assert_called_once()
|
||||||
|
# height is 10, range(0, 10, 2) is [0, 2, 4, 6, 8] -> 5 calls
|
||||||
|
self.assertEqual(mock_draw_list.add_line.call_count, 5)
|
||||||
|
|
||||||
|
# Verify some calls
|
||||||
|
mock_draw_list.add_line.assert_any_call((0.0, 0.0), (100.0, 0.0), 0x12345678, 1.0)
|
||||||
|
mock_draw_list.add_line.assert_any_call((0.0, 8.0), (100.0, 8.0), 0x12345678, 1.0)
|
||||||
|
|
||||||
|
@patch("src.theme_nerv_fx.time")
|
||||||
|
def test_status_flicker_get_alpha(self, mock_time):
|
||||||
|
flicker = StatusFlicker()
|
||||||
|
|
||||||
|
# Test at multiple points in time to ensure range [0.7, 1.0]
|
||||||
|
# sin(0) = 0 -> 0.85
|
||||||
|
mock_time.time.return_value = 0.0
|
||||||
|
self.assertAlmostEqual(flicker.get_alpha(), 0.85)
|
||||||
|
|
||||||
|
# sin(pi/2) = 1 -> 1.0
|
||||||
|
# time * 20.0 = pi/2 -> time = pi/40
|
||||||
|
mock_time.time.return_value = math.pi / 40.0
|
||||||
|
self.assertAlmostEqual(flicker.get_alpha(), 1.0)
|
||||||
|
|
||||||
|
# sin(3pi/2) = -1 -> 0.7
|
||||||
|
# time * 20.0 = 3pi/2 -> time = 3pi/40
|
||||||
|
mock_time.time.return_value = 3 * math.pi / 40.0
|
||||||
|
self.assertAlmostEqual(flicker.get_alpha(), 0.7)
|
||||||
|
|
||||||
|
# Verify range for many samples
|
||||||
|
for i in range(100):
|
||||||
|
mock_time.time.return_value = i * 0.1
|
||||||
|
alpha = flicker.get_alpha()
|
||||||
|
self.assertGreaterEqual(alpha, 0.7)
|
||||||
|
self.assertLessEqual(alpha, 1.0)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user