Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 8 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -137,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}
Expand All @@ -154,4 +159,4 @@ foreach(STUB_FILE ${STUB_FILES})
endforeach()


install(TARGETS polyscope_bindings LIBRARY DESTINATION .)
install(TARGETS polyscope_bindings LIBRARY DESTINATION .)
4 changes: 3 additions & 1 deletion src/cpp/imgui/imgui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
}
178 changes: 178 additions & 0 deletions src/cpp/imgui/imgui_file_dialog.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
#include "ImGuiFileDialog.h"

#include "imgui_utils.h"


namespace nb = nanobind;
using namespace nb::literals;


/// 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_& 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_<IGFD::FileDialogConfig>(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>(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);

// 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,
const std::optional<std::string>& filters, const std::optional<IGFD::FileDialogConfig>& 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, 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>(ImGuiWindowFlags_NoCollapse),
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",
[](const std::optional<std::string>& 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>(IGFD_ResultMode_KeepInputFile));
m.def("GetFilePathName", [](IGFD_ResultMode mode) {
return IGFD::FileDialog::Instance()->GetFilePathName(mode);
}, nb::arg("mode") = static_cast<IGFD_ResultMode>(IGFD_ResultMode_AddIfNoFileExt));
m.def("GetCurrentFileName", [](IGFD_ResultMode mode) {
return IGFD::FileDialog::Instance()->GetCurrentFileName(mode);
}, nb::arg("mode") = static_cast<IGFD_ResultMode>(IGFD_ResultMode_AddIfNoFileExt));
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_<IGFD::FileDialog>(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<std::string>& filters, const std::optional<IGFD::FileDialogConfig>& 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, 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>(ImGuiWindowFlags_NoCollapse),
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<const std::string&>(&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>(IGFD_ResultMode_KeepInputFile))
.def("GetFilePathName", &IGFD::FileDialog::GetFilePathName,
nb::arg("mode") = static_cast<IGFD_ResultMode>(IGFD_ResultMode_AddIfNoFileExt))
.def("GetCurrentFileName", &IGFD::FileDialog::GetCurrentFileName,
nb::arg("mode") = static_cast<IGFD_ResultMode>(IGFD_ResultMode_AddIfNoFileExt))
.def("GetCurrentPath", &IGFD::FileDialog::GetCurrentPath)
.def("GetCurrentFilter", &IGFD::FileDialog::GetCurrentFilter);

{
auto e = nb::enum_<ImGuiFileDialogFlags_>(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_<IGFD_ResultMode_>(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
}
}
1 change: 1 addition & 0 deletions src/polyscope/imgui/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from polyscope_bindings.imgui import * # noqa F403
from polyscope_bindings.imgui import file_dialog as file_dialog
39 changes: 39 additions & 0 deletions test/tests/test_imgui.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,45 @@


class TestImGuiBindings(unittest.TestCase):
def test_file_dialog_state(self):
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",
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 = file_dialog.FileDialog()
self.assertFalse(dialog.IsOpened())
self.assertFalse(dialog.IsOpened("unused"))
self.assertEqual(dialog.GetOpenedKey(), "")
self.assertFalse(dialog.IsOk())

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"))
file_dialog.Close()
self.assertFalse(file_dialog.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
Expand Down