From ecef6175e7941d91c04a52e17b70321641c04dd3 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Mon, 11 May 2026 22:36:16 -0400 Subject: [PATCH] feat(gui): Add ImGuiScope base class and scope helpers --- src/imgui_scopes.py | 82 +++++++-------- tests/test_imgui_scopes.py | 202 +++++++++++++++++++++++++++++++++++++ 2 files changed, 243 insertions(+), 41 deletions(-) create mode 100644 tests/test_imgui_scopes.py diff --git a/src/imgui_scopes.py b/src/imgui_scopes.py index cd6c626..9be3407 100644 --- a/src/imgui_scopes.py +++ b/src/imgui_scopes.py @@ -1,78 +1,78 @@ from __future__ import annotations -import imgui_bundle +from imgui_bundle import imgui +from imgui_bundle import imgui_node_editor class ImGuiScope: - _entered: bool - _opened: bool | tuple - _begin_fn: object - _end_fn: object - _args: tuple[object, ...] - _kwargs: dict[str, object] + _entered: bool + _opened: bool | tuple + _begin_fn: object + _end_fn: object + _args: tuple[object, ...] + _kwargs: dict[str, object] - def __init__(self, begin_fn: object, end_fn: object, *args: object, **kwargs: object) -> None: - self._begin_fn = begin_fn - self._end_fn = end_fn - self._args = args - self._kwargs = kwargs - self._opened = False - self._entered = False + def __init__(self, begin_fn: object, end_fn: object, *args: object, **kwargs: object) -> None: + self._begin_fn = begin_fn + self._end_fn = end_fn + self._args = args + self._kwargs = kwargs + self._opened = False + self._entered = False - def __enter__(self) -> bool | tuple: - result = self._begin_fn(*self._args, **self._kwargs) - if isinstance(result, tuple): - self._opened = result[0] - else: - self._opened = result - self._entered = bool(self._opened) - return self._opened + def __enter__(self) -> bool | tuple: + result = self._begin_fn(*self._args, **self._kwargs) + if isinstance(result, tuple): + self._opened = result[0] + else: + self._opened = result + self._entered = bool(self._opened) + return self._opened - def __exit__(self, *args: object) -> bool: - if self._entered: - self._end_fn() - return False + def __exit__(self, *args: object) -> bool: + if self._entered: + self._end_fn() + return False def imgui_window(name: str, visible: bool = True, flags: int = 0) -> ImGuiScope: - return ImGuiScope(imgui_bundle.imgui.begin, imgui_bundle.imgui.end, name, visible, flags) + return ImGuiScope(imgui.begin, imgui.end, name, visible, flags) def imgui_table(name: str, columns: int, flags: int = 0) -> ImGuiScope: - return ImGuiScope(imgui_bundle.imgui.begin_table, imgui_bundle.imgui.end_table, name, columns, flags) + return ImGuiScope(imgui.begin_table, imgui.end_table, name, columns, flags) def imgui_menu_bar() -> ImGuiScope: - return ImGuiScope(imgui_bundle.imgui.begin_menu_bar, imgui_bundle.imgui.end_menu_bar) + return ImGuiScope(imgui.begin_menu_bar, imgui.end_menu_bar) def imgui_menu(label: str) -> ImGuiScope: - return ImGuiScope(imgui_bundle.imgui.begin_menu, imgui_bundle.imgui.end_menu, label) + return ImGuiScope(imgui.begin_menu, imgui.end_menu, label) def imgui_child(id_str: str, width: float = 0, height: float = 0, flags: int = 0) -> ImGuiScope: - return ImGuiScope(imgui_bundle.imgui.begin_child, imgui_bundle.imgui.end_child, id_str, width, height, flags) + return ImGuiScope(imgui.begin_child, imgui.end_child, id_str, width, height, flags) def imgui_group() -> ImGuiScope: - return ImGuiScope(imgui_bundle.imgui.begin_group, imgui_bundle.imgui.end_group) + return ImGuiScope(imgui.begin_group, imgui.end_group) def imgui_popup(id_str: str) -> ImGuiScope: - return ImGuiScope(imgui_bundle.imgui.begin_popup, imgui_bundle.imgui.end_popup, id_str) + return ImGuiScope(imgui.begin_popup, imgui.end_popup, id_str) def imgui_tooltip() -> ImGuiScope: - return ImGuiScope(imgui_bundle.imgui.begin_tooltip, imgui_bundle.imgui.end_tooltip) + return ImGuiScope(imgui.begin_tooltip, imgui.end_tooltip) def imgui_clipper(count: int) -> ImGuiScope: - return ImGuiScope( - lambda n: imgui_bundle.imgui.listing_builder.begin_clipper(n, -1), - imgui_bundle.imgui.listing_builder.end_clipper, - count - ) + return ImGuiScope( + lambda n: imgui.listing_builder.begin_clipper(n, -1), + imgui.listing_builder.end_clipper, + count + ) def node_editor_scope(name: str) -> ImGuiScope: - ed = imgui_bundle.imgui_node_editor - return ImGuiScope(ed.begin, ed.end, name) \ No newline at end of file + return ImGuiScope(imgui_node_editor.begin, imgui_node_editor.end, name) \ No newline at end of file diff --git a/tests/test_imgui_scopes.py b/tests/test_imgui_scopes.py new file mode 100644 index 0000000..4a9f4c0 --- /dev/null +++ b/tests/test_imgui_scopes.py @@ -0,0 +1,202 @@ +from __future__ import annotations +from unittest.mock import patch, MagicMock +import pytest + + +class TestImGuiScope: + def test_enter_calls_begin_with_args(self) -> None: + from src.imgui_scopes import ImGuiScope + mock_begin = MagicMock(return_value=True) + mock_end = MagicMock() + scope = ImGuiScope(mock_begin, mock_end, "arg1", kwarg="val") + with scope: + pass + mock_begin.assert_called_once_with("arg1", kwarg="val") + + def test_exit_calls_end_when_entered(self) -> None: + from src.imgui_scopes import ImGuiScope + mock_begin = MagicMock(return_value=True) + mock_end = MagicMock() + scope = ImGuiScope(mock_begin, mock_end) + with scope: + pass + mock_end.assert_called_once() + + def test_exit_does_not_call_end_when_not_entered(self) -> None: + from src.imgui_scopes import ImGuiScope + mock_begin = MagicMock(return_value=False) + mock_end = MagicMock() + scope = ImGuiScope(mock_begin, mock_end) + with scope: + pass + mock_end.assert_not_called() + + def test_enter_returns_opened_value(self) -> None: + from src.imgui_scopes import ImGuiScope + mock_begin = MagicMock(return_value=True) + mock_end = MagicMock() + scope = ImGuiScope(mock_begin, mock_end) + result = scope.__enter__() + assert result is True + + def test_enter_handles_tuple_return(self) -> None: + from src.imgui_scopes import ImGuiScope + mock_begin = MagicMock(return_value=(True, "extra")) + mock_end = MagicMock() + scope = ImGuiScope(mock_begin, mock_end) + with scope: + pass + mock_end.assert_called_once() + + +class TestImguiWindow: + def test_imgui_window_returns_scope(self) -> None: + from src.imgui_scopes import imgui_window, ImGuiScope + scope = imgui_window("Test") + assert isinstance(scope, ImGuiScope) + + def test_imgui_window_calls_begin_end(self) -> None: + from src.imgui_scopes import imgui_window + with patch("src.imgui_scopes.imgui.begin", return_value=True) as mock_begin: + with patch("src.imgui_scopes.imgui.end") as mock_end: + scope = imgui_window("Test") + with scope: + pass + mock_begin.assert_called_once_with("Test", True, 0) + mock_end.assert_called_once() + + +class TestImguiTable: + def test_imgui_table_returns_scope(self) -> None: + from src.imgui_scopes import imgui_table, ImGuiScope + scope = imgui_table("TestTable", 3) + assert isinstance(scope, ImGuiScope) + + def test_imgui_table_calls_begin_end(self) -> None: + from src.imgui_scopes import imgui_table + with patch("src.imgui_scopes.imgui.begin_table", return_value=True) as mock_begin: + with patch("src.imgui_scopes.imgui.end_table") as mock_end: + scope = imgui_table("TestTable", 3) + with scope: + pass + mock_begin.assert_called_once_with("TestTable", 3, 0) + mock_end.assert_called_once() + + +class TestImguiMenuBar: + def test_imgui_menu_bar_returns_scope(self) -> None: + from src.imgui_scopes import imgui_menu_bar, ImGuiScope + scope = imgui_menu_bar() + assert isinstance(scope, ImGuiScope) + + def test_imgui_menu_bar_calls_begin_end(self) -> None: + from src.imgui_scopes import imgui_menu_bar + with patch("src.imgui_scopes.imgui.begin_menu_bar", return_value=True) as mock_begin: + with patch("src.imgui_scopes.imgui.end_menu_bar") as mock_end: + scope = imgui_menu_bar() + with scope: + pass + mock_begin.assert_called_once_with() + mock_end.assert_called_once() + + +class TestImguiMenu: + def test_imgui_menu_returns_scope(self) -> None: + from src.imgui_scopes import imgui_menu, ImGuiScope + scope = imgui_menu("File") + assert isinstance(scope, ImGuiScope) + + def test_imgui_menu_calls_begin_end(self) -> None: + from src.imgui_scopes import imgui_menu + with patch("src.imgui_scopes.imgui.begin_menu", return_value=True) as mock_begin: + with patch("src.imgui_scopes.imgui.end_menu") as mock_end: + scope = imgui_menu("File") + with scope: + pass + mock_begin.assert_called_once_with("File") + mock_end.assert_called_once() + + +class TestImguiChild: + def test_imgui_child_returns_scope(self) -> None: + from src.imgui_scopes import imgui_child, ImGuiScope + scope = imgui_child("child_id", 100, 200) + assert isinstance(scope, ImGuiScope) + + def test_imgui_child_calls_begin_end(self) -> None: + from src.imgui_scopes import imgui_child + with patch("src.imgui_scopes.imgui.begin_child", return_value=True) as mock_begin: + with patch("src.imgui_scopes.imgui.end_child") as mock_end: + scope = imgui_child("child_id", 100, 200) + with scope: + pass + mock_begin.assert_called_once_with("child_id", 100, 200, 0) + mock_end.assert_called_once() + + +class TestImguiGroup: + def test_imgui_group_returns_scope(self) -> None: + from src.imgui_scopes import imgui_group, ImGuiScope + scope = imgui_group() + assert isinstance(scope, ImGuiScope) + + def test_imgui_group_calls_begin_end(self) -> None: + from src.imgui_scopes import imgui_group + with patch("src.imgui_scopes.imgui.begin_group", return_value=True) as mock_begin: + with patch("src.imgui_scopes.imgui.end_group") as mock_end: + scope = imgui_group() + with scope: + pass + mock_begin.assert_called_once_with() + mock_end.assert_called_once() + + +class TestImguiPopup: + def test_imgui_popup_returns_scope(self) -> None: + from src.imgui_scopes import imgui_popup, ImGuiScope + scope = imgui_popup("popup_id") + assert isinstance(scope, ImGuiScope) + + def test_imgui_popup_calls_begin_end(self) -> None: + from src.imgui_scopes import imgui_popup + with patch("src.imgui_scopes.imgui.begin_popup", return_value=True) as mock_begin: + with patch("src.imgui_scopes.imgui.end_popup") as mock_end: + scope = imgui_popup("popup_id") + with scope: + pass + mock_begin.assert_called_once_with("popup_id") + mock_end.assert_called_once() + + +class TestImguiTooltip: + def test_imgui_tooltip_returns_scope(self) -> None: + from src.imgui_scopes import imgui_tooltip, ImGuiScope + scope = imgui_tooltip() + assert isinstance(scope, ImGuiScope) + + def test_imgui_tooltip_calls_begin_end(self) -> None: + from src.imgui_scopes import imgui_tooltip + with patch("src.imgui_scopes.imgui.begin_tooltip", return_value=True) as mock_begin: + with patch("src.imgui_scopes.imgui.end_tooltip") as mock_end: + scope = imgui_tooltip() + with scope: + pass + mock_begin.assert_called_once_with() + mock_end.assert_called_once() + + +class TestNodeEditorScope: + def test_node_editor_scope_returns_scope(self) -> None: + from src.imgui_scopes import node_editor_scope, ImGuiScope + scope = node_editor_scope("TestNode") + assert isinstance(scope, ImGuiScope) + + def test_node_editor_scope_calls_begin_end(self) -> None: + from src.imgui_scopes import node_editor_scope + with patch("src.imgui_scopes.imgui_node_editor.begin", return_value=True) as mock_begin: + with patch("src.imgui_scopes.imgui_node_editor.end") as mock_end: + scope = node_editor_scope("TestNode") + with scope: + pass + mock_begin.assert_called_once_with("TestNode") + mock_end.assert_called_once() \ No newline at end of file