diff --git a/src/experimental/platform/gui.cc b/src/experimental/platform/gui.cc index 5c7c92e8..7419f9dc 100644 --- a/src/experimental/platform/gui.cc +++ b/src/experimental/platform/gui.cc @@ -22,6 +22,7 @@ #include #include +#include #include #include #include "experimental/platform/helpers.h" @@ -29,12 +30,123 @@ namespace mujoco::platform { +static constexpr int kToolsBarHeight = 48; +static constexpr int kStatusBarHeight = 32; +static constexpr float kOptionsRelWidth = 0.22f; +static constexpr float kInspectorRelWidth = 0.18f; +static constexpr float kInfoRelHeight = 0.3f; + static ImVec2 GetFlexElementSize(int num_cols) { const float width = (ImGui::GetContentRegionAvail().x / num_cols) - ImGui::GetStyle().FramePadding.x * 2; return ImVec2(width, 0); } +void SetupTheme(GuiTheme theme) { + ImGuiStyle& s = ImGui::GetStyle(); + if (theme == GuiTheme::kDark) { + ImGui::StyleColorsDark(&s); + } else { + ImGui::StyleColorsLight(&s); + } + s.FrameBorderSize = 1; +} + +ImVec4 ConfigureDockingLayout() { + ImGuiViewport* viewport = ImGui::GetMainViewport(); + + const ImVec2 dockspace_pos{ + viewport->WorkPos.x, + viewport->WorkPos.y + kToolsBarHeight + }; + const ImVec2 dockspace_size{ + viewport->WorkSize.x, + viewport->WorkSize.y - kToolsBarHeight - kStatusBarHeight + }; + + ImGuiID root = ImGui::GetID("Root"); + const bool first_time = (ImGui::DockBuilderGetNode(root) == nullptr); + + if (first_time) { + ImGui::DockBuilderRemoveNode(root); + ImGui::DockBuilderAddNode(root, ImGuiDockNodeFlags_DockSpace); + ImGui::DockBuilderSetNodeSize(root, dockspace_size); + + // Slice up the main dock space. + ImGuiID main = root; + + ImGuiID options = 0; + ImGui::DockBuilderSplitNode(main, ImGuiDir_Left, kOptionsRelWidth, + &options, &main); + + ImGuiID inspector = 0; + ImGui::DockBuilderSplitNode(main, ImGuiDir_Right, kInspectorRelWidth, + &inspector, &main); + + ImGuiID info = 0; + ImGui::DockBuilderSplitNode(inspector, ImGuiDir_Down, kInfoRelHeight, + &info, &inspector); + + ImGui::DockBuilderDockWindow("Dockspace", main); + ImGui::DockBuilderDockWindow("Options", options); + ImGui::DockBuilderDockWindow("Inspector", inspector); + ImGui::DockBuilderDockWindow("Info", info); + ImGui::DockBuilderFinish(root); + } + + // Create a dummy window filling the entire workspace in which we can perform + // docking. + ImGui::SetNextWindowPos(dockspace_pos); + ImGui::SetNextWindowSize(dockspace_size); + ImGui::SetNextWindowViewport(viewport->ID); + + const ImGuiWindowFlags kWorkspaceFlags = + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoBringToFrontOnFocus | + ImGuiWindowFlags_NoNavFocus | ImGuiWindowFlags_NoBackground; + + platform::ScopedStyle style; + style.Var(ImGuiStyleVar_WindowRounding, 0.0f); + style.Var(ImGuiStyleVar_WindowBorderSize, 0.0f); + style.Var(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + ImGui::Begin("Dockspace", nullptr, kWorkspaceFlags); + style.Reset(); + + const ImGuiDockNodeFlags kDockSpaceFlags = + ImGuiDockNodeFlags_PassthruCentralNode | + ImGuiDockNodeFlags_NoDockingOverCentralNode | + ImGuiDockNodeFlags_AutoHideTabBar; + ImGui::DockSpace(root, ImVec2(0.0f, 0.0f), kDockSpaceFlags); + ImGui::End(); + + const ImGuiWindowFlags kFixedFlags = ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoDocking; + + // Toolbar is fixed at the top. + ImGui::SetNextWindowPos(viewport->WorkPos, ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(viewport->Size.x, kToolsBarHeight), ImGuiCond_Always); + ImGui::Begin("ToolBar", nullptr, kFixedFlags); + ImGui::End(); + + // StatusBar is fixed at the bottom. + ImGui::SetNextWindowPos(ImVec2(0, viewport->Size.y - kStatusBarHeight), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(viewport->Size.x, kStatusBarHeight), ImGuiCond_Always); + ImGui::Begin("StatusBar", nullptr, kFixedFlags); + ImGui::End(); + + const int settings_width = dockspace_size.x * kOptionsRelWidth; + const int inspector_width = dockspace_size.x * kInspectorRelWidth; + const float workspace_x = dockspace_pos.x + settings_width; + const float workspace_y = dockspace_pos.y; + const float workspace_w = dockspace_size.x - settings_width - inspector_width; + const float workspace_h = dockspace_size.y; + return ImVec4(workspace_x, workspace_y, workspace_w, workspace_h); +} + void SensorGui(const mjModel* model, const mjData* data) { ImPlot::PushStyleVar(ImPlotStyleVar_FitPadding, ImVec2(0.1f, 0.1f)); if (ImPlot::BeginPlot("Sensors", ImVec2(-1, 0), diff --git a/src/experimental/platform/gui.h b/src/experimental/platform/gui.h index d541005c..938d473f 100644 --- a/src/experimental/platform/gui.h +++ b/src/experimental/platform/gui.h @@ -25,10 +25,37 @@ #include +#include #include namespace mujoco::platform { +// Standard default UX themes for MuJoCo applications. +enum class GuiTheme { + kLight, + kDark, +}; + +// Updates the ImGui internal style state to match the requested theme. +void SetupTheme(GuiTheme theme); + +// Configures the ImGui docking module to the standard layout used by Studio. +// This includes the following named sections: +// "ToolBar": fixed size bar spanning the top of the window; for placing +// buttons and other controls that are always needed. +// "StatusBar": fixed size bar spanning the bottom of the window; for +// placing information and controls that are always needed. +// "Options": resizable section of the left; designed for GUI elements that +// are used to configure the simulation (e.g. PhysicsGui). +// "Inspector": resizable section of the right; designed for inspecting +// or manipulating mjData elements (e.g. ControlsGui). +// "Info": resizable section below the inspector; designed for displaying +// basic simulation information (e.g. InfoGui); hidden by default. +// +// Returns the size and position of the remaining workspace area which can then +// be used to place additional elements (e.g. floating charts). +ImVec4 ConfigureDockingLayout(); + // UX for controlling the physics simulation parameters (e.g. integrator, // solver, etc.) in mjModel. void PhysicsGui(mjModel* model, float min_width); diff --git a/src/experimental/studio/app.cc b/src/experimental/studio/app.cc index f2617d6e..38f51e74 100644 --- a/src/experimental/studio/app.cc +++ b/src/experimental/studio/app.cc @@ -34,7 +34,6 @@ #include #include -#include #include #include #include "experimental/platform/gui.h" @@ -122,12 +121,6 @@ static constexpr const char* ICON_NEXT_FRAME = ICON_FA_CARET_RIGHT; static constexpr const char* ICON_CURR_FRAME = ICON_FA_FAST_FORWARD; static constexpr const char* ICON_SPEED = ICON_FA_TACHOMETER; -static constexpr int kToolsBarHeight = 48; -static constexpr int kStatusBarHeight = 32; -static constexpr float kOptionsRelWidth = 0.22f; -static constexpr float kInspectorRelWidth = 0.18f; -static constexpr float kInfoRelHeight = 0.3f; - // UI labels for mjtLabel. static constexpr const char* kLabelNames[] = { "None", "Body", "Joint", "Geom", "Site", "Camera", @@ -758,8 +751,8 @@ void App::MoveCamera(platform::CameraMotion motion, mjtNum reldx, mjtNum reldy) } void App::BuildGui() { - SetupStyle(ui_.style); - const ImVec4 workspace_rect = ConfigureDockingLayout(); + SetupTheme(ui_.theme); + const ImVec4 workspace_rect = platform::ConfigureDockingLayout(); // Place charts in bottom right corner of the workspace. const ImVec2 chart_size(250, 250); @@ -890,114 +883,11 @@ void App::BuildGui() { } } -void App::SetupStyle(Style style) { - if (tmp_.style_editor) { - return; +void App::SetupTheme(platform::GuiTheme theme) { + if (!tmp_.style_editor) { + platform::SetupTheme(theme); + ui_.theme = theme; } - - ImGuiStyle& s = ImGui::GetStyle(); - if (style == kDark) { - ImGui::StyleColorsDark(&s); - } else { - ImGui::StyleColorsLight(&s); - } - s.FrameBorderSize = 1; - ui_.style = style; -} - -ImVec4 App::ConfigureDockingLayout() { - ImGuiViewport* viewport = ImGui::GetMainViewport(); - - const ImVec2 dockspace_pos{ - viewport->WorkPos.x, - viewport->WorkPos.y + kToolsBarHeight - }; - const ImVec2 dockspace_size{ - viewport->WorkSize.x, - viewport->WorkSize.y - kToolsBarHeight - kStatusBarHeight - }; - - ImGuiID root = ImGui::GetID("Root"); - const bool first_time = (ImGui::DockBuilderGetNode(root) == nullptr); - - if (first_time) { - ImGui::DockBuilderRemoveNode(root); - ImGui::DockBuilderAddNode(root, ImGuiDockNodeFlags_DockSpace); - ImGui::DockBuilderSetNodeSize(root, dockspace_size); - - // Slice up the main dock space. - ImGuiID main = root; - - ImGuiID options = 0; - ImGui::DockBuilderSplitNode(main, ImGuiDir_Left, kOptionsRelWidth, - &options, &main); - - ImGuiID inspector = 0; - ImGui::DockBuilderSplitNode(main, ImGuiDir_Right, kInspectorRelWidth, - &inspector, &main); - - ImGuiID info = 0; - ImGui::DockBuilderSplitNode(inspector, ImGuiDir_Down, kInfoRelHeight, - &info, &inspector); - - ImGui::DockBuilderDockWindow("Dockspace", main); - ImGui::DockBuilderDockWindow("Options", options); - ImGui::DockBuilderDockWindow("Inspector", inspector); - ImGui::DockBuilderDockWindow("Info", info); - ImGui::DockBuilderFinish(root); - } - - // Create a dummy window filling the entire workspace in which we can perform - // docking. - ImGui::SetNextWindowPos(dockspace_pos); - ImGui::SetNextWindowSize(dockspace_size); - ImGui::SetNextWindowViewport(viewport->ID); - - const ImGuiWindowFlags kWorkspaceFlags = - ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | - ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | - ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoBringToFrontOnFocus | - ImGuiWindowFlags_NoNavFocus | ImGuiWindowFlags_NoBackground; - - platform::ScopedStyle style; - style.Var(ImGuiStyleVar_WindowRounding, 0.0f); - style.Var(ImGuiStyleVar_WindowBorderSize, 0.0f); - style.Var(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); - ImGui::Begin("Dockspace", nullptr, kWorkspaceFlags); - style.Reset(); - - const ImGuiDockNodeFlags kDockSpaceFlags = - ImGuiDockNodeFlags_PassthruCentralNode | - ImGuiDockNodeFlags_NoDockingOverCentralNode | - ImGuiDockNodeFlags_AutoHideTabBar; - ImGui::DockSpace(root, ImVec2(0.0f, 0.0f), kDockSpaceFlags); - ImGui::End(); - - const ImGuiWindowFlags kFixedFlags = ImGuiWindowFlags_NoTitleBar | - ImGuiWindowFlags_NoMove | - ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoScrollbar | - ImGuiWindowFlags_NoDocking; - - // Toolbar is fixed at the top. - ImGui::SetNextWindowPos(viewport->WorkPos, ImGuiCond_Always); - ImGui::SetNextWindowSize(ImVec2(viewport->Size.x, kToolsBarHeight), ImGuiCond_Always); - ImGui::Begin("ToolBar", nullptr, kFixedFlags); - ImGui::End(); - - // StatusBar is fixed at the bottom. - ImGui::SetNextWindowPos(ImVec2(0, viewport->Size.y - kStatusBarHeight), ImGuiCond_Always); - ImGui::SetNextWindowSize(ImVec2(viewport->Size.x, kStatusBarHeight), ImGuiCond_Always); - ImGui::Begin("StatusBar", nullptr, kFixedFlags); - ImGui::End(); - - const int settings_width = dockspace_size.x * kOptionsRelWidth; - const int inspector_width = dockspace_size.x * kInspectorRelWidth; - const float workspace_x = dockspace_pos.x + settings_width; - const float workspace_y = dockspace_pos.y; - const float workspace_w = dockspace_size.x - settings_width - inspector_width; - const float workspace_h = dockspace_size.y; - return ImVec4(workspace_x, workspace_y, workspace_w, workspace_h); } void App::ModelOptionsGui() { @@ -1246,11 +1136,13 @@ void App::ToolBarGui() { // Style selection. ImGui::SameLine(); - if (ImGui::Button(ui_.style == kDark ? ICON_DARKMODE : ICON_LIGHTMODE)) { - if (ui_.style == kDark) { - SetupStyle(kLight); + if (ImGui::Button(ui_.theme == platform::GuiTheme::kDark + ? ICON_DARKMODE + : ICON_LIGHTMODE)) { + if (ui_.theme == platform::GuiTheme::kDark) { + SetupTheme(platform::GuiTheme::kLight); } else { - SetupStyle(kDark); + SetupTheme(platform::GuiTheme::kDark); } } ImGui::SetItemTooltip("%s", "Switch Style"); diff --git a/src/experimental/studio/app.h b/src/experimental/studio/app.h index d6444759..9b312d2e 100644 --- a/src/experimental/studio/app.h +++ b/src/experimental/studio/app.h @@ -25,8 +25,8 @@ #include #include -#include #include +#include "experimental/platform/gui.h" #include "experimental/platform/helpers.h" #include "experimental/platform/interaction.h" #include "experimental/platform/renderer.h" @@ -65,18 +65,13 @@ class App { static int LoadAssetCallback(const char* path, void* user_data, unsigned char** out, std::uint64_t* out_size); - enum Style { - kLight, - kDark, - }; - // UI state that is persisted across application runs struct UiState { char watch_field[1000] = "qpos"; int watch_index = 0; int camera_idx = platform::kTumbleCameraIdx; int key_idx = 0; - Style style = kLight; + platform::GuiTheme theme = platform::GuiTheme::kLight; using Dict = std::unordered_map; Dict ToDict() const; @@ -148,8 +143,7 @@ class App { void HandleKeyboardEvents(); void MoveCamera(platform::CameraMotion motion, mjtNum reldx, mjtNum reldy); - void SetupStyle(Style style); - ImVec4 ConfigureDockingLayout(); + void SetupTheme(platform::GuiTheme theme); void MainMenuGui(); void ToolBarGui();