From 6039d4680a0f6bdecd76754ad0115d3d07916a84 Mon Sep 17 00:00:00 2001 From: Saran Tunyasuvunakool Date: Sat, 20 Jun 2026 03:33:42 -0700 Subject: [PATCH] Modify the UI appearance of MuJoCo Studio. PiperOrigin-RevId: 935247135 Change-Id: I4c35cf2f0cdfa36aa6d3c65c52a752edab266531 --- CMakeLists.txt | 8 +- ...cmake => atkinson_hyperlegible_next.cmake} | 18 +- .../CMakeLists.txt | 2 +- src/experimental/platform/hal/window.cc | 7 +- src/experimental/platform/ux/gui.cc | 337 ++++++++++++------ src/experimental/platform/ux/gui.h | 3 + src/experimental/platform/ux/gui_spec.cc | 13 +- src/experimental/platform/ux/imgui_widgets.cc | 2 +- src/experimental/platform/ux/imgui_widgets.h | 25 ++ src/experimental/studio/CMakeLists.txt | 8 +- src/experimental/studio/app.cc | 33 +- src/experimental/studio/app.h | 2 +- src/experimental/studio/live.js | 5 +- 13 files changed, 304 insertions(+), 159 deletions(-) rename cmake/third_party_deps/{opensans.cmake => atkinson_hyperlegible_next.cmake} (50%) rename cmake/third_party_deps/{opensans => atkinson_hyperlegible_next}/CMakeLists.txt (92%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 73d1a1f0..b35975af 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -62,15 +62,15 @@ if(EMSCRIPTEN) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread") endif() - # Filament uses the WEBGL variable (not EMSCRIPTEN) to identify web builds. + # Filament uses the WASM variable (not EMSCRIPTEN) to identify web builds. # Without this, it falls into the LINUX path and tries to compile with # futex, X11, Vulkan, etc. if(MUJOCO_USE_FILAMENT) - set(WEBGL ON CACHE BOOL "Filament WebGL mode" FORCE) + set(WASM ON CACHE BOOL "Filament WASM mode" FORCE) endif() # Automatically generate host tool imports for Filament cross-compilation. - # When WEBGL is true, Filament hardcodes the path to: + # When WASM is true, Filament hardcodes the path to: # ${FILAMENT}/${IMPORT_EXECUTABLES_DIR}/ImportExecutables-Release.cmake # So we write our file with that exact name and set IMPORT_EXECUTABLES_DIR # to point at CMAKE_BINARY_DIR (which is ../../ relative to filament-src). @@ -90,7 +90,7 @@ if(EMSCRIPTEN) "add_executable(cmgen IMPORTED)\n" "set_property(TARGET cmgen PROPERTY IMPORTED_LOCATION \"${CMGEN_EXE}\")\n" ) - # Filament's WEBGL path resolves: ${FILAMENT}/${IMPORT_EXECUTABLES_DIR}/ImportExecutables-Release.cmake + # Filament's WASM path resolves: ${FILAMENT}/${IMPORT_EXECUTABLES_DIR}/ImportExecutables-Release.cmake # FILAMENT = _deps/filament-src, so ../../ resolves to CMAKE_BINARY_DIR. set(IMPORT_EXECUTABLES_DIR "../../" CACHE PATH "" FORCE) else() diff --git a/cmake/third_party_deps/opensans.cmake b/cmake/third_party_deps/atkinson_hyperlegible_next.cmake similarity index 50% rename from cmake/third_party_deps/opensans.cmake rename to cmake/third_party_deps/atkinson_hyperlegible_next.cmake index 1209edbe..f6cd377e 100644 --- a/cmake/third_party_deps/opensans.cmake +++ b/cmake/third_party_deps/atkinson_hyperlegible_next.cmake @@ -1,4 +1,4 @@ -# Copyright 2025 DeepMind Technologies Limited +# 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. @@ -12,17 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -set(MUJOCO_DEP_VERSION_opensans - 3d45774008254746e09bce4f8922b515a7b6a26b - CACHE STRING "Tag/version of `opensans` to be fetched." +set(MUJOCO_DEP_VERSION_atkinson_hyperlegible_next + 5d633f80fc654ef5fffa7cfc257528685158dcef + CACHE STRING "Tag/version of `atkinson_hyperlegible_next` to be fetched." ) -mark_as_advanced(MUJOCO_DEP_VERSION_opensans) +mark_as_advanced(MUJOCO_DEP_VERSION_atkinson_hyperlegible_next) include(FindOrFetch) fetchpackage( - PACKAGE_NAME opensans - GIT_REPO https://github.com/googlefonts/opensans.git - GIT_TAG ${MUJOCO_DEP_VERSION_opensans} - CUSTOM_CMAKE "${CMAKE_SOURCE_DIR}/cmake/third_party_deps/opensans/CMakeLists.txt" + PACKAGE_NAME atkinson_hyperlegible_next + GIT_REPO https://github.com/googlefonts/atkinson-hyperlegible-next.git + GIT_TAG ${MUJOCO_DEP_VERSION_atkinson_hyperlegible_next} + CUSTOM_CMAKE "${CMAKE_SOURCE_DIR}/cmake/third_party_deps/atkinson_hyperlegible_next/CMakeLists.txt" ) diff --git a/cmake/third_party_deps/opensans/CMakeLists.txt b/cmake/third_party_deps/atkinson_hyperlegible_next/CMakeLists.txt similarity index 92% rename from cmake/third_party_deps/opensans/CMakeLists.txt rename to cmake/third_party_deps/atkinson_hyperlegible_next/CMakeLists.txt index 430de44d..6a1e13eb 100644 --- a/cmake/third_party_deps/opensans/CMakeLists.txt +++ b/cmake/third_party_deps/atkinson_hyperlegible_next/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright 2025 DeepMind Technologies Limited +# 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. diff --git a/src/experimental/platform/hal/window.cc b/src/experimental/platform/hal/window.cc index c7e55a9a..f1fb63ee 100644 --- a/src/experimental/platform/hal/window.cc +++ b/src/experimental/platform/hal/window.cc @@ -70,9 +70,10 @@ static void InitImGui(SDL_Window* window, float content_scale, ImFontConfig main_cfg; main_cfg.FontDataOwnedByAtlas = false; font = - mju_openResource("", "font:OpenSans-Regular.ttf", nullptr, nullptr, 0); + mju_openResource("", "font:AtkinsonHyperlegibleNext[wght].ttf", + nullptr, nullptr, 0); size = mju_readResource(font, const_cast(&data)); - io.Fonts->AddFontFromMemoryTTF(data, size, 20.f, &main_cfg); + io.Fonts->AddFontFromMemoryTTF(data, size, 16.f, &main_cfg); ImFontConfig icon_cfg; icon_cfg.FontDataOwnedByAtlas = false; @@ -81,7 +82,7 @@ static void InitImGui(SDL_Window* window, float content_scale, nullptr, 0); size = mju_readResource(font, const_cast(&data)); constexpr ImWchar icon_ranges[] = {0xf000, 0xf3ff, 0x000}; - io.Fonts->AddFontFromMemoryTTF(data, size, 14.f, &icon_cfg, icon_ranges); + io.Fonts->AddFontFromMemoryTTF(data, size, 13.f, &icon_cfg, icon_ranges); // Note: we purposefully do not "close" the font resources as ImGui may // need them again to resize fonts. diff --git a/src/experimental/platform/ux/gui.cc b/src/experimental/platform/ux/gui.cc index ec29d5cb..79dd6d94 100644 --- a/src/experimental/platform/ux/gui.cc +++ b/src/experimental/platform/ux/gui.cc @@ -48,104 +48,224 @@ static SpeedStatus IsSpeedMisaligned(const StepControl& step_control) { } // namespace static ImVec2 GetFlexElementSize(int num_cols) { - const float width = (ImGui::GetContentRegionAvail().x / num_cols) - + const float width = (GetStableAvailWidth() / num_cols) - ImGui::GetStyle().FramePadding.x * 2; return ImVec2(width, 0); } +bool SectionHeader(const char* label, ImGuiTreeNodeFlags flags, float arrow_scale) { + const bool is_framed = (flags & ImGuiTreeNodeFlags_Framed) != 0; + + ImGuiContext& g = *GImGui; + ImGuiWindow* window = ImGui::GetCurrentWindow(); + if (window->SkipItems) return false; + + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + + // Control the bar height via FramePadding.y. + // Main (framed): a bit of padding. Sub (unframed): less padding to be visually thinner. + const float pad_y = (is_framed ? 3.0f : 2.0f) * style.FontScaleDpi; + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, + ImVec2(style.FramePadding.x, pad_y)); + + // Push transparent text color to hide the default arrow and label rendered by + // TreeNodeBehavior. + ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(0, 0, 0, 0)); + + // Record cursor position before the widget so we can compute the arrow position. + const ImVec2 cursor_before = ImGui::GetCursorScreenPos(); + + // Use TreeNodeBehavior (the same function CollapsingHeader uses internally). + // This gives us proper click-to-toggle and open/close state management. + // We do NOT add _Leaf here so toggling works, and we don't add _NoTreePushOnOpen + // so the callers' TreePop() still works. + // + // For unframed nodes, add _FramePadding so TreeNodeBehavior uses FramePadding.y + // for layout (making arrow Y position predictable = cursor.y + pad_y). + // Also add _SpanAvailWidth for consistent full-width hit testing. + ImGuiTreeNodeFlags effective_flags = flags; + if (!is_framed) { + effective_flags |= ImGuiTreeNodeFlags_FramePadding | + ImGuiTreeNodeFlags_SpanAvailWidth; + } + bool is_open = ImGui::TreeNodeBehavior(id, effective_flags, label); + + // Restore style state before manual rendering (so we get the correct text color). + ImGui::PopStyleColor(); + ImGui::PopStyleVar(); + + // --- Render custom arrow and text manually --- + ImDrawList* dl = ImGui::GetWindowDrawList(); + const ImU32 text_col = ImGui::GetColorU32(ImGuiCol_Text); + + // Match position offsets used by TreeNodeBehavior. + // Arrow coordinate: (cursor_before.x + FramePadding.x, cursor_before.y + pad_y) + const float pad_x = style.FramePadding.x; + const float arrow_x = cursor_before.x + pad_x; + const float arrow_y = cursor_before.y + pad_y; + const float full_arrow_size = g.FontSize; + + // Draw the custom smaller arrow, centered within the same region. + const float custom_size = full_arrow_size * arrow_scale; + const float offset = (full_arrow_size - custom_size) * 0.5f; + const ImGuiDir arrow_dir = is_open ? ImGuiDir_Down : ImGuiDir_Right; + ImGui::RenderArrow(dl, ImVec2(arrow_x + offset, arrow_y + offset), + text_col, arrow_dir, arrow_scale); + + // Render text manually. + // text_offset_x = FontSize + padding.x * (display_frame ? 3 : 2) + const float text_offset_x = full_arrow_size + pad_x * (is_framed ? 3.0f : 2.0f); + ImGui::RenderText(ImVec2(cursor_before.x + text_offset_x, cursor_before.y + pad_y), label); + + return is_open; +} + void SetupTheme(GuiTheme theme) { ImGuiStyle& s = ImGui::GetStyle(); ImVec4* c = s.Colors; if (theme == GuiTheme::kDark) { ImGui::StyleColorsDark(&s); - 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); + + // Cool slate backgrounds with subtle blue undertone. + c[ImGuiCol_Text] = ImVec4(0.92, 0.93, 0.95, 1.00); + c[ImGuiCol_TextDisabled] = ImVec4(0.45, 0.47, 0.50, 1.00); + c[ImGuiCol_WindowBg] = ImVec4(0.16, 0.17, 0.19, 1.00); + c[ImGuiCol_ChildBg] = ImVec4(0.16, 0.17, 0.19, 1.00); + c[ImGuiCol_PopupBg] = ImVec4(0.24, 0.25, 0.28, 1.00); + c[ImGuiCol_Border] = ImVec4(0.10, 0.10, 0.12, 0.40); + c[ImGuiCol_BorderShadow] = ImVec4(0.00, 0.00, 0.00, 0.00); + + // Input fields: subtle inset, no transparency. + c[ImGuiCol_FrameBg] = ImVec4(0.22, 0.23, 0.26, 1.00); + c[ImGuiCol_FrameBgHovered] = ImVec4(0.26, 0.27, 0.30, 1.00); + c[ImGuiCol_FrameBgActive] = ImVec4(0.30, 0.31, 0.35, 1.00); + + // Title bars: darkest layer. + c[ImGuiCol_TitleBg] = ImVec4(0.12, 0.13, 0.15, 1.00); + c[ImGuiCol_TitleBgActive] = ImVec4(0.14, 0.15, 0.17, 1.00); + c[ImGuiCol_TitleBgCollapsed] = ImVec4(0.12, 0.13, 0.15, 0.90); + c[ImGuiCol_MenuBarBg] = ImVec4(0.12, 0.13, 0.15, 1.00); + + // Scrollbars: transparent background, visible thumb only. + c[ImGuiCol_ScrollbarBg] = ImVec4(0.00, 0.00, 0.00, 0.00); + c[ImGuiCol_ScrollbarGrab] = ImVec4(0.34, 0.35, 0.38, 1.00); + c[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.42, 0.43, 0.46, 1.00); + c[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.55, 0.56, 0.60, 1.00); + + // Interactive elements. + c[ImGuiCol_CheckMark] = ImVec4(0.35, 0.50, 0.72, 1.00); + c[ImGuiCol_SliderGrab] = ImVec4(0.28, 0.40, 0.58, 1.00); + c[ImGuiCol_SliderGrabActive] = ImVec4(0.35, 0.50, 0.72, 1.00); + + // Buttons: slightly lighter than frame background (0.22) to provide subtle but visible contrast. + c[ImGuiCol_Button] = ImVec4(0.28, 0.29, 0.33, 1.00); + c[ImGuiCol_ButtonHovered] = ImVec4(0.34, 0.36, 0.40, 1.00); + c[ImGuiCol_ButtonActive] = ImVec4(0.32, 0.45, 0.66, 1.00); + + // Headers and tree nodes: muted warm-slate accent. + c[ImGuiCol_Header] = ImVec4(0.22, 0.23, 0.26, 1.00); + c[ImGuiCol_HeaderHovered] = ImVec4(0.32, 0.36, 0.42, 0.60); + c[ImGuiCol_HeaderActive] = ImVec4(0.36, 0.41, 0.48, 0.85); + + // Separators: crisp dividers. + c[ImGuiCol_Separator] = ImVec4(0.08, 0.08, 0.10, 0.50); + c[ImGuiCol_SeparatorHovered] = ImVec4(0.36, 0.41, 0.48, 0.50); + c[ImGuiCol_SeparatorActive] = ImVec4(0.36, 0.41, 0.48, 0.80); + + c[ImGuiCol_ResizeGrip] = ImVec4(0.36, 0.41, 0.48, 0.25); + c[ImGuiCol_ResizeGripHovered] = ImVec4(0.36, 0.41, 0.48, 0.67); + c[ImGuiCol_ResizeGripActive] = ImVec4(0.36, 0.41, 0.48, 0.95); + + c[ImGuiCol_PlotLines] = ImVec4(0.35, 0.50, 0.72, 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_NavCursor] = ImVec4(0.26, 0.59, 0.98, 1.00); + c[ImGuiCol_PlotHistogram] = ImVec4(0.35, 0.50, 0.72, 1.00); + c[ImGuiCol_PlotHistogramHovered] = ImVec4(0.36, 0.41, 0.48, 1.00); + c[ImGuiCol_TextSelectedBg] = ImVec4(0.32, 0.36, 0.42, 0.50); + c[ImGuiCol_ModalWindowDimBg] = ImVec4(0.00, 0.00, 0.00, 0.50); + c[ImGuiCol_DragDropTarget] = ImVec4(0.35, 0.50, 0.72, 0.90); + c[ImGuiCol_NavCursor] = ImVec4(0.36, 0.41, 0.48, 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_TabSelected] = ImVec4(0.33, 0.33, 0.33, 1.00); - c[ImGuiCol_TabDimmed] = ImVec4(0.25, 0.25, 0.25, 1.00); - c[ImGuiCol_TabDimmedSelected] = 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; + c[ImGuiCol_NavWindowingDimBg] = ImVec4(0.00, 0.00, 0.00, 0.30); + + // Tabs. + c[ImGuiCol_DockingEmptyBg] = ImVec4(0.14, 0.15, 0.17, 1.00); + c[ImGuiCol_Tab] = ImVec4(0.16, 0.17, 0.19, 1.00); + c[ImGuiCol_TabHovered] = ImVec4(0.32, 0.36, 0.42, 0.60); + c[ImGuiCol_TabSelected] = ImVec4(0.22, 0.23, 0.26, 1.00); + c[ImGuiCol_TabDimmed] = ImVec4(0.14, 0.15, 0.17, 1.00); + c[ImGuiCol_TabDimmedSelected] = ImVec4(0.20, 0.21, 0.24, 1.00); + c[ImGuiCol_DockingPreview] = ImVec4(0.36, 0.41, 0.48, 0.40); } 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); + + // Clean, warm-white palette with blue accent. + ImVec4 text = ImVec4(0.14, 0.15, 0.18, 1.00); + ImVec4 text_dim = ImVec4(0.45, 0.47, 0.50, 1.00); + ImVec4 bg = ImVec4(0.96, 0.96, 0.97, 1.00); + ImVec4 surface = ImVec4(1.00, 1.00, 1.00, 1.00); + ImVec4 border = ImVec4(0.82, 0.83, 0.85, 1.00); + ImVec4 input = ImVec4(0.93, 0.93, 0.95, 1.00); + ImVec4 accent = ImVec4(0.38, 0.60, 0.90, 1.00); + ImVec4 accent_hover = ImVec4(0.38, 0.60, 0.90, 0.18); + ImVec4 accent_dim = ImVec4(0.38, 0.60, 0.90, 0.10); + ImVec4 header = ImVec4(0.90, 0.91, 0.92, 1.00); + ImVec4 grab = ImVec4(0.70, 0.71, 0.73, 1.00); c[ImGuiCol_Text] = text; - c[ImGuiCol_WindowBg] = background; - c[ImGuiCol_ChildBg] = background; - c[ImGuiCol_PopupBg] = white; + c[ImGuiCol_TextDisabled] = text_dim; + c[ImGuiCol_WindowBg] = bg; + c[ImGuiCol_ChildBg] = bg; + c[ImGuiCol_PopupBg] = ImVec4(0.94, 0.94, 0.95, 1.00); 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_BorderShadow] = ImVec4(0.00, 0.00, 0.00, 0.00); + + c[ImGuiCol_FrameBg] = input; + c[ImGuiCol_FrameBgHovered] = accent_hover; + c[ImGuiCol_FrameBgActive] = ImVec4(0.38, 0.60, 0.90, 0.30); + + c[ImGuiCol_TitleBg] = header; + c[ImGuiCol_TitleBgActive] = ImVec4(0.88, 0.89, 0.90, 1.00); + c[ImGuiCol_TitleBgCollapsed] = ImVec4(0.92, 0.93, 0.94, 0.90); 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_ScrollbarBg] = ImVec4(0.00, 0.00, 0.00, 0.00); c[ImGuiCol_ScrollbarGrab] = grab; - c[ImGuiCol_ScrollbarGrabHovered] = dark; - c[ImGuiCol_ScrollbarGrabActive] = darker; + c[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.56, 0.57, 0.60, 1.00); + c[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.42, 0.43, 0.46, 1.00); + + c[ImGuiCol_CheckMark] = accent; + c[ImGuiCol_SliderGrab] = accent; + c[ImGuiCol_SliderGrabActive] = ImVec4(0.30, 0.52, 0.82, 1.00); + + // Buttons: slightly darker than frame background (0.93) to provide subtle but visible contrast. + c[ImGuiCol_Button] = ImVec4(0.86, 0.87, 0.89, 1.00); + c[ImGuiCol_ButtonHovered] = accent_hover; + c[ImGuiCol_ButtonActive] = accent; + c[ImGuiCol_Header] = header; + c[ImGuiCol_HeaderHovered] = accent_hover; + c[ImGuiCol_HeaderActive] = ImVec4(0.38, 0.60, 0.90, 0.30); + + c[ImGuiCol_Separator] = border; + c[ImGuiCol_SeparatorHovered] = accent; + c[ImGuiCol_SeparatorActive] = accent; + + c[ImGuiCol_ResizeGrip] = accent_dim; + c[ImGuiCol_ResizeGripHovered] = accent_hover; + c[ImGuiCol_ResizeGripActive] = accent; + + c[ImGuiCol_Tab] = bg; + c[ImGuiCol_TabHovered] = accent_hover; + c[ImGuiCol_TabSelected] = surface; + c[ImGuiCol_TabDimmed] = bg; + c[ImGuiCol_TabDimmedSelected] = ImVec4(0.97, 0.97, 0.98, 1.00); + c[ImGuiCol_DockingEmptyBg] = header; + c[ImGuiCol_DockingPreview] = ImVec4(0.22, 0.47, 0.82, 0.30); + + c[ImGuiCol_TextSelectedBg] = accent_hover; + c[ImGuiCol_DragDropTarget] = accent; + c[ImGuiCol_NavCursor] = accent; + c[ImGuiCol_ModalWindowDimBg] = ImVec4(0.00, 0.00, 0.00, 0.30); } else { ImGui::StyleColorsDark(&s); ImVec4 black = ImVec4(0.00, 0.00, 0.00, 1.0); @@ -190,12 +310,12 @@ void SetupTheme(GuiTheme theme) { float scale = s.FontScaleDpi; - int hspacing = 4; - int vspacing = 6; - float rounding = 4.0f; + int hspacing = 3; + int vspacing = 2; + float rounding = 2.0f; s.DisplaySafeAreaPadding = ImVec2(0, 0); - s.WindowPadding = ImTrunc(ImVec2(hspacing * scale, vspacing * scale)); - s.FramePadding = ImTrunc(ImVec2(hspacing * scale, 2.0f * scale)); + s.WindowPadding = ImTrunc(ImVec2(6.0f * scale, 6.0f * scale)); + s.FramePadding = ImTrunc(ImVec2(hspacing * scale, 3.0f * scale)); s.ItemSpacing = ImTrunc(ImVec2(hspacing * scale, vspacing * scale)); s.ItemInnerSpacing = ImTrunc(ImVec2(hspacing * scale, vspacing * scale)); s.WindowRounding = ImTrunc(rounding * scale); @@ -206,10 +326,10 @@ void SetupTheme(GuiTheme theme) { s.GrabRounding = ImTrunc(rounding * scale); s.PopupRounding = ImTrunc(rounding * scale); s.WindowBorderSize = 0.0f; - s.FrameBorderSize = 1.0f; + s.FrameBorderSize = 0.0f; s.PopupBorderSize = 1.0f; - s.IndentSpacing = ImTrunc(6.0f * scale); - s.ScrollbarSize = ImTrunc(12.0f * scale); + s.IndentSpacing = ImTrunc(10.0f * scale); + s.ScrollbarSize = ImTrunc(10.0f * scale); s.GrabMinSize = ImTrunc(5.0f * scale); s.WindowMenuButtonPosition = ImGuiDir_None; s.TabCloseButtonMinWidthSelected = 0.0f; @@ -237,7 +357,7 @@ ImVec4 ConfigureDockingLayout() { const float scale = ImGui::GetWindowDpiScale(); const float font_scale = ImGui::GetIO().FontGlobalScale; - const float kOptionsRelWidth = 0.22f; + const float kOptionsRelWidth = 0.15f; const float kInspectorRelWidth = 0.22f; const float kStatsRelHeight = 0.3f; const float kToolsBarHeight = 36.f * scale * font_scale; @@ -368,10 +488,10 @@ ImVec4 ConfigureDockingLayout() { void StepControlGui(const mjModel* model, StepControl* step_control, int& speed_index) { platform::ScopedStyle style; - style.Var(ImGuiStyleVar_FrameRounding, 8.f * ImGui::GetStyle().FontScaleDpi); - const ImColor yellow(255, 215, 0, 255); - const ImColor green(40, 180, 40, 255); + bool is_dark = ImGui::GetStyle().Colors[ImGuiCol_WindowBg].x < 0.5f; + const ImColor yellow = is_dark ? ImColor(158, 115, 18, 255) : ImColor(255, 215, 0, 255); + const ImColor green = is_dark ? ImColor(40, 125, 60, 255) : ImColor(40, 180, 40, 255); auto make_button = [&](const char* icon, StepControl::PauseState target_state, ImColor color, ImDrawFlags corners, @@ -455,7 +575,6 @@ bool ThemeSelectGui(GuiTheme* theme, const ImVec2& size) { static constexpr const char* ICON_LIGHTMODE = ICON_FA_CIRCLE_O; static constexpr const char* ICON_CLASSICMODE = ICON_FA_ADJUST; const char* theme_icons[] = {ICON_LIGHTMODE, ICON_DARKMODE, ICON_CLASSICMODE}; - const char* theme_tooltips[] = {"Light Mode", "Dark Mode", "Classic Mode"}; int theme_idx = static_cast(*theme); if (ImGui::Button(theme_icons[theme_idx], size)) { @@ -463,7 +582,7 @@ bool ThemeSelectGui(GuiTheme* theme, const ImVec2& size) { *theme = static_cast(theme_idx); return true; } - ImGui::SetItemTooltip("%s", theme_tooltips[theme_idx]); + ImGui::SetItemTooltip("%s", "Toggle theme"); return false; } @@ -660,7 +779,7 @@ void SensorGui(const mjModel* model, const mjData* data) { void StateGui(const mjModel* model, mjData* data, std::vector& state, int& state_sig, float min_width) { const float available_width = - ImGui::GetContentRegionAvail().x - ImGui::GetTreeNodeToLabelSpacing(); + GetStableAvailWidth() - ImGui::GetTreeNodeToLabelSpacing(); const int num_cols = std::clamp( static_cast(std::floor(available_width / min_width)), 1, 4); const ImVec2 size = GetFlexElementSize(num_cols); @@ -828,7 +947,7 @@ void WatchGui(const mjModel* model, const mjData* data, char* field_name, void PhysicsGui(mjModel* model, float min_width) { const float available_width = - ImGui::GetContentRegionAvail().x - ImGui::GetTreeNodeToLabelSpacing(); + GetStableAvailWidth() - ImGui::GetTreeNodeToLabelSpacing(); const int num_cols = std::clamp( static_cast(std::floor(available_width / min_width)), 1, 6); @@ -850,8 +969,8 @@ void PhysicsGui(mjModel* model, float min_width) { ImGui::Combo("Solver", &opt.solver, opts3, IM_ARRAYSIZE(opts3)); - if (ImGui::TreeNodeEx("Algorithmic Parameters", - ImGuiTreeNodeFlags_DefaultOpen)) { + if (SectionHeader("Algorithmic Parameters", + ImGuiTreeNodeFlags_DefaultOpen)) { 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}); @@ -867,7 +986,7 @@ void PhysicsGui(mjModel* model, float min_width) { ImGui::TreePop(); } - if (ImGui::TreeNodeEx("Physical Parameters")) { + if (SectionHeader("Physical Parameters")) { ImGui_InputN("Gravity", opt.gravity, 3); ImGui_InputN("Wind", opt.wind, 3); ImGui_InputN("Magnetic", opt.magnetic, 3); @@ -877,7 +996,7 @@ void PhysicsGui(mjModel* model, float min_width) { ImGui::TreePop(); }; - if (ImGui::TreeNodeEx("Flags", ImGuiTreeNodeFlags_DefaultOpen)) { + if (SectionHeader("Flags", ImGuiTreeNodeFlags_DefaultOpen)) { if (ImGui::BeginTable("##PhysicsFlagsTable", num_cols)) { const ImVec2 size = GetFlexElementSize(num_cols); for (int i = 0; i < mjNDISABLE; ++i) { @@ -895,7 +1014,7 @@ void PhysicsGui(mjModel* model, float min_width) { ImGui::TreePop(); } - if (ImGui::TreeNodeEx("Contact Override")) { + if (SectionHeader("Contact Override")) { ImGui_Input("Margin", &opt.o_margin, {.min = 0.0, .max = 1}); ImGui_InputN("Sol Imp", opt.o_solimp, 5, {.format = "%0.3f"}); ImGui_InputN("Sol Ref", opt.o_solref, 2, {.format = "%0.3f"}); @@ -903,7 +1022,7 @@ void PhysicsGui(mjModel* model, float min_width) { ImGui::TreePop(); } - if (ImGui::TreeNodeEx("Actuator Groups")) { + if (SectionHeader("Actuator Groups")) { if (ImGui::BeginTable("##ActuatorGroupsTable", num_cols)) { const ImVec2 size = GetFlexElementSize(num_cols); for (int i = 0; i < 6; ++i) { @@ -933,14 +1052,14 @@ void VisualizationGui(mjModel* model, mjvOption* vis_options, mjvCamera* camera, ImGui::SliderInt("Tree depth", &vis_options->bvh_depth, 0, 20); ImGui::SliderInt("Flex layer", &vis_options->flex_layer, 0, 10); - if (ImGui::TreeNodeEx("Headlight")) { + if (SectionHeader("Headlight")) { ImGui_SwitchToggle("Active", &vis.headlight.active); ImGui::ColorEdit3("Ambient", vis.headlight.ambient); ImGui::ColorEdit3("Diffuse", vis.headlight.diffuse); ImGui::ColorEdit3("Specular", vis.headlight.specular); ImGui::TreePop(); } - if (ImGui::TreeNodeEx("Free Camera")) { + if (SectionHeader("Free Camera")) { ImGui_SwitchToggle("Orthographic", &vis.global.orthographic); ImGui_Input("FOV", &vis.global.fovy, {.format = "%0.2f"}); ImGui_InputN("Center", stat.center, 3, {.format = "%0.2f"}); @@ -951,7 +1070,7 @@ void VisualizationGui(mjModel* model, mjvOption* vis_options, mjvCamera* camera, } ImGui::TreePop(); } - if (ImGui::TreeNodeEx("Global")) { + if (SectionHeader("Global")) { ImGui_Input("Extent", &stat.extent); const char* opts[] = {"Box", "Ellipsoid"}; ImGui::SliderInt("Inertia", &vis.global.ellipsoidinertia, 0, 1, @@ -959,7 +1078,7 @@ void VisualizationGui(mjModel* model, mjvOption* vis_options, mjvCamera* camera, ImGui_ButtonToggle("BVH active", &vis.global.bvactive); ImGui::TreePop(); } - if (ImGui::TreeNodeEx("Mapping")) { + if (SectionHeader("Mapping")) { ImGui::PushItemWidth(ImGui::GetWindowWidth() * 0.3f); ImGui_Input("Stiffness", &vis.map.stiffness); ImGui_Input("Rot stiffness", &vis.map.stiffnessrot); @@ -976,7 +1095,7 @@ void VisualizationGui(mjModel* model, mjvOption* vis_options, mjvCamera* camera, ImGui::PopItemWidth(); ImGui::TreePop(); } - if (ImGui::TreeNodeEx("Scale")) { + if (SectionHeader("Scale")) { ImGui::PushItemWidth(ImGui::GetWindowWidth() * 0.3f); ImGui_Input("All (meansize)", &stat.meansize, {.format = "%0.3f"}); ImGui_Input("Force width", &vis.scale.forcewidth); @@ -1033,7 +1152,7 @@ void VisualizationGui(mjModel* model, mjvOption* vis_options, mjvCamera* camera, void RenderingGui(const mjModel* model, mjvOption* vis_options, mjtByte* render_flags, float min_width) { const float available_width = - ImGui::GetContentRegionAvail().x - ImGui::GetTreeNodeToLabelSpacing(); + GetStableAvailWidth() - ImGui::GetTreeNodeToLabelSpacing(); const int num_cols = std::clamp( static_cast(std::floor(available_width / min_width)), 1, 6); @@ -1063,7 +1182,7 @@ void RenderingGui(const mjModel* model, mjvOption* vis_options, } void GroupsGui(const mjModel* model, mjvOption* vis_options, float min_width) { - const float available_width = ImGui::GetContentRegionAvail().x; + const float available_width = GetStableAvailWidth(); // We limit the number of columns to 1, 2, 3, or 6 depending on how much // space the window has available. int num_cols = std::clamp( diff --git a/src/experimental/platform/ux/gui.h b/src/experimental/platform/ux/gui.h index 3edd0425..2d1fda3a 100644 --- a/src/experimental/platform/ux/gui.h +++ b/src/experimental/platform/ux/gui.h @@ -40,6 +40,9 @@ enum class GuiTheme { kClassic, }; +// Render a section header with custom, visually-balanced smaller expand/collapse arrow. +bool SectionHeader(const char* label, ImGuiTreeNodeFlags flags = 0, float arrow_scale = 0.55f); + // Updates the ImGui internal style state to match the requested theme. void SetupTheme(GuiTheme theme); diff --git a/src/experimental/platform/ux/gui_spec.cc b/src/experimental/platform/ux/gui_spec.cc index a723cbfe..82d869de 100644 --- a/src/experimental/platform/ux/gui_spec.cc +++ b/src/experimental/platform/ux/gui_spec.cc @@ -20,6 +20,7 @@ #include #include #include +#include "experimental/platform/ux/gui.h" #include "experimental/platform/ux/imgui_widgets.h" #include "experimental/platform/ux/spec_editor.h" @@ -143,7 +144,7 @@ static void BodyChildrenGui(const char* heading, mjtObj type, constexpr ImGuiTreeNodeFlags tree_flags = ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_DrawLinesFull; - if (ImGui::TreeNodeEx(heading, tree_flags)) { + if (SectionHeader(heading, tree_flags, 0.55f)) { while (iter) { mjsElement* next = mjs_nextChild(body, iter, 0); SelectableElement(iter, element, editor); @@ -163,7 +164,7 @@ static void ElementListGui(const char* heading, mjtObj type, constexpr ImGuiTreeNodeFlags tree_flags = ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_Framed; - if (ImGui::TreeNodeEx(heading, tree_flags)) { + if (SectionHeader(heading, tree_flags, 0.55f)) { while (iter) { mjsElement* next = mjs_nextElement(spec, iter); SelectableElement(iter, element, editor); @@ -187,7 +188,7 @@ static void BodyTreeGuiRecursive(mjsElement** element, mjsBody* body, flags |= ImGuiTreeNodeFlags_Selected; } - const bool tree_open = ImGui::TreeNodeEx(label.c_str(), flags); + const bool tree_open = SectionHeader(label.c_str(), flags, 0.55f); if (ImGui::IsItemClicked()) { *element = body->element; } @@ -223,7 +224,7 @@ void SpecTreeGui(mjsElement** element, mjSpec* spec, SpecEditor* editor) { style.Var(ImGuiStyleVar_FramePadding, ImVec2(4, 0)); style.Color(ImGuiCol_Border, ImGuiCol_WindowBg); - if (ImGui::TreeNodeEx("Body Tree", flags)) { + if (SectionHeader("Body Tree", flags, 0.65f)) { mjsElement* root = mjs_firstElement(spec, mjOBJ_BODY); if (root) { mjsBody* body = mjs_asBody(root); @@ -241,7 +242,7 @@ void SpecTreeGui(mjsElement** element, mjSpec* spec, SpecEditor* editor) { ImGui::PushID("$spec$"); // Non-tree elements. - if (ImGui::TreeNodeEx("Elements", flags)) { + if (SectionHeader("Elements", flags, 0.65f)) { list("Actuators", mjOBJ_ACTUATOR); list("Sensors", mjOBJ_SENSOR); list("Flexes", mjOBJ_FLEX); @@ -258,7 +259,7 @@ void SpecTreeGui(mjsElement** element, mjSpec* spec, SpecEditor* editor) { } // Assets. - if (ImGui::TreeNodeEx("Assets", flags)) { + if (SectionHeader("Assets", flags, 0.65f)) { list("Meshes", mjOBJ_MESH); list("Height Fields", mjOBJ_HFIELD); list("Skins", mjOBJ_SKIN); diff --git a/src/experimental/platform/ux/imgui_widgets.cc b/src/experimental/platform/ux/imgui_widgets.cc index c21e3a99..c306e4b8 100644 --- a/src/experimental/platform/ux/imgui_widgets.cc +++ b/src/experimental/platform/ux/imgui_widgets.cc @@ -65,7 +65,7 @@ KeyValues ReadIniSection(const std::string& contents, ImGui_DataPtrTable::ImGui_DataPtrTable(float w1, float w2) { ImGui::BeginTable("##PropertiesTable", 2, ImGuiTableFlags_RowBg); - const float width = ImGui::GetContentRegionAvail().x; + const float width = GetStableAvailWidth(); ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, width * w1); ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, width * w2); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(3.0f, 0.0f)); diff --git a/src/experimental/platform/ux/imgui_widgets.h b/src/experimental/platform/ux/imgui_widgets.h index 53ea5bbc..912edd52 100644 --- a/src/experimental/platform/ux/imgui_widgets.h +++ b/src/experimental/platform/ux/imgui_widgets.h @@ -33,6 +33,31 @@ namespace mujoco::platform { +// Helper to get available content region width that does not change/reflow +// when the vertical scrollbar appears or disappears. +// Walks up the parent window chain to find the scroll-owning ancestor (the +// actual panel window, not a child window), and reserves scrollbar width +// when that panel's scrollbar is not currently visible. +inline float GetStableAvailWidth() { + ImGuiWindow* window = ImGui::GetCurrentWindow(); + float avail_x = ImGui::GetContentRegionAvail().x; + + // Walk up past child windows to the actual panel that owns the scrollbar. + ImGuiWindow* scroll_owner = window; + while (scroll_owner && + (scroll_owner->Flags & ImGuiWindowFlags_ChildWindow) && + scroll_owner->ParentWindow) { + scroll_owner = scroll_owner->ParentWindow; + } + + if (scroll_owner && + !(scroll_owner->Flags & ImGuiWindowFlags_NoScrollbar) && + !scroll_owner->ScrollbarY) { + avail_x -= ImGui::GetStyle().ScrollbarSize; + } + return avail_x; +} + // FontAwesome icon codes. static constexpr const char ICON_FA_ADJUST[] = "\xEF\x81\x82"; static constexpr const char ICON_FA_ARROWS[] = "\xEF\x81\x87"; diff --git a/src/experimental/studio/CMakeLists.txt b/src/experimental/studio/CMakeLists.txt index 4fcbb3b6..75f26d34 100644 --- a/src/experimental/studio/CMakeLists.txt +++ b/src/experimental/studio/CMakeLists.txt @@ -16,7 +16,7 @@ cmake_minimum_required(VERSION 3.16) include(third_party_deps/dear_imgui) include(third_party_deps/implot) -include(third_party_deps/opensans) +include(third_party_deps/atkinson_hyperlegible_next) include(third_party_deps/font_awesome) # Common configuration shared between mujoco_studio and mujoco_live. @@ -67,9 +67,9 @@ function(configure_studio_target TARGET_NAME) COMMAND ${CMAKE_COMMAND} -E make_directory ${STUDIO_ASSETS_DIR} COMMAND ${CMAKE_COMMAND} -E copy - ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/../_deps/opensans-src/fonts/ttf/OpenSans-Regular.ttf - ${STUDIO_ASSETS_DIR} - COMMENT "Copying OpenSans-Regular.ttf assets to build directory" + "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/../_deps/atkinson_hyperlegible_next-src/fonts/variable/AtkinsonHyperlegibleNext[wght].ttf" + ${STUDIO_ASSETS_DIR}/AtkinsonHyperlegibleNext[wght].ttf + COMMENT "Copying AtkinsonHyperlegibleNext font to build directory" ) add_custom_command( TARGET ${TARGET_NAME} diff --git a/src/experimental/studio/app.cc b/src/experimental/studio/app.cc index 3c6a64f2..41f76fae 100644 --- a/src/experimental/studio/app.cc +++ b/src/experimental/studio/app.cc @@ -862,12 +862,8 @@ void App::BuildGui() { MainMenuGui(); { - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); if (ImGui::Begin("ToolBar")) { - ImGui::PopStyleVar(); ToolBarGui(); - } else { - ImGui::PopStyleVar(); } ImGui::End(); } @@ -1029,14 +1025,14 @@ void App::ModelOptionsGui() { ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_Framed; ImGui::BeginChild("PhysicsGui", {0, 0}, child_flags); - if (ImGui::TreeNodeEx("Physics", node_flags)) { + if (platform::SectionHeader("Physics", node_flags, 0.65f)) { platform::PhysicsGui(model(), min_width); ImGui::TreePop(); } ImGui::EndChild(); ImGui::BeginChild("RenderingGui", {0, 0}, child_flags); - if (ImGui::TreeNodeEx("Rendering", node_flags)) { + if (platform::SectionHeader("Rendering", node_flags, 0.65f)) { platform::RenderingGui(model(), &vis_options_, renderer_->GetRenderFlags(), min_width); ImGui::TreePop(); @@ -1044,14 +1040,14 @@ void App::ModelOptionsGui() { ImGui::EndChild(); ImGui::BeginChild("GroupsGui", {0, 0}, child_flags); - if (ImGui::TreeNodeEx("Visibility Groups", node_flags)) { + if (platform::SectionHeader("Visibility Groups", node_flags, 0.65f)) { platform::GroupsGui(model(), &vis_options_, min_width); ImGui::TreePop(); } ImGui::EndChild(); ImGui::BeginChild("VisualizationGui", {0, 0}, child_flags); - if (ImGui::TreeNodeEx("Visualization", node_flags)) { + if (platform::SectionHeader("Visualization", node_flags, 0.65f)) { platform::VisualizationGui(model(), &vis_options_, &camera_, min_width); ImGui::TreePop(); } @@ -1071,14 +1067,14 @@ void App::DataInspectorGui() { ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_Framed; ImGui::BeginChild("JointsGui", {0, 0}, child_flags); - if (ImGui::TreeNodeEx("Joints", node_flags)) { + if (platform::SectionHeader("Joints", node_flags, 0.65f)) { platform::JointsGui(model(), data(), &vis_options_); ImGui::TreePop(); } ImGui::EndChild(); ImGui::BeginChild("ControlsGui", {0, 0}, child_flags); - if (ImGui::TreeNodeEx("Controls", node_flags)) { + if (platform::SectionHeader("Controls", node_flags, 0.65f)) { float noise_scale = 0; float noise_rate = 0; @@ -1093,14 +1089,14 @@ void App::DataInspectorGui() { ImGui::EndChild(); ImGui::BeginChild("SensorGui", {0, 0}, child_flags); - if (ImGui::TreeNodeEx("Sensor", node_flags)) { + if (platform::SectionHeader("Sensor", node_flags, 0.65f)) { platform::SensorGui(model(), data()); ImGui::TreePop(); } ImGui::EndChild(); ImGui::BeginChild("WatchGui", {0, 0}, child_flags); - if (ImGui::TreeNodeEx("Watch", node_flags)) { + if (platform::SectionHeader("Watch", node_flags, 0.65f)) { platform::WatchGui(model(), data(), ui_.watch_field, sizeof(ui_.watch_field), ui_.watch_index); ImGui::TreePop(); @@ -1108,7 +1104,7 @@ void App::DataInspectorGui() { ImGui::EndChild(); ImGui::BeginChild("StateGui", {0, 0}, child_flags); - if (ImGui::TreeNodeEx("State", node_flags)) { + if (platform::SectionHeader("State", node_flags, 0.65f)) { platform::StateGui(model(), data(), tmp_.state, tmp_.state_sig, min_width); ImGui::TreePop(); } @@ -1212,7 +1208,9 @@ void App::SpecEditorGui() { ImGui::EndDisabled(); ImGui::TableNextColumn(); - ImGui::PushStyleColor(ImGuiCol_Button, ImColor(40, 180, 40, 255).Value); + bool is_dark = ImGui::GetStyle().Colors[ImGuiCol_WindowBg].x < 0.5f; + ImColor compile_green = is_dark ? ImColor(40, 125, 60, 255) : ImColor(40, 180, 40, 255); + ImGui::PushStyleColor(ImGuiCol_Button, compile_green.Value); if (ImGui::Button("Compile and Reload", ImVec2(-1, 0))) { pending_op_ = [this]() { auto tmp_holder = spec_editor_.Compile(); @@ -1426,13 +1424,10 @@ void App::ToolBarGui() { const float label_width = platform::GetExpectedLabelWidth(); const float copy_btn_width = ImGui::GetFrameHeight(); - const float theme_width = - ImGui::CalcTextSize(platform::ICON_FA_CIRCLE_O).x + - ImGui::GetStyle().FramePadding.x * 2; const float sp = ImGui::GetStyle().ItemSpacing.x; const float right_width = copy_btn_width + label_width + sp + label_width + sp + label_width + sp + - theme_width; + copy_btn_width; const float separator_width = ImGui::GetFrameHeight() * .6f; ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch); @@ -1568,6 +1563,7 @@ void App::StatusBarGui() { } void App::MainMenuGui() { + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(12.0f * ImGui::GetStyle().FontScaleDpi, ImGui::GetStyle().ItemSpacing.y)); if (ImGui::BeginMainMenuBar()) { if (ImGui::BeginMenu("File")) { #ifndef __EMSCRIPTEN__ @@ -1763,6 +1759,7 @@ void App::MainMenuGui() { } ImGui::EndMainMenuBar(); } + ImGui::PopStyleVar(); } void App::FileDialogGui() { diff --git a/src/experimental/studio/app.h b/src/experimental/studio/app.h index fa678ad2..c0effb00 100644 --- a/src/experimental/studio/app.h +++ b/src/experimental/studio/app.h @@ -110,7 +110,7 @@ class App { int watch_index = 0; int camera_idx = platform::kTumbleCameraIdx; int key_idx = 0; - platform::GuiTheme theme = platform::GuiTheme::kLight; + platform::GuiTheme theme = platform::GuiTheme::kDark; float font_scale = 1.0f; int window_width = 0; int window_height = 0; diff --git a/src/experimental/studio/live.js b/src/experimental/studio/live.js index 932ba04f..af830e75 100644 --- a/src/experimental/studio/live.js +++ b/src/experimental/studio/live.js @@ -142,7 +142,7 @@ var Module = { const assetsToPrefetch = [ "assets/fontawesome-webfont.ttf", "assets/ibl.ktx", - "assets/OpenSans-Regular.ttf", + "assets/AtkinsonHyperlegibleNext[wght].ttf", "assets/pbr.filamat", "assets/pbr_transparent.filamat", "assets/pbr_packed.filamat", @@ -188,8 +188,7 @@ var Module = { Promise.all(assetPromises) .then(() => { try { - const prefersDark = !(window.matchMedia && - window.matchMedia('(prefers-color-scheme: light)').matches); + const prefersDark = true; Module.init("MuJoCo Live", prefersDark); // Check for a ?model= URL parameter and load from URL.