diff --git a/src/experimental/platform/ux/gui.cc b/src/experimental/platform/ux/gui.cc index 0c4b4ace..6453d13c 100644 --- a/src/experimental/platform/ux/gui.cc +++ b/src/experimental/platform/ux/gui.cc @@ -1289,11 +1289,35 @@ void NoiseGui(StepControl* step_control) { step_control->SetNoiseParameters(noise_scale, noise_rate); } +// Slider row with a clipped label and a right-aligned reset button; the +// button is disabled while the value is at its reset target. +static void SliderRowWithReset(const char* name, mjtNum* value, mjtNum min, + mjtNum max, mjtNum reset) { + const float btn = ImGui::GetFrameHeight(); + const float spacing = ImGui::GetStyle().ItemSpacing.x; + const float usable_end = + ImGui::GetWindowContentRegionMax().x - btn - spacing; + const float usable = usable_end - ImGui::GetCursorPosX(); + ImGui::SetNextItemWidth(usable * 0.5f); + char hidden[104]; + std::snprintf(hidden, sizeof(hidden), "##%s", name); + ImGui_Slider(hidden, value, min, max); + ImGui::SameLine(); + const ImVec2 clip_min = ImGui::GetCursorScreenPos(); + const ImVec2 clip_max(ImGui::GetWindowPos().x + usable_end, + clip_min.y + ImGui::GetFrameHeight()); + ImGui::PushClipRect(clip_min, clip_max, true); + ImGui::TextUnformatted(name); + ImGui::PopClipRect(); + ImGui::BeginDisabled(*value == reset); + if (ImGui_ResetButton(name)) { + *value = reset; + } + ImGui::EndDisabled(); +} + void JointsGui(const mjModel* model, const mjData* data, const mjvOption* vis_options) { - const float item_width = ImGui::GetWindowWidth() * .6f; - ImGui::PushItemWidth(item_width); - char name[100]; for (int i = 0; i < model->njnt; ++i) { if (model->jnt_type[i] != mjJNT_HINGE && @@ -1326,23 +1350,13 @@ void JointsGui(const mjModel* model, const mjData* data, } const int data_adr = model->jnt_qposadr[i]; - ImGui_Slider(name, &data->qpos[data_adr], min, max); - if (ImGui::BeginPopupContextItem()) { - if (ImGui::MenuItem("Reset to default")) { - data->qpos[data_adr] = model->qpos0[data_adr]; - } - ImGui::EndPopup(); - } + SliderRowWithReset(name, &data->qpos[data_adr], min, max, + model->qpos0[data_adr]); } - - ImGui::PopItemWidth(); } void ControlsGui(const mjModel* model, mjData* data, const mjvOption* vis_options) { - const float item_width = ImGui::GetWindowWidth() * .6f; - ImGui::PushItemWidth(item_width); - if (ImGui::Button("Clear All")) { mj_resetCtrl(model, data); } @@ -1383,17 +1397,10 @@ void ControlsGui(const mjModel* model, mjData* data, min = model->actuator_ctrlrange[2 * j + 0]; max = model->actuator_ctrlrange[2 * j + 1]; } - ImGui_Slider(name, &data->ctrl[j], min, max); - if (ImGui::BeginPopupContextItem()) { - if (ImGui::MenuItem("Reset to 0")) { - data->ctrl[j] = mju_clip(0.0, min, max); - } - ImGui::EndPopup(); - } + SliderRowWithReset(name, &data->ctrl[j], min, max, + mju_clip(0.0, min, max)); } } - - ImGui::PopItemWidth(); } static int GetPlotXLimit(const mjData* data) { diff --git a/src/experimental/platform/ux/imgui_widgets.cc b/src/experimental/platform/ux/imgui_widgets.cc index 02baf0bb..30115dfd 100644 --- a/src/experimental/platform/ux/imgui_widgets.cc +++ b/src/experimental/platform/ux/imgui_widgets.cc @@ -343,6 +343,18 @@ bool ImGui_Slider(const char* name, mjtNum* value, mjtNum min, mjtNum max) { return res; } +bool ImGui_ResetButton(const char* id, const char* icon, + const char* tooltip) { + const float size = ImGui::GetFrameHeight(); + ImGui::SameLine(); + ImGui::SetCursorPosX(ImGui::GetWindowContentRegionMax().x - size); + ImGui::PushID(id); + const bool clicked = ImGui::SmallButton(icon); + ImGui::PopID(); + ImGui::SetItemTooltip("%s", tooltip); + return clicked; +} + bool ImGui_BeginHSplit(const char* id, float* height, bool* open) { const ImVec2 region = ImGui::GetContentRegionAvail(); if (*height < 0) { diff --git a/src/experimental/platform/ux/imgui_widgets.h b/src/experimental/platform/ux/imgui_widgets.h index df61cd73..29dbd860 100644 --- a/src/experimental/platform/ux/imgui_widgets.h +++ b/src/experimental/platform/ux/imgui_widgets.h @@ -460,6 +460,11 @@ void ImGui_EndHSplit(bool open); // ImGui Slider that supports both float and double types. bool ImGui_Slider(const char* name, mjtNum* value, mjtNum min, mjtNum max); +// Small right-aligned reset button on the current line; returns true when +// clicked. Use after a widget to give it a visible reset affordance. +bool ImGui_ResetButton(const char* id, const char* icon = ICON_FA_UNDO, + const char* tooltip = "Reset"); + template bool ImGui_Checkbox(const char* name, T& value) { static_assert(std::is_integral());