diff --git a/src/experimental/platform/CMakeLists.txt b/src/experimental/platform/CMakeLists.txt index ef5ef471..dfd44fa6 100644 --- a/src/experimental/platform/CMakeLists.txt +++ b/src/experimental/platform/CMakeLists.txt @@ -37,6 +37,8 @@ target_compile_definitions(${MUJOCO_PLATFORM_TARGET_NAME} target_sources(${MUJOCO_PLATFORM_TARGET_NAME} PUBLIC + file_dialog.cc + file_dialog.h gui.cc gui.h helpers.cc diff --git a/src/experimental/platform/file_dialog.cc b/src/experimental/platform/file_dialog.cc new file mode 100644 index 00000000..661b86dd --- /dev/null +++ b/src/experimental/platform/file_dialog.cc @@ -0,0 +1,67 @@ +// Copyright 2026 DeepMind Technologies Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "experimental/platform/file_dialog.h" + +#include + +#include +#include +#include + +#include + +namespace mujoco::platform { + +static DialogResult ImGui_FileDialog(std::string_view path) { + static constexpr int kBufferSize = 1024; + char buf[kBufferSize]; + std::strncpy(buf, path.data(), sizeof(buf)); + buf[sizeof(buf) - 1] = '\0'; + + DialogResult::Status status = DialogResult::kPending; + + ImGui::Text("Filename"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(600); + if (ImGui::InputText("##Filename", buf, kBufferSize, ImGuiInputTextFlags_EnterReturnsTrue)) { + status = DialogResult::kAccepted; + } + if (ImGui::Button("OK", ImVec2(120, 0)) || ImGui::IsKeyChordPressed(ImGuiKey_Enter)) { + status = DialogResult::kAccepted; + } + ImGui::SetItemDefaultFocus(); + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(120, 0)) || ImGui::IsKeyChordPressed(ImGuiKey_Escape)) { + status = DialogResult::kCancelled; + } + + return DialogResult{.status = status, .path = std::string(buf)}; +} + +DialogResult OpenFileDialog(std::string_view path, + std::span filters) { + return ImGui_FileDialog(path); +} + +DialogResult SaveFileDialog(std::string_view path, + std::span filters) { + return ImGui_FileDialog(path); +} + +DialogResult SelectPathDialog(std::string_view path) { + return ImGui_FileDialog(path); +} + +} // namespace mujoco::platform diff --git a/src/experimental/platform/file_dialog.h b/src/experimental/platform/file_dialog.h new file mode 100644 index 00000000..303cf850 --- /dev/null +++ b/src/experimental/platform/file_dialog.h @@ -0,0 +1,59 @@ +// Copyright 2026 DeepMind Technologies Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef MUJOCO_SRC_EXPERIMENTAL_PLATFORM_FILE_DIALOG_H_ +#define MUJOCO_SRC_EXPERIMENTAL_PLATFORM_FILE_DIALOG_H_ + +#include +#include +#include + +namespace mujoco::platform { + +// Result returned by various file dialog functions. +struct DialogResult { + // The status of the dialog. + enum Status { + // The dialog is still open. + kPending, + // A file/folder was selected. + kAccepted, + // The dialog was dismissed. + kCancelled, + // An error occurred. + kError, + }; + + // The status of the dialog. + Status status = kPending; + + // The path selected by the dialog if Status is kAccepted. If status is + // kError, the path may contain an error message. Empty otherwise. + std::string path; +}; + +// Opens a file dialog used for selecting a file to open. +DialogResult OpenFileDialog(std::string_view path, + std::span filters = {}); + +// Opens a file dialog used for selecting a file to save. +DialogResult SaveFileDialog(std::string_view path, + std::span filters = {}); + +// Opens a file dialog used for selecting a folder. +DialogResult SelectPathDialog(std::string_view path); + +} // namespace mujoco::platform + +#endif // MUJOCO_SRC_EXPERIMENTAL_PLATFORM_FILE_DIALOG_H_ diff --git a/src/experimental/platform/imgui_widgets.cc b/src/experimental/platform/imgui_widgets.cc index 97e38102..10a7b374 100644 --- a/src/experimental/platform/imgui_widgets.cc +++ b/src/experimental/platform/imgui_widgets.cc @@ -66,27 +66,6 @@ bool ImGui_Slider(const char* name, mjtNum* value, mjtNum min, mjtNum max) { return res; } -bool ImGui_FileDialog(char* buf, int len) { - bool ok = false; - ImGui::Text("Filename"); - ImGui::SameLine(); - ImGui::SetNextItemWidth(600); - if (ImGui::InputText("##Filename", buf, len, ImGuiInputTextFlags_EnterReturnsTrue)) { - ok = true; - ImGui::CloseCurrentPopup(); - }; - if (ImGui::Button("OK", ImVec2(120, 0)) || ImGui_IsChordJustPressed(ImGuiKey_Enter)) { - ok = true; - ImGui::CloseCurrentPopup(); - } - ImGui::SetItemDefaultFocus(); - ImGui::SameLine(); - if (ImGui::Button("Cancel", ImVec2(120, 0)) || ImGui_IsChordJustPressed(ImGuiKey_Escape)) { - ImGui::CloseCurrentPopup(); - } - return ok; -} - void MaybeSaveToClipboard(const std::string& contents) { if (ImGui::GetIO().SetClipboardTextFn) { ImGui::GetIO().SetClipboardTextFn(nullptr, contents.c_str()); diff --git a/src/experimental/platform/imgui_widgets.h b/src/experimental/platform/imgui_widgets.h index 478bbfc2..0b9f7d58 100644 --- a/src/experimental/platform/imgui_widgets.h +++ b/src/experimental/platform/imgui_widgets.h @@ -131,9 +131,6 @@ struct ScopedStyle { int num_vars = 0; }; -// ImGui file dialog. -bool ImGui_FileDialog(char* buf, int len); - // ImGui Slider that supports both float and double types. bool ImGui_Slider(const char* name, mjtNum* value, mjtNum min, mjtNum max); diff --git a/src/experimental/platform/window.cc b/src/experimental/platform/window.cc index 4e63b87e..93872016 100644 --- a/src/experimental/platform/window.cc +++ b/src/experimental/platform/window.cc @@ -164,6 +164,13 @@ void Window::SetTitle(std::string_view title) { SDL_SetWindowTitle(sdl_window_, title.data()); } +void Window::DisableWindowResizing() { + SDL_SetWindowResizable(sdl_window_, SDL_FALSE); +} +void Window::EnableWindowResizing() { + SDL_SetWindowResizable(sdl_window_, SDL_TRUE); +} + std::string Window::GetDropFile() { std::string tmp; std::swap(tmp, drop_file_); diff --git a/src/experimental/platform/window.h b/src/experimental/platform/window.h index a4124328..3ef69359 100644 --- a/src/experimental/platform/window.h +++ b/src/experimental/platform/window.h @@ -82,6 +82,12 @@ class Window { // the value will be cleared until the next time a file is dropped. std::string GetDropFile(); + // Enables window resizing. + void EnableWindowResizing(); + + // Disables window resizing. + void DisableWindowResizing(); + // Returns the handle to the underlying native window. void* GetNativeWindowHandle() { return native_window_; } diff --git a/src/experimental/studio/app.cc b/src/experimental/studio/app.cc index 8075338d..bf981f98 100644 --- a/src/experimental/studio/app.cc +++ b/src/experimental/studio/app.cc @@ -33,6 +33,7 @@ #include #include #include "experimental/platform/gui.h" +#include "experimental/platform/file_dialog.h" #include "experimental/platform/helpers.h" #include "experimental/platform/imgui_widgets.h" #include "experimental/platform/interaction.h" @@ -64,12 +65,6 @@ static void ToggleWindow(bool& window) { ImGui::GetIO().WantSaveIniSettings = true; } -static void ShowFileDialog(bool& dialog) { - #ifndef __EMSCRIPTEN__ - dialog = true; - #endif -} - static void SelectParentPerturb(const mjModel* model, mjvPerturb& perturb) { if (perturb.select > 0) { perturb.select = model->body_parentid[perturb.select]; @@ -314,12 +309,18 @@ void App::UpdateFilePaths(const std::string& resolved_path) { const std::filesystem::path path(resolved_path); const std::string base_path = path.parent_path().string() + "/"; const std::string model_name = path.stem().string(); - tmp_.last_load_file = base_path + model_name + ".xml"; - tmp_.last_save_mjb_file = base_path + model_name + "_saved.mjb"; - tmp_.last_save_xml_file = base_path + model_name + "_saved.xml"; - tmp_.last_print_model_file = base_path + model_name + "_MJMODEL.TXT"; - tmp_.last_print_data_file = base_path + model_name + "_MJDATA.TXT"; - tmp_.last_save_screenshot_file = base_path + "screenshot.webp"; + tmp_.last_path[UiTempState::FileDialog_Load] = + base_path + model_name + ".xml"; + tmp_.last_path[UiTempState::FileDialog_SaveMjb] = + base_path + model_name + "_saved.mjb"; + tmp_.last_path[UiTempState::FileDialog_SaveXml] = + base_path + model_name + "_saved.xml"; + tmp_.last_path[UiTempState::FileDialog_PrintModel] = + base_path + model_name + "_MJMODEL.TXT"; + tmp_.last_path[UiTempState::FileDialog_PrintData] = + base_path + model_name + "_MJDATA.TXT"; + tmp_.last_path[UiTempState::FileDialog_SaveScreenshot] = + base_path + "screenshot.webp"; } } @@ -415,7 +416,7 @@ bool App::Update() { // Only update the simulation if a popup window is not open. Note that the // simulation itself will only update if it is not paused. - if (!tmp_.modal_open) { + if (tmp_.file_dialog == UiTempState::FileDialog_None) { UpdatePhysics(); } @@ -603,17 +604,17 @@ void App::HandleKeyboardEvents() { // Menu shortcuts. if (ImGui_IsChordJustPressed(ImGuiKey_O | ImGuiMod_Ctrl)) { - ShowFileDialog(tmp_.load_popup); + tmp_.file_dialog = UiTempState::FileDialog_Load; } else if (ImGui_IsChordJustPressed(ImGuiKey_S | ImGuiMode_CtrlShift)) { - ShowFileDialog(tmp_.save_mjb_popup); + tmp_.file_dialog = UiTempState::FileDialog_SaveMjb; } else if (ImGui_IsChordJustPressed(ImGuiKey_S | ImGuiMod_Ctrl)) { - ShowFileDialog(tmp_.save_xml_popup); + tmp_.file_dialog = UiTempState::FileDialog_SaveXml; } else if (ImGui_IsChordJustPressed(ImGuiKey_M | ImGuiMod_Ctrl)) { - ShowFileDialog(tmp_.print_model_popup); + tmp_.file_dialog = UiTempState::FileDialog_PrintModel; } else if (ImGui_IsChordJustPressed(ImGuiKey_D | ImGuiMod_Ctrl)) { - ShowFileDialog(tmp_.print_data_popup); + tmp_.file_dialog = UiTempState::FileDialog_PrintData; } else if (ImGui_IsChordJustPressed(ImGuiKey_P | ImGuiMod_Ctrl)) { - ShowFileDialog(tmp_.save_screenshot_popup); + tmp_.file_dialog = UiTempState::FileDialog_SaveScreenshot; } else if (ImGui_IsChordJustPressed(ImGuiKey_C | ImGuiMod_Ctrl)) { std::string keyframe = platform::KeyframeToString(model_, data_, false); platform::MaybeSaveToClipboard(keyframe); @@ -1596,24 +1597,24 @@ void App::MainMenuGui() { if (ImGui::BeginMenu("File")) { #ifndef __EMSCRIPTEN__ if (ImGui::MenuItem("Open Model File", "Ctrl+O")) { - ShowFileDialog(tmp_.load_popup); + tmp_.file_dialog = UiTempState::FileDialog_Load; } ImGui::Separator(); if (ImGui::MenuItem("Save XML", "Ctrl+S")) { - ShowFileDialog(tmp_.save_xml_popup); + tmp_.file_dialog = UiTempState::FileDialog_SaveXml; } if (ImGui::MenuItem("Save MJB", "Ctrl+Shift+S")) { - ShowFileDialog(tmp_.save_mjb_popup); + tmp_.file_dialog = UiTempState::FileDialog_SaveMjb; } if (ImGui::MenuItem("Save Screenshot", "Ctrl+P")) { - ShowFileDialog(tmp_.save_screenshot_popup); + tmp_.file_dialog = UiTempState::FileDialog_SaveScreenshot; } ImGui::Separator(); if (ImGui::MenuItem("Print Model", "Ctrl+M")) { - ShowFileDialog(tmp_.print_model_popup); + tmp_.file_dialog = UiTempState::FileDialog_PrintModel; } if (ImGui::MenuItem("Print Data", "Ctrl+D")) { - ShowFileDialog(tmp_.print_data_popup); + tmp_.file_dialog = UiTempState::FileDialog_PrintData; } ImGui::Separator(); #endif // !__EMSCRIPTEN__ @@ -1728,110 +1729,66 @@ void App::MainMenuGui() { } void App::FileDialogGui() { + if (tmp_.file_dialog == UiTempState::FileDialog_None) { + return; + } + ImVec2 center = ImGui::GetMainViewport()->GetCenter(); ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); + if (ImGui::BeginPopupModal("FileDialog", NULL, + ImGuiWindowFlags_AlwaysAutoResize)) { + const platform::DialogResult res = platform::OpenFileDialog(tmp_.filename); - if (tmp_.load_popup) { - ImGui::OpenPopup("LoadModel"); - tmp_.load_popup = false; - strncpy(tmp_.filename, tmp_.last_load_file.c_str(), - tmp_.last_load_file.size()); - tmp_.filename[tmp_.last_load_file.size()] = 0; - } - if (tmp_.save_xml_popup) { - ImGui::OpenPopup("SaveXML"); - tmp_.save_xml_popup = false; - strncpy(tmp_.filename, tmp_.last_save_xml_file.c_str(), - tmp_.last_save_xml_file.size()); - tmp_.filename[tmp_.last_save_xml_file.size()] = 0; - } - if (tmp_.save_mjb_popup) { - ImGui::OpenPopup("SaveMJB"); - tmp_.save_mjb_popup = false; - strncpy(tmp_.filename, tmp_.last_save_mjb_file.c_str(), - tmp_.last_save_mjb_file.size()); - tmp_.filename[tmp_.last_save_mjb_file.size()] = 0; - } - if (tmp_.save_screenshot_popup) { - ImGui::OpenPopup("SaveWebp"); - tmp_.save_screenshot_popup = false; - strncpy(tmp_.filename, tmp_.last_save_screenshot_file.c_str(), - tmp_.last_save_screenshot_file.size()); - tmp_.filename[tmp_.last_save_screenshot_file.size()] = 0; - } - if (tmp_.print_model_popup) { - ImGui::OpenPopup("PrintModel"); - tmp_.print_model_popup = false; - strncpy(tmp_.filename, tmp_.last_print_model_file.c_str(), - tmp_.last_print_model_file.size()); - tmp_.filename[tmp_.last_print_model_file.size()] = 0; - } - if (tmp_.print_data_popup) { - ImGui::OpenPopup("PrintData"); - tmp_.print_data_popup = false; - strncpy(tmp_.filename, tmp_.last_print_data_file.c_str(), - tmp_.last_print_data_file.size()); - tmp_.filename[tmp_.last_print_data_file.size()] = 0; + if (res.status == platform::DialogResult::kAccepted) { + tmp_.last_path[tmp_.file_dialog] = res.path; + switch (tmp_.file_dialog) { + case UiTempState::FileDialog_Load: + RequestModelLoad(res.path); + break; + case UiTempState::FileDialog_SaveXml: + mj_saveLastXML(res.path.c_str(), model_, nullptr, 0); + break; + case UiTempState::FileDialog_SaveMjb: + mj_saveModel(model_, res.path.c_str(), nullptr, 0); + break; + case UiTempState::FileDialog_SaveScreenshot: { + const int width = window_->GetWidth(); + const int height = window_->GetHeight(); + std::vector buffer(width * height * 3); + renderer_->RenderToTexture(model_, data_, &camera_, width, height, + buffer.data()); + platform::SaveToWebp(width, height, buffer.data(), res.path); + break; + } + case UiTempState::FileDialog_PrintModel: + mj_printModel(model_, res.path.c_str()); + break; + case UiTempState::FileDialog_PrintData: + mj_printData(model_, data_, res.path.c_str()); + break; + default: + break; + } + } + + if (res.status != platform::DialogResult::kError) { + strncpy(tmp_.filename, res.path.c_str(), res.path.size()); + tmp_.filename[res.path.size()] = 0; + } + if (res.status != platform::DialogResult::kPending) { + tmp_.file_dialog = UiTempState::FileDialog_None; + ImGui::CloseCurrentPopup(); + window_->EnableWindowResizing(); + } + ImGui::EndPopup(); } - tmp_.modal_open = - ImGui::IsPopupOpen("LoadModel") || ImGui::IsPopupOpen("SaveXML") || - ImGui::IsPopupOpen("SaveMJB") || ImGui::IsPopupOpen("SaveWebp") || - ImGui::IsPopupOpen("PrintModel") || ImGui::IsPopupOpen("PrintData"); - - if (ImGui::BeginPopupModal("LoadModel", NULL, - ImGuiWindowFlags_AlwaysAutoResize)) { - if (platform::ImGui_FileDialog(tmp_.filename, sizeof(tmp_.filename))) { - RequestModelLoad(tmp_.filename); - tmp_.last_load_file = tmp_.filename; - } - ImGui::EndPopup(); - } - if (ImGui::BeginPopupModal("SaveXML", NULL, - ImGuiWindowFlags_AlwaysAutoResize)) { - if (platform::ImGui_FileDialog(tmp_.filename, sizeof(tmp_.filename))) { - char err[1000] = ""; - mj_saveLastXML(tmp_.filename, model_, err, 1000); - tmp_.last_save_xml_file = tmp_.filename; - } - ImGui::EndPopup(); - } - if (ImGui::BeginPopupModal("SaveMJB", NULL, - ImGuiWindowFlags_AlwaysAutoResize)) { - if (platform::ImGui_FileDialog(tmp_.filename, sizeof(tmp_.filename))) { - mj_saveModel(model_, tmp_.filename, nullptr, 0); - tmp_.last_save_mjb_file = tmp_.filename; - } - ImGui::EndPopup(); - } - if (ImGui::BeginPopupModal("SaveWebp", NULL, - ImGuiWindowFlags_AlwaysAutoResize)) { - if (platform::ImGui_FileDialog(tmp_.filename, sizeof(tmp_.filename))) { - const int width = window_->GetWidth(); - const int height = window_->GetHeight(); - std::vector buffer(width * height * 3); - renderer_->RenderToTexture(model_, data_, &camera_, width, height, - buffer.data()); - platform::SaveToWebp(width, height, buffer.data(), tmp_.filename); - tmp_.last_save_screenshot_file = tmp_.filename; - } - ImGui::EndPopup(); - } - if (ImGui::BeginPopupModal("PrintModel", NULL, - ImGuiWindowFlags_AlwaysAutoResize)) { - if (platform::ImGui_FileDialog(tmp_.filename, sizeof(tmp_.filename))) { - mj_printModel(model_, tmp_.filename); - tmp_.last_print_model_file = tmp_.filename; - } - ImGui::EndPopup(); - } - if (ImGui::BeginPopupModal("PrintData", NULL, - ImGuiWindowFlags_AlwaysAutoResize)) { - if (platform::ImGui_FileDialog(tmp_.filename, sizeof(tmp_.filename))) { - mj_printData(model_, data_, tmp_.filename); - tmp_.last_print_data_file = tmp_.filename; - } - ImGui::EndPopup(); + if (!ImGui::IsPopupOpen("FileDialog")) { + const std::string& src = tmp_.last_path[tmp_.file_dialog]; + strncpy(tmp_.filename, src.c_str(), src.size()); + tmp_.filename[src.size()] = 0; + ImGui::OpenPopup("FileDialog"); + window_->DisableWindowResizing(); } } diff --git a/src/experimental/studio/app.h b/src/experimental/studio/app.h index 85dcf136..16e75f35 100644 --- a/src/experimental/studio/app.h +++ b/src/experimental/studio/app.h @@ -104,8 +104,6 @@ class App { bool style_editor = false; bool imgui_demo = false; bool implot_demo = false; - bool modal_open = false; - bool load_popup = false; // Controls. bool perturb_active = false; @@ -129,18 +127,19 @@ class App { std::vector pips; // File dialogs. + enum FileDialog { + FileDialog_None, + FileDialog_Load, + FileDialog_SaveXml, + FileDialog_SaveMjb, + FileDialog_PrintModel, + FileDialog_PrintData, + FileDialog_SaveScreenshot, + NumFileDialogs, + }; + FileDialog file_dialog = FileDialog_None; + std::string last_path[NumFileDialogs]; char filename[1000] = ""; - std::string last_load_file; - bool save_xml_popup = false; - std::string last_save_xml_file; - bool save_mjb_popup = false; - std::string last_save_mjb_file; - bool save_screenshot_popup = false; - std::string last_save_screenshot_file; - bool print_model_popup = false; - std::string last_print_model_file; - bool print_data_popup = false; - std::string last_print_data_file; }; // Requests that the model be loaded from the given file at the next update.