// Copyright 2025 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.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "experimental/studio/app.h"

#include <algorithm>
#include <array>
#include <cfloat>
#include <cstddef>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <functional>
#include <memory>
#include <optional>
#include <span>
#include <string>
#include <string_view>
#include <thread>
#include <utility>
#include <vector>

#include <imgui.h>
#include <implot.h>
#include <mujoco/mujoco.h>
#include "experimental/platform/hal/graphics_mode.h"
#include "experimental/platform/hal/renderer.h"
#include "experimental/platform/hal/window.h"
#include "experimental/platform/helpers.h"
#include "experimental/platform/sim/model_holder.h"
#include "experimental/platform/sim/step_control.h"
#include "experimental/platform/ux/file_dialog.h"
#include "experimental/platform/ux/gui.h"
#include "experimental/platform/ux/gui_spec.h"
#include "experimental/platform/ux/imgui_widgets.h"
#include "experimental/platform/ux/interaction.h"
#include "experimental/platform/ux/picture_gui.h"
#include "experimental/platform/ux/plugin.h"

namespace mujoco::studio {

using PauseState = platform::StepControl::PauseState;

static void ToggleFlag(mjtByte& flag) { flag = flag ? 0 : 1; }

static void ToggleWindow(bool& window) {
  window = !window;
  ImGui::GetIO().WantSaveIniSettings = true;
}

static void SelectParentPerturb(const mjModel* model, mjvPerturb& perturb) {
  if (perturb.select > 0) {
    perturb.select = model->body_parentid[perturb.select];
    perturb.flexselect = -1;
    perturb.skinselect = -1;
    if (perturb.select <= 0) {
      perturb.active = 0;
    }
  }
  // TODO: update selected element!
}

static constexpr const char* ICON_COPY_CAMERA = platform::ICON_FA_COPY;
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_UNDO_SPEC = platform::ICON_FA_UNDO;
static constexpr const char* ICON_REDO_SPEC = platform::ICON_FA_REPEAT;

App::App(Config config)
    : app_title_(std::move(config.title)),
      ini_path_(std::move(config.ini_path)),
      gfx_mode_(config.gfx_mode) {
  SwitchGraphicsMode(config.width, config.height, config.gfx_mode);

  if (config.initial_theme.has_value()) {
    ui_.theme = *config.initial_theme;
  }

  ImPlot::CreateContext();
  mjv_defaultPerturb(&perturb_);
  mjv_defaultCamera(&camera_);
  mjv_defaultOption(&vis_options_);

  profiler_.Clear();
}

void App::SwitchGraphicsMode(int width, int height,
                             platform::GraphicsMode mode) {
  renderer_.reset();
  window_.reset();
  gfx_mode_ = mode;

  platform::Window::Config window_config;
  window_config.gfx_mode = gfx_mode_;
  window_ = std::make_unique<platform::Window>(app_title_, width, height,
                                               window_config);
  renderer_ = std::make_unique<platform::Renderer>(
      window_->GetNativeWindowHandle(), gfx_mode_);

  // TODO: Figure out why this breaks on some platforms.
  // LoadSettings();
  if (ui_.window_width > 0 && ui_.window_height > 0) {
    window_->Resize(ui_.window_width, ui_.window_height);
  }
}

void App::Recompile() {
  mj_recompile(model_holder_->spec(), model_holder_->vfs(),
               model_holder_->model(), model_holder_->data());
  const int state_size = mj_stateSize(model(), mjSTATE_INTEGRATION);
  sim_history_.Init(state_size);
}

void App::RequestModelLoad(std::string model_file) {
  pending_load_ = std::move(model_file);
}

void App::RequestModelReload() {
  if (model_kind_ == kModelFromFile ||
      (model_kind_ == kEmptyModel && !model_path_.empty())) {
    pending_load_ = model_path_;
    preserve_camera_on_load_ = true;
  }
}

void App::InitEmptyModel() {
  model_holder_ = platform::ModelHolder::FromSpec(mj_makeSpec());
  OnModelLoaded("", kEmptyModel);
  spec_editor_.Reset(*spec());
}

void App::LoadModelFromFile(const std::string& filepath) {
  const std::string resolved_file =
      platform::ResolveFile(filepath, search_paths_);
  model_holder_ = platform::ModelHolder::FromFile(resolved_file);
  if (model_holder_->ok()) {
    OnModelLoaded(filepath, kModelFromFile);
    spec_editor_.Reset(*spec());
    UpdateFilePaths(resolved_file);
    if (model() && model()->names) {
      // Assumes the first string in the model is the name of the model itself.
      window_->SetTitle(app_title_ + " : " + std::string(model()->names));
    } else {
      window_->SetTitle(app_title_ + " : " +
                        std::filesystem::path(filepath).stem().string());
    }
  } else {
    SetLoadError(std::string(model_holder_->error()));
    // Keep track of the attempted load in case the user fixes the error and
    // tries to reload the same file again.
    model_path_ = resolved_file;
  }
}

void App::LoadModelFromBuffer(std::span<const std::byte> buffer,
                              std::string_view content_type,
                              std::string_view filename) {
  model_holder_ =
      platform::ModelHolder::FromBuffer(buffer, content_type, filename);
  if (model_holder_->ok()) {
    OnModelLoaded(std::string(filename), kModelFromFile);
  } else {
    SetLoadError(std::string(model_holder_->error()));
  }
  spec_editor_.Reset(*spec());
}

void App::OnModelLoaded(std::string filename, ModelKind model_kind) {
  load_error_ = "";
  step_error_ = "";
  edit_error_ = "";

  model_path_ = std::move(filename);

  if (model_kind_ == kEmptyModel) {
    step_control_.SetPauseState(PauseState::kUnpaused);
  }
  model_kind_ = model_kind;
  if (model_kind_ == kEmptyModel) {
    step_control_.SetPauseState(PauseState::kNormalPaused);
  }

  // Reset/reinitialize everything that depends on the new mjModel.
  mjModel* model = model_holder_->model();
  renderer_->Init(model);
  const int state_size = mj_stateSize(model, mjSTATE_INTEGRATION);
  sim_history_.Init(state_size);

  if (!preserve_camera_on_load_) {
    const int model_cam = model->vis.global.cameraid;
    if (model_cam >= 0 && model_cam < model->ncam) {
      ui_.camera_idx = platform::SetCamera(model, &camera_, model_cam);
    } else {
      mjv_defaultFreeCamera(model, &camera_);
    }
  }
  preserve_camera_on_load_ = false;

  // Initialize the speed based on the model's default real-time setting.
  float min_error = FLT_MAX;
  const float desired = mju_log(100 * model->vis.global.realtime);
  for (int i = 0; i < platform::kPercentRealTime.size(); ++i) {
    const float speed = std::stof(platform::kPercentRealTime[i]);
    const float error = mju_abs(mju_log(speed) - desired);
    if (error < min_error) {
      min_error = error;
      SetSpeedIndex(i);
    }
  }

  platform::ForEachPlugin<platform::ModelPlugin>([&](auto* plugin) {
    if (plugin->post_model_loaded) {
      plugin->post_model_loaded(plugin, model_path_.c_str());
    }
  });
  tmp_.update_threadpool = true;
}

void App::UpdateFilePaths(const std::string& resolved_path) {
  if (!resolved_path.empty()) {
    const std::filesystem::path path(resolved_path);
    const std::string base_path = path.parent_path().string() + "/";
    const std::string model_name = path.stem().string();
    tmp_.last_path[UiTempState::FileDialog_Load] =
        base_path + model_name + ".xml";
    tmp_.last_path[UiTempState::FileDialog_SaveMjb] =
        base_path + model_name + "_saved.mjb";
    tmp_.last_path[UiTempState::FileDialog_SaveXml] =
        base_path + model_name + "_saved.xml";
    tmp_.last_path[UiTempState::FileDialog_PrintModel] =
        base_path + model_name + "_MJMODEL.TXT";
    tmp_.last_path[UiTempState::FileDialog_PrintData] =
        base_path + model_name + "_MJDATA.TXT";
    tmp_.last_path[UiTempState::FileDialog_SaveScreenshot] =
        base_path + "screenshot.webp";
  }
}

void App::SetLoadError(std::string error) {
  InitEmptyModel();
  load_error_ = std::move(error);
  step_error_ = "";
  edit_error_ = "";
}

void App::ResetPhysics() {
  mj_resetData(model(), data());
  mj_forward(model(), data());
  step_error_ = "";
  edit_error_ = "";
}

void App::UpdatePhysics() {
  if (!has_model()) {
    return;
  }

  if (tmp_.update_threadpool) {
    mju_threadpool(data(), ui_.nthread);
    tmp_.update_threadpool = false;
  }

  bool stepped = false;
  platform::ForEachPlugin<platform::ModelPlugin>([&](auto* plugin) {
    if (plugin->do_update) {
      if (plugin->do_update(plugin, model(), data())) {
        stepped = true;
      }
    }
  });

  if (!stepped) {
    if (step_control_.GetPauseState() != PauseState::kNormalPaused) {
      mju_zero(data()->xfrc_applied, 6 * model()->nbody);
      mjv_applyPerturbPose(model(), data(), &perturb_, 0);
      mjv_applyPerturbForce(model(), data(), &perturb_);
    } else {
      mjv_applyPerturbPose(model(), data(), &perturb_, 1);
    }

    if (has_data()) {
      for (int i = 0; i < mjNTIMER; i++) {
        data()->timer[i].duration = 0;
        data()->timer[i].number = 0;
      }
    }

    platform::StepControl::Status status =
        step_control_.Advance(model(), data());
    if (status == platform::StepControl::Status::kPaused) {
      // do nothing
    } else if (status == platform::StepControl::Status::kOk) {
      stepped = true;
      // If we are adding to the history we didn't have a divergence error
      step_error_ = "";
    } else if (status == platform::StepControl::Status::kAutoReset) {
      ResetPhysics();
    } else if (status == platform::StepControl::Status::kDiverged) {
      stepped = true;
      for (mjtWarning w : platform::StepControl::kDivergedWarnings) {
        if (data()->warning[w].number > 0) {
          step_error_ = mju_warningText(w, data()->warning[w].lastinfo);
        }
      }
    }
  }

  if (stepped) {
    profiler_.Update(model(), data());
    std::span<mjtNum> state = sim_history_.AddToHistory();
    if (!state.empty()) {
      mj_getState(model(), data(), state.data(), mjSTATE_INTEGRATION);
    }
  }
}

void App::LoadHistory(int offset) {
  std::span<mjtNum> state = sim_history_.SetIndex(offset);
  if (!state.empty()) {
    // Pause simulation when entering history mode.
    step_control_.SetPauseState(PauseState::kNormalPaused);

    // Load the state into the data buffer.
    mj_setState(model(), data(), state.data(), mjSTATE_INTEGRATION);
    mj_forward(model(), data());
  }
}

bool App::Update() {
  const platform::Window::Status status = window_->NewFrame();

  HandleWindowEvents();
  HandleMouseEvents();
  HandleKeyboardEvents();

  ProcessPendingLoads();

  // Only update the simulation if a popup window is not open. Note that the
  // simulation itself will only update if it is not paused.
  if (tmp_.file_dialog == UiTempState::FileDialog_None) {
    UpdatePhysics();
  }

  return status == platform::Window::Status::kRunning && !tmp_.should_exit;
}

void App::Render() {
  const float width = window_->GetWidth();
  const float height = window_->GetHeight();
  const float scale = window_->GetScale();
  if (IsHeadless(window_->GetGraphicsMode())) {
    pixels_.resize(width * height * 3);
  } else {
    pixels_.clear();
  }
  renderer_->Render(model(), data(), &perturb_, &camera_, &vis_options_,
                    width * scale, height * scale, pixels_);

  window_->EndFrame();
  window_->Present(pixels_);

  if (has_data()) {
    for (int i = 0; i < mjNTIMER; i++) {
      data()->timer[i].duration = 0;
      data()->timer[i].number = 0;
    }
  }
}

void App::ProcessPendingLoads() {
  // Check to see if we need to load a new model.
  if (pending_load_.has_value()) {
    std::string load_data = std::move(pending_load_.value());
    pending_load_.reset();

    if (load_data.empty()) {
      InitEmptyModel();
    } else {
      LoadModelFromFile(load_data);
    }
  }

  if (pending_op_) {
    pending_op_();
    pending_op_ = nullptr;
  }

  // Allow plugins to edit the spec as well.
  platform::ForEachPlugin<platform::SpecEditorPlugin>([&](auto* plugin) {
    if (plugin->pre_compile) {
      if (plugin->pre_compile(plugin, spec(), model(), data(), &camera_)) {
        Recompile();
        if (plugin->post_compile) {
          plugin->post_compile(plugin, spec(), model(), data());
        }
      };
    }
  });

  // Check plugins to see if we need to load a new model.
  platform::ForEachPlugin<platform::ModelPlugin>([&](auto* plugin) {
    if (plugin->get_model_to_load) {
      char model_name[1000] = "";
      char content_type[1000] = "";
      int size = 0;
      const char* buf = plugin->get_model_to_load(
          plugin, &size, content_type, sizeof(content_type), model_name,
          sizeof(model_name));
      if (buf && size) {
        const std::byte* bytes = reinterpret_cast<const std::byte*>(buf);
        LoadModelFromBuffer({bytes, bytes + size}, content_type, model_name);
      }
    }
  });
}

void App::HandleWindowEvents() {
  const std::string drop_file = window_->GetDropFile();
  if (!drop_file.empty()) {
    RequestModelLoad(drop_file);
  }
}

void App::HandleMouseEvents() {
  auto& io = ImGui::GetIO();
  if (io.WantCaptureMouse) {
    return;
  }

  if (!has_model() || !has_data()) {
    return;
  }

  // Normalize mouse positions and movement to display size.
  const float mouse_x = io.MousePos.x / io.DisplaySize.x;
  const float mouse_y = io.MousePos.y / io.DisplaySize.y;
  const float mouse_dx = io.MouseDelta.x / io.DisplaySize.x;
  const float mouse_dy = io.MouseDelta.y / io.DisplaySize.y;
  const float mouse_scroll = io.MouseWheel / 50.0f;
  const bool is_mouse_moving = (mouse_dx != 0.0f || mouse_dy != 0.0f);
  const bool is_any_mouse_down = ImGui::IsMouseDown(ImGuiMouseButton_Left) ||
                                 ImGui::IsMouseDown(ImGuiMouseButton_Right) ||
                                 ImGui::IsMouseDown(ImGuiMouseButton_Middle);
  const bool is_mouse_dragging = is_mouse_moving && is_any_mouse_down;

  // If no mouse buttons are down, end any active perturbations.
  if (!is_any_mouse_down) {
    perturb_.active = 0;
  }

  // Handle perturbation mouse actions.
  if (is_mouse_dragging && io.KeyCtrl) {
    if (perturb_.select > 0) {
      mjtMouse action = mjMOUSE_NONE;
      if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
        if (io.KeyAlt) {
          action = io.KeyShift ? mjMOUSE_MOVE_H : mjMOUSE_MOVE_V;
        } else {
          action = io.KeyShift ? mjMOUSE_ROTATE_H : mjMOUSE_ROTATE_V;
        }
      } else if (ImGui::IsMouseDown(ImGuiMouseButton_Right)) {
        action = io.KeyShift ? mjMOUSE_MOVE_H : mjMOUSE_MOVE_V;
      } else if (ImGui::IsMouseDown(ImGuiMouseButton_Middle)) {
        action = mjMOUSE_ZOOM;
      }
      const mjtPertBit active =
          (action == mjMOUSE_MOVE_V || action == mjMOUSE_MOVE_H)
              ? mjPERT_TRANSLATE
              : mjPERT_ROTATE;
      if (active != perturb_.active) {
        platform::InitPerturb(model(), data(), &camera_, &perturb_, active);
      }
      platform::MovePerturb(model(), data(), &camera_, &perturb_, action,
                            mouse_dx, mouse_dy);
    }
  }
  // Handle camera movement actions.
  else if (is_mouse_dragging) {
    if (ui_.camera_idx == platform::kFreeCameraIdx) {
      if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
        MoveCamera(platform::CameraMotion::PAN_TILT, mouse_dx, mouse_dy);
      }
    } else {
      if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
        MoveCamera(platform::CameraMotion::ORBIT, mouse_dx, mouse_dy);
      } else if (ImGui::IsMouseDown(ImGuiMouseButton_Middle)) {
        MoveCamera(platform::CameraMotion::ZOOM, mouse_dx, mouse_dy);
      }
    }

    // Right mouse movement is relative to the horizontal and vertical planes.
    if (ImGui::IsMouseDown(ImGuiMouseButton_Right) && io.KeyShift) {
      MoveCamera(platform::CameraMotion::PLANAR_MOVE_H, mouse_dx, mouse_dy);
    } else if (ImGui::IsMouseDown(ImGuiMouseButton_Right)) {
      MoveCamera(platform::CameraMotion::PLANAR_MOVE_V, mouse_dx, mouse_dy);
    }
  }

  // Mouse scroll zooms the camera towards/away from the lookat point.
  // Ignored by user-centered free cameras which don't have a lookat point.
  if (mouse_scroll != 0.0f && ui_.camera_idx != platform::kFreeCameraIdx) {
    MoveCamera(platform::CameraMotion::ZOOM, 0, -mouse_scroll);
  }

  // Left double click.
  if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
    platform::PickResult picked =
        platform::Pick(model(), data(), &camera_, mouse_x, mouse_y,
                       window_->GetAspectRatio(), &vis_options_);
    if (picked.body >= 0) {
      perturb_.select = picked.body;
      perturb_.flexselect = picked.flex;
      perturb_.skinselect = picked.skin;

      // Select the corresponding element in the spec.
      tmp_.curr_element = nullptr;
      if (has_spec()) {
        mjsElement* element = mjs_firstElement(spec(), mjOBJ_BODY);
        while (element) {
          if (mjs_getId(element) == picked.body) {
            tmp_.curr_element = element;
            break;
          }
          element = mjs_nextElement(spec(), element);
        }
      }

      // Compute the local position of the selected object in the world.
      mjtNum tmp[3];
      mju_sub3(tmp, picked.point, data()->xpos + 3 * picked.body);
      mju_mulMatTVec(perturb_.localpos, data()->xmat + 9 * picked.body, tmp, 3,
                     3);
    } else {
      mjv_defaultPerturb(&perturb_);
    }
  }

  // Right double click.
  if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Right)) {
    platform::PickResult picked =
        platform::Pick(model(), data(), &camera_, mouse_x, mouse_y,
                       window_->GetAspectRatio(), &vis_options_);
    mju_copy3(camera_.lookat, picked.point);
    if (picked.body > 0 && io.KeyCtrl) {
      // Switch camera to tracking mode and track the selected body.
      camera_.type = mjCAMERA_TRACKING;
      camera_.trackbodyid = picked.body;
      camera_.fixedcamid = -1;
      ui_.camera_idx = platform::kTrackingCameraIdx;
    }
  }
}

