Skip to content

Commit

Permalink
modified title bar to include radial menu and collapsing
Browse files Browse the repository at this point in the history
aim of this is to provide a cleaner, less cluttered, and maximised real-estate for charts, flow-graphs, etc.
This notably is useful on mobile and reduces the cognitive complexity.

Signed-off-by: Ralph J. Steinhagen <[email protected]>
  • Loading branch information
RalphSteinhagen committed Jul 29, 2023
1 parent 285da07 commit 5aad4bb
Show file tree
Hide file tree
Showing 5 changed files with 377 additions and 89 deletions.
5 changes: 5 additions & 0 deletions src/ui/app.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class App {
OpenDashboardPage openDashboardPage;
SDLState *sdlState;
bool running = true;
std::string mainViewMode = "";

bool prototypeMode = true;
std::chrono::milliseconds execTime; /// time it took to handle events and draw one frame
Expand All @@ -50,7 +51,11 @@ class App {
std::array<ImFont *, 2> fontBigger = { nullptr, nullptr }; /// 0: production 1: prototype use
std::array<ImFont *, 2> fontLarge = { nullptr, nullptr }; /// 0: production 1: prototype use
ImFont *fontIcons;
ImFont *fontIconsBig;
ImFont *fontIconsLarge;
ImFont *fontIconsSolid;
ImFont *fontIconsSolidBig;
ImFont *fontIconsSolidLarge;
std::chrono::seconds editPaneCloseDelay{ 15 };

private:
Expand Down
1 change: 1 addition & 0 deletions src/ui/app_header/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
add_library(
app_header INTERFACE
"fair_header.h"
RadialCircularMenu.hpp
)
target_include_directories(app_header INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(app_header INTERFACE stb fmt)
258 changes: 258 additions & 0 deletions src/ui/app_header/RadialCircularMenu.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
#ifndef OPENDIGITIZER_RADIALCIRCULARMENU_HPP
#define OPENDIGITIZER_RADIALCIRCULARMENU_HPP

#include <cmath>
#include <functional>
#include <imgui_internal.h>
#include <string>
#include <vector>

namespace fair {

namespace detail {
ImVec4 lightenColor(const ImVec4 &color, float percent) {
float h, s, v;
ImGui::ColorConvertRGBtoHSV(color.x, color.y, color.z, h, s, v);
s = std::max(0.0f, s * percent);
float r, g, b;
ImGui::ColorConvertHSVtoRGB(h, s, v, r, g, b);
return ImVec4(r, g, b, color.w);
}

ImVec4 darkenColor(const ImVec4 &color, float percent) {
float h, s, v;
ImGui::ColorConvertRGBtoHSV(color.x, color.y, color.z, h, s, v);
v = std::max(0.0f, v * percent);
float r, g, b;
ImGui::ColorConvertHSVtoRGB(h, s, v, r, g, b);
return ImVec4(r, g, b, color.w);
}
} // namespace detail

struct RadialButton {
using CallbackFun = std::variant<std::function<void()>, std::function<void(RadialButton &)>>;
std::string label;
float size;
CallbackFun onClick;
ImFont *font = nullptr;
std::string toolTip;
bool isTransparent = false;
float padding = std::max(ImGui::GetStyle().FramePadding.x, ImGui::GetStyle().FramePadding.y);
ImVec4 buttonColor = ImGui::GetStyleColorVec4(ImGuiCol_Button);

[[nodiscard]] bool create() {
const std::string buttonId = fmt::format("#{}", label);
ImGui::PushFont(font);
bool isClicked = false;
const ImVec2 textSize = ImGui::CalcTextSize(label.c_str());
const float maxSize = std::max(textSize.x, textSize.y);
const float actualButtonSize = std::max(size, 2.f * padding + maxSize);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, .5f * actualButtonSize);

if (!isTransparent) {
ImVec4 buttonColorHover = detail::lightenColor(buttonColor, 0.7f);
ImVec4 buttonColorActive = detail::darkenColor(buttonColor, 0.7f);
buttonColor.w = 1.0;
buttonColorHover.w = 1.0;
buttonColorActive.w = 1.0;
ImGui::PushStyleColor(ImGuiCol_Button, buttonColor);
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, buttonColorHover);
ImGui::PushStyleColor(ImGuiCol_ButtonActive, buttonColorActive);
}
if (ImGui::Button(label.c_str(), ImVec2{ actualButtonSize, actualButtonSize })) {
isClicked = true;
}
if (!isTransparent) {
ImGui::PopStyleColor(3);
}
ImGui::PopStyleVar();
ImGui::PopFont();

if (ImGui::IsItemHovered() && !toolTip.empty()) {
ImGui::SetTooltip("%s", toolTip.c_str());
}

return isClicked;
}
};

template<std::size_t unique_id>
class RadialCircularMenu {
static std::vector<RadialButton> _buttons;
static const std::string _popupId;
static float _animationProgress;
static bool _isOpen;
const float _padding = ImGui::GetStyle().WindowPadding.x;
ImVec2 _menuSize;
float _startAngle = 0.f;
float _stopAngle = 90.f;
float _extraRadius = 0.f;
float _animationSpeed = 0.25f;
float _timeOut = 0.5f; // Time in seconds to close the menu when the mouse is out of range

//
[[nodiscard]] float maxButtonSize(std::size_t firstButtonIndex = 0) const {
if (_buttons.empty()) {
return std::max(_menuSize.x, _menuSize.y);
}
float max = 0.0;
for (std::size_t index = firstButtonIndex; index < _buttons.size(); ++index) {
max = std::max(max, _buttons[index].size);
}
return max;
}

[[nodiscard]] std::pair<std::size_t, float> maxButtonNumberAndSizeForArc(float baseArcRadius, std::size_t firstButtonIndex = 0) const {
const float totalAngle = _stopAngle - _startAngle;
const float arcLength = std::numbers::pi_v<float> * baseArcRadius * (totalAngle / 180.f);
std::size_t buttonCount = 0UL;
float maxButtonSize = .0f;
float cumulativeLength = .0f;
for (auto i = firstButtonIndex; i < _buttons.size(); ++i) {
const auto &button = _buttons[i];
if ((cumulativeLength + button.size + _padding) > arcLength) {
break;
}
++buttonCount;
maxButtonSize = std::max(button.size, maxButtonSize);
cumulativeLength += button.size + _padding;
}
return { buttonCount, maxButtonSize };
}

public:
explicit RadialCircularMenu(ImVec2 menuSize = { 100, 100 }, float startAngle = 0.f, float stopAngle = 360.f, float extraRadius = 0.f, float animationSpeed = .25f, float timeOut = 0.5f)
: _menuSize(menuSize), _startAngle(startAngle), _stopAngle(stopAngle), _extraRadius(extraRadius), _animationSpeed(animationSpeed), _timeOut(timeOut) {
updateAndDraw();
}

void addButton(std::string label, auto onClick, float buttonSize = -1.f, std::string toolTip = "") {
if (_animationProgress > 0.f) { // we do not allow to add buttons when the popup is already open or animating
return;
}
_buttons.emplace_back(label, buttonSize, std::move(onClick), nullptr, std::move(toolTip));
_isOpen = true;
}

void addButton(std::string label, auto onClick, ImFont *font, std::string toolTip = "") {
if (_animationProgress > 0.f) { // we do not allow to add buttons when the popup is already open or animating
return;
}
float buttonSize = 0.f;
if (font == nullptr) {
buttonSize = ImGui::CalcTextSize(label.c_str()).y;
} else {
buttonSize = font->FontSize;
}
_buttons.emplace_back(label, buttonSize, std::move(onClick), font, std::move(toolTip));
_isOpen = true;
}

[[nodiscard]] bool isOpen() const noexcept {
return _isOpen;
}

void forceClose() {
_isOpen = false;
_animationProgress = 0.0;
_buttons.clear();
ImGui::CloseCurrentPopup();
}

void updateAndDraw() {
static float timeOutOfRadius = 0.0f;
static ImVec2 centre{ -1.f, -1.f };

const float deltaTime = ImGui::GetIO().DeltaTime;
_animationProgress = _isOpen ? std::min(1.0f, _animationProgress + deltaTime / _animationSpeed) : std::max(0.0f, _animationProgress - deltaTime / _animationSpeed);

if (!_isOpen && _animationProgress <= 0.f) {
centre = { -1.f, -1.f };
if (!_buttons.empty()) {
_buttons.clear();
}
return;
} else if (_isOpen && (centre.x < 0.f || centre.y < 0.f)) {
centre = ImGui::GetMousePos();
}

std::size_t nButtonRows = 1UL;
const float buttonSize = maxButtonSize();
if (_animationProgress >= 0.0f) {
const ImVec2 oldPos = ImGui::GetCursorPos();
float requiredPopupSize = 2.f * (_extraRadius + buttonSize * _buttons.size());
ImGui::SetNextWindowSize(ImVec2(requiredPopupSize, requiredPopupSize));
ImGui::SetNextWindowPos(ImVec2(centre.x - .5f * requiredPopupSize / 2, centre.y - .5f * requiredPopupSize));

ImGui::OpenPopup(_popupId.c_str());
if (ImGui::BeginPopup(_popupId.c_str(), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration)) {
nButtonRows = drawButtonsOnArc(centre, _extraRadius + buttonSize + _padding);
ImGui::EndPopup();
}
ImGui::SetCursorScreenPos(oldPos);
}

const ImVec2 mousePos = ImGui::GetMousePos();
const float mouseDistance = std::hypot(mousePos.x - centre.x, mousePos.y - centre.y);
const float mouseAngle = std::atan2(mousePos.y - centre.y, mousePos.x - centre.x) * 180.0f / std::numbers::pi_v<float>;
const float arcRadius = _extraRadius + static_cast<float>(nButtonRows + 1) * (buttonSize + _padding);
// the last statement is to keep the menu open when the mouse is outside the arc segment but on the calling button.
const bool mouseInArc = (mouseDistance <= arcRadius && mouseAngle >= _startAngle && mouseAngle <= _stopAngle) || mouseDistance <= std::max(_menuSize.x, _menuSize.y);
timeOutOfRadius = !mouseInArc ? timeOutOfRadius + deltaTime : 0.f;
if (timeOutOfRadius >= _timeOut) {
_isOpen = false;
}
}

private:
void drawButton(RadialButton &button) {
if (button.create()) {
std::visit([&](auto &&onClick) {
using T = std::decay_t<decltype(onClick)>;
if constexpr (std::is_same_v<T, std::function<void()>>) {
onClick();
} else if constexpr (std::is_same_v<T, std::function<void(RadialButton &)>>) {
onClick(button);
}
},
button.onClick);
}
}

[[nodiscard]] std::size_t drawButtonsOnArc(const ImVec2 &centre, float arcRadius) {
const float totalAngle = _stopAngle - _startAngle;

std::size_t currentRow = 0UL;
for (std::size_t buttonIndex = 0UL; buttonIndex < _buttons.size();) {
// get the maximum number of buttons and button size for the current arc row
const auto [maxButtonsInRow, maxButtonSizeInRow] = maxButtonNumberAndSizeForArc(arcRadius, buttonIndex);
const float angleBetweenButtons = totalAngle / maxButtonsInRow;
const float shiftFactor = 0.5f * totalAngle / maxButtonsInRow; // Shift every second row

for (std::size_t buttonsInRow = 0UL; buttonIndex < _buttons.size() && buttonsInRow < maxButtonsInRow; ++buttonIndex) {
auto &button = _buttons[buttonIndex];
const float angle = _startAngle + shiftFactor + static_cast<float>(buttonsInRow) * angleBetweenButtons;
const float angleRad = angle * _animationProgress * (std::numbers::pi_v<float> / 180.0f);

ImGui::SetCursorScreenPos(ImVec2(centre.x + arcRadius * std::cos(angleRad) - 0.5f * button.size, centre.y + arcRadius * std::sin(angleRad) - 0.5f * button.size));
drawButton(button);
++buttonsInRow;
}
++currentRow;
arcRadius += maxButtonSizeInRow + 2.f * _padding; // update arc radius
}
return currentRow;
}
};
template<std::size_t unique_id>
bool RadialCircularMenu<unique_id>::_isOpen = false;
template<std::size_t unique_id>
const std::string RadialCircularMenu<unique_id>::_popupId = fmt::format("RadialMenuPopup_{}", unique_id);
template<std::size_t unique_id>
float RadialCircularMenu<unique_id>::_animationProgress = 0.f;
template<std::size_t unique_id>
std::vector<RadialButton> RadialCircularMenu<unique_id>::_buttons;

} // namespace fair

#endif // OPENDIGITIZER_RADIALCIRCULARMENU_HPP
Loading

0 comments on commit 5aad4bb

Please sign in to comment.