12 Commits

12 changed files with 331 additions and 3 deletions
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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).
+1 -1
View File
@@ -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.
+4
View File
@@ -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)
+10
View File
@@ -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:
+8
View File
@@ -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:
+84
View File
@@ -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
+38
View File
@@ -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)
+47
View File
@@ -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)
+50
View File
@@ -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
+56
View File
@@ -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()