void App::HandleKeyboardEvents() {
  using platform::ImGui_IsChordJustPressed;
  if (ImGui::GetIO().WantCaptureKeyboard) {
    return;
  }

  constexpr auto ImGuiMod_CtrlShift = ImGuiMod_Ctrl | ImGuiMod_Shift;

  bool is_freecam_wasd = ui_.camera_idx == platform::kFreeCameraIdx;

  // Menu shortcuts.
  if (ImGui_IsChordJustPressed(ImGuiKey_O | ImGuiMod_Ctrl)) {
    tmp_.file_dialog = UiTempState::FileDialog_Load;
  } else if (ImGui_IsChordJustPressed(ImGuiKey_S | ImGuiMod_CtrlShift)) {
    tmp_.file_dialog = UiTempState::FileDialog_SaveMjb;
  } else if (ImGui_IsChordJustPressed(ImGuiKey_S | ImGuiMod_Ctrl)) {
    tmp_.file_dialog = UiTempState::FileDialog_SaveXml;
  } else if (ImGui_IsChordJustPressed(ImGuiKey_M | ImGuiMod_Ctrl)) {
    tmp_.file_dialog = UiTempState::FileDialog_PrintModel;
  } else if (ImGui_IsChordJustPressed(ImGuiKey_D | ImGuiMod_Ctrl)) {
    tmp_.file_dialog = UiTempState::FileDialog_PrintData;
  } else if (ImGui_IsChordJustPressed(ImGuiKey_P | ImGuiMod_Ctrl)) {
    tmp_.file_dialog = UiTempState::FileDialog_SaveScreenshot;
  } else if (ImGui_IsChordJustPressed(ImGuiKey_C | ImGuiMod_Ctrl)) {
    std::string keyframe = platform::KeyframeToString(model(), data(), false);
    platform::MaybeSaveToClipboard(keyframe);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_L | ImGuiMod_Ctrl)) {
    RequestModelReload();
  } else if (ImGui_IsChordJustPressed(ImGuiKey_Q | ImGuiMod_Ctrl)) {
    tmp_.should_exit = true;
  } else if (ImGui_IsChordJustPressed(ImGuiKey_A | ImGuiMod_Ctrl)) {
    mjv_defaultFreeCamera(model(), &camera_);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_Tab | ImGuiMod_Shift)) {
    tmp_.inspector_panel = !tmp_.inspector_panel;
  } else if (ImGui_IsChordJustPressed(ImGuiKey_Tab)) {
    tmp_.options_panel = !tmp_.options_panel;
  } else if (ImGui_IsChordJustPressed(ImGuiKey_Minus | ImGuiMod_Ctrl)) {
    float old_scale = ui_.font_scale;
    ui_.font_scale = std::clamp(ui_.font_scale - 0.1f, 0.5f, 3.0f);
    platform::RescaleDock(ui_.font_scale / old_scale);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_Equal | ImGuiMod_Ctrl)) {
    float old_scale = ui_.font_scale;
    ui_.font_scale = std::clamp(ui_.font_scale + 0.1f, 0.5f, 3.0f);
    platform::RescaleDock(ui_.font_scale / old_scale);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_Minus)) {
    SetSpeedIndex(tmp_.speed_index + 1);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_Equal)) {
    SetSpeedIndex(tmp_.speed_index - 1);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_LeftArrow)) {
    if (step_control_.GetPauseState() == PauseState::kNormalPaused) {
      LoadHistory(sim_history_.GetIndex() - 1);
    }
  } else if (ImGui_IsChordJustPressed(ImGuiKey_RightArrow)) {
    if (step_control_.GetPauseState() == PauseState::kNormalPaused) {
      if (sim_history_.GetIndex() == 0) {
        step_control_.RequestSingleStep();
      } else {
        LoadHistory(sim_history_.GetIndex() + 1);
      }
    }
  } else if (ImGui_IsChordJustPressed(ImGuiMod_Ctrl | ImGuiKey_Space)) {
    if (step_control_.GetPauseState() == PauseState::kViscousPaused) {
      step_control_.SetPauseState(PauseState::kUnpaused);
    } else {
      step_control_.SetPauseState(PauseState::kViscousPaused);
    }
  } else if (ImGui_IsChordJustPressed(ImGuiKey_Space)) {
    if (step_control_.GetPauseState() == PauseState::kViscousPaused) {
      step_control_.SetPauseState(PauseState::kNormalPaused);
    } else if (step_control_.GetPauseState() == PauseState::kUnpaused) {
      step_control_.SetPauseState(PauseState::kNormalPaused);
    } else {
      step_control_.SetPauseState(PauseState::kUnpaused);
    }
  } else if (ImGui_IsChordJustPressed(ImGuiKey_Backspace)) {
    ResetPhysics();
  } else if (ImGui_IsChordJustPressed(ImGuiKey_Delete)) {
    // TODO: SpecDeleteElement(tmp_.curr_element);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_PageUp)) {
    SelectParentPerturb(model(), perturb_);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_F1)) {
    ToggleWindow(tmp_.help);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_F2)) {
    ToggleWindow(tmp_.stats);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_F3)) {
    ToggleWindow(tmp_.profiler);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_F6)) {
    vis_options_.frame = (vis_options_.frame + 1) % mjNFRAME;
  } else if (ImGui_IsChordJustPressed(ImGuiKey_F7)) {
    vis_options_.label = (vis_options_.label + 1) % mjNLABEL;
  } else if (ImGui_IsChordJustPressed(ImGuiKey_F11)) {
    tmp_.full_screen = !tmp_.full_screen;
  } else if (ImGui_IsChordJustPressed(ImGuiKey_H)) {
    ToggleFlag(vis_options_.flags[mjVIS_CONVEXHULL]);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_X)) {
    ToggleFlag(vis_options_.flags[mjVIS_TEXTURE]);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_J)) {
    ToggleFlag(vis_options_.flags[mjVIS_JOINT]);
  } else if (!is_freecam_wasd && ImGui_IsChordJustPressed(ImGuiKey_Q)) {
    ToggleFlag(vis_options_.flags[mjVIS_CAMERA]);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_U)) {
    ToggleFlag(vis_options_.flags[mjVIS_ACTUATOR]);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_Comma)) {
    ToggleFlag(vis_options_.flags[mjVIS_ACTIVATION]);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_Z)) {
    ToggleFlag(vis_options_.flags[mjVIS_LIGHT]);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_V)) {
    ToggleFlag(vis_options_.flags[mjVIS_TENDON]);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_Y)) {
    ToggleFlag(vis_options_.flags[mjVIS_RANGEFINDER]);
  } else if (!is_freecam_wasd && ImGui_IsChordJustPressed(ImGuiKey_E)) {
    ToggleFlag(vis_options_.flags[mjVIS_CONSTRAINT]);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_I)) {
    ToggleFlag(vis_options_.flags[mjVIS_INERTIA]);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_Apostrophe)) {
    ToggleFlag(vis_options_.flags[mjVIS_SCLINERTIA]);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_B)) {
    ToggleFlag(vis_options_.flags[mjVIS_PERTFORCE]);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_O)) {
    ToggleFlag(vis_options_.flags[mjVIS_PERTOBJ]);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_C)) {
    ToggleFlag(vis_options_.flags[mjVIS_CONTACTPOINT]);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_N)) {
    ToggleFlag(vis_options_.flags[mjVIS_ISLAND]);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_F)) {
    ToggleFlag(vis_options_.flags[mjVIS_CONTACTFORCE]);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_P)) {
    ToggleFlag(vis_options_.flags[mjVIS_CONTACTSPLIT]);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_T)) {
    ToggleFlag(vis_options_.flags[mjVIS_TRANSPARENT]);
  } else if (!is_freecam_wasd && ImGui_IsChordJustPressed(ImGuiKey_A)) {
    ToggleFlag(vis_options_.flags[mjVIS_AUTOCONNECT]);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_M)) {
    ToggleFlag(vis_options_.flags[mjVIS_COM]);
  } else if (!is_freecam_wasd && ImGui_IsChordJustPressed(ImGuiKey_D)) {
    ToggleFlag(vis_options_.flags[mjVIS_STATIC]);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_Semicolon)) {
    ToggleFlag(vis_options_.flags[mjVIS_SKIN]);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_GraveAccent)) {
    ToggleFlag(vis_options_.flags[mjVIS_BODYBVH]);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_Backslash)) {
    ToggleFlag(vis_options_.flags[mjVIS_MESHBVH]);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_0 | ImGuiMod_Shift)) {
    ToggleFlag(vis_options_.sitegroup[0]);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_1 | ImGuiMod_Shift)) {
    ToggleFlag(vis_options_.sitegroup[1]);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_2 | ImGuiMod_Shift)) {
    ToggleFlag(vis_options_.sitegroup[2]);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_3 | ImGuiMod_Shift)) {
    ToggleFlag(vis_options_.sitegroup[3]);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_4 | ImGuiMod_Shift)) {
    ToggleFlag(vis_options_.sitegroup[4]);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_5 | ImGuiMod_Shift)) {
    ToggleFlag(vis_options_.sitegroup[5]);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_0)) {
    ToggleFlag(vis_options_.geomgroup[0]);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_1)) {
    ToggleFlag(vis_options_.geomgroup[1]);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_2)) {
    ToggleFlag(vis_options_.geomgroup[2]);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_3)) {
    ToggleFlag(vis_options_.geomgroup[3]);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_4)) {
    ToggleFlag(vis_options_.geomgroup[4]);
  } else if (ImGui_IsChordJustPressed(ImGuiKey_5)) {
    ToggleFlag(vis_options_.geomgroup[5]);
  } else if (has_model() && ImGui_IsChordJustPressed(ImGuiKey_Escape)) {
    ui_.camera_idx =
        platform::SetCamera(model(), &camera_, platform::kTumbleCameraIdx);
  } else if (has_model() && ImGui_IsChordJustPressed(ImGuiKey_LeftBracket)) {
    ui_.camera_idx = platform::SetCamera(model(), &camera_, ui_.camera_idx - 1);
  } else if (has_model() && ImGui_IsChordJustPressed(ImGuiKey_RightBracket)) {
    ui_.camera_idx = platform::SetCamera(model(), &camera_, ui_.camera_idx + 1);
    // WASD camera controls for free camera.
  } else if (is_freecam_wasd &&
             (ImGui::IsKeyDown(ImGuiKey_W) || ImGui::IsKeyDown(ImGuiKey_S) ||
              ImGui::IsKeyDown(ImGuiKey_A) || ImGui::IsKeyDown(ImGuiKey_D) ||
              ImGui::IsKeyDown(ImGuiKey_Q) || ImGui::IsKeyDown(ImGuiKey_E))) {
    bool moved = false;

    // Move (dolly) forward/backward using W and S keys.
    if (ImGui::IsKeyDown(ImGuiKey_W)) {
      MoveCamera(platform::CameraMotion::TRUCK_DOLLY, 0, tmp_.cam_speed);
      moved = true;
    } else if (ImGui::IsKeyDown(ImGuiKey_S)) {
      MoveCamera(platform::CameraMotion::TRUCK_DOLLY, 0, -tmp_.cam_speed);
      moved = true;
    }

    // Strafe (truck) left/right using A and D keys.
    if (ImGui::IsKeyDown(ImGuiKey_A)) {
      MoveCamera(platform::CameraMotion::TRUCK_DOLLY, -tmp_.cam_speed, 0);
      moved = true;
    } else if (ImGui::IsKeyDown(ImGuiKey_D)) {
      MoveCamera(platform::CameraMotion::TRUCK_DOLLY, tmp_.cam_speed, 0);
      moved = true;
    }

    // Move (pedestal) up/down using Q and E keys.
    if (ImGui::IsKeyDown(ImGuiKey_Q)) {
      MoveCamera(platform::CameraMotion::TRUCK_PEDESTAL, 0, tmp_.cam_speed);
      moved = true;
    } else if (ImGui::IsKeyDown(ImGuiKey_E)) {
      MoveCamera(platform::CameraMotion::TRUCK_PEDESTAL, 0, -tmp_.cam_speed);
      moved = true;
    }

    if (moved) {
      tmp_.cam_speed += 0.001f;

      const float max_speed = ImGui::GetIO().KeyShift ? 0.1 : 0.01f;
      if (tmp_.cam_speed > max_speed) {
        tmp_.cam_speed = max_speed;
      }
    } else {
      tmp_.cam_speed = 0.001f;
    }
  } else {
    platform::ForEachPlugin<platform::KeyHandlerPlugin>([&](auto* plugin) {
      if (plugin->key_chord && plugin->on_key_pressed) {
        if (ImGui_IsChordJustPressed(plugin->key_chord)) {
          plugin->on_key_pressed(plugin);
        }
      }
    });
  }
}

