Studio: toolbar cosmetic improvements

- Remove hard-coded button sizes (48×32) from toolbar icons; buttons now use default ImGui sizing, matching the existing copy-camera button.
- Remove the Unload button (available via menu).
- Add ImGui_ColorButtonEx widget with per-corner rounding via ImDrawFlags, enabling segmented button-group visuals.
- Use segmented rounding for the pause/viscous/play triplet: rounded-left, square, rounded-right.
- Make pause and play buttons 40% wider than the viscous button for emphasis.
- Replace the theme combo dropdown with a cycling button.
- Reduce toolbar height from 48px to 34px and vertically center content via WindowPadding.
- Zero out table CellPadding in the toolbar for precise vertical centering.
- Remove the speed combo's inflated vertical padding.

PiperOrigin-RevId: 906853958
Change-Id: I6e4ec7243adb9e0f1f06f8779fa896a6865c46c7
This commit is contained in:
Yuval Tassa
2026-04-28 03:12:12 -07:00
committed by Copybara-Service
parent 25a9114705
commit 6f275b79c6
5 changed files with 103 additions and 80 deletions
+36 -43
View File
@@ -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<int>(*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<GuiTheme>(theme_idx);
return true;
}
ImGui::SetItemTooltip("%s", "Theme");
ImGui::SetItemTooltip("%s", theme_tooltips[theme_idx]);
return changed;
return false;
}
bool LabelSelectionGui(mjvOption* opts) {
+1 -1
View File
@@ -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);
@@ -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) {
@@ -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;
+16 -33
View File
@@ -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() {