Modify the UI appearance of MuJoCo Studio.
PiperOrigin-RevId: 935247135 Change-Id: I4c35cf2f0cdfa36aa6d3c65c52a752edab266531
This commit is contained in:
committed by
Copybara-Service
parent
b1e3753a1a
commit
6039d4680a
+4
-4
@@ -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()
|
||||
|
||||
+9
-9
@@ -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"
|
||||
)
|
||||
+1
-1
@@ -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.
|
||||
@@ -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<const void**>(&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<const void**>(&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.
|
||||
|
||||
+228
-109
@@ -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<int>(*theme);
|
||||
if (ImGui::Button(theme_icons[theme_idx], size)) {
|
||||
@@ -463,7 +582,7 @@ bool ThemeSelectGui(GuiTheme* theme, const ImVec2& size) {
|
||||
*theme = static_cast<GuiTheme>(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<mjtNum>& 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<int>(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<int>(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<int>(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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <imgui.h>
|
||||
#include <mujoco/mjxmacro.h>
|
||||
#include <mujoco/mujoco.h>
|
||||
#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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user