void App::LoadSettings() {
  if (!ini_path_.empty()) {
    std::string settings = platform::LoadText(ini_path_);
    if (!settings.empty()) {
      ui_.FromDict(platform::ReadIniSection(settings, "[Studio][UX]"));
      ImGui::LoadIniSettingsFromMemory(settings.data(), settings.size());

      platform::KeyValues plugin_names =
          platform::ReadIniSection(settings, "[Studio][Plugins]");
      platform::ForEachPlugin<platform::GuiPlugin>([&](auto* plugin) {
        auto it = plugin_names.find(plugin->name);
        if (it != plugin_names.end()) {
          plugin->active = std::stoi(it->second) != 0;
        }
      });
    }
  }
}

void App::SaveSettings() {
  if (!ini_path_.empty()) {
    std::string settings = ImGui::SaveIniSettingsToMemory();
    if (window_) {
      ui_.window_width = window_->GetWidth();
      ui_.window_height = window_->GetHeight();
    }
    platform::AppendIniSection(settings, "[Studio][UX]", ui_.ToDict());

    platform::KeyValues plugin_names;
    platform::ForEachPlugin<platform::GuiPlugin>([&](auto* plugin) {
      plugin_names[plugin->name] = std::to_string((int)plugin->active);
    });
    platform::AppendIniSection(settings, "[Studio][Plugins]", plugin_names);

    platform::SaveText(settings, ini_path_);
  }
}

