From 5f95225fd07d517f8b314c0a2fdea421dc7fb163 Mon Sep 17 00:00:00 2001 From: Oras Phongpanangam Date: Fri, 31 Jul 2026 08:41:43 -0400 Subject: [PATCH 01/11] add file dialog bindings --- CMakeLists.txt | 3 +- src/cpp/imgui/imgui.cpp | 4 +- src/cpp/imgui/imgui_file_dialog.cpp | 153 ++++++++++++++++++++++++++++ test/tests/test_imgui.py | 38 +++++++ 4 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 src/cpp/imgui/imgui_file_dialog.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c7679ff..dfef0e8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,6 +52,7 @@ nanobind_add_module(polyscope_bindings # ImGui related things src/cpp/imgui/imgui.cpp + src/cpp/imgui/imgui_file_dialog.cpp src/cpp/imgui/imgui_enums.cpp src/cpp/imgui/imgui_structs.cpp @@ -154,4 +155,4 @@ foreach(STUB_FILE ${STUB_FILES}) endforeach() -install(TARGETS polyscope_bindings LIBRARY DESTINATION .) \ No newline at end of file +install(TARGETS polyscope_bindings LIBRARY DESTINATION .) diff --git a/src/cpp/imgui/imgui.cpp b/src/cpp/imgui/imgui.cpp index 20f21c1..d8e82a2 100644 --- a/src/cpp/imgui/imgui.cpp +++ b/src/cpp/imgui/imgui.cpp @@ -15,6 +15,7 @@ void bind_imgui_io(nb::module_& m); void bind_imgui_style(nb::module_& m); void bind_imgui_drawlist(nb::module_& m); void bind_imgui_fonts(nb::module_& m); +void bind_imgui_file_dialog(nb::module_& m); void bind_imgui_api_main(nb::module_& m); void bind_imgui_api_context_creation(nb::module_& m); @@ -75,6 +76,7 @@ void bind_imgui(nb::module_& m) { bind_imgui_style(imgui_module); bind_imgui_drawlist(imgui_module); bind_imgui_fonts(imgui_module); + bind_imgui_file_dialog(imgui_module); bind_imgui_api_main(imgui_module); bind_imgui_api_context_creation(imgui_module); @@ -125,4 +127,4 @@ void bind_imgui(nb::module_& m) { bind_imgui_api_settings(imgui_module); bind_imgui_api_debug(imgui_module); bind_imgui_api_allocators(imgui_module); -} \ No newline at end of file +} diff --git a/src/cpp/imgui/imgui_file_dialog.cpp b/src/cpp/imgui/imgui_file_dialog.cpp new file mode 100644 index 0000000..18da316 --- /dev/null +++ b/src/cpp/imgui/imgui_file_dialog.cpp @@ -0,0 +1,153 @@ +#include "ImGuiFileDialog.h" + +#include +#include +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +using Vec2T = std::tuple; + +void bind_imgui_file_dialog(nb::module_& m) { + nb::class_(m, "FileDialogConfig") + .def( + nb::new_([](const std::string& path, const std::string& file_name, + const std::string& file_path_name, int32_t count_selection_max, + ImGuiFileDialogFlags flags, float side_pane_width) { + IGFD::FileDialogConfig config; + config.path = path; + config.fileName = file_name; + config.filePathName = file_path_name; + config.countSelectionMax = count_selection_max; + config.flags = flags; + config.sidePaneWidth = side_pane_width; + return config; + }), + nb::kw_only(), nb::arg("path") = "", nb::arg("fileName") = "", + nb::arg("filePathName") = "", nb::arg("countSelectionMax") = 1, + nb::arg("flags") = static_cast(ImGuiFileDialogFlags_None), + nb::arg("sidePaneWidth") = 250.0f) + .def_rw("path", &IGFD::FileDialogConfig::path) + .def_rw("fileName", &IGFD::FileDialogConfig::fileName) + .def_rw("filePathName", &IGFD::FileDialogConfig::filePathName) + .def_rw("countSelectionMax", &IGFD::FileDialogConfig::countSelectionMax) + .def_rw("flags", &IGFD::FileDialogConfig::flags) + .def_rw("sidePaneWidth", &IGFD::FileDialogConfig::sidePaneWidth); + + auto file_dialog_class = nb::class_(m, "FileDialog"); + file_dialog_class + .def(nb::init<>()) + .def_static("Instance", []() { return IGFD::FileDialog::Instance(); }, nb::rv_policy::reference) + .def( + "OpenDialog", + [](IGFD::FileDialog& dialog, const std::string& key, const std::string& title, + const std::optional& filters, const std::optional& config) { + dialog.OpenDialog(key, title, filters ? filters->c_str() : nullptr, + config.value_or(IGFD::FileDialogConfig{})); + }, + nb::arg("key"), nb::arg("title"), nb::arg("filters").none(), + nb::arg("config").none() = nb::none()) + .def( + "Display", + [](IGFD::FileDialog& dialog, const std::string& key, ImGuiWindowFlags flags, + const Vec2T& min_size, const Vec2T& max_size) { + return dialog.Display(key, flags, ImVec2(std::get<0>(min_size), std::get<1>(min_size)), + ImVec2(std::get<0>(max_size), std::get<1>(max_size))); + }, + nb::arg("key"), nb::arg("flags") = static_cast(ImGuiWindowFlags_NoCollapse), + nb::arg("min_size") = Vec2T{0.0f, 0.0f}, nb::arg("max_size") = Vec2T{FLT_MAX, FLT_MAX}) + .def("Close", &IGFD::FileDialog::Close) + .def("IsOpened", nb::overload_cast<>(&IGFD::FileDialog::IsOpened, nb::const_)) + .def("IsOpened", nb::overload_cast(&IGFD::FileDialog::IsOpened, nb::const_), + nb::arg("key")) + .def("GetOpenedKey", &IGFD::FileDialog::GetOpenedKey) + .def("IsOk", &IGFD::FileDialog::IsOk) + .def( + "GetSelection", + [](IGFD::FileDialog& dialog, IGFD_ResultMode mode) { + nb::dict selection; + for (const auto& [filename, path] : dialog.GetSelection(mode)) { + selection[nb::str(filename.c_str())] = nb::str(path.c_str()); + } + return selection; + }, + nb::arg("mode") = static_cast(IGFD_ResultMode_KeepInputFile)) + .def("GetFilePathName", &IGFD::FileDialog::GetFilePathName, + nb::arg("mode") = static_cast(IGFD_ResultMode_AddIfNoFileExt)) + .def("GetCurrentFileName", &IGFD::FileDialog::GetCurrentFileName, + nb::arg("mode") = static_cast(IGFD_ResultMode_AddIfNoFileExt)) + .def("GetCurrentPath", &IGFD::FileDialog::GetCurrentPath) + .def("GetCurrentFilter", &IGFD::FileDialog::GetCurrentFilter); + + m.def( + "OpenDialog", + [](const std::string& key, const std::string& title, + const std::optional& filters, const std::optional& config) { + IGFD::FileDialog::Instance()->OpenDialog(key, title, filters ? filters->c_str() : nullptr, + config.value_or(IGFD::FileDialogConfig{})); + }, + nb::arg("key"), nb::arg("title"), nb::arg("filters").none(), + nb::arg("config").none() = nb::none()); + m.def( + "Display", + [](const std::string& key, ImGuiWindowFlags flags, const Vec2T& min_size, const Vec2T& max_size) { + return IGFD::FileDialog::Instance()->Display( + key, flags, ImVec2(std::get<0>(min_size), std::get<1>(min_size)), + ImVec2(std::get<0>(max_size), std::get<1>(max_size))); + }, + nb::arg("key"), nb::arg("flags") = static_cast(ImGuiWindowFlags_NoCollapse), + nb::arg("min_size") = Vec2T{0.0f, 0.0f}, nb::arg("max_size") = Vec2T{FLT_MAX, FLT_MAX}); + m.def("Close", []() { IGFD::FileDialog::Instance()->Close(); }); + m.def( + "IsOpened", + [](const std::optional& key) { + return key ? IGFD::FileDialog::Instance()->IsOpened(*key) : IGFD::FileDialog::Instance()->IsOpened(); + }, + nb::arg("key").none() = nb::none()); + m.def("GetOpenedKey", []() { return IGFD::FileDialog::Instance()->GetOpenedKey(); }); + m.def("IsOk", []() { return IGFD::FileDialog::Instance()->IsOk(); }); + m.def( + "GetSelection", + [](IGFD_ResultMode mode) { + nb::dict selection; + for (const auto& [filename, path] : IGFD::FileDialog::Instance()->GetSelection(mode)) { + selection[nb::str(filename.c_str())] = nb::str(path.c_str()); + } + return selection; + }, + nb::arg("mode") = static_cast(IGFD_ResultMode_KeepInputFile)); + m.def("GetFilePathName", [](IGFD_ResultMode mode) { + return IGFD::FileDialog::Instance()->GetFilePathName(mode); + }, nb::arg("mode") = static_cast(IGFD_ResultMode_AddIfNoFileExt)); + m.def("GetCurrentFileName", [](IGFD_ResultMode mode) { + return IGFD::FileDialog::Instance()->GetCurrentFileName(mode); + }, nb::arg("mode") = static_cast(IGFD_ResultMode_AddIfNoFileExt)); + m.def("GetCurrentPath", []() { return IGFD::FileDialog::Instance()->GetCurrentPath(); }); + m.def("GetCurrentFilter", []() { return IGFD::FileDialog::Instance()->GetCurrentFilter(); }); + +#define BIND_IGFD_CONSTANT(name) m.attr(#name) = nb::int_(static_cast(name)) + BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_None); + BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_ConfirmOverwrite); + BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_DontShowHiddenFiles); + BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_DisableCreateDirectoryButton); + BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_HideColumnType); + BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_HideColumnSize); + BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_HideColumnDate); + BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_NoDialog); + BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_ReadOnlyFileNameField); + BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_CaseInsensitiveExtentionFiltering); + BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_Modal); + BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_DisableThumbnailMode); + BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_DisablePlaceMode); + BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_DisableQuickPathSelection); + BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_ShowDevicesButton); + BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_NaturalSorting); + BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_OptionalFileName); + BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_Default); + BIND_IGFD_CONSTANT(IGFD_ResultMode_AddIfNoFileExt); + BIND_IGFD_CONSTANT(IGFD_ResultMode_OverwriteFileExt); + BIND_IGFD_CONSTANT(IGFD_ResultMode_KeepInputFile); +#undef BIND_IGFD_CONSTANT +} diff --git a/test/tests/test_imgui.py b/test/tests/test_imgui.py index 50fa51c..a9b9a79 100644 --- a/test/tests/test_imgui.py +++ b/test/tests/test_imgui.py @@ -7,6 +7,44 @@ class TestImGuiBindings(unittest.TestCase): + def test_file_dialog_state(self): + flags = psim.ImGuiFileDialogFlags_Modal | psim.ImGuiFileDialogFlags_ConfirmOverwrite + config = psim.FileDialogConfig( + path=".", + fileName="mesh.obj", + filePathName="./mesh.obj", + countSelectionMax=3, + flags=flags, + sidePaneWidth=300.0, + ) + + self.assertEqual(config.path, ".") + self.assertEqual(config.fileName, "mesh.obj") + self.assertEqual(config.filePathName, "./mesh.obj") + self.assertEqual(config.countSelectionMax, 3) + self.assertEqual(config.flags, flags) + self.assertEqual(config.sidePaneWidth, 300.0) + + dialog = psim.FileDialog() + self.assertFalse(dialog.IsOpened()) + self.assertFalse(dialog.IsOpened("unused")) + self.assertEqual(dialog.GetOpenedKey(), "") + self.assertFalse(dialog.IsOk()) + + psim.Close() + self.assertFalse(psim.IsOpened()) + self.addCleanup(psim.Close) + psim.OpenDialog("directory", "Choose directory", None) + self.assertTrue(psim.IsOpened()) + self.assertTrue(psim.IsOpened("directory")) + self.assertEqual(psim.GetOpenedKey(), "directory") + self.assertFalse(psim.IsOk()) + singleton = psim.FileDialog.Instance() + self.assertIs(singleton, psim.FileDialog.Instance()) + self.assertTrue(singleton.IsOpened("directory")) + psim.Close() + self.assertFalse(psim.IsOpened()) + def test_context_creation(self): # Test context creation functions using capsules # Note: We don't actually switch contexts in this test, just verify the bindings work From 510d53ff79f01d63871af11a0ffbe5562c8891b5 Mon Sep 17 00:00:00 2001 From: Oras Phongpanangam Date: Fri, 31 Jul 2026 09:09:38 -0400 Subject: [PATCH 02/11] add some comments --- src/cpp/imgui/imgui_file_dialog.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/cpp/imgui/imgui_file_dialog.cpp b/src/cpp/imgui/imgui_file_dialog.cpp index 18da316..8ce092d 100644 --- a/src/cpp/imgui/imgui_file_dialog.cpp +++ b/src/cpp/imgui/imgui_file_dialog.cpp @@ -11,6 +11,7 @@ using namespace nb::literals; using Vec2T = std::tuple; void bind_imgui_file_dialog(nb::module_& m) { + // Configuration value passed to OpenDialog(). Pointer and callback fields are intentionally omitted. nb::class_(m, "FileDialogConfig") .def( nb::new_([](const std::string& path, const std::string& file_name, @@ -36,6 +37,7 @@ void bind_imgui_file_dialog(nb::module_& m) { .def_rw("flags", &IGFD::FileDialogConfig::flags) .def_rw("sidePaneWidth", &IGFD::FileDialogConfig::sidePaneWidth); + // Advanced API for independently owned dialogs, plus access to the upstream library-owned singleton. auto file_dialog_class = nb::class_(m, "FileDialog"); file_dialog_class .def(nb::init<>()) @@ -81,6 +83,7 @@ void bind_imgui_file_dialog(nb::module_& m) { .def("GetCurrentPath", &IGFD::FileDialog::GetCurrentPath) .def("GetCurrentFilter", &IGFD::FileDialog::GetCurrentFilter); + // Ordinary API backed by FileDialog::Instance(), so callers do not need to retain a dialog object. m.def( "OpenDialog", [](const std::string& key, const std::string& title, @@ -127,6 +130,7 @@ void bind_imgui_file_dialog(nb::module_& m) { m.def("GetCurrentPath", []() { return IGFD::FileDialog::Instance()->GetCurrentPath(); }); m.def("GetCurrentFilter", []() { return IGFD::FileDialog::Instance()->GetCurrentFilter(); }); + // Export flags and result modes #define BIND_IGFD_CONSTANT(name) m.attr(#name) = nb::int_(static_cast(name)) BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_None); BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_ConfirmOverwrite); From 298bbca6874109c6fa6be1f5e11cb77e2153112c Mon Sep 17 00:00:00 2001 From: Oras Phongpanangam Date: Fri, 31 Jul 2026 09:10:13 -0400 Subject: [PATCH 03/11] pin polyscope to fork to use ImguiFileDialog TODO: change back to origin once merged --- deps/polyscope | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/polyscope b/deps/polyscope index dcbaedb..6862301 160000 --- a/deps/polyscope +++ b/deps/polyscope @@ -1 +1 @@ -Subproject commit dcbaedb6cef9467e8f037dab7c89c116b0fc04dc +Subproject commit 68623012449f4fce727d313763ce51f7c55a22b5 From 6bde534192215df9c47dc7ff8b817d4e101bf3d3 Mon Sep 17 00:00:00 2001 From: Oras Phongpanangam Date: Fri, 31 Jul 2026 09:25:09 -0400 Subject: [PATCH 04/11] reorder --- src/cpp/imgui/imgui_file_dialog.cpp | 92 ++++++++++++++--------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/src/cpp/imgui/imgui_file_dialog.cpp b/src/cpp/imgui/imgui_file_dialog.cpp index 8ce092d..5441d39 100644 --- a/src/cpp/imgui/imgui_file_dialog.cpp +++ b/src/cpp/imgui/imgui_file_dialog.cpp @@ -37,52 +37,6 @@ void bind_imgui_file_dialog(nb::module_& m) { .def_rw("flags", &IGFD::FileDialogConfig::flags) .def_rw("sidePaneWidth", &IGFD::FileDialogConfig::sidePaneWidth); - // Advanced API for independently owned dialogs, plus access to the upstream library-owned singleton. - auto file_dialog_class = nb::class_(m, "FileDialog"); - file_dialog_class - .def(nb::init<>()) - .def_static("Instance", []() { return IGFD::FileDialog::Instance(); }, nb::rv_policy::reference) - .def( - "OpenDialog", - [](IGFD::FileDialog& dialog, const std::string& key, const std::string& title, - const std::optional& filters, const std::optional& config) { - dialog.OpenDialog(key, title, filters ? filters->c_str() : nullptr, - config.value_or(IGFD::FileDialogConfig{})); - }, - nb::arg("key"), nb::arg("title"), nb::arg("filters").none(), - nb::arg("config").none() = nb::none()) - .def( - "Display", - [](IGFD::FileDialog& dialog, const std::string& key, ImGuiWindowFlags flags, - const Vec2T& min_size, const Vec2T& max_size) { - return dialog.Display(key, flags, ImVec2(std::get<0>(min_size), std::get<1>(min_size)), - ImVec2(std::get<0>(max_size), std::get<1>(max_size))); - }, - nb::arg("key"), nb::arg("flags") = static_cast(ImGuiWindowFlags_NoCollapse), - nb::arg("min_size") = Vec2T{0.0f, 0.0f}, nb::arg("max_size") = Vec2T{FLT_MAX, FLT_MAX}) - .def("Close", &IGFD::FileDialog::Close) - .def("IsOpened", nb::overload_cast<>(&IGFD::FileDialog::IsOpened, nb::const_)) - .def("IsOpened", nb::overload_cast(&IGFD::FileDialog::IsOpened, nb::const_), - nb::arg("key")) - .def("GetOpenedKey", &IGFD::FileDialog::GetOpenedKey) - .def("IsOk", &IGFD::FileDialog::IsOk) - .def( - "GetSelection", - [](IGFD::FileDialog& dialog, IGFD_ResultMode mode) { - nb::dict selection; - for (const auto& [filename, path] : dialog.GetSelection(mode)) { - selection[nb::str(filename.c_str())] = nb::str(path.c_str()); - } - return selection; - }, - nb::arg("mode") = static_cast(IGFD_ResultMode_KeepInputFile)) - .def("GetFilePathName", &IGFD::FileDialog::GetFilePathName, - nb::arg("mode") = static_cast(IGFD_ResultMode_AddIfNoFileExt)) - .def("GetCurrentFileName", &IGFD::FileDialog::GetCurrentFileName, - nb::arg("mode") = static_cast(IGFD_ResultMode_AddIfNoFileExt)) - .def("GetCurrentPath", &IGFD::FileDialog::GetCurrentPath) - .def("GetCurrentFilter", &IGFD::FileDialog::GetCurrentFilter); - // Ordinary API backed by FileDialog::Instance(), so callers do not need to retain a dialog object. m.def( "OpenDialog", @@ -130,6 +84,52 @@ void bind_imgui_file_dialog(nb::module_& m) { m.def("GetCurrentPath", []() { return IGFD::FileDialog::Instance()->GetCurrentPath(); }); m.def("GetCurrentFilter", []() { return IGFD::FileDialog::Instance()->GetCurrentFilter(); }); + // Advanced API for independently owned dialogs, plus access to the upstream library-owned singleton. + auto file_dialog_class = nb::class_(m, "FileDialog"); + file_dialog_class + .def(nb::init<>()) + .def_static("Instance", []() { return IGFD::FileDialog::Instance(); }, nb::rv_policy::reference) + .def( + "OpenDialog", + [](IGFD::FileDialog& dialog, const std::string& key, const std::string& title, + const std::optional& filters, const std::optional& config) { + dialog.OpenDialog(key, title, filters ? filters->c_str() : nullptr, + config.value_or(IGFD::FileDialogConfig{})); + }, + nb::arg("key"), nb::arg("title"), nb::arg("filters").none(), + nb::arg("config").none() = nb::none()) + .def( + "Display", + [](IGFD::FileDialog& dialog, const std::string& key, ImGuiWindowFlags flags, + const Vec2T& min_size, const Vec2T& max_size) { + return dialog.Display(key, flags, ImVec2(std::get<0>(min_size), std::get<1>(min_size)), + ImVec2(std::get<0>(max_size), std::get<1>(max_size))); + }, + nb::arg("key"), nb::arg("flags") = static_cast(ImGuiWindowFlags_NoCollapse), + nb::arg("min_size") = Vec2T{0.0f, 0.0f}, nb::arg("max_size") = Vec2T{FLT_MAX, FLT_MAX}) + .def("Close", &IGFD::FileDialog::Close) + .def("IsOpened", nb::overload_cast<>(&IGFD::FileDialog::IsOpened, nb::const_)) + .def("IsOpened", nb::overload_cast(&IGFD::FileDialog::IsOpened, nb::const_), + nb::arg("key")) + .def("GetOpenedKey", &IGFD::FileDialog::GetOpenedKey) + .def("IsOk", &IGFD::FileDialog::IsOk) + .def( + "GetSelection", + [](IGFD::FileDialog& dialog, IGFD_ResultMode mode) { + nb::dict selection; + for (const auto& [filename, path] : dialog.GetSelection(mode)) { + selection[nb::str(filename.c_str())] = nb::str(path.c_str()); + } + return selection; + }, + nb::arg("mode") = static_cast(IGFD_ResultMode_KeepInputFile)) + .def("GetFilePathName", &IGFD::FileDialog::GetFilePathName, + nb::arg("mode") = static_cast(IGFD_ResultMode_AddIfNoFileExt)) + .def("GetCurrentFileName", &IGFD::FileDialog::GetCurrentFileName, + nb::arg("mode") = static_cast(IGFD_ResultMode_AddIfNoFileExt)) + .def("GetCurrentPath", &IGFD::FileDialog::GetCurrentPath) + .def("GetCurrentFilter", &IGFD::FileDialog::GetCurrentFilter); + // Export flags and result modes #define BIND_IGFD_CONSTANT(name) m.attr(#name) = nb::int_(static_cast(name)) BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_None); From f1c4fa9d3fa9459998473b6bc587836432dbc14a Mon Sep 17 00:00:00 2001 From: Oras Phongpanangam Date: Fri, 31 Jul 2026 09:46:17 -0400 Subject: [PATCH 05/11] add default dialog min_size for python convenience --- src/cpp/imgui/imgui_file_dialog.cpp | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/cpp/imgui/imgui_file_dialog.cpp b/src/cpp/imgui/imgui_file_dialog.cpp index 5441d39..8c7da30 100644 --- a/src/cpp/imgui/imgui_file_dialog.cpp +++ b/src/cpp/imgui/imgui_file_dialog.cpp @@ -10,6 +10,16 @@ using namespace nb::literals; using Vec2T = std::tuple; +/// Resolve the sentinel default to half the viewport, since ImGuiFileDialog's default minimum size +/// allows the window to collapse +ImVec2 resolve_min_size(const Vec2T& min_size) { + if (std::get<0>(min_size) < 0.0f && std::get<1>(min_size) < 0.0f) { + const ImVec2 viewport_size = ImGui::GetMainViewport()->Size; + return ImVec2(viewport_size.x * 0.5f, viewport_size.y * 0.5f); + } + return ImVec2(std::get<0>(min_size), std::get<1>(min_size)); +} + void bind_imgui_file_dialog(nb::module_& m) { // Configuration value passed to OpenDialog(). Pointer and callback fields are intentionally omitted. nb::class_(m, "FileDialogConfig") @@ -50,12 +60,11 @@ void bind_imgui_file_dialog(nb::module_& m) { m.def( "Display", [](const std::string& key, ImGuiWindowFlags flags, const Vec2T& min_size, const Vec2T& max_size) { - return IGFD::FileDialog::Instance()->Display( - key, flags, ImVec2(std::get<0>(min_size), std::get<1>(min_size)), - ImVec2(std::get<0>(max_size), std::get<1>(max_size))); + return IGFD::FileDialog::Instance()->Display(key, flags, resolve_min_size(min_size), + ImVec2(std::get<0>(max_size), std::get<1>(max_size))); }, nb::arg("key"), nb::arg("flags") = static_cast(ImGuiWindowFlags_NoCollapse), - nb::arg("min_size") = Vec2T{0.0f, 0.0f}, nb::arg("max_size") = Vec2T{FLT_MAX, FLT_MAX}); + nb::arg("min_size") = Vec2T{-1.0f, -1.0f}, nb::arg("max_size") = Vec2T{FLT_MAX, FLT_MAX}); m.def("Close", []() { IGFD::FileDialog::Instance()->Close(); }); m.def( "IsOpened", @@ -102,11 +111,11 @@ void bind_imgui_file_dialog(nb::module_& m) { "Display", [](IGFD::FileDialog& dialog, const std::string& key, ImGuiWindowFlags flags, const Vec2T& min_size, const Vec2T& max_size) { - return dialog.Display(key, flags, ImVec2(std::get<0>(min_size), std::get<1>(min_size)), + return dialog.Display(key, flags, resolve_min_size(min_size), ImVec2(std::get<0>(max_size), std::get<1>(max_size))); }, nb::arg("key"), nb::arg("flags") = static_cast(ImGuiWindowFlags_NoCollapse), - nb::arg("min_size") = Vec2T{0.0f, 0.0f}, nb::arg("max_size") = Vec2T{FLT_MAX, FLT_MAX}) + nb::arg("min_size") = Vec2T{-1.0f, -1.0f}, nb::arg("max_size") = Vec2T{FLT_MAX, FLT_MAX}) .def("Close", &IGFD::FileDialog::Close) .def("IsOpened", nb::overload_cast<>(&IGFD::FileDialog::IsOpened, nb::const_)) .def("IsOpened", nb::overload_cast(&IGFD::FileDialog::IsOpened, nb::const_), From 163fb7d5a8a34cde698d6d7f84ecea940eba6f1e Mon Sep 17 00:00:00 2001 From: Oras Phongpanangam Date: Fri, 31 Jul 2026 09:57:49 -0400 Subject: [PATCH 06/11] use nb enum --- src/cpp/imgui/imgui_file_dialog.cpp | 61 +++++++++++++++++------------ 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/src/cpp/imgui/imgui_file_dialog.cpp b/src/cpp/imgui/imgui_file_dialog.cpp index 8c7da30..74d1a6a 100644 --- a/src/cpp/imgui/imgui_file_dialog.cpp +++ b/src/cpp/imgui/imgui_file_dialog.cpp @@ -139,28 +139,41 @@ void bind_imgui_file_dialog(nb::module_& m) { .def("GetCurrentPath", &IGFD::FileDialog::GetCurrentPath) .def("GetCurrentFilter", &IGFD::FileDialog::GetCurrentFilter); - // Export flags and result modes -#define BIND_IGFD_CONSTANT(name) m.attr(#name) = nb::int_(static_cast(name)) - BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_None); - BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_ConfirmOverwrite); - BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_DontShowHiddenFiles); - BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_DisableCreateDirectoryButton); - BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_HideColumnType); - BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_HideColumnSize); - BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_HideColumnDate); - BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_NoDialog); - BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_ReadOnlyFileNameField); - BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_CaseInsensitiveExtentionFiltering); - BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_Modal); - BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_DisableThumbnailMode); - BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_DisablePlaceMode); - BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_DisableQuickPathSelection); - BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_ShowDevicesButton); - BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_NaturalSorting); - BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_OptionalFileName); - BIND_IGFD_CONSTANT(ImGuiFileDialogFlags_Default); - BIND_IGFD_CONSTANT(IGFD_ResultMode_AddIfNoFileExt); - BIND_IGFD_CONSTANT(IGFD_ResultMode_OverwriteFileExt); - BIND_IGFD_CONSTANT(IGFD_ResultMode_KeepInputFile); -#undef BIND_IGFD_CONSTANT + { + auto e = nb::enum_(m, "ImGuiFileDialogFlags_", nb::is_arithmetic(), + nb::is_flag()); +#define BIND_IGFD_ENUM_VALUE(name) \ + e.value(#name, name); \ + m.attr(#name) = ImGuiFileDialogFlags_::name + BIND_IGFD_ENUM_VALUE(ImGuiFileDialogFlags_None); + BIND_IGFD_ENUM_VALUE(ImGuiFileDialogFlags_ConfirmOverwrite); + BIND_IGFD_ENUM_VALUE(ImGuiFileDialogFlags_DontShowHiddenFiles); + BIND_IGFD_ENUM_VALUE(ImGuiFileDialogFlags_DisableCreateDirectoryButton); + BIND_IGFD_ENUM_VALUE(ImGuiFileDialogFlags_HideColumnType); + BIND_IGFD_ENUM_VALUE(ImGuiFileDialogFlags_HideColumnSize); + BIND_IGFD_ENUM_VALUE(ImGuiFileDialogFlags_HideColumnDate); + BIND_IGFD_ENUM_VALUE(ImGuiFileDialogFlags_NoDialog); + BIND_IGFD_ENUM_VALUE(ImGuiFileDialogFlags_ReadOnlyFileNameField); + BIND_IGFD_ENUM_VALUE(ImGuiFileDialogFlags_CaseInsensitiveExtentionFiltering); + BIND_IGFD_ENUM_VALUE(ImGuiFileDialogFlags_Modal); + BIND_IGFD_ENUM_VALUE(ImGuiFileDialogFlags_DisableThumbnailMode); + BIND_IGFD_ENUM_VALUE(ImGuiFileDialogFlags_DisablePlaceMode); + BIND_IGFD_ENUM_VALUE(ImGuiFileDialogFlags_DisableQuickPathSelection); + BIND_IGFD_ENUM_VALUE(ImGuiFileDialogFlags_ShowDevicesButton); + BIND_IGFD_ENUM_VALUE(ImGuiFileDialogFlags_NaturalSorting); + BIND_IGFD_ENUM_VALUE(ImGuiFileDialogFlags_OptionalFileName); + BIND_IGFD_ENUM_VALUE(ImGuiFileDialogFlags_Default); +#undef BIND_IGFD_ENUM_VALUE + } + + { + auto e = nb::enum_(m, "IGFD_ResultMode_", nb::is_arithmetic(), nb::is_flag()); +#define BIND_IGFD_ENUM_VALUE(name) \ + e.value(#name, name); \ + m.attr(#name) = IGFD_ResultMode_::name + BIND_IGFD_ENUM_VALUE(IGFD_ResultMode_AddIfNoFileExt); + BIND_IGFD_ENUM_VALUE(IGFD_ResultMode_OverwriteFileExt); + BIND_IGFD_ENUM_VALUE(IGFD_ResultMode_KeepInputFile); +#undef BIND_IGFD_ENUM_VALUE + } } From ca769a76f1c26dbf0c0c278b6421e45b5afdc024 Mon Sep 17 00:00:00 2001 From: Oras Phongpanangam Date: Fri, 31 Jul 2026 10:00:33 -0400 Subject: [PATCH 07/11] use a nested namespace imgui.file_dialog --- src/cpp/imgui/imgui_file_dialog.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/cpp/imgui/imgui_file_dialog.cpp b/src/cpp/imgui/imgui_file_dialog.cpp index 74d1a6a..28563e5 100644 --- a/src/cpp/imgui/imgui_file_dialog.cpp +++ b/src/cpp/imgui/imgui_file_dialog.cpp @@ -20,7 +20,9 @@ ImVec2 resolve_min_size(const Vec2T& min_size) { return ImVec2(std::get<0>(min_size), std::get<1>(min_size)); } -void bind_imgui_file_dialog(nb::module_& m) { +void bind_imgui_file_dialog(nb::module_& imgui_module) { + auto m = imgui_module.def_submodule("file_dialog", "ImGuiFileDialog bindings"); + // Configuration value passed to OpenDialog(). Pointer and callback fields are intentionally omitted. nb::class_(m, "FileDialogConfig") .def( From a50986babc06ac20cef5f9fd295891ed7fb594fa Mon Sep 17 00:00:00 2001 From: Oras Phongpanangam Date: Fri, 31 Jul 2026 10:00:56 -0400 Subject: [PATCH 08/11] update polyscope --- deps/polyscope | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/polyscope b/deps/polyscope index 6862301..4e9b3d9 160000 --- a/deps/polyscope +++ b/deps/polyscope @@ -1 +1 @@ -Subproject commit 68623012449f4fce727d313763ce51f7c55a22b5 +Subproject commit 4e9b3d99c80cfdb7e2f391b93841dddfd683e112 From 101456115d14c485a0d35a7649020a2f7cf25e66 Mon Sep 17 00:00:00 2001 From: Oras Phongpanangam Date: Fri, 31 Jul 2026 10:04:50 -0400 Subject: [PATCH 09/11] simplify include --- src/cpp/imgui/imgui_file_dialog.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/cpp/imgui/imgui_file_dialog.cpp b/src/cpp/imgui/imgui_file_dialog.cpp index 28563e5..f04cc96 100644 --- a/src/cpp/imgui/imgui_file_dialog.cpp +++ b/src/cpp/imgui/imgui_file_dialog.cpp @@ -1,14 +1,11 @@ #include "ImGuiFileDialog.h" -#include -#include -#include -#include +#include "imgui_utils.h" + namespace nb = nanobind; using namespace nb::literals; -using Vec2T = std::tuple; /// Resolve the sentinel default to half the viewport, since ImGuiFileDialog's default minimum size /// allows the window to collapse From 374df40537a6491cd21aac1133ce422090e490a6 Mon Sep 17 00:00:00 2001 From: Oras Phongpanangam Date: Fri, 31 Jul 2026 10:23:06 -0400 Subject: [PATCH 10/11] update stub --- .gitignore | 2 +- CMakeLists.txt | 8 ++++++-- src/polyscope/imgui/__init__.py | 1 + test/tests/test_imgui.py | 31 ++++++++++++++++--------------- 4 files changed, 24 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index 077920f..e7168d2 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,7 @@ __pycache__/ *.egg-info # Stubs that get generated as part of the build process -src/polyscope_bindings/**.pyi +src/polyscope_bindings/**/*.pyi # Editor and OS things imgui.ini diff --git a/CMakeLists.txt b/CMakeLists.txt index dfef0e8..3f8d890 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -138,15 +138,19 @@ nanobind_add_stub( OUTPUT_PATH . OUTPUT __init__.pyi - imgui.pyi + imgui/__init__.pyi + imgui/file_dialog.pyi implot.pyi ) # Copy stub files into the appropriate place in the source directory after build -set(STUB_FILES __init__.pyi imgui.pyi implot.pyi) +set(STUB_FILES __init__.pyi imgui/__init__.pyi imgui/file_dialog.pyi implot.pyi) foreach(STUB_FILE ${STUB_FILES}) + get_filename_component(STUB_DIR ${STUB_FILE} DIRECTORY) add_custom_command( TARGET polyscope_stub POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory + ${CMAKE_CURRENT_SOURCE_DIR}/src/polyscope_bindings/${STUB_DIR} COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_BINARY_DIR}/polyscope_bindings/${STUB_FILE} ${CMAKE_CURRENT_SOURCE_DIR}/src/polyscope_bindings/${STUB_FILE} diff --git a/src/polyscope/imgui/__init__.py b/src/polyscope/imgui/__init__.py index 107a524..b346e15 100644 --- a/src/polyscope/imgui/__init__.py +++ b/src/polyscope/imgui/__init__.py @@ -1 +1,2 @@ from polyscope_bindings.imgui import * # noqa F403 +from polyscope_bindings.imgui import file_dialog as file_dialog diff --git a/test/tests/test_imgui.py b/test/tests/test_imgui.py index a9b9a79..6b8d50a 100644 --- a/test/tests/test_imgui.py +++ b/test/tests/test_imgui.py @@ -8,8 +8,9 @@ class TestImGuiBindings(unittest.TestCase): def test_file_dialog_state(self): - flags = psim.ImGuiFileDialogFlags_Modal | psim.ImGuiFileDialogFlags_ConfirmOverwrite - config = psim.FileDialogConfig( + file_dialog = psim.file_dialog + flags = file_dialog.ImGuiFileDialogFlags_Modal | file_dialog.ImGuiFileDialogFlags_ConfirmOverwrite + config = file_dialog.FileDialogConfig( path=".", fileName="mesh.obj", filePathName="./mesh.obj", @@ -25,25 +26,25 @@ def test_file_dialog_state(self): self.assertEqual(config.flags, flags) self.assertEqual(config.sidePaneWidth, 300.0) - dialog = psim.FileDialog() + dialog = file_dialog.FileDialog() self.assertFalse(dialog.IsOpened()) self.assertFalse(dialog.IsOpened("unused")) self.assertEqual(dialog.GetOpenedKey(), "") self.assertFalse(dialog.IsOk()) - psim.Close() - self.assertFalse(psim.IsOpened()) - self.addCleanup(psim.Close) - psim.OpenDialog("directory", "Choose directory", None) - self.assertTrue(psim.IsOpened()) - self.assertTrue(psim.IsOpened("directory")) - self.assertEqual(psim.GetOpenedKey(), "directory") - self.assertFalse(psim.IsOk()) - singleton = psim.FileDialog.Instance() - self.assertIs(singleton, psim.FileDialog.Instance()) + file_dialog.Close() + self.assertFalse(file_dialog.IsOpened()) + self.addCleanup(file_dialog.Close) + file_dialog.OpenDialog("directory", "Choose directory", None) + self.assertTrue(file_dialog.IsOpened()) + self.assertTrue(file_dialog.IsOpened("directory")) + self.assertEqual(file_dialog.GetOpenedKey(), "directory") + self.assertFalse(file_dialog.IsOk()) + singleton = file_dialog.FileDialog.Instance() + self.assertIs(singleton, file_dialog.FileDialog.Instance()) self.assertTrue(singleton.IsOpened("directory")) - psim.Close() - self.assertFalse(psim.IsOpened()) + file_dialog.Close() + self.assertFalse(file_dialog.IsOpened()) def test_context_creation(self): # Test context creation functions using capsules From da671d9e7a075f637ee721293e3044a63cc203b1 Mon Sep 17 00:00:00 2001 From: Oras Phongpanangam Date: Fri, 31 Jul 2026 10:23:29 -0400 Subject: [PATCH 11/11] update polyscope --- deps/polyscope | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/polyscope b/deps/polyscope index 4e9b3d9..e7a4371 160000 --- a/deps/polyscope +++ b/deps/polyscope @@ -1 +1 @@ -Subproject commit 4e9b3d99c80cfdb7e2f391b93841dddfd683e112 +Subproject commit e7a43718402802725b5fee4b17491b305a327755