diff --git a/src/gui_2.py b/src/gui_2.py index e090686..1930c41 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -21,6 +21,7 @@ from src import theme_2 as theme from src import theme_nerv_fx as theme_fx from src import api_hooks import numpy as np +import OpenGL.GL as gl from src import log_registry from src import log_pruner from src import models @@ -28,6 +29,7 @@ from src import app_controller from src import mcp_client from src import markdown_helper from src import bg_shader +from src.shader_manager import ShaderManager import re import subprocess if sys.platform == "win32": @@ -211,7 +213,16 @@ class App: self._nerv_flicker = theme_fx.StatusFlicker() self.ui_tool_filter_category = "All" self.ui_discussion_split_h = 300.0 - self.shader_uniforms = {'crt': 1.0, 'scanline': 0.5, 'bloom': 0.8} + self.shader_uniforms = { + 'crt': 1.0, + 'scanline': 0.5, + 'bloom': 0.8, + 'frosted_blur_radius': theme.get_frosted_blur_radius(), + 'frosted_tint_intensity': theme.get_frosted_tint_intensity(), + 'frosted_opacity': theme.get_frosted_opacity() + } + + self.shader_manager = ShaderManager() def _handle_approve_tool(self, user_data=None) -> None: """UI-level wrapper for approving a pending tool execution ask.""" @@ -341,6 +352,84 @@ class App: imgui.pop_style_var(2) imgui.pop_id() + def _render_frosted_background(self, pos: imgui.ImVec2, size: imgui.ImVec2) -> None: + if size.x <= 0 or size.y <= 0: return + if self.shader_manager.fbo_width != int(size.x) or self.shader_manager.fbo_height != int(size.y): + self.shader_manager.setup_capture_fbo(int(size.x), int(size.y)) + display_size = imgui.get_io().display_size + gl.glBindTexture(gl.GL_TEXTURE_2D, self.shader_manager.capture_tex) + gl_y = int(display_size.y - pos.y - size.y) + gl.glCopyTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA, int(pos.x), gl_y, int(size.x), int(size.y), 0) + gl.glBindTexture(gl.GL_TEXTURE_2D, 0) + self.shader_manager.capture_begin(int(size.x), int(size.y)) + self.shader_manager.render_blur( + self.shader_manager.capture_tex, + int(size.x), + int(size.y), + self.shader_uniforms['frosted_blur_radius'], + self.shader_uniforms['frosted_tint_intensity'], + self.shader_uniforms['frosted_opacity'] + ) + self.shader_manager.capture_end() + imgui.get_background_draw_list().add_image( + self.shader_manager.blur_tex, + pos, + imgui.ImVec2(pos.x + size.x, pos.y + size.y) + ) + + def _render_operations_hub(self) -> None: + exp, opened = imgui.begin("Operations Hub", self.show_windows["Operations Hub"]) + self.show_windows["Operations Hub"] = bool(opened) + if exp: + self._render_frosted_background(imgui.get_window_pos(), imgui.get_window_size()) + imgui.text("Focus Agent:") + imgui.same_line() + focus_label = self.ui_focus_agent or "All" + if imgui.begin_combo("##focus_agent", focus_label, imgui.ComboFlags_.width_fit_preview): + if imgui.selectable("All", self.ui_focus_agent is None)[0]: + self.ui_focus_agent = None + for tier in ["Tier 2", "Tier 3", "Tier 4"]: + if imgui.selectable(tier, self.ui_focus_agent == tier)[0]: + self.ui_focus_agent = tier + imgui.end_combo() + imgui.same_line() + if self.ui_focus_agent: + if imgui.button("x##clear_focus"): + self.ui_focus_agent = None + + imgui.push_style_var(imgui.StyleVar_.item_spacing, imgui.ImVec2(10, 4)) + ch1, self.ui_separate_tool_calls_panel = imgui.checkbox("Pop Out Tool Calls", self.ui_separate_tool_calls_panel) + if ch1: self.show_windows["Tool Calls"] = self.ui_separate_tool_calls_panel + imgui.same_line() + ch2, self.ui_separate_usage_analytics = imgui.checkbox("Pop Out Usage Analytics", self.ui_separate_usage_analytics) + if ch2: self.show_windows["Usage Analytics"] = self.ui_separate_usage_analytics + imgui.same_line() + ch3, self.ui_separate_external_tools = imgui.checkbox('Pop Out External Tools', self.ui_separate_external_tools) + if ch3: self.show_windows['External Tools'] = self.ui_separate_external_tools + imgui.pop_style_var() + + show_tc_tab = not self.ui_separate_tool_calls_panel + show_usage_tab = not self.ui_separate_usage_analytics + + if imgui.begin_tab_bar("ops_tabs"): + if imgui.begin_tab_item("Comms History")[0]: + self._render_comms_history_panel() + imgui.end_tab_item() + if show_tc_tab: + if imgui.begin_tab_item("Tool Calls")[0]: + self._render_tool_calls_panel() + imgui.end_tab_item() + if show_usage_tab: + if imgui.begin_tab_item("Usage Analytics")[0]: + self._render_usage_analytics_panel() + imgui.end_tab_item() + if not self.ui_separate_external_tools: + if imgui.begin_tab_item("External Tools")[0]: + self._render_external_tools_panel() + imgui.end_tab_item() + imgui.end_tab_bar() + imgui.end() + def _show_menus(self) -> None: if imgui.begin_menu("manual slop"): if imgui.menu_item("Quit", "Ctrl+Q", False)[0]: @@ -440,6 +529,17 @@ class App: changed_crt, self.shader_uniforms['crt'] = imgui.slider_float('CRT Curvature', self.shader_uniforms['crt'], 0.0, 2.0) changed_scan, self.shader_uniforms['scanline'] = imgui.slider_float('Scanline Intensity', self.shader_uniforms['scanline'], 0.0, 1.0) changed_bloom, self.shader_uniforms['bloom'] = imgui.slider_float('Bloom Threshold', self.shader_uniforms['bloom'], 0.0, 1.0) + + imgui.separator() + imgui.text("Frosted Glass") + changed_fbr, self.shader_uniforms['frosted_blur_radius'] = imgui.slider_float('Blur Radius', self.shader_uniforms['frosted_blur_radius'], 0.0, 32.0) + if changed_fbr: theme.set_frosted_blur_radius(self.shader_uniforms['frosted_blur_radius']) + + changed_fti, self.shader_uniforms['frosted_tint_intensity'] = imgui.slider_float('Tint Intensity', self.shader_uniforms['frosted_tint_intensity'], 0.0, 1.0) + if changed_fti: theme.set_frosted_tint_intensity(self.shader_uniforms['frosted_tint_intensity']) + + changed_fo, self.shader_uniforms['frosted_opacity'] = imgui.slider_float('Frosted Opacity', self.shader_uniforms['frosted_opacity'], 0.0, 1.0) + if changed_fo: theme.set_frosted_opacity(self.shader_uniforms['frosted_opacity']) imgui.end() def _gui_func(self) -> None: @@ -660,56 +760,7 @@ class App: imgui.end() if self.show_windows.get("Operations Hub", False): - exp, opened = imgui.begin("Operations Hub", self.show_windows["Operations Hub"]) - self.show_windows["Operations Hub"] = bool(opened) - if exp: - imgui.text("Focus Agent:") - imgui.same_line() - focus_label = self.ui_focus_agent or "All" - if imgui.begin_combo("##focus_agent", focus_label, imgui.ComboFlags_.width_fit_preview): - if imgui.selectable("All", self.ui_focus_agent is None)[0]: - self.ui_focus_agent = None - for tier in ["Tier 2", "Tier 3", "Tier 4"]: - if imgui.selectable(tier, self.ui_focus_agent == tier)[0]: - self.ui_focus_agent = tier - imgui.end_combo() - imgui.same_line() - if self.ui_focus_agent: - if imgui.button("x##clear_focus"): - self.ui_focus_agent = None - if exp: - imgui.push_style_var(imgui.StyleVar_.item_spacing, imgui.ImVec2(10, 4)) - ch1, self.ui_separate_tool_calls_panel = imgui.checkbox("Pop Out Tool Calls", self.ui_separate_tool_calls_panel) - if ch1: self.show_windows["Tool Calls"] = self.ui_separate_tool_calls_panel - imgui.same_line() - ch2, self.ui_separate_usage_analytics = imgui.checkbox("Pop Out Usage Analytics", self.ui_separate_usage_analytics) - if ch2: self.show_windows["Usage Analytics"] = self.ui_separate_usage_analytics - imgui.same_line() - ch3, self.ui_separate_external_tools = imgui.checkbox('Pop Out External Tools', self.ui_separate_external_tools) - if ch3: self.show_windows['External Tools'] = self.ui_separate_external_tools - imgui.pop_style_var() - - show_tc_tab = not self.ui_separate_tool_calls_panel - show_usage_tab = not self.ui_separate_usage_analytics - - if imgui.begin_tab_bar("ops_tabs"): - if imgui.begin_tab_item("Comms History")[0]: - self._render_comms_history_panel() - imgui.end_tab_item() - if show_tc_tab: - if imgui.begin_tab_item("Tool Calls")[0]: - self._render_tool_calls_panel() - imgui.end_tab_item() - if show_usage_tab: - if imgui.begin_tab_item("Usage Analytics")[0]: - self._render_usage_analytics_panel() - imgui.end_tab_item() - if not self.ui_separate_external_tools: - if imgui.begin_tab_item("External Tools")[0]: - self._render_external_tools_panel() - imgui.end_tab_item() - imgui.end_tab_bar() - imgui.end() + self._render_operations_hub() if self.ui_separate_message_panel and self.show_windows.get("Message", False): exp, opened = imgui.begin("Message", self.show_windows["Message"]) @@ -1904,7 +1955,6 @@ class App: imgui.text(f"History: {key}") hist_data = self.perf_monitor.get_history(key) if hist_data: - import numpy as np imgui.plot_lines(f"##plot_{key}", np.array(hist_data, dtype=np.float32), graph_size=imgui.ImVec2(-1, 60)) else: imgui.text_disabled(f"(no history data for {key})") @@ -2052,6 +2102,7 @@ def hello(): if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_screenshots_panel") def _render_discussion_panel(self) -> None: + self._render_frosted_background(imgui.get_window_pos(), imgui.get_window_size()) if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_discussion_panel") # THINKING indicator is_thinking = self.ai_status in ['sending...', 'streaming...', 'running powershell...'] @@ -3993,6 +4044,8 @@ def hello(): def _post_init(self) -> None: theme.apply_current() + self.shader_manager.setup_background_shader() + self.shader_manager.setup_frosted_glass_shader() def run(self) -> None: """Initializes the ImGui runner and starts the main application loop.""" diff --git a/src/shader_manager.py b/src/shader_manager.py index fd4b67f..2899763 100644 --- a/src/shader_manager.py +++ b/src/shader_manager.py @@ -8,6 +8,7 @@ class ShaderManager: self.blur_program = None self.capture_fbo = None self.capture_tex = None + self.blur_tex = None self.fbo_width = 0 self.fbo_height = 0 @@ -16,14 +17,21 @@ class ShaderManager: gl.glDeleteFramebuffers(1, [self.capture_fbo]) if self.capture_tex is not None: gl.glDeleteTextures(1, [self.capture_tex]) + if self.blur_tex is not None: + gl.glDeleteTextures(1, [self.blur_tex]) self.capture_fbo = gl.glGenFramebuffers(1) self.capture_tex = gl.glGenTextures(1) gl.glBindTexture(gl.GL_TEXTURE_2D, self.capture_tex) gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA, width, height, 0, gl.GL_RGBA, gl.GL_UNSIGNED_BYTE, None) gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR) gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR) + self.blur_tex = gl.glGenTextures(1) + gl.glBindTexture(gl.GL_TEXTURE_2D, self.blur_tex) + gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA, width, height, 0, gl.GL_RGBA, gl.GL_UNSIGNED_BYTE, None) + gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR) + gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR) gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.capture_fbo) - gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER, gl.GL_COLOR_ATTACHMENT0, gl.GL_TEXTURE_2D, self.capture_tex, 0) + gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER, gl.GL_COLOR_ATTACHMENT0, gl.GL_TEXTURE_2D, self.blur_tex, 0) if gl.glCheckFramebufferStatus(gl.GL_FRAMEBUFFER) != gl.GL_FRAMEBUFFER_COMPLETE: raise RuntimeError("Framebuffer not complete") gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0) @@ -232,3 +240,25 @@ void main() { } """ self.blur_program = self.compile_shader(vertex_src, fragment_src) + + def render_blur(self, texture_id, width, height, radius, tint, opacity): + if not self.blur_program: + return + gl.glUseProgram(self.blur_program) + gl.glActiveTexture(gl.GL_TEXTURE0) + gl.glBindTexture(gl.GL_TEXTURE_2D, texture_id) + u_tex_loc = gl.glGetUniformLocation(self.blur_program, "u_texture") + if u_tex_loc != -1: + gl.glUniform1i(u_tex_loc, 0) + u_radius_loc = gl.glGetUniformLocation(self.blur_program, "u_blur_radius") + if u_radius_loc != -1: + gl.glUniform1f(u_radius_loc, float(radius)) + u_tint_loc = gl.glGetUniformLocation(self.blur_program, "u_tint_intensity") + if u_tint_loc != -1: + gl.glUniform1f(u_tint_loc, float(tint)) + u_opacity_loc = gl.glGetUniformLocation(self.blur_program, "u_opacity") + if u_opacity_loc != -1: + gl.glUniform1f(u_opacity_loc, float(opacity)) + gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4) + gl.glBindTexture(gl.GL_TEXTURE_2D, 0) + gl.glUseProgram(0) diff --git a/src/theme_2.py b/src/theme_2.py index e71f508..9b0f426 100644 --- a/src/theme_2.py +++ b/src/theme_2.py @@ -235,6 +235,9 @@ _current_font_size: float = 16.0 _current_scale: float = 1.0 _transparency: float = 1.0 _child_transparency: float = 1.0 +_frosted_blur_radius: float = 8.0 +_frosted_tint_intensity: float = 0.1 +_frosted_opacity: float = 1.0 # ------------------------------------------------------------------ public API @@ -269,6 +272,27 @@ def set_child_transparency(val: float) -> None: _child_transparency = val apply(_current_palette) +def get_frosted_blur_radius() -> float: + return _frosted_blur_radius + +def set_frosted_blur_radius(val: float) -> None: + global _frosted_blur_radius + _frosted_blur_radius = val + +def get_frosted_tint_intensity() -> float: + return _frosted_tint_intensity + +def set_frosted_tint_intensity(val: float) -> None: + global _frosted_tint_intensity + _frosted_tint_intensity = val + +def get_frosted_opacity() -> float: + return _frosted_opacity + +def set_frosted_opacity(val: float) -> None: + global _frosted_opacity + _frosted_opacity = val + def apply(palette_name: str) -> None: """ Apply a named palette by setting all ImGui style colors and applying global professional styling. @@ -350,13 +374,16 @@ def save_to_config(config: dict) -> None: config["theme"]["scale"] = _current_scale config["theme"]["transparency"] = _transparency config["theme"]["child_transparency"] = _child_transparency + config["theme"]["frosted_blur_radius"] = _frosted_blur_radius + config["theme"]["frosted_tint_intensity"] = _frosted_tint_intensity + config["theme"]["frosted_opacity"] = _frosted_opacity 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] from config. Font is handled separately at startup.""" import sys - global _current_font_path, _current_font_size, _current_scale, _current_palette, _transparency, _child_transparency + global _current_font_path, _current_font_size, _current_scale, _current_palette, _transparency, _child_transparency, _frosted_blur_radius, _frosted_tint_intensity, _frosted_opacity t = config.get("theme", {}) sys.stderr.write(f"[DEBUG theme_2] load_from_config raw: {t}\n") sys.stderr.flush() @@ -369,6 +396,9 @@ def load_from_config(config: dict) -> None: _current_scale = float(t.get("scale", 1.0)) _transparency = float(t.get("transparency", 1.0)) _child_transparency = float(t.get("child_transparency", 1.0)) + _frosted_blur_radius = float(t.get("frosted_blur_radius", 8.0)) + _frosted_tint_intensity = float(t.get("frosted_tint_intensity", 0.1)) + _frosted_opacity = float(t.get("frosted_opacity", 1.0)) sys.stderr.write(f"[DEBUG theme_2] load_from_config effective: palette={_current_palette}, transparency={_transparency}\n") sys.stderr.flush() diff --git a/tests/test_gui_frosted_integration.py b/tests/test_gui_frosted_integration.py new file mode 100644 index 0000000..e4520da --- /dev/null +++ b/tests/test_gui_frosted_integration.py @@ -0,0 +1,28 @@ +import pytest +from unittest.mock import patch, MagicMock + +def test_gui_frosted_background_call(): + # Mock ShaderManager and OpenGL functions + with patch("src.gui_2.ShaderManager") as mock_sm_class, \ + patch("src.gui_2.gl") as mock_gl, \ + patch("src.gui_2.imgui") as mock_imgui: + + mock_sm = mock_sm_class.return_value + mock_sm.fbo_width = 0 + mock_sm.fbo_height = 0 + mock_sm.capture_tex = 1 + mock_sm.blur_tex = 2 + + mock_imgui.get_io().display_size = MagicMock(x=1920, y=1080) + + from src.gui_2 import App + app = App() + + # Simulate frame + app._render_frosted_background(pos=MagicMock(x=10, y=10), size=MagicMock(x=100, y=100)) + + assert mock_sm.setup_capture_fbo.called + assert mock_gl.glCopyTexImage2D.called + assert mock_sm.render_blur.called + assert mock_sm.capture_begin.called + assert mock_sm.capture_end.called