void App::SetSpeedIndex(int idx) {
  platform::SetSpeedIndex(&step_control_, tmp_.speed_index, idx);
}

void App::MoveCamera(platform::CameraMotion motion, mjtNum reldx,
                     mjtNum reldy) {
  platform::MoveCamera(model(), data(), &camera_, motion, reldx, reldy);
}

void App::BuildGui() {
  if (tmp_.full_screen) {
    return;
  }

  if (!tmp_.style_editor) {
    platform::SetupTheme(ui_.theme);
  }

  ImGui::GetIO().FontGlobalScale = ui_.font_scale;

  const ImVec4 workspace_rect = platform::ConfigureDockingLayout();

  // Place charts in bottom right corner of the workspace.
  const ImVec2 chart_size(250, 500);
  const ImVec2 chart_pos(workspace_rect.x + workspace_rect.z - chart_size.x,
                         workspace_rect.y + workspace_rect.w - chart_size.y);

  MainMenuGui();

  {
    ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
    if (ImGui::Begin("ToolBar")) {
      ImGui::PopStyleVar();
      ToolBarGui();
    } else {
      ImGui::PopStyleVar();
    }
    ImGui::End();
  }

  {
    platform::ScopedStyle style;
    style.Var(ImGuiStyleVar_CellPadding, ImVec2(0, 0));
    style.Var(ImGuiStyleVar_FramePadding, ImVec2(0, 0));
    style.Var(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
    if (ImGui::Begin("StatusBar")) {
      StatusBarGui();
    }
    ImGui::End();
  }

  if (tmp_.options_panel) {
    if (ImGui::Begin("Options", &tmp_.options_panel)) {
      ModelOptionsGui();
    }
    ImGui::End();
  }

  if (tmp_.inspector_panel) {
    if (ImGui::Begin("Explorer", &tmp_.inspector_panel)) {
      SpecExplorerGui();
    }
    ImGui::End();

    if (ImGui::Begin("Editor", &tmp_.inspector_panel)) {
      SpecEditorGui();
    }
    ImGui::End();

    if (ImGui::Begin("Inspector", &tmp_.inspector_panel)) {
      DataInspectorGui();
    }
    ImGui::End();
  }

  if (tmp_.profiler) {
    if (ImGui::Begin("Profiler", &tmp_.profiler,
                     ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) {
      platform::ProfilerGui(model(), data(), &profiler_);
    }
    ImGui::End();
  }

  if (tmp_.picture_in_picture) {
    if (ImGui::Begin("Picture-in-Picture", &tmp_.picture_in_picture)) {
      platform::PipGui(model(), data(), window_->GetAspectRatio(),
                       renderer_.get(), &tmp_.pips);
    }
    ImGui::End();
  }

  if (tmp_.help) {
    platform::ScopedStyle style;
    style.Var(ImGuiStyleVar_Alpha, 0.8f);
    ImGui::SetNextWindowPos(ImVec2(workspace_rect.x, workspace_rect.y),
                            ImGuiCond_Appearing);
    ImGui::SetNextWindowSize(ImVec2(0, 0), ImGuiCond_Appearing);
    if (ImGui::Begin("Help", &tmp_.help,
                     ImGuiWindowFlags_AlwaysAutoResize)) {
      ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(2.0f, 5.0f));
      HelpGui();
      ImGui::PopStyleVar();
    }
    ImGui::End();
  }

  if (tmp_.stats) {
    platform::ScopedStyle style;
    style.Var(ImGuiStyleVar_Alpha, 0.6f);
    if (ImGui::Begin("Stats", &tmp_.stats)) {
      const float fps = renderer_->GetFps();
      platform::StatsGui(
          model(), data(),
          step_control_.GetPauseState() == PauseState::kNormalPaused, fps);
    }
    ImGui::End();
  }

  // Display a drag-and-drop message if no model is loaded.
  if (model_kind_ == kEmptyModel) {
#ifndef __EMSCRIPTEN__
    const char* text = "Load model file or drag-and-drop model file here.";

    const float width = window_->GetWidth() * ImGui::GetWindowDpiScale();
    const float height = window_->GetHeight() * ImGui::GetWindowDpiScale();
    const ImVec2 text_size = ImGui::CalcTextSize(text);

    ImGui::SetNextWindowPos(ImVec2((width - text_size.x) / 2, height / 2),
                            ImGuiCond_Always);
    ImGui::SetNextWindowSize(ImVec2(text_size.x + 10, text_size.y + 10),
                             ImGuiCond_Always);
    const ImGuiWindowFlags kOverlayFlags =
        ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoBackground |
        ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoDocking;
    if (ImGui::Begin("##Overlay", 0, kOverlayFlags)) {
      platform::ScopedStyle style;
      style.Color(ImGuiCol_Text, ImVec4(1, 1, 1, 1));
      ImGui::Text("%s", text);
    }
    ImGui::End();
#endif  // !__EMSCRIPTEN__
  }

  FileDialogGui();

  if (tmp_.imgui_demo) {
    ImGui::ShowDemoWindow();
  }
  if (tmp_.implot_demo) {
    ImPlot::ShowDemoWindow();
  }
  if (tmp_.style_editor) {
    if (ImGui::Begin("Style Editor", &tmp_.style_editor)) {
      ImGui::ShowStyleEditor();
    }
    ImGui::End();
  }

  platform::ForEachPlugin<platform::GuiPlugin>([](auto* plugin) {
    if (!plugin->update) {
      return;
    }
    if (ImGui::BeginMainMenuBar()) {
      if (ImGui::BeginMenu("Plugins")) {
        if (ImGui::MenuItem(plugin->name, "", plugin->active)) {
          plugin->active = !plugin->active;
        }
        ImGui::EndMenu();
      }
      ImGui::EndMainMenuBar();
    }
    if (plugin->active) {
      ImGui::Begin(plugin->name);
      plugin->update(plugin);
      ImGui::End();
    }
  });

  ImGuiIO& io = ImGui::GetIO();
  if (tmp_.first_frame) {
    LoadSettings();
    tmp_.first_frame = false;
  }
  if (io.WantSaveIniSettings) {
    SaveSettings();
    io.WantSaveIniSettings = false;
  }
}

void App::ModelOptionsGui() {
  const float min_width = platform::GetExpectedLabelWidth();
  const ImGuiChildFlags child_flags =
      ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize;
  const ImGuiTreeNodeFlags node_flags =
      ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_Framed;

  ImGui::BeginChild("PhysicsGui", {0, 0}, child_flags);
  if (ImGui::TreeNodeEx("Physics", node_flags)) {
    platform::PhysicsGui(model(), min_width);
    ImGui::TreePop();
  }
  ImGui::EndChild();

  ImGui::BeginChild("RenderingGui", {0, 0}, child_flags);
  if (ImGui::TreeNodeEx("Rendering", node_flags)) {
    platform::RenderingGui(model(), &vis_options_, renderer_->GetRenderFlags(),
                           min_width);
    ImGui::TreePop();
  }
  ImGui::EndChild();

  ImGui::BeginChild("GroupsGui", {0, 0}, child_flags);
  if (ImGui::TreeNodeEx("Visibility Groups", node_flags)) {
    platform::GroupsGui(model(), &vis_options_, min_width);
    ImGui::TreePop();
  }
  ImGui::EndChild();

  ImGui::BeginChild("VisualizationGui", {0, 0}, child_flags);
  if (ImGui::TreeNodeEx("Visualization", node_flags)) {
    platform::VisualizationGui(model(), &vis_options_, &camera_, min_width);
    ImGui::TreePop();
  }
  ImGui::EndChild();
}

void App::DataInspectorGui() {
  if (!has_data()) {
    ImGui::Text("No mjData loaded.");
    return;
  }

  const float min_width = platform::GetExpectedLabelWidth();
  const ImGuiChildFlags child_flags =
      ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize;
  const ImGuiTreeNodeFlags node_flags =
      ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_Framed;

  ImGui::BeginChild("JointsGui", {0, 0}, child_flags);
  if (ImGui::TreeNodeEx("Joints", node_flags)) {
    platform::JointsGui(model(), data(), &vis_options_);
    ImGui::TreePop();
  }
  ImGui::EndChild();

  ImGui::BeginChild("ControlsGui", {0, 0}, child_flags);
  if (ImGui::TreeNodeEx("Controls", node_flags)) {

    float noise_scale = 0;
    float noise_rate = 0;
    step_control_.GetNoiseParameters(noise_scale, noise_rate);
    platform::NoiseGui(model(), data(), noise_scale, noise_rate);
    step_control_.SetNoiseParameters(noise_scale, noise_rate);
    ImGui::Separator();

    platform::ControlsGui(model(), data(), &vis_options_);
    ImGui::TreePop();
  }
  ImGui::EndChild();

  ImGui::BeginChild("SensorGui", {0, 0}, child_flags);
  if (ImGui::TreeNodeEx("Sensor", node_flags)) {
    platform::SensorGui(model(), data());
    ImGui::TreePop();
  }
  ImGui::EndChild();

  ImGui::BeginChild("WatchGui", {0, 0}, child_flags);
  if (ImGui::TreeNodeEx("Watch", node_flags)) {
    platform::WatchGui(model(), data(), ui_.watch_field,
                       sizeof(ui_.watch_field), ui_.watch_index);
    ImGui::TreePop();
  }
  ImGui::EndChild();

  ImGui::BeginChild("StateGui", {0, 0}, child_flags);
  if (ImGui::TreeNodeEx("State", node_flags)) {
    platform::StateGui(model(), data(), tmp_.state, tmp_.state_sig, min_width);
    ImGui::TreePop();
  }
  ImGui::EndChild();
}

void App::SpecExplorerGui() {
  if (!has_spec()) {
    ImGui::Text("No mjSpec loaded.");
    return;
  }

  // Initialize the split height.
  const ImVec2 region = ImGui::GetContentRegionAvail();
  if (tmp_.explorer_split < 0) {
    tmp_.explorer_split = region.y * 0.7f;
  }
  tmp_.explorer_split = std::clamp(tmp_.explorer_split, 20.f, region.y - 40.f);

  mjsElement* element = tmp_.curr_element;
  bool open = element != nullptr;
  if (platform::ImGui_BeginHSplit("SpecExplorerTree", &tmp_.explorer_split,
                                  &open)) {
    platform::SpecTreeGui(&element, spec());

    if (element != tmp_.curr_element) {
      tmp_.curr_element = element;

      // A different element was selected, so select it for perturb.
      if (element) {
        open = true;
        const int element_id = mjs_getId(element);
        if (element->elemtype == mjOBJ_BODY && perturb_.select != element_id) {
          mjv_defaultPerturb(&perturb_);
          perturb_.select = element_id;
        }
      }
    }

    if (platform::ImGui_HSplit("SpecExplorerProperties", &tmp_.explorer_split,
                               &open)) {
      ImGui::Text("%s", mju_type2Str(tmp_.curr_element->elemtype));
      ImGui::SameLine();
      ImGui::Text("(%d)", mjs_getId(tmp_.curr_element));
      ImGui::SameLine(ImGui::GetContentRegionAvail().x - 100.0f);

      auto mode_button = [&](const char* label, const char* tooltip,
                             SpecPropertiesMode mode) {
        platform::ScopedStyle style;
        style.Var(ImGuiStyleVar_FramePadding, ImVec2(0, 0));
        if (tmp_.spec_prop_mode == mode) {
          style.Color(ImGuiCol_Button, ImGuiCol_ButtonActive);
        }
        if (ImGui::Button(label, ImVec2(24.0f, 20.0f))) {
          tmp_.spec_prop_mode = mode;
        }
        ImGui::SetItemTooltip("%s", tooltip);
      };

      mode_button("S", "Spec", SpecPropertiesMode::kSpec);
      ImGui::SameLine();
      mode_button("M", "Model", SpecPropertiesMode::kModel);
      ImGui::SameLine();
      mode_button("D", "Data", SpecPropertiesMode::kData);

      ImGui::Separator();

      if (tmp_.spec_prop_mode == SpecPropertiesMode::kSpec) {
        platform::ElementSpecGui(element);
      } else if (tmp_.spec_prop_mode == SpecPropertiesMode::kModel) {
        platform::ElementModelGui(model(), tmp_.curr_element);
      } else {
        platform::ElementDataGui(data(), tmp_.curr_element);
      }
    }
    if (!open) {
      tmp_.curr_element = nullptr;
    }
  }
  platform::ImGui_EndHSplit(open);
}

void App::SpecEditorGui() {
  if (ImGui::BeginChild("SpecEditor", ImVec2(-1, 36))) {
    if (ImGui::BeginTable("##SpecEditorHeader", 3)) {
      ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 50.0f);
      ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch);
      ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 30.0f);

      ImGui::TableNextColumn();
      ImGui::BeginDisabled(!spec_editor_.CanUndo());
      if (ImGui::Button(ICON_UNDO_SPEC)) {
        spec_editor_.Undo();
      }
      ImGui::EndDisabled();
      ImGui::SameLine();
      ImGui::BeginDisabled(!spec_editor_.CanRedo());
      if (ImGui::Button(ICON_REDO_SPEC)) {
        spec_editor_.Redo();
      }
      ImGui::EndDisabled();

      ImGui::TableNextColumn();
      ImGui::PushStyleColor(ImGuiCol_Button, ImColor(40, 180, 40, 255).Value);
      if (ImGui::Button("Compile and Reload", ImVec2(-1, 0))) {
        pending_op_ = [this]() {
          auto tmp_holder = spec_editor_.Compile();
          if (tmp_holder->ok()) {
            model_holder_ = std::move(tmp_holder);
            OnModelLoaded(model_name_, model_kind_);
          } else {
            load_error_ = std::move(tmp_holder->error());
          }
        };
      }
      ImGui::PopStyleColor();

      ImGui::TableNextColumn();
      if (ImGui::Button(" + ")) {
        ImGui::OpenPopupOnItemClick("SpecAddElement", 0);
      }
      if (ImGui::BeginPopupContextItem("SpecAddElement")) {
        auto option = [&](const char* label, mjtObj type) {
          if (ImGui::Selectable(label)) {
            mjsElement* element = spec_editor_.AddElement(type);
            spec_editor_.SetActiveElement(element);
          }
        };

        option("Actuator", mjOBJ_ACTUATOR);
        option("Equality", mjOBJ_EQUALITY);
        option("Exclude", mjOBJ_EXCLUDE);
        option("Flex", mjOBJ_FLEX);
        option("Height Field", mjOBJ_HFIELD);
        option("Key", mjOBJ_KEY);
        option("Material", mjOBJ_MATERIAL);
        option("Mesh", mjOBJ_MESH);
        option("Numeric", mjOBJ_NUMERIC);
        option("Pair", mjOBJ_PAIR);
        option("Sensor", mjOBJ_SENSOR);
        option("Skin", mjOBJ_SKIN);
        option("Tendon", mjOBJ_TENDON);
        option("Text", mjOBJ_TEXT);
        option("Texture", mjOBJ_TEXTURE);
        option("Tuple", mjOBJ_TUPLE);
        ImGui::EndPopup();
      }

      ImGui::EndTable();
    }
  }
  ImGui::EndChild();

  // Initialize the split height.
  const ImVec2 region = ImGui::GetContentRegionAvail();
  if (tmp_.editor_split < 0) {
    tmp_.editor_split = region.y * 0.7f;
  }
  tmp_.editor_split = std::clamp(tmp_.editor_split, 20.0f, region.y - 40.0f);

  mjsElement* element = spec_editor_.GetActiveElement();

  bool open = element != nullptr;
  if (platform::ImGui_BeginHSplit("SpecEditorTree", &tmp_.editor_split,
                                  &open)) {
    platform::SpecTreeGui(&element, spec_editor_.GetActiveSpec(),
                          &spec_editor_);
    open = element != nullptr;
    spec_editor_.SetActiveElement(element);

    if (platform::ImGui_HSplit("SpecEditorProperties", &tmp_.editor_split,
                               &open)) {
      ImGui::Text("%s", mju_type2Str(element->elemtype));
      ImGui::SameLine();
      ImGui::Text("(%d)", mjs_getId(element));
      ImGui::Separator();

      platform::ElementSpecGui(element, &spec_editor_);
    }
    platform::ImGui_EndHSplit(open);
    if (!open) {
      spec_editor_.SetActiveElement(nullptr);
    }
  }
}

