diff --git a/conductor/code_styleguides/python.md b/conductor/code_styleguides/python.md index 7fdc0ae..d524f98 100644 --- a/conductor/code_styleguides/python.md +++ b/conductor/code_styleguides/python.md @@ -131,7 +131,31 @@ max-locals = 8 max-args = 5 ``` -## 11. Structural Dependency Mapping (SDM) +### 11. ImGui Defer Patterns + +To prevent `PopID` or `End` leaks in immediate-mode rendering, and to keep code flat (0-1 levels of nesting) for AI agents, use the following patterns: + +- **The Context Manager Pattern (Mandatory for complex blocks):** + Wrap all `Begin/End` blocks in `imscope` context managers (from `src/imgui_scopes.py`). + ```python + with imscope.window("My Window") as (exp, opened): + if exp: + imgui.text("Hello") + + with imscope.tab_item("My Tab") as (exp, _): + if exp: + self._render_tab_content() + ``` + This adds only 1 space of indentation (project standard) and guarantees the corresponding `End` is called even on early returns or exceptions. **Crucial:** Always check the `exp` (expanded/visible) state before rendering content to avoid ID conflicts and performance overhead. + +- **The Flat Dispatch Pattern (Recommended for the main loop):** + To avoid nesting multiple window checks, use a dispatch helper that encapsulates the state check and the scope. + ```python + self._render_window_if_open("My Window", self._render_my_panel) + ``` + This keeps the main GUI loop as a flat sequence of declarative calls. + +## 12. Structural Dependency Mapping (SDM) To assist AI agents in evaluating refactoring impact across dynamic codebases, all major definitions SHOULD include terse SDM tags at the end of their docstrings. diff --git a/conductor/tracks.md b/conductor/tracks.md index 4a856b8..6198a2a 100644 --- a/conductor/tracks.md +++ b/conductor/tracks.md @@ -54,6 +54,10 @@ This file tracks all major tracks for the project. Each track has its own detail *Link: [./tracks/context_comp_presets_20260510/](./tracks/context_comp_presets_20260510/)* *Goal: Implement Context Preset save/load with validation, and Context Preview before sending to agent.* +12. [ ] **Track: GUI Architecture Refinement & AI-Friendliness** + *Link: [./tracks/gui_architecture_refinement_20260512/](./tracks/gui_architecture_refinement_20260512/)* + *Goal: Reduce nesting and compactness of ImGui code in `gui_2.py`, and formalize ImGui Defer patterns.* + --- ## Hot Reload Feature diff --git a/conductor/tracks/gui_architecture_refinement_20260512/plan.md b/conductor/tracks/gui_architecture_refinement_20260512/plan.md new file mode 100644 index 0000000..4e9cf1f --- /dev/null +++ b/conductor/tracks/gui_architecture_refinement_20260512/plan.md @@ -0,0 +1,45 @@ +# Plan: GUI Architecture Refinement & AI-Friendliness + +**Track ID:** gui_architecture_refinement_20260512 +**Status:** [~] Draft + +## Objective +Reduce nesting and improve compactness of ImGui code in `gui_2.py` to make it more AI-friendly. Formalize the "defer/scope" patterns (inspired by Go's `defer` and Ryan Fleury's macros) in the project style guides to prevent `PopID` / `End` leaks. + +## Background & Motivation +The main GUI render loop (`_gui_func__abusrd_try_scope`) has grown to over 600 lines with deep nesting. Raw `imgui.begin()` and `imgui.end()` calls are prone to leaks if an early return occurs or if the return value of `begin` is ignored. While `imscope` context managers solve the leak issue, they still introduce nesting. We need a way to keep the code extremely flat (0-1 levels of nesting) while maintaining safety. + +## Proposed Solution + +### 1. Update Style Guides (`python.md` & `workflow.md`) +Introduce a new section explicitly defining the "ImGui Defer Patterns": +- **The Context Manager Pattern:** Use `with imscope.window("Name"):` to automatically handle `End()`. This adds only 1 space of indentation (per project rules). +- **The Flat Dispatch Pattern:** To avoid nesting multiple windows, use dispatch helpers like `self._render_window_if_open(name, render_func)` which encapsulate the state-checking, `Begin`, `End`, and execution logic. + +### 2. Implement Flat Dispatch Helper +Create a helper method in `App`: +```python + def _render_window_if_open(self, name: str, render_func: Callable[[], None], flag_condition: bool = True) -> None: + if not flag_condition or not self.show_windows.get(name, False): return + with imscope.window(name, self.show_windows[name]) as (exp, opened): + self.show_windows[name] = bool(opened) + if exp: render_func() +``` + +### 3. Refactor `gui_2.py` +- Extract inline hub definitions (e.g., "Operations Hub", "Discussion Hub", "AI Settings") from `_gui_func__abusrd_try_scope` into dedicated methods (`_render_operations_hub`, etc.). +- Replace the massive `if self.show_windows.get...` blocks in `_gui_func__abusrd_try_scope` with a flat sequence of `_render_window_if_open` calls. +- Rename `_gui_func__abusrd_try_scope` to a cleaner name (e.g., `_gui_func_body`) once stabilized. + +## Implementation Steps + +1. [x] Edit `conductor/code_styleguides/python.md` to add "ImGui Defer Patterns" under the "AI-Agent Specific Conventions" or "Anti-OOP" section. +2. [x] Edit `conductor/workflow.md` to reference the mandatory use of `imscope` or dispatch helpers for ImGui code. +3. [x] Add `_render_window_if_open` to `gui_2.py`. +4. [x] Extract `_render_operations_hub`, `_render_discussion_hub`, and `_render_ai_settings_hub` in `gui_2.py`. +5. [x] Flatten `_gui_func__abusrd_try_scope` using the new helper. + +## Verification & Testing +- Ensure the app launches successfully without `PopID` errors. +- Verify that toggling windows via the menu still opens and closes them correctly. +- Run `uv run pytest tests/test_gui_startup_smoke.py` and `uv run pytest tests/test_gui_window_controls.py`. \ No newline at end of file diff --git a/conductor/workflow.md b/conductor/workflow.md index 3867ada..55af969 100644 --- a/conductor/workflow.md +++ b/conductor/workflow.md @@ -9,6 +9,7 @@ - Use `./scripts/ai_style_formatter.py` for formatting validation - **NO COMMENTS** unless explicitly requested - Type hints required for all public functions +- **ImGui Defer Patterns:** Use `imscope` context managers or `_render_window_if_open` dispatch helpers to prevent resource leaks and keep the main loop flat. See `conductor/code_styleguides/python.md` for details. ### CRITICAL: Native Edit Tool Destroys Indentation diff --git a/src/gui_2.py b/src/gui_2.py index 04152ea..cce759d 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -653,6 +653,67 @@ class App: imgui.pop_style_var(2) imgui.pop_id() + def _render_window_if_open(self, name: str, render_func: Callable[[], None], flag_condition: bool = True) -> None: + """Helper to render a window only if its toggle is active.""" + if not flag_condition or not self.show_windows.get(name, False): return + with imscope.window(name, self.show_windows[name]) as (exp, opened): + self.show_windows[name] = bool(opened) + if exp: render_func() + + def _render_project_settings_hub(self) -> None: + with imscope.tab_bar('context_hub_tabs'): + with imscope.tab_item('Projects'): self._render_projects_panel() + with imscope.tab_item('Paths'): self._render_paths_panel() + + def _render_ai_settings_hub(self) -> None: + self._render_persona_selector_panel() + if imgui.collapsing_header("Provider & Model"): self._render_provider_panel() + if imgui.collapsing_header("System Prompts"): self._render_system_prompts_panel() + if imgui.collapsing_header("RAG Settings"): self._render_rag_panel() + self._render_agent_tools_panel() + + def _render_discussion_hub(self) -> None: + with imscope.tab_bar("discussion_hub_tabs"): + with imscope.tab_item("Discussion"): self._render_discussion_tab() + with imscope.tab_item("Context Composition"): self._render_context_composition_panel() + with imscope.tab_item("Snapshot"): self._render_snapshot_tab() + with imscope.tab_item("Takes"): self._render_takes_panel() + + def _render_operations_hub(self) -> 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, show_usage_tab = not self.ui_separate_tool_calls_panel, not self.ui_separate_usage_analytics + with imscope.tab_bar("ops_tabs"): + with imscope.tab_item("Comms History"): self._render_comms_history_panel() + if show_tc_tab: + with imscope.tab_item("Tool Calls"): self._render_tool_calls_panel() + if show_usage_tab: + with imscope.tab_item("Usage Analytics"): self._render_usage_analytics_panel() + if not self.ui_separate_external_tools: + with imscope.tab_item("External Tools"): + self._render_external_tools_panel() + imgui.separator(); imgui.text("") + try: self._render_external_editor_panel() + except Exception as e: imgui.text_colored(vec4(1, 0.3, 0.3, 1), f"Error: {str(e)}") + with imscope.tab_item("Workspace Layouts"): + imgui.text("Experimental: Auto-switch layout by Tier") + ch, self.controller.ui_auto_switch_layout = imgui.checkbox("Enable Auto-Switch", self.controller.ui_auto_switch_layout) + if self.controller.ui_auto_switch_layout: + imgui.separator(); imgui.text("Tier Bindings (select profile for each tier)") + profiles = [""] + [p.name for p in self.controller.workspace_profiles.values()] + for t in ["Tier 1", "Tier 2", "Tier 3", "Tier 4"]: + curr = self.controller.ui_tier_layout_bindings.get(t, ""); idx = profiles.index(curr) if curr in profiles else 0 + ch_combo, new_idx = imgui.combo(t, idx, profiles) + if ch_combo: self.controller.ui_tier_layout_bindings[t] = profiles[new_idx] + def _show_menus(self) -> None: """ [C: tests/test_gui_window_controls.py:test_gui_window_controls_minimize_maximize_close] @@ -732,16 +793,16 @@ class App: if hwnd: btn_w = 40 - display_w = imgui.get_io().display_size.x - right_x = display_w - (btn_w * 3) + # Use window width (points) instead of display_size (pixels) for correct scaling + window_w = imgui.get_window_width() + bar_h = imgui.get_window_height() + right_x = window_w - (btn_w * 3) # Drag area check using an explicit invisible button spanning the empty space curr_x = imgui.get_cursor_pos_x() drag_w = right_x - curr_x if drag_w > 0: - # Use a small positive height to satisfy IM_ASSERT(size_arg.y != 0.0f) - # The menu bar naturally constrains the hit box height anyway. - imgui.invisible_button("##drag_area", (drag_w, 20.0)) + imgui.invisible_button("##drag_area", (drag_w, bar_h)) if imgui.is_item_active() and imgui.is_mouse_dragging(0): # CRITICAL: We must reset ImGui's mouse_down state BEFORE passing control to Windows. # Otherwise, the Windows modal drag loop swallows the WM_LBUTTONUP event, @@ -757,17 +818,18 @@ class App: except Exception: is_max = False - imgui.set_cursor_pos_x(right_x) - if imgui.button("_", (btn_w, 0)): + # Explicitly set Y to 0 and match button height to bar height for perfect alignment + imgui.set_cursor_pos((right_x, 0)) + if imgui.button("_", (btn_w, bar_h)): win32gui.ShowWindow(hwnd, win32con.SW_MINIMIZE) - imgui.set_cursor_pos_x(right_x + btn_w) - if imgui.button("[=]" if is_max else "[]", (btn_w, 0)): + imgui.set_cursor_pos((right_x + btn_w, 0)) + if imgui.button("[=]" if is_max else "[]", (btn_w, bar_h)): win32gui.ShowWindow(hwnd, win32con.SW_RESTORE if is_max else win32con.SW_MAXIMIZE) - imgui.set_cursor_pos_x(right_x + btn_w * 2) + imgui.set_cursor_pos((right_x + btn_w * 2, 0)) imgui.push_style_color(imgui.Col_.button_hovered, vec4(200, 50, 50, 255)) - if imgui.button("X", (btn_w, 0)): + if imgui.button("X", (btn_w, bar_h)): win32gui.PostMessage(hwnd, win32con.WM_CLOSE, 0, 0) imgui.pop_style_color() @@ -985,182 +1047,29 @@ class App: else: self._tool_log_cache = log_raw self._tool_log_dirty = False - #region: Project Settings - if self.show_windows.get("Project Settings", False): - with imscope.window("Project Settings", self.show_windows["Project Settings"]) as (exp, opened): - self.show_windows["Project Settings"] = bool(opened) - if exp and imscope.tab_bar('context_hub_tabs'): - if imscope.tab_item('Projects'): self._render_projects_panel() - if imscope.tab_item('Paths'): self._render_paths_panel() - #endregion: Project Settings + self._render_window_if_open("Project Settings", self._render_project_settings_hub) + self._render_window_if_open("Files & Media", self._render_files_and_media) + self._render_window_if_open("AI Settings", self._render_ai_settings_hub) + self._render_window_if_open("Usage Analytics", self._render_usage_analytics_panel, self.ui_separate_usage_analytics) + self._render_window_if_open("MMA Dashboard", self._render_mma_dashboard) + self._render_window_if_open("Task DAG", self._render_task_dag_panel, self.ui_separate_task_dag) - #region: Files & Media window - if self.show_windows.get("Files & Media", False): - exp, opened = imgui.begin("Files & Media", self.show_windows["Files & Media"]) - self.show_windows["Files & Media"] = bool(opened) - if opened and exp: self._render_files_and_media() - imgui.end() - #endregion: Files & Media window - - #region: AI Settings - if self.show_windows.get("AI Settings", False): - with imscope.window("AI Settings", self.show_windows["AI Settings"]) as (exp, opened): - self.show_windows["AI Settings"] = bool(opened) - if exp: - self._render_persona_selector_panel() - if imgui.collapsing_header("Provider & Model"): self._render_provider_panel() - if imgui.collapsing_header("System Prompts"): self._render_system_prompts_panel() - if imgui.collapsing_header("RAG Settings"): self._render_rag_panel() - self._render_agent_tools_panel() - #endregion: AI Settings - - if self.ui_separate_usage_analytics and self.show_windows.get("Usage Analytics", False): - exp, opened = imgui.begin("Usage Analytics", self.show_windows["Usage Analytics"]) - self.show_windows["Usage Analytics"] = bool(opened) - if exp: - self._render_usage_analytics_panel() - imgui.end() - - if self.show_windows.get("MMA Dashboard", False): - exp, opened = imgui.begin("MMA Dashboard", self.show_windows["MMA Dashboard"]) - self.show_windows["MMA Dashboard"] = bool(opened) - if exp: - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_mma_dashboard") - self._render_mma_dashboard() - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_mma_dashboard") - imgui.end() - - #region: Seprate Task Dag to Tier 4 - if self.ui_separate_task_dag and self.show_windows.get("Task DAG", False): - exp, opened = imgui.begin("Task DAG", self.show_windows["Task DAG"]) - self.show_windows["Task DAG"] = bool(opened) - if exp: self._render_task_dag_panel() - imgui.end() - - if self.ui_separate_tier1 and self.show_windows.get("Tier 1: Strategy", False): - exp, opened = imgui.begin("Tier 1: Strategy", self.show_windows["Tier 1: Strategy"]) - self.show_windows["Tier 1: Strategy"] = bool(opened) - if exp: self._render_tier_stream_panel("Tier 1", "Tier 1") - imgui.end() - - if self.ui_separate_tier2 and self.show_windows.get("Tier 2: Tech Lead", False): - exp, opened = imgui.begin("Tier 2: Tech Lead", self.show_windows["Tier 2: Tech Lead"]) - self.show_windows["Tier 2: Tech Lead"] = bool(opened) - if exp: self._render_tier_stream_panel("Tier 2", "Tier 2 (Tech Lead)") - imgui.end() - - if self.ui_separate_tier3 and self.show_windows.get("Tier 3: Workers", False): - exp, opened = imgui.begin("Tier 3: Workers", self.show_windows["Tier 3: Workers"]) - self.show_windows["Tier 3: Workers"] = bool(opened) - if exp: self._render_tier_stream_panel("Tier 3", None) - imgui.end() - - if self.ui_separate_tier4 and self.show_windows.get("Tier 4: QA", False): - exp, opened = imgui.begin("Tier 4: QA", self.show_windows["Tier 4: QA"]) - self.show_windows["Tier 4: QA"] = bool(opened) - if exp: self._render_tier_stream_panel("Tier 4", "Tier 4 (QA)") - imgui.end() - #endregion: Separate Task Dag to Tier 4 + self._render_window_if_open("Tier 1: Strategy", lambda: self._render_tier_stream_panel("Tier 1", "Tier 1"), self.ui_separate_tier1) + self._render_window_if_open("Tier 2: Tech Lead", lambda: self._render_tier_stream_panel("Tier 2", "Tier 2 (Tech Lead)"), self.ui_separate_tier2) + self._render_window_if_open("Tier 3: Workers", lambda: self._render_tier_stream_panel("Tier 3", None), self.ui_separate_tier3) + self._render_window_if_open("Tier 4: QA", lambda: self._render_tier_stream_panel("Tier 4", "Tier 4 (QA)"), self.ui_separate_tier4) if self.show_windows.get("Theme", False): self._render_theme_panel() - #region: Discussion Hub - if self.show_windows.get("Discussion Hub", False): - with imscope.window("Discussion Hub", self.show_windows["Discussion Hub"]) as (exp, opened): - self.show_windows["Discussion Hub"] = bool(opened) - if exp: - if imscope.tab_bar("discussion_hub_tabs"): - if imscope.tab_item("Discussion"): - self._render_discussion_tab() - if imscope.tab_item("Context Composition"): - self._render_context_composition_panel() - if imscope.tab_item("Snapshot"): - self._render_snapshot_tab() - if imscope.tab_item("Takes"): - self._render_takes_panel() - #endregion: Discussion Hub + self._render_window_if_open("Discussion Hub", self._render_discussion_hub) + self._render_window_if_open("Operations Hub", self._render_operations_hub) - #region: Operations Hub - 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.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 - - with imscope.tab_bar("ops_tabs"): - with imscope.tab_item("Comms History"): self._render_comms_history_panel() - if show_tc_tab: - with imscope.tab_item("Tool Calls"): self._render_tool_calls_panel() - if show_usage_tab: - with imscope.tab_item("Usage Analytics"): self._render_usage_analytics_panel() - if not self.ui_separate_external_tools: - with imscope.tab_item("External Tools"): - self._render_external_tools_panel() - imgui.separator() - imgui.text("") - try: - self._render_external_editor_panel() - except Exception as e: - imgui.text_colored(vec4(1, 0.3, 0.3, 1), f"Error: {str(e)}") - with imscope.tab_item("Workspace Layouts"): - imgui.text("Experimental: Auto-switch layout by Tier") - ch, self.controller.ui_auto_switch_layout = imgui.checkbox("Enable Auto-Switch", self.controller.ui_auto_switch_layout) - if self.controller.ui_auto_switch_layout: - imgui.separator() - imgui.text("Tier Bindings (select profile for each tier)") - profiles = [""] + [p.name for p in self.controller.workspace_profiles.values()] - for t in ["Tier 1", "Tier 2", "Tier 3", "Tier 4"]: - curr = self.controller.ui_tier_layout_bindings.get(t, "") - idx = profiles.index(curr) if curr in profiles else 0 - ch_combo, new_idx = imgui.combo(t, idx, profiles) - if ch_combo: self.controller.ui_tier_layout_bindings[t] = profiles[new_idx] - imgui.end() - #endregion: Operations Hub - - #region: Separate Message, Response, Tool Calls, External Tools - if self.ui_separate_message_panel and self.show_windows.get("Message", False): - exp, opened = imgui.begin("Message", self.show_windows["Message"]) - self.show_windows["Message"] = bool(opened) - if exp: self._render_message_panel() - imgui.end() - - if self.ui_separate_response_panel and self.show_windows.get("Response", False): - exp, opened = imgui.begin("Response", self.show_windows["Response"]) - self.show_windows["Response"] = bool(opened) - if exp: self._render_response_panel() - imgui.end() - - if self.ui_separate_tool_calls_panel and self.show_windows.get("Tool Calls", False): - exp, opened = imgui.begin("Tool Calls", self.show_windows["Tool Calls"]) - self.show_windows["Tool Calls"] = bool(opened) - if exp: self._render_tool_calls_panel() - imgui.end() - - if self.ui_separate_external_tools and self.show_windows.get('External Tools', False): - exp, opened = imgui.begin('External Tools', self.show_windows['External Tools']) - self.show_windows['External Tools'] = bool(opened) - if exp: self._render_external_tools_panel() - imgui.end() - - if self.show_windows.get("Log Management", False): - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_log_management") - self._render_log_management() - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_log_management") - #endregion: Separate Message, Response, Tool Calls, External Tools - - if self.show_windows.get("Diagnostics", False): self._render_diagnostics_panel() + self._render_window_if_open("Message", self._render_message_panel, self.ui_separate_message_panel) + self._render_window_if_open("Response", self._render_response_panel, self.ui_separate_response_panel) + self._render_window_if_open("Tool Calls", self._render_tool_calls_panel, self.ui_separate_tool_calls_panel) + self._render_window_if_open("External Tools", self._render_external_tools_panel, self.ui_separate_external_tools) + self._render_window_if_open("Log Management", self._render_log_management) + self._render_window_if_open("Diagnostics", self._render_diagnostics_panel) self.perf_monitor.end_frame() diff --git a/src/imgui_scopes.py b/src/imgui_scopes.py index ff70c06..64b5687 100644 --- a/src/imgui_scopes.py +++ b/src/imgui_scopes.py @@ -24,7 +24,8 @@ class _ScopeChild: self._sy = size_y self._flags = flags def __enter__(self): - return imgui.begin_child(self._id, self._sx, self._sy, self._flags) + res = imgui.begin_child(self._id, self._sx, self._sy, self._flags) + return res def __exit__(self, *args): imgui.end_child() return False @@ -35,38 +36,51 @@ class _ScopeTable: self._name = name self._columns = columns self._flags = flags + self._active = False def __enter__(self): - return imgui.begin_table(self._name, self._columns, self._flags) + self._active = imgui.begin_table(self._name, self._columns, self._flags) + return self._active def __exit__(self, *args): - imgui.end_table() + if self._active: + imgui.end_table() return False def menu_bar(): return _ScopeMenuBar() class _ScopeMenuBar: + def __init__(self): + self._active = False def __enter__(self): - return imgui.begin_menu_bar() + self._active = imgui.begin_menu_bar() + return self._active def __exit__(self, *args): - imgui.end_menu_bar() + if self._active: + imgui.end_menu_bar() return False def menu(label: str): return _ScopeMenu(label) class _ScopeMenu: def __init__(self, label: str): self._label = label + self._active = False def __enter__(self): - return imgui.begin_menu(self._label) + self._active = imgui.begin_menu(self._label) + return self._active def __exit__(self, *args): - imgui.end_menu() + if self._active: + imgui.end_menu() return False def popup(id_str: str): return _ScopePopup(id_str) class _ScopePopup: def __init__(self, id_str: str): self._id = id_str + self._active = False def __enter__(self): - return imgui.begin_popup(self._id) + self._active = imgui.begin_popup(self._id) + return self._active def __exit__(self, *args): - imgui.end_popup() + if self._active: + imgui.end_popup() return False def tooltip(): return _ScopeTooltip() @@ -120,10 +134,13 @@ class _ScopeTabBar: def __init__(self, id_str: str, flags: int): self._id = id_str self._flags = flags + self._active = False def __enter__(self): - return imgui.begin_tab_bar(self._id, self._flags) + self._active = imgui.begin_tab_bar(self._id, self._flags) + return self._active def __exit__(self, *args): - imgui.end_tab_bar() + if self._active: + imgui.end_tab_bar() return False def tab_item(label: str, flags: int = 0): return _ScopeTabItem(label, flags) @@ -131,11 +148,12 @@ class _ScopeTabItem: def __init__(self, label: str, flags: int): self._label = label self._flags = flags + self._expanded = False self._open = None def __enter__(self): - exp, self._open = imgui.begin_tab_item(self._label, None, self._flags) - return exp, self._open + self._expanded, self._open = imgui.begin_tab_item(self._label, None, self._flags) + return self._expanded, self._open def __exit__(self, *args): - if self._open: + if self._expanded: imgui.end_tab_item() return False