diff --git a/src/experimental/platform/gui.cc b/src/experimental/platform/gui.cc index e8ecf14e..1c27ab38 100644 --- a/src/experimental/platform/gui.cc +++ b/src/experimental/platform/gui.cc @@ -44,12 +44,163 @@ static ImVec2 GetFlexElementSize(int num_cols) { void SetupTheme(GuiTheme theme) { ImGuiStyle& s = ImGui::GetStyle(); + ImVec4* c = s.Colors; if (theme == GuiTheme::kDark) { ImGui::StyleColorsDark(&s); - } else { + c[ImGuiCol_Text] = ImVec4(1.00, 1.00, 1.00, 1.00); + c[ImGuiCol_TextDisabled] = ImVec4(0.40, 0.40, 0.40, 1.00); + c[ImGuiCol_ChildBg] = ImVec4(0.25, 0.25, 0.25, 1.00); + c[ImGuiCol_WindowBg] = ImVec4(0.25, 0.25, 0.25, 1.00); + c[ImGuiCol_PopupBg] = ImVec4(0.25, 0.25, 0.25, 1.00); + c[ImGuiCol_Border] = ImVec4(0.12, 0.12, 0.12, 0.71); + c[ImGuiCol_BorderShadow] = ImVec4(1.00, 1.00, 1.00, 0.06); + c[ImGuiCol_FrameBg] = ImVec4(0.42, 0.42, 0.42, 0.54); + c[ImGuiCol_FrameBgHovered] = ImVec4(0.42, 0.42, 0.42, 0.40); + c[ImGuiCol_FrameBgActive] = ImVec4(0.56, 0.56, 0.56, 0.67); + c[ImGuiCol_TitleBg] = ImVec4(0.19, 0.19, 0.19, 1.00); + c[ImGuiCol_TitleBgActive] = ImVec4(0.22, 0.22, 0.22, 1.00); + c[ImGuiCol_TitleBgCollapsed] = ImVec4(0.17, 0.17, 0.17, 0.90); + c[ImGuiCol_MenuBarBg] = ImVec4(0.34, 0.34, 0.34, 1.00); + c[ImGuiCol_ScrollbarBg] = ImVec4(0.24, 0.24, 0.24, 0.53); + c[ImGuiCol_ScrollbarGrab] = ImVec4(0.41, 0.41, 0.41, 1.00); + c[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.52, 0.52, 0.52, 1.00); + c[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.76, 0.76, 0.76, 1.00); + c[ImGuiCol_CheckMark] = ImVec4(0.65, 0.65, 0.65, 1.00); + c[ImGuiCol_SliderGrab] = ImVec4(0.52, 0.52, 0.52, 1.00); + c[ImGuiCol_SliderGrabActive] = ImVec4(0.64, 0.64, 0.64, 1.00); + c[ImGuiCol_Button] = ImVec4(0.54, 0.54, 0.54, 0.35); + c[ImGuiCol_ButtonHovered] = ImVec4(0.52, 0.52, 0.52, 0.59); + c[ImGuiCol_ButtonActive] = ImVec4(0.76, 0.76, 0.76, 1.00); + c[ImGuiCol_Header] = ImVec4(0.38, 0.38, 0.38, 1.00); + c[ImGuiCol_HeaderHovered] = ImVec4(0.47, 0.47, 0.47, 1.00); + c[ImGuiCol_HeaderActive] = ImVec4(0.76, 0.76, 0.76, 0.77); + c[ImGuiCol_Separator] = ImVec4(0.00, 0.00, 0.00, 0.18); + c[ImGuiCol_SeparatorHovered] = ImVec4(0.70, 0.67, 0.60, 0.29); + c[ImGuiCol_SeparatorActive] = ImVec4(0.70, 0.67, 0.60, 0.67); + c[ImGuiCol_ResizeGrip] = ImVec4(0.26, 0.59, 0.98, 0.25); + c[ImGuiCol_ResizeGripHovered] = ImVec4(0.26, 0.59, 0.98, 0.67); + c[ImGuiCol_ResizeGripActive] = ImVec4(0.26, 0.59, 0.98, 0.95); + c[ImGuiCol_PlotLines] = ImVec4(0.61, 0.61, 0.61, 1.00); + c[ImGuiCol_PlotLinesHovered] = ImVec4(1.00, 0.43, 0.35, 1.00); + c[ImGuiCol_PlotHistogram] = ImVec4(0.90, 0.70, 0.00, 1.00); + c[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00, 0.60, 0.00, 1.00); + c[ImGuiCol_TextSelectedBg] = ImVec4(0.73, 0.73, 0.73, 0.35); + c[ImGuiCol_ModalWindowDimBg] = ImVec4(0.80, 0.80, 0.80, 0.35); + c[ImGuiCol_DragDropTarget] = ImVec4(1.00, 1.00, 0.00, 0.90); + c[ImGuiCol_NavHighlight] = ImVec4(0.26, 0.59, 0.98, 1.00); + c[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00, 1.00, 1.00, 0.70); + c[ImGuiCol_NavWindowingDimBg] = ImVec4(0.80, 0.80, 0.80, 0.20); + c[ImGuiCol_DockingEmptyBg] = ImVec4(0.38, 0.38, 0.38, 1.00); + c[ImGuiCol_Tab] = ImVec4(0.25, 0.25, 0.25, 1.00); + c[ImGuiCol_TabHovered] = ImVec4(0.40, 0.40, 0.40, 1.00); + c[ImGuiCol_TabActive] = ImVec4(0.33, 0.33, 0.33, 1.00); + c[ImGuiCol_TabUnfocused] = ImVec4(0.25, 0.25, 0.25, 1.00); + c[ImGuiCol_TabUnfocusedActive] = ImVec4(0.33, 0.33, 0.33, 1.00); + c[ImGuiCol_DockingPreview] = ImVec4(0.85, 0.85, 0.85, 0.28); + c[ImGuiCol_WindowBg].w = 1.0f; + } else if (theme == GuiTheme::kLight) { ImGui::StyleColorsLight(&s); + ImVec4 white = ImVec4(1.00, 1.00, 1.00, 1.00); + ImVec4 transparent = ImVec4(0.00, 0.00, 0.00, 0.00); + ImVec4 dark = ImVec4(0.00, 0.00, 0.00, 0.20); + ImVec4 darker = ImVec4(0.00, 0.00, 0.00, 0.50); + ImVec4 background = ImVec4(0.95, 0.95, 0.95, 1.00); + ImVec4 text = ImVec4(0.10, 0.10, 0.10, 1.00); + ImVec4 border = ImVec4(0.60, 0.60, 0.60, 1.00); + ImVec4 grab = ImVec4(0.69, 0.69, 0.69, 1.00); + ImVec4 header = ImVec4(0.86, 0.86, 0.86, 1.00); + ImVec4 active = ImVec4(0.00, 0.47, 0.84, 1.00); + ImVec4 hover = ImVec4(0.00, 0.47, 0.84, 0.20); + + c[ImGuiCol_Text] = text; + c[ImGuiCol_WindowBg] = background; + c[ImGuiCol_ChildBg] = background; + c[ImGuiCol_PopupBg] = white; + c[ImGuiCol_Border] = border; + c[ImGuiCol_BorderShadow] = transparent; + c[ImGuiCol_Button] = header; + c[ImGuiCol_ButtonHovered] = hover; + c[ImGuiCol_ButtonActive] = active; + c[ImGuiCol_FrameBg] = white; + c[ImGuiCol_FrameBgHovered] = hover; + c[ImGuiCol_FrameBgActive] = active; + c[ImGuiCol_MenuBarBg] = header; + c[ImGuiCol_Header] = header; + c[ImGuiCol_HeaderHovered] = hover; + c[ImGuiCol_HeaderActive] = active; + c[ImGuiCol_CheckMark] = text; + c[ImGuiCol_SliderGrab] = grab; + c[ImGuiCol_SliderGrabActive] = darker; + c[ImGuiCol_ScrollbarBg] = header; + c[ImGuiCol_ScrollbarGrab] = grab; + c[ImGuiCol_ScrollbarGrabHovered] = dark; + c[ImGuiCol_ScrollbarGrabActive] = darker; + } else { + ImGui::StyleColorsDark(&s); + ImVec4 black = ImVec4(0.00, 0.00, 0.00, 1.0); + ImVec4 window = ImVec4(0.25, 0.25, 0.25, 1.0); + ImVec4 font_active = ImVec4(1.00, 1.00, 1.00, 1.0); + ImVec4 font_inactive = ImVec4(0.50, 0.50, 0.50, 1.0); + ImVec4 thumb = ImVec4(0.12, 0.12, 0.12, 1.0); + ImVec4 section = ImVec4(0.40, 0.15, 0.15, 1.0); + ImVec4 button = ImVec4(0.60, 0.40, 0.40, 1.0); + ImVec4 check = ImVec4(0.40, 0.40, 0.70, 1.0); + ImVec4 frame = ImVec4(0.40, 0.30, 0.40, 1.0); + ImVec4 slider = ImVec4(0.60, 0.40, 0.60, 1.0); + + c[ImGuiCol_WindowBg] = window; + c[ImGuiCol_ChildBg] = black; + c[ImGuiCol_PopupBg] = window; + c[ImGuiCol_Text] = font_active; + c[ImGuiCol_TextDisabled] = font_inactive; + c[ImGuiCol_CheckMark] = font_active; + c[ImGuiCol_Header] = section; + c[ImGuiCol_HeaderHovered] = section; + c[ImGuiCol_HeaderActive] = section; + c[ImGuiCol_TitleBgActive] = window; + c[ImGuiCol_ScrollbarBg] = window; + c[ImGuiCol_ScrollbarGrab] = thumb; + c[ImGuiCol_ScrollbarGrabHovered] = thumb; + c[ImGuiCol_ScrollbarGrabActive] = thumb; + c[ImGuiCol_FrameBg] = frame; + c[ImGuiCol_FrameBgHovered] = frame; + c[ImGuiCol_FrameBgActive] = frame; + c[ImGuiCol_SliderGrab] = slider; + c[ImGuiCol_SliderGrabActive] = slider; + c[ImGuiCol_Button] = window; + c[ImGuiCol_ButtonHovered] = button; + c[ImGuiCol_ButtonActive] = button; + c[ImGuiCol_Tab] = window; + c[ImGuiCol_TabHovered] = check; + c[ImGuiCol_TabSelected] = check; + c[ImGuiCol_TabDimmed] = window; + c[ImGuiCol_TabDimmedSelected] = check; } - s.FrameBorderSize = 1; + + int hspacing = 4; + int vspacing = 6; + float rounding = 4.0f; + s.DisplaySafeAreaPadding = ImVec2(0, 0); + s.WindowPadding = ImVec2(hspacing, vspacing); + s.FramePadding = ImVec2(hspacing, vspacing); + s.ItemSpacing = ImVec2(hspacing, vspacing); + s.ItemInnerSpacing = ImVec2(hspacing, vspacing); + s.WindowRounding = rounding; + s.FrameRounding = rounding; + s.TabRounding = rounding; + s.ScrollbarRounding = rounding; + s.ChildRounding = rounding; + s.GrabRounding = rounding; + s.PopupRounding = rounding; + s.WindowBorderSize = 0.0f; + s.FrameBorderSize = 1.0f; + s.PopupBorderSize = 1.0f; + s.IndentSpacing = 20.0f; + s.ScrollbarSize = 12.0f; + s.GrabMinSize = 5.0f; + s.WindowMenuButtonPosition = ImGuiDir_None; + s.TabCloseButtonMinWidthSelected = 0.0f; + s.DockingNodeHasCloseButton = false; } ImVec4 ConfigureDockingLayout() { @@ -112,37 +263,48 @@ ImVec4 ConfigureDockingLayout() { 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; + // Main workspace area in which we can dock other windows. + { + 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); + const ImGuiDockNodeFlags kDockSpaceFlags = + ImGuiDockNodeFlags_PassthruCentralNode | + ImGuiDockNodeFlags_NoDockingOverCentralNode | + ImGuiDockNodeFlags_AutoHideTabBar; + ImGui::DockSpace(root, ImVec2(0.0f, 0.0f), kDockSpaceFlags); + ImGui::End(); + } + // 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(); + { + platform::ScopedStyle style; + style.Var(ImGuiStyleVar_WindowBorderSize, 1.0f); + style.Var(ImGuiStyleVar_WindowRounding, 0.0f); + 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(); + { + platform::ScopedStyle style; + style.Var(ImGuiStyleVar_WindowBorderSize, 1.0f); + style.Var(ImGuiStyleVar_WindowRounding, 0.0f); + 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; @@ -154,6 +316,10 @@ ImVec4 ConfigureDockingLayout() { } void SensorGui(const mjModel* model, const mjData* data) { + if (model->nsensor == 0) { + return; + } + ImPlot::PushStyleVar(ImPlotStyleVar_FitPadding, ImVec2(0.1f, 0.1f)); if (ImPlot::BeginPlot("Sensors", ImVec2(-1, 0), ImPlotFlags_NoLegend | ImPlotFlags_NoMouseText)) { @@ -356,6 +522,9 @@ void StateGui(const mjModel* model, mjData* data, std::vector& state, void WatchGui(const mjModel* model, const mjData* data, char* field_name, int field_len, int& field_index) { + const float item_width = ImGui::GetWindowWidth() * .6f; + ImGui::PushItemWidth(item_width); + ImGui::InputText("Field", field_name, field_len); ImGui::InputInt("Index", &field_index); const mjtNum* value = static_cast( @@ -375,6 +544,8 @@ void WatchGui(const mjModel* model, const mjData* data, char* field_name, ImGui::InputText("Value", buf, sizeof(buf), ImGuiInputTextFlags_ReadOnly); ImGui::EndDisabled(); } + + ImGui::PopItemWidth(); } void PhysicsGui(mjModel* model, float min_width) { @@ -383,6 +554,9 @@ void PhysicsGui(mjModel* model, float min_width) { const int num_cols = std::clamp( static_cast(std::floor(available_width / min_width)), 1, 6); + const float item_width = ImGui::GetWindowWidth() * .6f; + ImGui::PushItemWidth(item_width); + auto& opt = model->opt; const char* opts0[] = {"Euler", "RK4", "implicit", "implicitfast"}; @@ -397,21 +571,15 @@ void PhysicsGui(mjModel* model, float min_width) { const char* opts3[] = {"PGS", "CG", "Newton"}; ImGui::Combo("Solver", &opt.solver, opts3, IM_ARRAYSIZE(opts3)); - if (ImGui::TreeNodeEx("Disable Flags", ImGuiTreeNodeFlags_DefaultOpen)) { - if (ImGui::BeginTable("##DisableFlagsTable", num_cols)) { + if (ImGui::TreeNodeEx("Flags", ImGuiTreeNodeFlags_DefaultOpen)) { + if (ImGui::BeginTable("##PhysicsFlagsTable", num_cols)) { const ImVec2 size = GetFlexElementSize(num_cols); for (int i = 0; i < mjNDISABLE; ++i) { ImGui::TableNextColumn(); - ImGui_BitToggle(mjDISABLESTRING[i], &opt.disableflags, 1 << i, size); + int flipped = ~opt.disableflags; + ImGui_BitToggle(mjDISABLESTRING[i], &flipped, 1 << i, size); + opt.disableflags = ~flipped; } - ImGui::EndTable(); - } - ImGui::TreePop(); - } - - if (ImGui::TreeNodeEx("Enable Flags", ImGuiTreeNodeFlags_DefaultOpen)) { - if (ImGui::BeginTable("##EnableFlagsTable", num_cols)) { - const ImVec2 size = GetFlexElementSize(num_cols); for (int i = 0; i < mjNENABLE; ++i) { ImGui::TableNextColumn(); ImGui_BitToggle(mjENABLESTRING[i], &opt.enableflags, 1 << i, size); @@ -421,14 +589,16 @@ void PhysicsGui(mjModel* model, float min_width) { ImGui::TreePop(); } - if (ImGui::TreeNodeEx("Actuator Group Disable")) { - if (ImGui::BeginTable("##EnableFlagsTable", num_cols)) { + if (ImGui::TreeNodeEx("Actuator Groups")) { + if (ImGui::BeginTable("##ActuatorGroupsTable", num_cols)) { const ImVec2 size = GetFlexElementSize(num_cols); for (int i = 0; i < 6; ++i) { char label[64]; std::snprintf(label, sizeof(label), "Act Group %d", i); ImGui::TableNextColumn(); - ImGui_BitToggle(label, &opt.disableactuator, 1 << i, size); + int flipped = ~opt.disableactuator; + ImGui_BitToggle(label, &flipped, 1 << i, size); + opt.disableactuator = ~flipped; } ImGui::EndTable(); } @@ -436,41 +606,40 @@ void PhysicsGui(mjModel* model, float min_width) { }; if (ImGui::TreeNodeEx("Algorithmic Parameters")) { - float w = ImGui::GetWindowWidth() * .6f; - ImGui_Input("Timestep", &opt.timestep, {0, 1, 0.01, 0.1, w}); - ImGui_Input("Iterations", &opt.iterations, {0, 1000, 1, 10, w}); - ImGui_Input("Tolerance", &opt.tolerance, {0, 1, 1e-7, 1e-6, w}); - ImGui_Input("LS Iter", &opt.ls_iterations, {0, 100, 1, 0.1, w}); - ImGui_Input("LS Tol", &opt.ls_tolerance, {0, 0.1, 0.01, 0.1, w}); - ImGui_Input("Noslip Iter", &opt.noslip_iterations, {0, 1000, 1, 100, w}); - ImGui_Input("Noslip Tol", &opt.noslip_tolerance, {0, 1, 0.01, 0.1, w}); - ImGui_Input("CCD Iter", &opt.ccd_iterations, {0, 1000, 1, 100, w}); - ImGui_Input("CCD Tol", &opt.ccd_tolerance, {0, 1, 0.01, 0.1, w}); - ImGui_Input("Sleep Tol", &opt.sleep_tolerance, {0, 1, 0.01, 0.1, w}); - ImGui_Input("SDF Iter", &opt.sdf_iterations, {1, 20, 1, 10, w}); - ImGui_Input("SDF Init", &opt.sdf_initpoints, {1, 100, 1, 10, w}); + ImGui_Input("Timestep", &opt.timestep, {0, 1, 0.01, 0.1}); + ImGui_Input("Iterations", &opt.iterations, {0, 1000, 1, 10}); + ImGui_Input("Tolerance", &opt.tolerance, {0, 1, 1e-7, 1e-6}); + ImGui_Input("LS Iter", &opt.ls_iterations, {0, 100, 1, 0.1}); + ImGui_Input("LS Tol", &opt.ls_tolerance, {0, 0.1, 0.01, 0.1}); + ImGui_Input("Noslip Iter", &opt.noslip_iterations, {0, 1000, 1, 100}); + ImGui_Input("Noslip Tol", &opt.noslip_tolerance, {0, 1, 0.01, 0.1}); + ImGui_Input("CCD Iter", &opt.ccd_iterations, {0, 1000, 1, 100}); + ImGui_Input("CCD Tol", &opt.ccd_tolerance, {0, 1, 0.01, 0.1}); + ImGui_Input("Sleep Tol", &opt.sleep_tolerance, {0, 1, 0.01, 0.1}); + ImGui_Input("SDF Iter", &opt.sdf_iterations, {1, 20, 1, 10}); + ImGui_Input("SDF Init", &opt.sdf_initpoints, {1, 100, 1, 10}); ImGui::TreePop(); } if (ImGui::TreeNodeEx("Physical Parameters")) { - float w = ImGui::GetWindowWidth() * .6f; - ImGui_InputN("Gravity", opt.gravity, 3, {.width = w}); - ImGui_InputN("Wind", opt.wind, 3, {.width = w}); - ImGui_InputN("Magnetic", opt.magnetic, 3, {.width = w}); - ImGui_Input("Density", &opt.density, {.min = .1, .max = 1, .width = w}); - ImGui_Input("Viscosity", &opt.viscosity, {.min = .1, .max = 1, .width = w}); - ImGui_Input("Imp Ratio", &opt.impratio, {.min = .1, .max = 1, .width = w}); + ImGui_InputN("Gravity", opt.gravity, 3); + ImGui_InputN("Wind", opt.wind, 3); + ImGui_InputN("Magnetic", opt.magnetic, 3); + ImGui_Input("Density", &opt.density, {.min = .1, .max = 1}); + ImGui_Input("Viscosity", &opt.viscosity, {.min = .1, .max = 1}); + ImGui_Input("Imp Ratio", &opt.impratio, {.min = .1, .max = 1}); ImGui::TreePop(); }; if (ImGui::TreeNodeEx("Contact Override")) { - float w = ImGui::GetWindowWidth() * .6f; - ImGui_Input("Margin", &opt.o_margin, {.min = 0.1, .max = 1, .width = w}); - ImGui_InputN("Sol Imp", opt.o_solimp, 5, {.width = w, .format = "%0.1f"}); - ImGui_InputN("Sol Ref", opt.o_solref, 2, {.width = w, .format = "%0.1f"}); - ImGui_InputN("Friction", opt.o_friction, 5, {.width = w, .format = "%.1f"}); + ImGui_Input("Margin", &opt.o_margin, {.min = 0.1, .max = 1}); + ImGui_InputN("Sol Imp", opt.o_solimp, 5, {.format = "%0.1f"}); + ImGui_InputN("Sol Ref", opt.o_solref, 2, {.format = "%0.1f"}); + ImGui_InputN("Friction", opt.o_friction, 5, {.format = "%.1f"}); ImGui::TreePop(); } + + ImGui::PopItemWidth(); } void VisualizationGui(mjModel* model, mjvOption* vis_options, mjvCamera* camera, @@ -478,6 +647,9 @@ void VisualizationGui(mjModel* model, mjvOption* vis_options, mjvCamera* camera, auto& vis = model->vis; auto& stat = model->stat; + const float item_width = ImGui::GetWindowWidth() * .6f; + ImGui::PushItemWidth(item_width); + ImGui::SliderInt("Tree depth", &vis_options->bvh_depth, 0, 20); ImGui::SliderInt("Flex layer", &vis_options->flex_layer, 0, 10); @@ -574,6 +746,8 @@ void VisualizationGui(mjModel* model, mjvOption* vis_options, mjvCamera* camera, ImGui::ColorEdit4("BV Active", vis.rgba.bvactive); ImGui::TreePop(); } + + ImGui::PopItemWidth(); } void RenderingGui(const mjModel* model, mjvOption* vis_options, @@ -659,12 +833,18 @@ void GroupsGui(const mjModel* model, mjvOption* vis_options, float min_width) { void NoiseGui(const mjModel* model, const mjData* data, float& noise_scale, float& noise_rate) { + const float item_width = ImGui::GetWindowWidth() * .6f; + ImGui::PushItemWidth(item_width); ImGui::SliderFloat("Scale", &noise_scale, 0, 1); ImGui::SliderFloat("Rate", &noise_rate, 0, 4); + ImGui::PopItemWidth(); } void JointsGui(const mjModel* model, const mjData* data, const mjvOption* vis_options) { + const float item_width = ImGui::GetWindowWidth() * .6f; + ImGui::PushItemWidth(item_width); + char name[100]; for (int i = 0; i < model->njnt; ++i) { if (model->jnt_type[i] != mjJNT_HINGE && @@ -699,10 +879,15 @@ void JointsGui(const mjModel* model, const mjData* data, const int data_adr = model->jnt_qposadr[i]; ImGui_Slider(name, &data->qpos[data_adr], min, max); } + + ImGui::PopItemWidth(); } void ControlsGui(const mjModel* model, const mjData* data, const mjvOption* vis_options) { + const float item_width = ImGui::GetWindowWidth() * .6f; + ImGui::PushItemWidth(item_width); + if (ImGui::Button("Clear All")) { mju_zero(data->ctrl, model->nu); } @@ -733,8 +918,9 @@ void ControlsGui(const mjModel* model, const mjData* data, } ImGui_Slider(name, &data->ctrl[i], min, max); } -} + ImGui::PopItemWidth(); +} void ConvergenceGui(const mjModel* model, mjData* data) { if (ImPlot::BeginPlot("Convergence (log 10)", ImVec2(-1, 0), diff --git a/src/experimental/platform/gui.h b/src/experimental/platform/gui.h index da0f0ad5..2341023e 100644 --- a/src/experimental/platform/gui.h +++ b/src/experimental/platform/gui.h @@ -34,6 +34,7 @@ namespace mujoco::platform { enum class GuiTheme { kLight, kDark, + kClassic, }; // Updates the ImGui internal style state to match the requested theme. diff --git a/src/experimental/platform/imgui_widgets.h b/src/experimental/platform/imgui_widgets.h index f35dfc6b..a3b7087f 100644 --- a/src/experimental/platform/imgui_widgets.h +++ b/src/experimental/platform/imgui_widgets.h @@ -22,11 +22,32 @@ #include #include -#include // For ButtonEx and PressedOnClick +#include #include namespace mujoco::platform { +// FontAwesome icon codes. +static constexpr const char ICON_FA_ARROWS[] = u8"\uf047"; +static constexpr const char ICON_FA_CAMERA[] = u8"\uf03d"; +static constexpr const char ICON_FA_CARET_LEFT[] = u8"\uf0d9"; +static constexpr const char ICON_FA_CARET_RIGHT[] = u8"\uf0da"; +static constexpr const char ICON_FA_CHECK_SQUARE_O[] = u8"\uf05d"; +static constexpr const char ICON_FA_COMMENT[] = u8"\uf0e5"; +static constexpr const char ICON_FA_COPY[] = u8"\uf0c5"; +static constexpr const char ICON_FA_DIAMOND[] = u8"\uf219"; +static constexpr const char ICON_FA_EJECT[] = u8"\uf052"; +static constexpr const char ICON_FA_FAST_FORWARD[] = u8"\uf050"; +static constexpr const char ICON_FA_MOON[] = u8"\uf186"; +static constexpr const char ICON_FA_PAUSE[] = u8"\uf04c"; +static constexpr const char ICON_FA_PLAY[] = u8"\uf04b"; +static constexpr const char ICON_FA_REFRESH[] = u8"\uf021"; +static constexpr const char ICON_FA_SQUARE_O[] = u8"\uf1db"; +static constexpr const char ICON_FA_SUN[] = u8"\uf185"; +static constexpr const char ICON_FA_TACHOMETER[] = u8"\uf0e4"; +static constexpr const char ICON_FA_UNDO[] = u8"\uf0e2"; + + using KeyValues = std::unordered_map; // Appends key/value pairs to an Ini file. @@ -107,13 +128,15 @@ bool ImGui_ButtonToggle(const char* label, T* boolean, static_assert(std::is_integral_v, "Toggle only supports integral types."); ScopedStyle style; - if (!(*boolean)) { - ImColor button = ImGui::GetStyle().Colors[ImGuiCol_Button]; - button.Value.w = 0.0f; - style.Color(ImGuiCol_Button, button); - } + const int color = *boolean ? ImGuiCol_TabSelected : ImGuiCol_WindowBg; + style.Color(ImGuiCol_Button, ImGui::GetStyle().Colors[color]); + style.Var(ImGuiStyleVar_ButtonTextAlign, ImVec2(0.0f, 0.5f)); - if (ImGui::Button(label, size)) { + const std::string txt = + std::string(" ") + + std::string(*boolean ? ICON_FA_SQUARE_O : ICON_FA_CHECK_SQUARE_O) + " " + + label; + if (ImGui::Button(txt.c_str(), size)) { *boolean = !(*boolean); return true; } diff --git a/src/experimental/studio/app.cc b/src/experimental/studio/app.cc index e757295a..de3fe7fd 100644 --- a/src/experimental/studio/app.cc +++ b/src/experimental/studio/app.cc @@ -88,38 +88,22 @@ static void SelectParentPerturb(const mjModel* model, mjvPerturb& perturb) { } } -// FontAwesome icon codes. -static constexpr const char* ICON_FA_PLAY = "\xef\x81\x8b"; -static constexpr const char* ICON_FA_PAUSE = "\xef\x81\x8c"; -static constexpr const char* ICON_FA_COPY = "\xef\x83\x85"; -static constexpr const char* ICON_FA_EJECT = "\xef\x81\x92"; -static constexpr const char* ICON_FA_REFRESH = "\xef\x80\xa1"; -static constexpr const char* ICON_FA_COMMENT = "\xef\x83\xa5"; -static constexpr const char* ICON_FA_UNDO = "\xef\x83\xa2"; -static constexpr const char* ICON_FA_ARROWS = "\xef\x81\x87"; -static constexpr const char* ICON_FA_CAMERA = "\xef\x80\xbd"; -static constexpr const char* ICON_FA_MOON = "\xef\x86\x86"; -static constexpr const char* ICON_FA_SUN = "\xef\x86\x85"; -static constexpr const char* ICON_FA_CARET_LEFT = "\xef\x83\x99"; -static constexpr const char* ICON_FA_CARET_RIGHT = "\xef\x83\x9a"; -static constexpr const char* ICON_FA_FAST_FORWARD = "\xef\x81\x90"; -static constexpr const char* ICON_FA_TACHOMETER = "\xef\x83\xa4"; - -static constexpr const char* ICON_PLAY = ICON_FA_PLAY; -static constexpr const char* ICON_PAUSE = ICON_FA_PAUSE; -static constexpr const char* ICON_COPY_CAMERA = ICON_FA_COPY; -static constexpr const char* ICON_UNLOAD_MODEL = ICON_FA_EJECT; -static constexpr const char* ICON_RELOAD_MODEL = ICON_FA_REFRESH; -static constexpr const char* ICON_LABEL = ICON_FA_COMMENT; -static constexpr const char* ICON_RESET_MODEL = ICON_FA_UNDO; -static constexpr const char* ICON_FRAME = ICON_FA_ARROWS; -static constexpr const char* ICON_CAMERA = ICON_FA_CAMERA; -static constexpr const char* ICON_DARKMODE = ICON_FA_MOON; -static constexpr const char* ICON_LIGHTMODE = ICON_FA_SUN; -static constexpr const char* ICON_PREV_FRAME = ICON_FA_CARET_LEFT; -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 const char* ICON_PLAY = platform::ICON_FA_PLAY; +static constexpr const char* ICON_PAUSE = platform::ICON_FA_PAUSE; +static constexpr const char* ICON_COPY_CAMERA = platform::ICON_FA_COPY; +static constexpr const char* ICON_UNLOAD_MODEL = platform::ICON_FA_EJECT; +static constexpr const char* ICON_RELOAD_MODEL = platform::ICON_FA_REFRESH; +static constexpr const char* ICON_LABEL = platform::ICON_FA_COMMENT; +static constexpr const char* ICON_RESET_MODEL = platform::ICON_FA_UNDO; +static constexpr const char* ICON_FRAME = platform::ICON_FA_ARROWS; +static constexpr const char* ICON_CAMERA = platform::ICON_FA_CAMERA; +static constexpr const char* ICON_DARKMODE = platform::ICON_FA_MOON; +static constexpr const char* ICON_LIGHTMODE = platform::ICON_FA_SUN; +static constexpr const char* ICON_CLASSICMODE = platform::ICON_FA_DIAMOND; +static constexpr const char* ICON_PREV_FRAME = platform::ICON_FA_CARET_LEFT; +static constexpr const char* ICON_NEXT_FRAME = platform::ICON_FA_CARET_RIGHT; +static constexpr const char* ICON_CURR_FRAME = platform::ICON_FA_FAST_FORWARD; +static constexpr const char* ICON_SPEED = platform::ICON_FA_TACHOMETER; // UI labels for mjtLabel. static constexpr const char* kLabelNames[] = { @@ -145,7 +129,7 @@ App::App(int width, int height, std::string ini_path, const platform::LoadAssetFn& load_asset_fn) : ini_path_(std::move(ini_path)), load_asset_fn_(load_asset_fn) { window_ = std::make_unique("MuJoCo Studio", width, height, - kWindowConfig, load_asset_fn); + kWindowConfig, load_asset_fn); ImPlot::CreateContext(); auto make_context_fn = [&](const mjModel* m, mjrContext* con) { @@ -480,7 +464,7 @@ void App::HandleMouseEvents() { platform::InitPerturb(model_, data_, &camera_, &perturb_, active); } platform::MovePerturb(model_, data_, &camera_, &perturb_, action, - mouse_dx, mouse_dy); + mouse_dx, mouse_dy); } } // Handle camera movement actions. @@ -515,7 +499,7 @@ void App::HandleMouseEvents() { if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { platform::PickResult picked = platform::Pick(model_, data_, &camera_, mouse_x, mouse_y, - window_->GetAspectRatio(), &vis_options_); + window_->GetAspectRatio(), &vis_options_); if (picked.body >= 0) { perturb_.select = picked.body; perturb_.flexselect = picked.flex; @@ -537,7 +521,7 @@ void App::HandleMouseEvents() { if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Right)) { platform::PickResult picked = platform::Pick(model_, data_, &camera_, mouse_x, mouse_y, - window_->GetAspectRatio(), &vis_options_); + window_->GetAspectRatio(), &vis_options_); mju_copy3(camera_.lookat, picked.point); if (picked.body > 0 && io.KeyCtrl) { // Switch camera to tracking mode and track the selected body. @@ -697,11 +681,14 @@ void App::HandleKeyboardEvents() { ToggleFlag(vis_options_.geomgroup[5]); } else if (model_) { if (ImGui_IsChordJustPressed(ImGuiKey_Escape)) { - ui_.camera_idx = platform::SetCamera(model_, &camera_, platform::kTumbleCameraIdx); + ui_.camera_idx = + platform::SetCamera(model_, &camera_, platform::kTumbleCameraIdx); } else if (ImGui_IsChordJustPressed(ImGuiKey_LeftBracket)) { - ui_.camera_idx = platform::SetCamera(model_, &camera_, ui_.camera_idx - 1); + ui_.camera_idx = + platform::SetCamera(model_, &camera_, ui_.camera_idx - 1); } else if (ImGui_IsChordJustPressed(ImGuiKey_RightBracket)) { - ui_.camera_idx = platform::SetCamera(model_, &camera_, ui_.camera_idx + 1); + ui_.camera_idx = + platform::SetCamera(model_, &camera_, ui_.camera_idx + 1); } // WASD camera controls for free camera. @@ -777,7 +764,8 @@ void App::SetSpeedIndex(int idx) { step_control_.SetSpeed(speed); } -void App::MoveCamera(platform::CameraMotion motion, mjtNum reldx, mjtNum reldy) { +void App::MoveCamera(platform::CameraMotion motion, mjtNum reldx, + mjtNum reldy) { platform::MoveCamera(model_, data_, &camera_, motion, reldx, reldy); } @@ -937,25 +925,39 @@ void App::SetupTheme(platform::GuiTheme theme) { void App::ModelOptionsGui() { const float min_width = GetExpectedLabelWidth(); - const ImGuiTreeNodeFlags flags = + const ImGuiChildFlags child_flags = + ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize; + const ImGuiTreeNodeFlags node_flags = ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_Framed; - if (ImGui::TreeNodeEx("Physics Settings", flags)) { + ImGui::BeginChild("PhysicsGui", {0, 0}, child_flags); + if (ImGui::TreeNodeEx("Physics Settings", node_flags)) { platform::PhysicsGui(model_, min_width); ImGui::TreePop(); } - if (ImGui::TreeNodeEx("Rendering Settings", flags)) { - platform::RenderingGui(model_, &vis_options_, renderer_->GetRenderFlags(), min_width); + ImGui::EndChild(); + + ImGui::BeginChild("RenderingGui", {0, 0}, child_flags); + if (ImGui::TreeNodeEx("Rendering Settings", node_flags)) { + platform::RenderingGui(model_, &vis_options_, renderer_->GetRenderFlags(), + min_width); ImGui::TreePop(); } - if (ImGui::TreeNodeEx("Visibility Groups", flags)) { - platform::GroupsGui(model_, &vis_options_, min_width); + ImGui::EndChild(); + + ImGui::BeginChild("GroupsGui", {0, 0}, child_flags); + if (ImGui::TreeNodeEx("Visibility Groups", node_flags)) { + platform::GroupsGui(model_, &vis_options_, min_width); ImGui::TreePop(); } - if (ImGui::TreeNodeEx("Visualization", flags)) { + ImGui::EndChild(); + + ImGui::BeginChild("VisualizationGui", {0, 0}, child_flags); + if (ImGui::TreeNodeEx("Visualization", node_flags)) { platform::VisualizationGui(model_, &vis_options_, &camera_, min_width); ImGui::TreePop(); } + ImGui::EndChild(); } void App::DataInspectorGui() { @@ -965,10 +967,13 @@ void App::DataInspectorGui() { } const float min_width = GetExpectedLabelWidth(); - const ImGuiTreeNodeFlags flags = + const ImGuiChildFlags child_flags = + ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize; + const ImGuiTreeNodeFlags node_flags = ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_Framed; - if (ImGui::TreeNodeEx("Noise", flags)) { + ImGui::BeginChild("NoiseGui", {0, 0}, child_flags); + if (ImGui::TreeNodeEx("Noise", node_flags)) { float noise_scale = 0; float noise_rate = 0; step_control_.GetNoiseParameters(noise_scale, noise_rate); @@ -976,27 +981,43 @@ void App::DataInspectorGui() { step_control_.SetNoiseParameters(noise_scale, noise_rate); ImGui::TreePop(); } - if (ImGui::TreeNodeEx("Joints", flags)) { + ImGui::EndChild(); + + ImGui::BeginChild("JointsGui", {0, 0}, child_flags); + if (ImGui::TreeNodeEx("Joints", node_flags)) { platform::JointsGui(model_, data_, &vis_options_); ImGui::TreePop(); } - if (ImGui::TreeNodeEx("Controls", flags)) { + ImGui::EndChild(); + + ImGui::BeginChild("ControlsGui", {0, 0}, child_flags); + if (ImGui::TreeNodeEx("Controls", node_flags)) { platform::ControlsGui(model_, data_, &vis_options_); ImGui::TreePop(); } - if (ImGui::TreeNodeEx("Sensor", flags)) { + ImGui::EndChild(); + + ImGui::BeginChild("SensorGui", {0, 0}, child_flags); + if (ImGui::TreeNodeEx("Sensor", node_flags)) { platform::SensorGui(model_, data_); ImGui::TreePop(); } - if (ImGui::TreeNodeEx("Watch", flags)) { + ImGui::EndChild(); + + ImGui::BeginChild("WatchGui", {0, 0}, child_flags); + if (ImGui::TreeNodeEx("Watch", node_flags)) { platform::WatchGui(model_, data_, ui_.watch_field, sizeof(ui_.watch_field), - ui_.watch_index); + ui_.watch_index); ImGui::TreePop(); } - if (ImGui::TreeNodeEx("State", flags)) { + ImGui::EndChild(); + + ImGui::BeginChild("StateGui", {0, 0}, child_flags); + if (ImGui::TreeNodeEx("State", node_flags)) { platform::StateGui(model_, data_, tmp_.state, tmp_.state_sig, min_width); ImGui::TreePop(); } + ImGui::EndChild(); } void DisplayElementTree(mjsElement* element) { @@ -1014,7 +1035,8 @@ void App::SpecExplorerGui() { return; } - const ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_SpanAvailWidth; + const ImGuiTreeNodeFlags flags = + ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_Framed; auto display_group = [this](mjtObj type, const std::string& prefix) { mjsElement* element = mjs_firstElement(spec_, type); @@ -1036,7 +1058,6 @@ void App::SpecExplorerGui() { } }; - if (ImGui::TreeNodeEx("Bodies", flags)) { // We don't use `display_group` here because we do additional selection // logic tied to the `perturb_` field. @@ -1055,7 +1076,8 @@ void App::SpecExplorerGui() { tmp_.element = element; tmp_.element_id = id; } - if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { + if (ImGui::IsItemHovered() && + ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { perturb_.select = id; } @@ -1096,8 +1118,7 @@ void App::PropertiesGui() { case mjOBJ_SITE: ImGui::Text("Site"); ImGui::Separator(); - platform::SitePropertiesGui(model_, data_, tmp_.element, - tmp_.element_id); + platform::SitePropertiesGui(model_, data_, tmp_.element, tmp_.element_id); break; default: // ignore other types @@ -1211,26 +1232,34 @@ void App::HelpGui() { void App::ToolBarGui() { if (ImGui::BeginTable("##ToolBarTable", 2)) { platform::ScopedStyle style; + const ImColor red(220, 40, 40, 255); + const ImColor green(40, 180, 40, 255); + const ImColor yellow(250, 230, 10, 255); + const int combo_flags = ImGuiComboFlags_NoArrowButton; ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 560); + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 520); ImGui::TableNextColumn(); + ImGui::Text("%s", " "); - // Reset/Reload/Unload. - style.Color(ImGuiCol_ButtonHovered, ImColor(220, 40, 40, 255)); + // Unload button. + ImGui::SameLine(); + style.Color(ImGuiCol_ButtonHovered, red); if (ImGui::Button(ICON_UNLOAD_MODEL, ImVec2(48, 32))) { RequestModelLoad(""); } ImGui::SetItemTooltip("%s", "Unload"); style.Reset(); + // Reload button. ImGui::SameLine(); if (ImGui::Button(ICON_RELOAD_MODEL, ImVec2(48, 32))) { RequestModelLoad(model_name_); } ImGui::SetItemTooltip("%s", "Reload"); + // Reset button. ImGui::SameLine(); if (ImGui::Button(ICON_RESET_MODEL, ImVec2(48, 32))) { ResetPhysics(); @@ -1240,8 +1269,7 @@ void App::ToolBarGui() { // Play/pause button. ImGui::SameLine(); const bool paused = step_control_.IsPaused(); - style.Color(ImGuiCol_Button, - paused ? ImColor(250, 230, 10, 255) : ImColor(40, 180, 40, 255)); + style.Color(ImGuiCol_Button, paused ? yellow : green); if (ImGui::Button(paused ? ICON_PLAY : ICON_PAUSE, ImVec2(120, 32))) { step_control_.TogglePause(); } @@ -1257,25 +1285,36 @@ void App::ToolBarGui() { ImGui::SetItemTooltip("%s", "Playback Speed"); ImGui::SameLine(); - ImGui::SetNextItemWidth(80); - int speed_index = tmp_.speed_index; - if (ImGui::Combo("##Speed", &speed_index, kPercentRealTime.data(), - kPercentRealTime.size())) { - SetSpeedIndex(speed_index); + ImGui::SetNextItemWidth(50); + if (ImGui::BeginCombo("##Speed", kPercentRealTime[tmp_.speed_index], + combo_flags)) { + for (int n = 0; n < kPercentRealTime.size(); n++) { + if (ImGui::Selectable(kPercentRealTime[n], (tmp_.speed_index == n))) { + SetSpeedIndex(n); + } + } + ImGui::EndCombo(); } + ImGui::SetItemTooltip("%s", "Playback Speed"); // Camera selection. + std::vector cameras = GetCameraNames(); ImGui::TableNextColumn(); ImGui::Text("%s", ICON_CAMERA); ImGui::SetItemTooltip("%s", "Camera"); ImGui::SameLine(); - std::vector cameras = GetCameraNames(); ImGui::SetNextItemWidth(GetExpectedLabelWidth()); int camera_idx = ui_.camera_idx - platform::kTumbleCameraIdx; - if (ImGui::Combo("##Camera", &camera_idx, cameras.data(), cameras.size())) { - ui_.camera_idx = ::mujoco::platform::SetCamera( - model_, &camera_, camera_idx + platform::kTumbleCameraIdx); + if (ImGui::BeginCombo("##Camera", cameras[camera_idx], combo_flags)) { + for (int n = 0; n < cameras.size(); n++) { + if (ImGui::Selectable(cameras[n], (camera_idx == n))) { + ui_.camera_idx = ::mujoco::platform::SetCamera( + model_, &camera_, camera_idx + platform::kTumbleCameraIdx); + } + } + ImGui::EndCombo(); } + ImGui::SetItemTooltip("%s", "Camera"); ImGui::SameLine(); if (ImGui::Button(ICON_COPY_CAMERA)) { std::string camera_string = platform::CameraToString(data_, &camera_); @@ -1293,8 +1332,16 @@ void App::ToolBarGui() { ImGui::SameLine(); ImGui::SetNextItemWidth(GetExpectedLabelWidth()); - ImGui::Combo("##Label", &vis_options_.label, kLabelNames, - IM_ARRAYSIZE(kLabelNames)); + if (ImGui::BeginCombo("##Label", kLabelNames[vis_options_.label], + combo_flags)) { + for (int n = 0; n < IM_ARRAYSIZE(kLabelNames); n++) { + if (ImGui::Selectable(kLabelNames[n], (vis_options_.label == n))) { + vis_options_.label = n; + } + } + ImGui::EndCombo(); + } + ImGui::SetItemTooltip("%s", "Label"); ImGui::SameLine(); ImGui::Text("%s", " |"); @@ -1306,24 +1353,42 @@ void App::ToolBarGui() { ImGui::SameLine(); ImGui::SetNextItemWidth(GetExpectedLabelWidth()); - ImGui::Combo("##Frame", &vis_options_.frame, kFrameNames, - IM_ARRAYSIZE(kFrameNames)); + if (ImGui::BeginCombo("##Frame", kFrameNames[vis_options_.frame], + combo_flags)) { + for (int n = 0; n < IM_ARRAYSIZE(kFrameNames); n++) { + if (ImGui::Selectable(kFrameNames[n], (vis_options_.frame == n))) { + vis_options_.frame = n; + } + } + ImGui::EndCombo(); + } + ImGui::SetItemTooltip("%s", "Frame"); ImGui::SameLine(); ImGui::Text("%s", " |"); // Style selection. ImGui::SameLine(); - if (ImGui::Button(ui_.theme == platform::GuiTheme::kDark - ? ICON_DARKMODE - : ICON_LIGHTMODE)) { - if (ui_.theme == platform::GuiTheme::kDark) { - SetupTheme(platform::GuiTheme::kLight); - } else { - SetupTheme(platform::GuiTheme::kDark); - } + switch (ui_.theme) { + case platform::GuiTheme::kLight: + if (ImGui::Button(ICON_LIGHTMODE)) { + SetupTheme(platform::GuiTheme::kDark); + } + ImGui::SetItemTooltip("%s", "Switch to Dark Mode"); + break; + case platform::GuiTheme::kDark: + if (ImGui::Button(ICON_DARKMODE)) { + SetupTheme(platform::GuiTheme::kClassic); + } + ImGui::SetItemTooltip("%s", "Switch to Classic Mode"); + break; + case platform::GuiTheme::kClassic: + if (ImGui::Button(ICON_CLASSICMODE)) { + SetupTheme(platform::GuiTheme::kLight); + } + ImGui::SetItemTooltip("%s", "Switch to Light Mode"); + break; } - ImGui::SetItemTooltip("%s", "Switch Style"); ImGui::EndTable(); } @@ -1345,10 +1410,12 @@ void App::StatusBarGui() { } else { const float desired_realtime = step_control_.GetSpeed(); const float measured_realtime = step_control_.GetSpeedMeasured(); - const float realtime_offset = mju_abs(measured_realtime - desired_realtime); + const float realtime_offset = + mju_abs(measured_realtime - desired_realtime); const bool misaligned = realtime_offset > 0.1 * desired_realtime; if (misaligned) { - ImGui::Text("Running: %g%% (%-4.1f%%)", desired_realtime, measured_realtime); + ImGui::Text("Running: %g%% (%-4.1f%%)", desired_realtime, + measured_realtime); } else { ImGui::Text("Running: %g%%", desired_realtime); } @@ -1476,8 +1543,8 @@ void App::MainMenuGui() { } ImGui::Separator(); - if (ImGui::MenuItem( - tmp_.options_panel ? "Hide Options" : "Show Left UI", "Tab")) { + if (ImGui::MenuItem(tmp_.options_panel ? "Hide Options" : "Show Left UI", + "Tab")) { tmp_.options_panel = !tmp_.options_panel; } if (ImGui::MenuItem( @@ -1530,19 +1597,22 @@ void App::FileDialogGui() { 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()); + 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()); + 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()); + 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) { @@ -1555,13 +1625,15 @@ void App::FileDialogGui() { 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()); + 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()); + 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; } @@ -1571,7 +1643,7 @@ void App::FileDialogGui() { ImGui::IsPopupOpen("PrintModel") || ImGui::IsPopupOpen("PrintData"); if (ImGui::BeginPopupModal("LoadModel", NULL, - ImGuiWindowFlags_AlwaysAutoResize)) { + ImGuiWindowFlags_AlwaysAutoResize)) { if (platform::ImGui_FileDialog(tmp_.filename, sizeof(tmp_.filename))) { RequestModelLoad(tmp_.filename); tmp_.last_load_file = tmp_.filename;