void App::HelpGui() {
  const float pad = ImGui::GetStyle().ItemSpacing.x;
  const float indent = pad * 2;
  const float col0 = ImGui::CalcTextSize("Toggle Visc Pause").x + pad;
  const float col1 = ImGui::CalcTextSize("Ctrl+Spc").x + pad + indent;
  const float col2 = ImGui::CalcTextSize("Center of Mass").x + pad + indent;
  const float col3 = ImGui::CalcTextSize("M").x + pad + indent;
  ImGui::Dummy(ImVec2(col0 + col1 + col2 + col3, 0));
  ImGui::Columns(4);
  ImGui::SetColumnWidth(0, col0);
  ImGui::SetColumnWidth(1, col1);
  ImGui::SetColumnWidth(2, col2);
  ImGui::SetColumnWidth(3, col3);

  ImGui::Text("Help");
  ImGui::Text("Stats");
  ImGui::Text("Profiler");
  ImGui::Text("Cycle Frames");
  ImGui::Text("Cycle Labels");
  ImGui::Text("Toggle Fullscreen");
  ImGui::Text("Free Camera");
  ImGui::Text("Toggle Pause");
  ImGui::Text("Toggle Visc Pause");
  ImGui::Text("Reset Sim");
  ImGui::Text("Toggle Left UI");
  ImGui::Text("Toggle Right UI");
  ImGui::Text("Speed Up");
  ImGui::Text("Speed Down");
  ImGui::Text("Prev Camera");
  ImGui::Text("Next Camera");
  ImGui::Text("Step Back");
  ImGui::Text("Step Forward");
  ImGui::Text("Select Parent");
  ImGui::Text("Align Camera");
  ImGui::Text("Copy Keyframe");
  ImGui::Text("Geom Group");
  ImGui::Text("Site Group");

  ImGui::NextColumn();
  ImGui::Indent(indent);
  ImGui::Text("F1");
  ImGui::Text("F2");
  ImGui::Text("F3");
  ImGui::Text("F6");
  ImGui::Text("F7");
  ImGui::Text("F11");
  ImGui::Text("Esc");
  ImGui::Text("Spc");
  ImGui::Text("Ctrl+Spc");
  ImGui::Text("Bksp");
  ImGui::Text("Tab");
  ImGui::Text("Sh+Tab");
  ImGui::Text("+");
  ImGui::Text("-");
  ImGui::Text("[");
  ImGui::Text("]");
  ImGui::Text("Left");
  ImGui::Text("Right");
  ImGui::Text("PgUp");
  ImGui::Text("Ctrl+A");
  ImGui::Text("Ctrl+C");
  ImGui::Text("0-5");
  ImGui::Text("Sh+0-5");

  ImGui::NextColumn();
  ImGui::Indent(indent);
  ImGui::Text("Activation");
  ImGui::Text("Auto Connect");
  ImGui::Text("Body Tree");
  ImGui::Text("Mesh Tree");
  ImGui::Text("Scale Inertia");
  ImGui::Text("Skin");
  ImGui::Text("Actuator");
  ImGui::Text("Camera");
  ImGui::Text("Center of Mass");
  ImGui::Text("Contact Force");
  ImGui::Text("Contact Point");
  ImGui::Text("Contact Split");
  ImGui::Text("Convex Hull");
  ImGui::Text("Constraint");
  ImGui::Text("Island");
  ImGui::Text("Joint");
  ImGui::Text("Light");
  ImGui::Text("Perturb Force");
  ImGui::Text("Perturb Object");
  ImGui::Text("Range Finder");
  ImGui::Text("Static Body");
  ImGui::Text("Tendon");
  ImGui::Text("Texture");
  ImGui::Text("Transparent");

  ImGui::NextColumn();
  ImGui::Indent(indent);
  ImGui::Text(",");
  ImGui::Text("K");
  ImGui::Text("`");
  ImGui::Text("\\");
  ImGui::Text("\"");
  ImGui::Text(";");
  ImGui::Text("U");
  ImGui::Text("L");
  ImGui::Text("M");
  ImGui::Text("F");
  ImGui::Text("C");
  ImGui::Text("P");
  ImGui::Text("H");
  ImGui::Text("N");
  ImGui::Text("I");
  ImGui::Text("J");
  ImGui::Text("Z");
  ImGui::Text("B");
  ImGui::Text("O");
  ImGui::Text("Y");
  ImGui::Text("G");
  ImGui::Text("V");
  ImGui::Text("X");
  ImGui::Text("T");

  ImGui::Columns();
}

