Add ImGui overlay utility and PAUSE/realtime overlays to Studio.

PiperOrigin-RevId: 937834952
Change-Id: I3f3a0500a89b34e2e03b2fe88344b928edeafe8f
This commit is contained in:
Yuval Tassa
2026-06-25 01:44:27 -07:00
committed by Copybara-Service
parent b6a0db7a17
commit 463b0fb27f
3 changed files with 175 additions and 16 deletions
@@ -15,6 +15,7 @@
#include "experimental/platform/ux/imgui_widgets.h"
#include <algorithm>
#include <cfloat>
#include <cstdint>
#include <cstring>
#include <sstream>
@@ -502,5 +503,118 @@ void DrawTextAt(const char* text, float x, float y, float z) {
ImGui::EndChild();
ImGui::End();
}
namespace {
float MeasureTextWidth(const char* text) {
float max_width = 0.0f;
const char* start = text;
while (*start) {
const char* end = start;
while (*end && *end != '\n') ++end;
max_width = std::max(max_width, ImGui::CalcTextSize(start, end).x);
start = *end ? end + 1 : end;
}
return max_width;
}
void SetNextWindowPosInside(OverlayPos pos, ImVec4 rect) {
// compute anchor point and pivot from position enum
ImVec2 anchor, pivot;
float x = rect.x, y = rect.y, w = rect.z, h = rect.w;
float scale = ImGui::GetWindowDpiScale();
float margin = 8.0f * scale;
switch (pos) {
case OverlayPos::kTopLeft:
anchor = {x + margin, y + margin};
pivot = {0.0f, 0.0f};
break;
case OverlayPos::kTop:
anchor = {x + w * 0.5f, y + margin};
pivot = {0.5f, 0.0f};
break;
case OverlayPos::kTopRight:
anchor = {x + w - margin, y + margin};
pivot = {1.0f, 0.0f};
break;
case OverlayPos::kBottomLeft:
anchor = {x + margin, y + h - margin};
pivot = {0.0f, 1.0f};
break;
case OverlayPos::kBottom:
anchor = {x + w * 0.5f, y + h - margin};
pivot = {0.5f, 1.0f};
break;
case OverlayPos::kBottomRight:
anchor = {x + w - margin, y + h - margin};
pivot = {1.0f, 1.0f};
break;
}
ImGui::SetNextWindowPos(anchor, ImGuiCond_Always, pivot);
}
} // namespace
bool BeginOverlay(const char* id, OverlayPos pos, ImVec4 rect,
float min_width, float alpha) {
SetNextWindowPosInside(pos, rect);
if (min_width > 0.0f) {
ImGui::SetNextWindowSizeConstraints(ImVec2(min_width, -1.0f),
ImVec2(FLT_MAX, -1.0f));
}
constexpr ImGuiWindowFlags kFlags =
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings;
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, alpha);
return ImGui::Begin(id, nullptr, kFlags);
}
void EndOverlay() {
ImGui::End();
ImGui::PopStyleVar();
}
void TextOverlay(const char* id, OverlayPos pos, ImVec4 workspace_rect,
const char* text, ImVec4 color, float font_scale,
float alpha, float min_width) {
float scale = ImGui::GetWindowDpiScale();
float padding = 30.0f * scale;
float max_width = std::max(0.0f, workspace_rect.z - 20.0f);
float text_width = MeasureTextWidth(text) * font_scale;
float min_target = std::min(min_width * scale, max_width);
float target = 0.0f;
if (text_width + padding > max_width || min_width > 0.0f) {
target = std::clamp(text_width + padding, min_target, max_width);
}
if (BeginOverlay(id, pos, workspace_rect, target, alpha)) {
if (font_scale != 1.0f) {
ImGui::SetWindowFontScale(font_scale);
}
bool has_color = color.x != 0 || color.y != 0 || color.z != 0 ||
color.w != 0;
if (has_color) {
ImGui::PushStyleColor(ImGuiCol_Text, color);
}
if (text_width + padding > max_width) {
ImGui::PushTextWrapPos(max_width - padding);
ImGui::TextUnformatted(text);
ImGui::PopTextWrapPos();
} else {
ImGui::TextUnformatted(text);
}
if (has_color) {
ImGui::PopStyleColor();
}
if (font_scale != 1.0f) {
ImGui::SetWindowFontScale(1.0f);
}
}
EndOverlay();
}
} // namespace mujoco::platform
@@ -692,6 +692,36 @@ ImPlotPairLayout ImPlot_ComputePairLayout();
// [1,1,1]).
void DrawTextAt(const char* text, float x, float y, float z);
// Grid position for overlays within a viewport rectangle.
enum class OverlayPos {
kTopLeft,
kTop,
kTopRight,
kBottomLeft,
kBottom,
kBottomRight,
};
// Begin an overlay window at the given grid position within a workspace rect.
// workspace_rect: (x, y, width, height) of the viewport area.
// min_width: minimum window width in scaled pixels (0 = auto).
// alpha: window transparency (0.0-1.0).
// Returns true if the overlay is visible (like ImGui::Begin).
// Must be paired with EndOverlay(), even if this returns false.
bool BeginOverlay(const char* id, OverlayPos pos, ImVec4 workspace_rect,
float min_width = 0.0f, float alpha = 0.8f);
void EndOverlay();
// One-liner: display a text overlay with automatic width sizing.
// Measures the longest line in `text`, clamps to workspace width, wraps text
// if needed.
// color: text color. If all components are 0, uses the theme default.
// font_scale: scale factor for the default font (1.0 = normal).
void TextOverlay(const char* id, OverlayPos pos, ImVec4 workspace_rect,
const char* text, ImVec4 color = ImVec4(0, 0, 0, 0),
float font_scale = 1.0f, float alpha = 0.8f,
float min_width = 0.0f);
} // namespace mujoco::platform
#endif // MUJOCO_SRC_EXPERIMENTAL_PLATFORM_UX_IMGUI_WIDGETS_H_
+31 -16
View File
@@ -978,28 +978,43 @@ void App::BuildGui() {
}
if (tmp_.info) {
platform::ScopedStyle style;
style.Var(ImGuiStyleVar_Alpha, 0.8f);
const float scale = ImGui::GetWindowDpiScale();
ImGui::SetNextWindowPos(
ImVec2(workspace_rect.x,
workspace_rect.y + workspace_rect.w),
ImGuiCond_Always, ImVec2(0.0f, 1.0f));
ImGui::SetNextWindowSizeConstraints(ImVec2(240.0f * scale, -1.0f),
ImVec2(FLT_MAX, -1.0f));
const ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_AlwaysAutoResize |
ImGuiWindowFlags_NoSavedSettings;
if (ImGui::Begin("Info", nullptr, flags)) {
if (platform::BeginOverlay("Info", platform::OverlayPos::kBottomLeft,
workspace_rect, 180.0f * scale)) {
const float fps = renderer_->GetFps();
platform::InfoGui(
model(), data(),
step_control_.GetPauseState() == PauseState::kNormalPaused, fps);
}
ImGui::End();
platform::EndOverlay();
}
// pause overlay
if (has_model() &&
step_control_.GetPauseState() == PauseState::kNormalPaused) {
platform::TextOverlay("Pause", platform::OverlayPos::kTop,
workspace_rect, "PAUSE", ImVec4(0, 0, 0, 0), 3.0f);
}
// realtime factor overlay
if (has_model() &&
step_control_.GetPauseState() == PauseState::kUnpaused) {
const float desired = step_control_.GetSpeed();
const float measured = step_control_.GetSpeedMeasured();
if (desired != 100.0f ||
mju_abs(measured - desired) > 0.1f * desired) {
char rtlabel[30];
bool misaligned = mju_abs(measured - desired) > 0.1f * desired;
if (misaligned) {
std::snprintf(rtlabel, sizeof(rtlabel), "%g%% (%-4.1f%%)",
desired, measured);
} else {
std::snprintf(rtlabel, sizeof(rtlabel), "%g%%", desired);
}
platform::TextOverlay("Realtime", platform::OverlayPos::kTopLeft,
workspace_rect, rtlabel, ImVec4(0, 0, 0, 0),
2.0f);
}
}
// Display a drag-and-drop message if no model is loaded.