diff --git a/src/experimental/platform/ux/gui.cc b/src/experimental/platform/ux/gui.cc index 9098092c..ccf1ed91 100644 --- a/src/experimental/platform/ux/gui.cc +++ b/src/experimental/platform/ux/gui.cc @@ -97,15 +97,15 @@ void SetupTheme(GuiTheme theme) { 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_NavCursor] = 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_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; } else if (theme == GuiTheme::kLight) { @@ -220,7 +220,7 @@ ImVec4 ConfigureDockingLayout() { const float kOptionsRelWidth = 0.22f; const float kInspectorRelWidth = 0.22f; const float kStatsRelHeight = 0.3f; - const float kToolsBarHeight = 48.f * scale; + const float kToolsBarHeight = 36.f * scale; const float kStatusBarHeight = 32.f * scale; const ImVec2 dockspace_pos{viewport->WorkPos.x, @@ -303,6 +303,9 @@ ImVec4 ConfigureDockingLayout() { platform::ScopedStyle style; style.Var(ImGuiStyleVar_WindowBorderSize, 1.0f); style.Var(ImGuiStyleVar_WindowRounding, 0.0f); + const float toolbar_vpad = + std::max(0.f, (kToolsBarHeight - ImGui::GetFrameHeight()) * 0.5f); + style.Var(ImGuiStyleVar_WindowPadding, ImVec2(4, toolbar_vpad)); ImGui::SetNextWindowPos(viewport->WorkPos, ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(viewport->Size.x, kToolsBarHeight), ImGuiCond_Always); @@ -335,18 +338,23 @@ ImVec4 ConfigureDockingLayout() { void StepControlGui(const mjModel* model, StepControl* step_control, int& speed_index) { platform::ScopedStyle style; - style.Var(ImGuiStyleVar_FrameRounding, 2.f); + style.Var(ImGuiStyleVar_FrameRounding, 8.f); const ImColor yellow(255, 215, 0, 255); const ImColor green(40, 180, 40, 255); - const float scale = ImGui::GetWindowDpiScale(); - ImVec2 button_size(48.f * scale, 32.f * scale); auto make_button = [&](const char* icon, StepControl::PauseState target_state, - ImColor color, const char* tooltip = "", - float hover_alpha = 1.f) { + ImColor color, ImDrawFlags corners, + const char* tooltip = "", + float hover_alpha = 1.f, float width_scale = 1.f) { + ImVec2 size(0, 0); + if (width_scale != 1.f) { + const ImGuiStyle& s = ImGui::GetStyle(); + const float w = ImGui::CalcTextSize(icon).x + s.FramePadding.x * 2; + size.x = w * width_scale; + } bool active = step_control->GetPauseState() == target_state; - if (ImGui_ColorButton(icon, active, color, button_size, hover_alpha)) { + if (ImGui_ColorButtonEx(icon, active, color, corners, size, hover_alpha)) { step_control->SetPauseState(target_state); } if (!std::string_view(tooltip).empty()) { @@ -355,26 +363,28 @@ void StepControlGui(const mjModel* model, StepControl* step_control, }; make_button(ICON_FA_PAUSE, StepControl::PauseState::kNormalPaused, yellow, - "Pause"); + ImDrawFlags_RoundCornersLeft, "Pause", .3f, 1.6f); ImGui::SameLine(0.f, 0.f); make_button(ICON_FA_MAGIC, StepControl::PauseState::kViscousPaused, yellow, - "Viscous Pause"); + ImDrawFlags_RoundCornersNone, "Viscous Pause", .3f, 1.3f); ImGui::SameLine(0.f, 0.f); - make_button(ICON_FA_PLAY, StepControl::PauseState::kUnpaused, green, "", .6f); + make_button(ICON_FA_PLAY, StepControl::PauseState::kUnpaused, green, + ImDrawFlags_RoundCornersRight, "", .3f, 1.6f); // Speed selection. - ImGui::SameLine(); - const float pad_y = (button_size.y - ImGui::GetFontSize()) * .5f; + style.Reset(); + ImGui::SameLine(0, ImGui::GetFrameHeight() * .6f); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, - ImVec2(ImGui::GetStyle().FramePadding.x + 5.f, pad_y)); + ImVec2(ImGui::GetStyle().FramePadding.x + 5.f, + ImGui::GetStyle().FramePadding.y)); const auto [misaligned, measured] = IsSpeedMisaligned(*step_control); char speed_preview[64]; if (misaligned) { - snprintf(speed_preview, sizeof(speed_preview), "%s%s (%-4.1f%%)", + snprintf(speed_preview, sizeof(speed_preview), "%s %s (%-4.1f%%)", ICON_FA_TACHOMETER, kPercentRealTime[speed_index], measured); } else { - snprintf(speed_preview, sizeof(speed_preview), "%s%s", ICON_FA_TACHOMETER, + snprintf(speed_preview, sizeof(speed_preview), "%s %s", ICON_FA_TACHOMETER, kPercentRealTime[speed_index]); } @@ -400,39 +410,22 @@ void StepControlGui(const mjModel* model, StepControl* step_control, } } -bool ThemeSelectGui(GuiTheme* theme) { +bool ThemeSelectGui(GuiTheme* theme, const ImVec2& size) { static constexpr const char* ICON_DARKMODE = ICON_FA_CIRCLE; 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"}; - const GuiTheme theme_values[] = { - GuiTheme::kLight, - GuiTheme::kDark, - GuiTheme::kClassic, - }; - - bool changed = false; int theme_idx = static_cast(*theme); - ImGui::SetNextItemWidth(ImGui::CalcTextSize(theme_icons[0]).x + - ImGui::GetStyle().FramePadding.x * 2); - if (ImGui::BeginCombo("##Theme", theme_icons[theme_idx], - ImGuiComboFlags_NoArrowButton)) { - for (int n = 0; n < IM_ARRAYSIZE(theme_icons); n++) { - if (ImGui::Selectable(theme_icons[n], (theme_idx == n))) { - *theme = theme_values[n]; - changed = true; - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("%s", theme_tooltips[n]); - } - } - ImGui::EndCombo(); + if (ImGui::Button(theme_icons[theme_idx], size)) { + theme_idx = (theme_idx + 1) % IM_ARRAYSIZE(theme_icons); + *theme = static_cast(theme_idx); + return true; } - ImGui::SetItemTooltip("%s", "Theme"); + ImGui::SetItemTooltip("%s", theme_tooltips[theme_idx]); - return changed; + return false; } bool LabelSelectionGui(mjvOption* opts) { diff --git a/src/experimental/platform/ux/gui.h b/src/experimental/platform/ux/gui.h index 4dc8b69c..e9b23dfc 100644 --- a/src/experimental/platform/ux/gui.h +++ b/src/experimental/platform/ux/gui.h @@ -81,7 +81,7 @@ void StepControlGui(const mjModel* model, StepControl* step_control, int& speed_index); // UX for selecting the GUI theme. -bool ThemeSelectGui(GuiTheme* theme); +bool ThemeSelectGui(GuiTheme* theme, const ImVec2& size = ImVec2(0, 0)); // UX for selecting the visualization label option. bool LabelSelectionGui(mjvOption* opts); diff --git a/src/experimental/platform/ux/imgui_widgets.cc b/src/experimental/platform/ux/imgui_widgets.cc index 7d0f1cba..305f292c 100644 --- a/src/experimental/platform/ux/imgui_widgets.cc +++ b/src/experimental/platform/ux/imgui_widgets.cc @@ -375,9 +375,7 @@ void ImGui_EndHSplit(bool open) { } void MaybeSaveToClipboard(const std::string& contents) { - if (ImGui::GetIO().SetClipboardTextFn) { - ImGui::GetIO().SetClipboardTextFn(nullptr, contents.c_str()); - } + ImGui::SetClipboardText(contents.c_str()); } ImPlotFlags ImPlot_SetupPlotFlags(ImVec2 plot_size) { diff --git a/src/experimental/platform/ux/imgui_widgets.h b/src/experimental/platform/ux/imgui_widgets.h index 187dabba..3ee85efa 100644 --- a/src/experimental/platform/ux/imgui_widgets.h +++ b/src/experimental/platform/ux/imgui_widgets.h @@ -551,6 +551,55 @@ inline bool ImGui_ColorButton(const char* label, bool active, ImColor color, return ImGui::Button(label, size); } +// Like ImGui_ColorButton, but with per-corner rounding control via ImDrawFlags. +// Use ImDrawFlags_RoundCornersLeft, ImDrawFlags_RoundCornersRight, +// ImDrawFlags_RoundCornersNone, ImDrawFlags_RoundCornersAll, etc. +inline bool ImGui_ColorButtonEx(const char* label, bool active, ImColor color, + ImDrawFlags corners, + const ImVec2& size = ImVec2(0, 0), + float hover_alpha = 0.5f) { + const ImGuiStyle& s = ImGui::GetStyle(); + const ImVec2 label_size = ImGui::CalcTextSize(label, nullptr, true); + const ImVec2 btn_size( + size.x > 0 ? size.x : label_size.x + s.FramePadding.x * 2, + size.y > 0 ? size.y : label_size.y + s.FramePadding.y * 2); + + const ImVec2 pos = ImGui::GetCursorScreenPos(); + ImGui::InvisibleButton(label, btn_size); + const bool clicked = ImGui::IsItemClicked(); + const bool hovered = ImGui::IsItemHovered(); + + // Determine background color. + const ImColor hover_color(color.Value.x, color.Value.y, color.Value.z, + color.Value.w * hover_alpha); + ImColor bg; + if (active) { + bg = color; + } else if (hovered) { + bg = hover_color; + } else { + bg = ImGui::GetColorU32(ImGuiCol_Button); + } + + // Draw background with per-corner rounding. + ImDrawList* dl = ImGui::GetWindowDrawList(); + const ImVec2 max(pos.x + btn_size.x, pos.y + btn_size.y); + dl->AddRectFilled(pos, max, bg, s.FrameRounding, corners); + + // Draw border. + if (s.FrameBorderSize > 0) { + dl->AddRect(pos, max, ImGui::GetColorU32(ImGuiCol_Border), + s.FrameRounding, corners, s.FrameBorderSize); + } + + // Draw label centered. + const ImVec2 text_pos(pos.x + (btn_size.x - label_size.x) * 0.5f, + pos.y + (btn_size.y - label_size.y) * 0.5f); + dl->AddText(text_pos, ImGui::GetColorU32(ImGuiCol_Text), label); + + return clicked; +} + // Begin a boxed section with outer borders - use EndBoxSection to close. inline bool BeginBoxSection(const char* id, ImGuiTableFlags extra_flags = 0) { ImGuiTableFlags flags = ImGuiTableFlags_BordersOuter | extra_flags; diff --git a/src/experimental/studio/app.cc b/src/experimental/studio/app.cc index dec270ae..25dd1676 100644 --- a/src/experimental/studio/app.cc +++ b/src/experimental/studio/app.cc @@ -72,13 +72,11 @@ static void SelectParentPerturb(const mjModel* model, mjvPerturb& perturb) { } 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_RESET_MODEL = platform::ICON_FA_UNDO; 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_RELOAD_SPEC = platform::ICON_FA_REFRESH; static constexpr const char* ICON_UNDO_SPEC = platform::ICON_FA_UNDO; static constexpr const char* ICON_REDO_SPEC = platform::ICON_FA_REPEAT; @@ -1402,13 +1400,12 @@ void App::HelpGui() { } void App::ToolBarGui() { + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(0, 0)); if (ImGui::BeginTable("##ToolBarTable", 2)) { platform::ScopedStyle style; - const ImColor red(220, 40, 40, 255); - - const float scale = ImGui::GetWindowDpiScale(); - const ImVec2 button_size(48.f * scale, 32.f * scale); - const ImVec2 play_button_size(80.f * scale, 32.f * scale); + style.Var(ImGuiStyleVar_ItemSpacing, + ImVec2(ImGui::GetStyle().ItemSpacing.x * 2.0f, + ImGui::GetStyle().ItemSpacing.y)); const float label_width = GetExpectedLabelWidth(); const float copy_btn_width = ImGui::CalcTextSize(ICON_COPY_CAMERA).x + @@ -1420,42 +1417,29 @@ void App::ToolBarGui() { const float right_width = label_width + sp + label_width + sp + label_width + sp + copy_btn_width + sp + theme_width; - const float separator_width = .2f * button_size.x; + const float separator_width = ImGui::GetFrameHeight() * .6f; ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, right_width); ImGui::TableNextColumn(); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetStyle().WindowPadding.x); - // Combined (Unload, Reload) widget + const float btn_size = ImGui::GetFrameHeight(); + const ImVec2 square_size(btn_size, btn_size); + + // Reload button. { style.Var(ImGuiStyleVar_FrameRounding, 2.0f); - - // Unload button. - { - const ImColor a = red; - const ImColor h(a.Value.x, a.Value.y, a.Value.z, a.Value.w * 0.6f); - style.Color(ImGuiCol_ButtonHovered, h); - style.Color(ImGuiCol_ButtonActive, a); - - if (ImGui::Button(ICON_UNLOAD_MODEL, button_size)) { - InitEmptyModel(); - } - ImGui::SetItemTooltip("%s", "Unload"); - style.Reset(); - } - - // Reload button. - ImGui::SameLine(0, 0); - if (ImGui::Button(ICON_RELOAD_MODEL, button_size)) { + if (ImGui::Button(ICON_RELOAD_MODEL, square_size)) { RequestModelReload(); } ImGui::SetItemTooltip("%s", "Reload"); } // Reset button. - ImGui::SameLine(0, separator_width); - if (ImGui::Button(ICON_RESET_MODEL, button_size)) { + ImGui::SameLine(0, 0.5 * separator_width); + if (ImGui::Button(ICON_RESET_MODEL, square_size)) { ResetPhysics(); } ImGui::SetItemTooltip("%s", "Reset"); @@ -1465,10 +1449,8 @@ void App::ToolBarGui() { platform::StepControlGui(model(), &step_control_, tmp_.speed_index); ImGui::TableNextColumn(); - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + - (button_size.y - ImGui::GetFrameHeight()) * 0.5f); - if (ImGui::Button(ICON_COPY_CAMERA)) { + if (ImGui::Button(ICON_COPY_CAMERA, square_size)) { std::string camera_string = platform::CameraToString(data(), &camera_); platform::MaybeSaveToClipboard(camera_string); } @@ -1488,13 +1470,14 @@ void App::ToolBarGui() { ImGui::SameLine(); ImGui::SetNextItemWidth(GetExpectedLabelWidth()); - if (platform::ThemeSelectGui(&ui_.theme)) { + if (platform::ThemeSelectGui(&ui_.theme, square_size)) { platform::SetupTheme(ui_.theme); ImGui::GetIO().WantSaveIniSettings = true; } ImGui::EndTable(); } + ImGui::PopStyleVar(); } void App::StatusBarGui() {