void App::ToolBarGui() {
  ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(0, 0));
  if (ImGui::BeginTable("##ToolBarTable", 2)) {
    platform::ScopedStyle style;
    style.Var(ImGuiStyleVar_ItemSpacing,
              ImVec2(ImGui::GetStyle().ItemSpacing.x * 2.0f,
                     ImGui::GetStyle().ItemSpacing.y));

    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;
    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);

    const float btn_size = ImGui::GetFrameHeight();
    const ImVec2 square_size(btn_size, btn_size);

    // Reload button.
    {
      style.Var(ImGuiStyleVar_FrameRounding, 2.0f);
      if (ImGui::Button(ICON_RELOAD_MODEL, square_size)) {
        RequestModelReload();
      }
      ImGui::SetItemTooltip("%s", "Reload");
    }

    // Reset button.
    ImGui::SameLine(0, 0.5 * separator_width);
    if (ImGui::Button(ICON_RESET_MODEL, square_size)) {
      ResetPhysics();
    }
    ImGui::SetItemTooltip("%s", "Reset");

    // Combined (Normal Pause, Viscous Pause, Play) widget and Speed selection.
    ImGui::SameLine(0, separator_width);
    platform::StepControlGui(model(), &step_control_, tmp_.speed_index);

    ImGui::SameLine(0, separator_width);
    ImGui::SetNextItemWidth(120);
    ImGui::BeginDisabled(std::thread::hardware_concurrency() <= 1);
    if (ImGui::SliderInt("##NumThreads", &ui_.nthread, 0, 8, "%d threads")) {
      tmp_.update_threadpool = true;
    }
    ImGui::EndDisabled();
    ImGui::SetItemTooltip("%s", "Number of threads in threadpool");

    ImGui::TableNextColumn();

    platform::CameraSelectionGui(model(), data(), camera_, ui_.camera_idx);

    ImGui::SameLine();
    platform::LabelSelectionGui(&vis_options_);

    ImGui::SameLine();
    platform::FrameSelectionGui(&vis_options_);

    ImGui::SameLine();
    if (platform::ThemeSelectGui(&ui_.theme, square_size)) {
      platform::SetupTheme(ui_.theme);
      ImGui::GetIO().WantSaveIniSettings = true;
    }

    ImGui::EndTable();
  }
  ImGui::PopStyleVar();
}

void App::StatusBarGui() {
  if (ImGui::BeginTable("##StatusBarTable", 2)) {
    ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch);
    ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 520);

    ImGui::TableNextColumn();

    if (!has_model()) {
      ImGui::Text("No model loaded");
    } else if (step_control_.GetPauseState() == PauseState::kViscousPaused) {
      ImGui::Text("Viscous Pause");
      ImGui::SetItemTooltip("Zero gravity, high viscosity, no spring forces");
    } else if (step_control_.GetPauseState() == PauseState::kNormalPaused) {
      ImGui::Text("Paused");
    } else {
      ImGui::Text("Running");
    }

    if (!step_error_.empty()) {
      ImGui::SameLine();
      ImGui::Text(" | Step Error: %s", step_error_.c_str());
    } else if (!load_error_.empty()) {
      ImGui::SameLine();
      ImGui::Text(" | Load Error: %s", load_error_.c_str());
    } else if (!edit_error_.empty()) {
      ImGui::SameLine();
      ImGui::Text(" | Edit Error: %s", edit_error_.c_str());
    }

    ImGui::TableNextColumn();
    ImGui::Text("%s", " |");

    // Frame scrubber.
    platform::ScopedStyle style;

    style.Var(ImGuiStyleVar_FrameBorderSize, 0);
    style.Color(ImGuiCol_Button, ImGui::GetStyle().Colors[ImGuiCol_WindowBg]);
    ImGui::SameLine();
    if (ImGui::Button(ICON_PREV_FRAME)) {
      LoadHistory(sim_history_.GetIndex() - 1);
    }
    ImGui::SetItemTooltip("%s", "Previous Frame");

    style.Reset();
    ImGui::SameLine();
    ImGui::SetNextItemWidth(450);
    int index = sim_history_.GetIndex();
    if (ImGui::SliderInt("##ScrubIndex", &index, 1 - sim_history_.Size(), 0)) {
      LoadHistory(index);
    }

    style.Var(ImGuiStyleVar_FrameBorderSize, 0);
    style.Color(ImGuiCol_Button, ImGui::GetStyle().Colors[ImGuiCol_WindowBg]);
    ImGui::SameLine();
    if (ImGui::Button(ICON_NEXT_FRAME)) {
      if (sim_history_.GetIndex() == 0) {
        step_control_.RequestSingleStep();
      } else {
        LoadHistory(sim_history_.GetIndex() + 1);
      }
    }
    ImGui::SetItemTooltip("%s", "Next Frame");

    ImGui::SameLine();
    if (ImGui::Button(ICON_CURR_FRAME)) {
      LoadHistory(0);
    }
    ImGui::SetItemTooltip("%s", "Current Frame");

    ImGui::EndTable();
  }
}

void App::MainMenuGui() {
  if (ImGui::BeginMainMenuBar()) {
    if (ImGui::BeginMenu("File")) {
#ifndef __EMSCRIPTEN__
      if (ImGui::MenuItem("Open Model File", "Ctrl+O")) {
        tmp_.file_dialog = UiTempState::FileDialog_Load;
      }
      ImGui::Separator();
      if (ImGui::MenuItem("Save XML", "Ctrl+S")) {
        tmp_.file_dialog = UiTempState::FileDialog_SaveXml;
      }
      if (ImGui::MenuItem("Save MJB", "Ctrl+Shift+S")) {
        tmp_.file_dialog = UiTempState::FileDialog_SaveMjb;
      }
      if (ImGui::MenuItem("Save Screenshot", "Ctrl+P")) {
        tmp_.file_dialog = UiTempState::FileDialog_SaveScreenshot;
      }
      ImGui::Separator();
      if (ImGui::MenuItem("Print Model", "Ctrl+M")) {
        tmp_.file_dialog = UiTempState::FileDialog_PrintModel;
      }
      if (ImGui::MenuItem("Print Data", "Ctrl+D")) {
        tmp_.file_dialog = UiTempState::FileDialog_PrintData;
      }
      ImGui::Separator();
#endif  // !__EMSCRIPTEN__
      if (ImGui::MenuItem("Unload", "Ctrl+U")) {
        InitEmptyModel();
      }
#ifndef __EMSCRIPTEN__
      ImGui::Separator();
      if (ImGui::MenuItem("Quit", "Ctrl+Q")) {
        tmp_.should_exit = true;
      }
#endif  // !__EMSCRIPTEN__
      ImGui::EndMenu();
    }

    if (ImGui::BeginMenu("Simulation")) {
      if (ImGui::MenuItem(
              "Pause", "Space",
              step_control_.GetPauseState() == PauseState::kNormalPaused)) {
        if (step_control_.GetPauseState() != PauseState::kNormalPaused) {
          step_control_.SetPauseState(PauseState::kNormalPaused);
        } else {
          step_control_.SetPauseState(PauseState::kUnpaused);
        }
      }
      if (ImGui::MenuItem("Reset", "Backspace")) {
        ResetPhysics();
      }
      if (ImGui::MenuItem("Reload", "Ctrl+L")) {
        RequestModelReload();
      }
      ImGui::Separator();
      if (ImGui::BeginMenu("Keyframes")) {
        ImGui::SetNextItemWidth(200);
        ImGui::SliderInt("##Key", &ui_.key_idx, 0, model()->nkey);
        if (ImGui::MenuItem("Load")) {
          mj_resetDataKeyframe(model(), data(), ui_.key_idx);
          mj_forward(model(), data());
        }
        if (ImGui::MenuItem("Save")) {
          mj_setKeyframe(model(), data(), ui_.key_idx);
        }
        if (ImGui::MenuItem("Copy")) {
          std::string str = platform::KeyframeToString(model(), data(), false);
          platform::MaybeSaveToClipboard(str);
        }
        ImGui::EndMenu();
      }
      ImGui::EndMenu();
    }

    if (ImGui::BeginMenu("View")) {
      if (ImGui::MenuItem("Save Config")) {
        SaveSettings();
      }
      if (ImGui::MenuItem("Reset Config")) {
        platform::SaveText("\n\n", ini_path_);
        LoadSettings();
      }
      ImGui::Separator();

      if (ImGui::MenuItem(tmp_.options_panel ? "Hide Options" : "Show Options",
                          "Tab")) {
        tmp_.options_panel = !tmp_.options_panel;
      }
      if (ImGui::MenuItem(
              tmp_.inspector_panel ? "Hide Inspector" : "Show Inspector",
              "Shift+Tab")) {
        tmp_.inspector_panel = !tmp_.inspector_panel;
      }
      if (ImGui::MenuItem("Full Screen", "F11")) {
        tmp_.full_screen = !tmp_.full_screen;
      }
      ImGui::Separator();

      if (ImGui::MenuItem("Picture-in-Picture")) {
        tmp_.picture_in_picture = !tmp_.picture_in_picture;
      }
      ImGui::Separator();

#ifdef __linux__
      if (ImGui::BeginMenu("Graphics Mode (Experimental)")) {
        std::optional<platform::GraphicsMode> mode;
        if (ImGui::MenuItem(
                "Classic OpenGL", nullptr,
                gfx_mode_ == platform::GraphicsMode::ClassicOpenGl)) {
          mode = platform::GraphicsMode::ClassicOpenGl;
        }
        if (ImGui::MenuItem(
                "Classic OpenGL Headless", nullptr,
                gfx_mode_ == platform::GraphicsMode::ClassicOpenGlHeadless)) {
          mode = platform::GraphicsMode::ClassicOpenGlHeadless;
        }
        if (ImGui::MenuItem(
                "Filament OpenGL", nullptr,
                gfx_mode_ == platform::GraphicsMode::FilamentOpenGl)) {
          mode = platform::GraphicsMode::FilamentOpenGl;
        }
        if (ImGui::MenuItem(
                "Filament OpenGL Headless", nullptr,
                gfx_mode_ == platform::GraphicsMode::FilamentOpenGlHeadless)) {
          mode = platform::GraphicsMode::FilamentOpenGlHeadless;
        }
        if (ImGui::MenuItem(
                "Filament OpenGL Software", nullptr,
                gfx_mode_ == platform::GraphicsMode::FilamentOpenGlSoftware)) {
          mode = platform::GraphicsMode::FilamentOpenGlSoftware;
        }
        if (ImGui::MenuItem(
                "Filament Vulkan", nullptr,
                gfx_mode_ == platform::GraphicsMode::FilamentVulkan)) {
          mode = platform::GraphicsMode::FilamentVulkan;
        }
        if (ImGui::MenuItem(
                "Filament Vulkan Software", nullptr,
                gfx_mode_ == platform::GraphicsMode::FilamentVulkanSoftware)) {
          mode = platform::GraphicsMode::FilamentVulkanSoftware;
        }
        if (mode.has_value()) {
          pending_op_ = [=, this]() {
            const int width = window_->GetWidth();
            const int height = window_->GetHeight();
            SwitchGraphicsMode(width, height, *mode);
            // TODO: figure out why ImGui doesn't work unless we do this twice.
            if (IsClassic(*mode)) {
              SwitchGraphicsMode(width, height, *mode);
            }
            renderer_->Init(model());
          };
        }
        ImGui::EndMenu();
      }
#endif  // __linux__

      ImGui::EndMenu();
    }

    if (ImGui::BeginMenu("Charts")) {
      if (ImGui::MenuItem("Profiler", "F3")) {
        ToggleWindow(tmp_.profiler);
      }
      ImGui::EndMenu();
    }
    if (ImGui::BeginMenu("Plugins")) {
      // Placeholder menu item that will be populated by plugins later on. We
      // do this now in so that the menu is present at the right place.
      ImGui::EndMenu();
    }
    if (ImGui::BeginMenu("Help")) {
      if (ImGui::MenuItem("Help", "F1", tmp_.help)) {
        ToggleWindow(tmp_.help);
      }
      if (ImGui::MenuItem("Stats", "F2", tmp_.stats)) {
        ToggleWindow(tmp_.stats);
      }
      ImGui::Separator();
      if (ImGui::MenuItem("Style Editor", "", tmp_.style_editor)) {
        tmp_.style_editor = !tmp_.style_editor;
      }
      ImGui::Separator();
      if (ImGui::MenuItem("ImGui Demo")) {
        tmp_.imgui_demo = !tmp_.imgui_demo;
      }
      if (ImGui::MenuItem("ImPlot Demo")) {
        tmp_.implot_demo = !tmp_.implot_demo;
      }
      ImGui::Separator();
      std::string version = "Version " + std::string(mj_versionString());
      ImGui::MenuItem(version.c_str());
      ImGui::EndMenu();
    }
    ImGui::EndMainMenuBar();
  }
}

void App::FileDialogGui() {
  if (tmp_.file_dialog == UiTempState::FileDialog_None) {
    return;
  }

  ImVec2 center = ImGui::GetMainViewport()->GetCenter();
  ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
  if (ImGui::BeginPopupModal("FileDialog", NULL,
                             ImGuiWindowFlags_AlwaysAutoResize)) {
    const platform::DialogResult res =
        tmp_.file_dialog == UiTempState::FileDialog_Load
            ? platform::OpenFileDialog(tmp_.filename)
            : platform::SaveFileDialog(tmp_.filename);

    if (res.status == platform::DialogResult::kAccepted) {
      tmp_.last_path[tmp_.file_dialog] = res.path;
      switch (tmp_.file_dialog) {
        case UiTempState::FileDialog_Load:
          RequestModelLoad(res.path);
          break;
        case UiTempState::FileDialog_SaveXml:
          mj_saveLastXML(res.path.c_str(), model(), nullptr, 0);
          break;
        case UiTempState::FileDialog_SaveMjb:
          mj_saveModel(model(), res.path.c_str(), nullptr, 0);
          break;
        case UiTempState::FileDialog_SaveScreenshot: {
          const int width = window_->GetWidth();
          const int height = window_->GetHeight();
          std::vector<std::byte> buffer(width * height * 3);
          renderer_->RenderToTexture(model(), data(), &camera_, width, height,
                                     buffer.data());
          platform::SaveToWebp(width, height, buffer.data(), res.path);
          break;
        }
        case UiTempState::FileDialog_PrintModel:
          mj_printModel(model(), res.path.c_str());
          break;
        case UiTempState::FileDialog_PrintData:
          mj_printData(model(), data(), res.path.c_str());
          break;
        default:
          break;
      }
    }

    if (res.status != platform::DialogResult::kError) {
      strncpy(tmp_.filename, res.path.c_str(), res.path.size());
      tmp_.filename[res.path.size()] = 0;
    }
    if (res.status != platform::DialogResult::kPending) {
      tmp_.file_dialog = UiTempState::FileDialog_None;
      ImGui::CloseCurrentPopup();
      window_->EnableWindowResizing();
    }
    ImGui::EndPopup();
  }

  if (!ImGui::IsPopupOpen("FileDialog") &&
      tmp_.file_dialog != UiTempState::FileDialog_None) {
    const std::string& src = tmp_.last_path[tmp_.file_dialog];
    strncpy(tmp_.filename, src.c_str(), src.size());
    tmp_.filename[src.size()] = 0;
    ImGui::OpenPopup("FileDialog");
    window_->DisableWindowResizing();
  }
}

App::UiState::Dict App::UiState::ToDict() const {
  return {
      {"theme", std::to_string(static_cast<int>(theme))},
      {"font_scale", std::to_string(font_scale)},
      {"window_width", std::to_string(window_width)},
      {"window_height", std::to_string(window_height)},
      {"nthread", std::to_string(nthread)},
  };
}

void App::UiState::FromDict(const Dict& dict) {
  using platform::ReadIniValue;

  *this = UiState();
  theme = ReadIniValue(dict, "theme", theme);
  window_width = ReadIniValue(dict, "window_width", window_width);
  window_height = ReadIniValue(dict, "window_height", window_height);
  font_scale = ReadIniValue(dict, "font_scale", font_scale);
  nthread = ReadIniValue(dict, "nthread", nthread);
}
}  // namespace mujoco::studio
