From 8d9f00ca5f9ceff81210076043b959d5fdd640d9 Mon Sep 17 00:00:00 2001 From: Fexty12573 Date: Sat, 7 Mar 2026 15:49:03 +0100 Subject: [PATCH 1/7] Resource picker visual improvements --- src/editor/editor.cpp | 79 +++++++++++++++++++++++++++++++++++++----- src/imgui/extensions.h | 2 ++ 2 files changed, 72 insertions(+), 9 deletions(-) diff --git a/src/editor/editor.cpp b/src/editor/editor.cpp index d619763..8a1393a 100644 --- a/src/editor/editor.cpp +++ b/src/editor/editor.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -592,6 +593,11 @@ void Editor::selectResource(u64 editorID, size_t resourceIndex) { void Editor::renderResourcePicker() { if (ImGui::Begin("Resource Picker##Editor", &m_pickerOpen)) { + const auto& style = ImGui::GetStyle(); + constexpr float thumbnailSize = 48.0f; + constexpr ImVec2 thumbnailSizeVec = { thumbnailSize, thumbnailSize }; + const float itemHeight = thumbnailSize + style.FramePadding.y * 2; + const auto& editor = g_projectManager->getActiveEditor(); if (!editor) { ImGui::Text("No editor open"); @@ -618,14 +624,16 @@ void Editor::renderResourcePicker() { const auto contentRegion = ImGui::GetContentRegionAvail(); if (ImGui::BeginListBox("##Resources", contentRegion)) { - const ImGuiStyle& style = ImGui::GetStyle(); const float itemWidth = contentRegion.x - (style.ItemSpacing.x + style.WindowPadding.x) * 1.3f; + const float dt = ImGui::GetIO().DeltaTime; for (size_t i = 0; i < resources.size(); ++i) { const auto& resource = resources[i]; const auto& texture = textures[resource.header.misc.textureIndex]; ImGui::PushID(i); + const auto itemId = ImGui::GetID("##Resource"); + const auto name = fmt::format("[{}] Tex {}x{}", i, texture.width, texture.height); auto bgColor = m_selectedResources[id] == i @@ -633,13 +641,15 @@ void Editor::renderResourcePicker() { : style.Colors[ImGuiCol_Button]; const auto cursor = ImGui::GetCursorScreenPos(); - if (ImGui::InvisibleButton("##Resource", { itemWidth, 32 })) { + if (ImGui::InvisibleButton("##Resource", { itemWidth, itemHeight })) { m_selectedResources[id] = i; editor->notifyResourceChanged(i); } - if (ImGui::IsItemHovered()) { - bgColor = style.Colors[ImGuiCol_ButtonHovered]; + const bool hovered = ImGui::IsItemHovered(); + const bool pressed = ImGui::IsItemActive(); + + if (hovered) { anyHovered = true; if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) { @@ -647,6 +657,22 @@ void Editor::renderResourcePicker() { } } + const auto targetColor = pressed + ? style.Colors[ImGuiCol_SliderGrabActive] + : hovered ? style.Colors[ImGuiCol_ButtonHovered] : bgColor; + + bgColor = iam_tween_color( + itemId, + 1, + targetColor, + 0.2f, + iam_ease_preset(iam_ease_linear), + iam_policy_crossfade, + iam_col_srgb, + dt, + bgColor + ); + const auto bgColor2 = bgColor * ImVec4(0.8f, 0.8f, 0.8f, 1.0f); // Draw a filled rectangle behind the item @@ -654,7 +680,7 @@ void Editor::renderResourcePicker() { const int firstVtx = drawList->VtxBuffer.Size; drawList->AddRectFilled( cursor, - { cursor.x + itemWidth, cursor.y + 32 }, + { cursor.x + itemWidth, cursor.y + itemHeight }, ImGui::ColorConvertFloat4ToU32(bgColor), style.FrameRounding ); @@ -662,19 +688,51 @@ void Editor::renderResourcePicker() { ImGui::ShadeVertsLinearColorGradientKeepAlpha(drawList, firstVtx, lastVtx, - cursor, { cursor.x, cursor.y + 32 }, + cursor, { cursor.x, cursor.y + itemHeight }, ImGui::ColorConvertFloat4ToU32(bgColor), ImGui::ColorConvertFloat4ToU32(bgColor2) ); - ImGui::SetCursorScreenPos(cursor); - ImGui::Image((ImTextureID)(uintptr_t)texture.glTexture->getHandle(), { 32, 32 }); + const ImVec2 offset = { + iam_tween_float( + itemId, + 0, + hovered ? 8.0f : 0.0f, + 0.4f, + iam_ease_preset(iam_ease_in_out_quad), + iam_policy_crossfade, + dt + ), + 0.0f + }; + + const auto imagePos = cursor + style.FramePadding + offset; + const auto frameMinPos = imagePos - ImVec2(2, 2); + const auto frameMaxPos = imagePos + ImVec2(thumbnailSize + 2, thumbnailSize + 2); + drawList->AddRectFilled( + frameMinPos, + frameMaxPos, + IM_COL32(60, 60, 60, 255), + 2 + ); + + drawList->AddRect( + frameMinPos, + frameMaxPos, + IM_COL32(230, 230, 230, 30), + 2 + ); + + ImGui::SetCursorScreenPos(imagePos); + ImGui::Image(texture.glTexture->getHandle(), thumbnailSizeVec); ImGui::SameLine(); + ImGui::PushFont(nullptr, Application::getFontSizeLarge()); const auto textHeight = ImGui::GetFontSize(); - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + (32 - textHeight) / 2.0f); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + (thumbnailSize - textHeight) / 2.0f); ImGui::TextUnformatted(name.c_str()); + ImGui::PopFont(); if (ImGui::BeginPopup("##ResourcePopup")) { if (ImGui::MenuItemIcon(ICON_FA_COPY, "Copy", "Ctrl+C", false, AppColors::LightGreen3)) { @@ -720,6 +778,9 @@ void Editor::renderResourcePicker() { ImGui::EndPopup(); } + ImGui::SetCursorScreenPos(cursor); + ImGui::Dummy({ itemWidth, itemHeight }); + ImGui::PopID(); } diff --git a/src/imgui/extensions.h b/src/imgui/extensions.h index d97b941..d63320d 100644 --- a/src/imgui/extensions.h +++ b/src/imgui/extensions.h @@ -5,6 +5,8 @@ namespace ImGui { +inline void PushID(size_t id) { PushID(reinterpret_cast(id)); } + bool GradientButton(const char* label, const ImVec2& size, ImU32 textColor, ImU32 bgColor, ImU32 bgColor2); bool RedButton(const char* label, const ImVec2& size = { 0.0f, 0.0f }); bool GreenButton(const char* label, const ImVec2& size = { 0.0f, 0.0f }); From 7c94b6eefa6e7a22ec68840a758cc47237f20d8a Mon Sep 17 00:00:00 2001 From: Fexty12573 Date: Sat, 7 Mar 2026 16:29:39 +0100 Subject: [PATCH 2/7] Improve alpha anim plot rendering --- src/editor/editor.cpp | 25 ++++++++++++++++++++----- src/editor/editor.h | 3 ++- src/spl/spl_anim.cpp | 32 +++++++++++++++++++++++++++++--- src/spl/spl_random.h | 26 ++++++++++++++++++-------- src/spl/spl_resource.h | 2 ++ 5 files changed, 71 insertions(+), 17 deletions(-) diff --git a/src/editor/editor.cpp b/src/editor/editor.cpp index 8a1393a..f2caf30 100644 --- a/src/editor/editor.cpp +++ b/src/editor/editor.cpp @@ -43,7 +43,7 @@ constexpr std::array s_emitterSpawnTypes = { } -Editor::Editor() : m_xAnimBuffer(), m_yAnimBuffer() { +Editor::Editor() : m_xAnimBuffer(), m_yAnimBuffer1() { m_gridRenderer = std::make_shared(s_gridDimensions, s_gridSpacing); m_debugRenderer = std::make_unique(1000); m_collisionGridRenderer = std::make_shared(s_gridDimensions / 2, s_gridSpacing); @@ -2134,10 +2134,10 @@ bool Editor::renderScaleAnimEditor(SPLScaleAnim& res) { NOTIFY(ImGui::SliderScalar("Out", ImGuiDataType_U8, &res.curve.out, &min, &max, "%u")); NOTIFY(ImGui::Checkbox("Loop", &res.flags.loop)); - res.plot(m_xAnimBuffer, m_yAnimBuffer); + res.plot(m_xAnimBuffer, m_yAnimBuffer1); if (ImPlot::BeginPlot("##scaleAnimPlot", {-1, 0}, ImPlotFlags_CanvasOnly)) { ImPlot::SetupAxes(nullptr, nullptr, ImPlotAxisFlags_AutoFit, ImPlotAxisFlags_AutoFit); - ImPlot::PlotLine("Scale", m_xAnimBuffer.data(), m_yAnimBuffer.data(), m_xAnimBuffer.size()); + ImPlot::PlotLine("Scale", m_xAnimBuffer.data(), m_yAnimBuffer1.data(), m_xAnimBuffer.size()); ImPlot::EndPlot(); } @@ -2312,12 +2312,27 @@ bool Editor::renderAlphaAnimEditor(SPLAlphaAnim& res) { NOTIFY(ImGui::SliderFloat("Random Range", &res.flags.randomRange, 0, 1)); NOTIFY(ImGui::Checkbox("Loop", &res.flags.loop)); - res.plot(m_xAnimBuffer, m_yAnimBuffer); + res.plotWith(m_xAnimBuffer, m_yAnimBuffer1, 0.0f); if (ImPlot::BeginPlot("##alphaAnimPlot", {-1, 0}, ImPlotFlags_CanvasOnly | ImPlotFlags_NoInputs)) { ImPlot::SetupAxisLimits(ImAxis_X1, 0, 1); ImPlot::SetupAxisLimits(ImAxis_Y1, minAlpha, maxAlpha); - ImPlot::PlotLine("Alpha", m_xAnimBuffer.data(), m_yAnimBuffer.data(), m_xAnimBuffer.size()); + + if (res.flags.randomRange > 0) { + res.plotWith(m_xAnimBuffer, m_yAnimBuffer2, res.flags.randomRange, 255); + ImPlot::PlotLine("Alpha Base", m_xAnimBuffer.data(), m_yAnimBuffer1.data(), m_xAnimBuffer.size()); + ImPlot::PlotShaded( + "Alpha Variance", + m_xAnimBuffer.data(), + m_yAnimBuffer2.data(), + m_yAnimBuffer1.data(), + m_xAnimBuffer.size(), + ImPlotSpec{ ImPlotProp_FillAlpha, 0.25f } + ); + } else { + ImPlot::PlotLine("Alpha", m_xAnimBuffer.data(), m_yAnimBuffer1.data(), m_xAnimBuffer.size()); + } + ImPlot::EndPlot(); } diff --git a/src/editor/editor.h b/src/editor/editor.h index 66fedcb..2f5da6c 100644 --- a/src/editor/editor.h +++ b/src/editor/editor.h @@ -156,7 +156,8 @@ class Editor { static constexpr glm::vec2 s_gridSpacing = { 1.0f, 1.0f }; std::array m_xAnimBuffer; - std::array m_yAnimBuffer; + std::array m_yAnimBuffer1; + std::array m_yAnimBuffer2; TempTexture* m_tempTexture = nullptr; float m_tempTextureScale = 1.0f; diff --git a/src/spl/spl_anim.cpp b/src/spl/spl_anim.cpp index 3ada538..1bb703c 100644 --- a/src/spl/spl_anim.cpp +++ b/src/spl/spl_anim.cpp @@ -108,6 +108,10 @@ void SPLAlphaAnim::apply(SPLParticle& ptcl, const SPLResource& resource, f32 lif } void SPLAlphaAnim::plot(std::span xs, std::span ys) const { + plotWith(xs, ys, flags.randomRange); +} + +void SPLAlphaAnim::plotWith(std::span xs, std::span ys, float variance) const { const size_t samples = std::min(xs.size(), ys.size()); for (size_t i = 0; i < samples; i++) { const f32 lifeRate = (f32)i / (f32)samples; @@ -115,13 +119,35 @@ void SPLAlphaAnim::plot(std::span xs, std::span ys) const { if (lifeRate < curve.getIn()) { ys[i] = glm::mix(alpha.start, alpha.mid, lifeRate / curve.getIn()); - } else if (lifeRate < curve.getOut()) { + } + else if (lifeRate < curve.getOut()) { ys[i] = alpha.mid; - } else { + } + else { + ys[i] = glm::mix(alpha.mid, alpha.end, (lifeRate - curve.getOut()) / (1.0f - curve.getOut())); + } + + ys[i] = SPLRandom::scaledRange(ys[i], variance); + } +} + +void SPLAlphaAnim::plotWith(std::span xs, std::span ys, float variance, u8 factor) const { + const size_t samples = std::min(xs.size(), ys.size()); + for (size_t i = 0; i < samples; i++) { + const f32 lifeRate = (f32)i / (f32)samples; + xs[i] = lifeRate; + + if (lifeRate < curve.getIn()) { + ys[i] = glm::mix(alpha.start, alpha.mid, lifeRate / curve.getIn()); + } + else if (lifeRate < curve.getOut()) { + ys[i] = alpha.mid; + } + else { ys[i] = glm::mix(alpha.mid, alpha.end, (lifeRate - curve.getOut()) / (1.0f - curve.getOut())); } - ys[i] = SPLRandom::scaledRange(ys[i], flags.randomRange); + ys[i] = SPLRandom::scaledRange(ys[i], variance, factor); } } diff --git a/src/spl/spl_random.h b/src/spl/spl_random.h index 812c5cb..1db8864 100644 --- a/src/spl/spl_random.h +++ b/src/spl/spl_random.h @@ -53,10 +53,7 @@ class SPLRandom { // So the value range is [n * (1 - variance / 2), n * (1 + variance / 2)] static f32 scaledRange(f32 n, f32 variance) { #if SPL_ACCURATE_RANDOM - const fx32 nx = FX_F32_TO_FX32(n); - const fx32 range = (fx32)(variance * 255.0f); - const fx32 v = (nx * (255 - ((range * (fx32)nextU32(8)) >> 8))) >> 8; - return FX_FX32_TO_F32(v); + return scaledRange(n, variance, nextU32(8)); #else variance = glm::clamp(variance, 0.0f, 1.0f); const f32 min = n * (1.0f - variance / 2.0f); @@ -68,10 +65,7 @@ class SPLRandom { // Generates a random float in the range [n, n * (1 + variance)] static f32 scaledRange2(f32 n, f32 variance) { #if SPL_ACCURATE_RANDOM - const fx32 nx = FX_F32_TO_FX32(n); - const fx32 range = (fx32)(variance * 255.0f); - const fx32 v = (nx * (255 + range - ((range * (fx32)nextU32(8)) >> 7))) >> 8; - return FX_FX32_TO_F32(v); + return scaledRange2(n, variance, nextU32(8)); #else const f32 min = n; const f32 max = n * (1.0f + variance); @@ -80,6 +74,22 @@ class SPLRandom { #endif } + // Deterministic version of scaledRange + static f32 scaledRange(f32 n, f32 variance, u32 factor) { + const fx32 nx = FX_F32_TO_FX32(n); + const fx32 range = (fx32)(variance * 255.0f); + const fx32 v = (nx * (255 - ((range * (fx32)factor) >> 8))) >> 8; + return FX_FX32_TO_F32(v); + } + + // Deterministic version of scaledRange2 + static f32 scaledRange2(f32 n, f32 variance, u32 factor) { + const fx32 nx = FX_F32_TO_FX32(n); + const fx32 range = (fx32)(variance * 255.0f); + const fx32 v = (nx * (255 + range - ((range * (fx32)factor) >> 7))) >> 8; + return FX_FX32_TO_F32(v); + } + static f32 range(f32 min, f32 max) { return min + nextF32() * (max - min); } diff --git a/src/spl/spl_resource.h b/src/spl/spl_resource.h index c7fb48b..ffbc012 100644 --- a/src/spl/spl_resource.h +++ b/src/spl/spl_resource.h @@ -474,6 +474,8 @@ struct SPLAlphaAnim final : SPLAnim { void apply(SPLParticle& ptcl, const SPLResource& resource, f32 lifeRate) const override; void plot(std::span xs, std::span ys) const; + void plotWith(std::span xs, std::span ys, float variance) const; + void plotWith(std::span xs, std::span ys, float variance, u8 factor) const; static SPLAlphaAnim createDefault() { return SPLAlphaAnim( From 03861435a5b268242e88323bfcc19026238044f0 Mon Sep 17 00:00:00 2001 From: Fexty12573 Date: Wed, 11 Mar 2026 11:13:58 +0100 Subject: [PATCH 3/7] Add small animations to icon buttons --- src/application.cpp | 8 +-- src/editor/editor.cpp | 2 +- src/imgui/extensions.cpp | 106 +++++++++++++++++++++++++++++++++++---- 3 files changed, 102 insertions(+), 14 deletions(-) diff --git a/src/application.cpp b/src/application.cpp index 98ad4e6..8b12996 100644 --- a/src/application.cpp +++ b/src/application.cpp @@ -807,10 +807,10 @@ void Application::renderMenuBar() { const auto viewport = (ImGuiViewportP*)ImGui::GetMainViewport(); constexpr auto flags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar; - constexpr float framePaddingY = 4.0f; - constexpr float itemHeight = 24.0f; - constexpr float barHeight = itemHeight + 2.0f; - constexpr ImVec2 size = { itemHeight, itemHeight }; + const float framePaddingY = 4.0f; + const float itemHeight = ImGui::GetFontSize() + framePaddingY * 2.0f; + const float barHeight = itemHeight + 2.0f; + const ImVec2 size = { itemHeight, itemHeight }; ImGui::PushStyleColor(ImGuiCol_Button, 0x00000000); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, AppColors::DarkGray); diff --git a/src/editor/editor.cpp b/src/editor/editor.cpp index f2caf30..9547a2f 100644 --- a/src/editor/editor.cpp +++ b/src/editor/editor.cpp @@ -698,7 +698,7 @@ void Editor::renderResourcePicker() { itemId, 0, hovered ? 8.0f : 0.0f, - 0.4f, + 0.25f, iam_ease_preset(iam_ease_in_out_quad), iam_policy_crossfade, dt diff --git a/src/imgui/extensions.cpp b/src/imgui/extensions.cpp index d9b3437..9105bb6 100644 --- a/src/imgui/extensions.cpp +++ b/src/imgui/extensions.cpp @@ -1,6 +1,7 @@ #include "extensions.h" #include +#include #include namespace ImGui { @@ -226,25 +227,85 @@ void ImGui::VerticalSeparator(float height) { } bool ImGui::IconButton(const char* icon, const ImVec2& size, ImU32 tint, bool enabled) { - ImGui::BeginDisabled(!enabled); + BeginDisabled(!enabled); + + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) { + EndDisabled(); + return false; + } + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(icon); + + const ImVec2 icon_size = CalcTextSize(icon); + const ImVec2 pos = window->DC.CursorPos; + const ImVec2 item_size = CalcItemSize(size, icon_size.x + style.FramePadding.x * 2.0f, icon_size.y + style.FramePadding.y * 2.0f); + + const ImRect bb(pos, pos + item_size); + ItemSize(item_size, style.FramePadding.y); + if (!ItemAdd(bb, id)) { + EndDisabled(); + return false; + } + + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held); + + // Render button frame + const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); + RenderNavCursor(bb, id); + RenderFrame(bb.Min, bb.Max, col, true, style.FrameRounding); + + if (g.LogEnabled) + LogSetNextTextDecoration("[", "]"); + + // Animate icon scale on hover/active (scales from center) + const float target_scale = (held && hovered) ? 0.9f : hovered ? 1.2f : 1.0f; + const float icon_scale = iam_tween_float( + id, + ImHashStr("icon_scale"), + target_scale, + 0.07f, + iam_ease_preset(iam_ease_linear), + iam_policy_crossfade, + g.IO.DeltaTime, + 1.0f + ); + + const float scaled_font_size = g.FontSizeBase * icon_scale; + const ImVec2 button_center = ImVec2((bb.Min.x + bb.Max.x) * 0.5f, (bb.Min.y + bb.Max.y) * 0.5f); + + // Measure icon at the actual scaled font size for accurate centering + PushFont(g.Font, scaled_font_size); + + const ImVec2 scaled_icon_size = CalcTextSize(icon); + const ImVec2 icon_pos = ImVec2( + ImFloor(button_center.x - scaled_icon_size.x * 0.5f), + ImFloor(button_center.y - scaled_icon_size.y * 0.5f) + ); + if (tint != 0) { if (enabled) { - ImGui::PushStyleColor(ImGuiCol_Text, tint); + PushStyleColor(ImGuiCol_Text, tint); } else { - const auto disabledTint = ImGui::ColorConvertU32ToFloat4(tint) * ImVec4(0.8f, 0.8f, 0.8f, 1.0f); - ImGui::PushStyleColor(ImGuiCol_TextDisabled, disabledTint); + const auto disabledTint = ColorConvertU32ToFloat4(tint) * ImVec4(0.8f, 0.8f, 0.8f, 1.0f); + PushStyleColor(ImGuiCol_Text, disabledTint); } } - const bool clicked = ImGui::Button(icon, size); + window->DrawList->AddText(icon_pos, GetColorU32(ImGuiCol_Text), icon); + + PopFont(); if (tint != 0) { - ImGui::PopStyleColor(); + PopStyleColor(); } - ImGui::EndDisabled(); + EndDisabled(); - return clicked; + return pressed; } bool ImGui::IconButton(const char* icon, const char* text, ImU32 iconTint, bool enabled) { @@ -307,10 +368,37 @@ bool ImGui::IconButton(const char* icon, const char* text, ImU32 iconTint, bool ImVec2 icon_pos = ImVec2(bb.Min.x + style.FramePadding.x, icon_y); ImVec2 text_pos = ImVec2(icon_pos.x + icon_size.x + spacing, text_y); + // Animate icon scale on hover/active + const float target_scale = (held && hovered) ? 0.9f : hovered ? 1.2f : 1.0f; + const float icon_scale = iam_tween_float( + id, + ImHashStr("icon_scale"), + target_scale, + 0.07f, + iam_ease_preset(iam_ease_linear), + iam_policy_crossfade, + g.IO.DeltaTime, + 1.0f + ); + + const float scaled_font_size = g.FontSizeBase * icon_scale; + const ImVec2 icon_center = ImVec2(icon_pos.x + icon_size.x * 0.5f, icon_pos.y + icon_size.y * 0.5f); + + // Measure icon at the actual scaled font size for accurate centering + PushFont(g.Font, scaled_font_size); + + const ImVec2 scaled_icon_size = CalcTextSize(icon); + const ImVec2 scaled_icon_pos = ImVec2( + ImFloor(icon_center.x - scaled_icon_size.x * 0.5f), + ImFloor(icon_center.y - scaled_icon_size.y * 0.5f) + ); + if (iconTint != 0) PushStyleColor(ImGuiCol_Text, iconTint); - window->DrawList->AddText(icon_pos, GetColorU32(ImGuiCol_Text), icon); + window->DrawList->AddText(scaled_icon_pos, GetColorU32(ImGuiCol_Text), icon); if (iconTint != 0) PopStyleColor(); + PopFont(); + window->DrawList->AddText(text_pos, GetColorU32(ImGuiCol_Text), text); //PopStyleVar(); // FramePadding From 6169d1d8bb80b3b108420d75ab6b890f01d3d034 Mon Sep 17 00:00:00 2001 From: Fexty12573 Date: Wed, 11 Mar 2026 11:19:39 +0100 Subject: [PATCH 4/7] Center menu item icons --- src/imgui/extensions.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/imgui/extensions.cpp b/src/imgui/extensions.cpp index 9105bb6..8184177 100644 --- a/src/imgui/extensions.cpp +++ b/src/imgui/extensions.cpp @@ -17,7 +17,7 @@ static bool IsRootOfOpenMenuSet() const ImGuiPopupData* upper_popup = &g.OpenPopupStack[g.BeginPopupStack.Size]; if (window->DC.NavLayerCurrent != upper_popup->ParentNavLayer) return false; - return upper_popup->Window && (upper_popup->Window->Flags & ImGuiWindowFlags_ChildMenu) && ImGui::IsWindowChildOf(upper_popup->Window, window, true, false); + return upper_popup->Window && (upper_popup->Window->Flags & ImGuiWindowFlags_ChildMenu) && IsWindowChildOf(upper_popup->Window, window, true, false); } static bool MenuItemColor(const char* label, const char* icon, const char* shortcut, ImU32 iconTint, bool selected, bool enabled) { @@ -75,8 +75,10 @@ static bool MenuItemColor(const char* label, const char* icon, const char* short RenderText(pos + ImVec2(offsets->OffsetLabel, 0.0f), label); if (icon_w > 0.0f) { + // Make sure all icons are centered in the icon column + const float icon_offset = (offsets->OffsetLabel - offsets->OffsetIcon - icon_w) * 0.5f; if (iconTint != 0) PushStyleColor(ImGuiCol_Text, iconTint); - RenderText(pos + ImVec2(offsets->OffsetIcon, 0.0f), icon); + RenderText(pos + ImVec2(offsets->OffsetIcon + icon_offset, 0.0f), icon); if (iconTint != 0) PopStyleColor(); } if (shortcut_w > 0.0f) From 1278c687c378a975bb6939e5e690cf9770a0e66c Mon Sep 17 00:00:00 2001 From: Fexty12573 Date: Sun, 28 Jun 2026 18:07:58 +0200 Subject: [PATCH 5/7] Add frame timer metric --- src/application.cpp | 44 ++++++++++++++++++++++-- src/application.h | 8 +++++ src/util/circular_buffer.h | 70 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 src/util/circular_buffer.h diff --git a/src/application.cpp b/src/application.cpp index 8b12996..f2b975e 100644 --- a/src/application.cpp +++ b/src/application.cpp @@ -1118,9 +1118,47 @@ void Application::renderPreferences() { void Application::renderPerformanceWindow() { if (ImGui::Begin("Performance", &m_performanceWindowOpen)) { ImGui::SeparatorText("Application"); - ImGui::Text("FPS: %.1f", ImGui::GetIO().Framerate); - ImGui::Text("Delta Time: %.3f ms", m_deltaTime * 1000.0f); - ImGui::Text("Frame Time: %.3f ms", ImGui::GetIO().DeltaTime * 1000.0f); + const auto& io = ImGui::GetIO(); + const auto fps = io.Framerate; + const auto dtMs = m_deltaTime * 1000.0f; + const auto ftMs = io.DeltaTime * 1000.0f; + + ImGui::Text("FPS: %.1f", fps); + ImGui::Text("Delta Time: %.3f ms", dtMs); + ImGui::Text("Frame Time: %.3f ms", ftMs); + + if (m_measurement_accum >= 0.02f) { + m_fps_buffer.push(fps); + m_ft_buffer.push(ftMs); + m_measurement_accum -= 0.02f; + } + m_measurement_accum += m_deltaTime; + + ImGui::SliderFloat("History", &m_history_secs, 1.0f, 30.0f, "%.0f s"); + + const int maxSamples = static_cast(m_history_secs / 0.02f); + + if (ImPlot::BeginPlot("##FPS", { -1, 150 })) { + const int fpsCount = static_cast(m_fps_buffer.size()); + const int fpsOffset = static_cast(m_fps_buffer.offset()); + ImPlot::SetupAxes(nullptr, "FPS", ImPlotAxisFlags_NoTickLabels, ImPlotAxisFlags_AutoFit); + ImPlot::SetupAxisLimits(ImAxis_X1, 0, maxSamples, ImPlotCond_Always); + ImPlotSpec spec; + spec.Offset = fpsOffset; + ImPlot::PlotLine("FPS", m_fps_buffer.data(), fpsCount, 1.0, 0, spec); + ImPlot::EndPlot(); + } + + if (ImPlot::BeginPlot("##FrameTime", {-1, 150})) { + const int ftCount = static_cast(m_ft_buffer.size()); + const int ftOffset = static_cast(m_ft_buffer.offset()); + ImPlot::SetupAxes(nullptr, "ms", ImPlotAxisFlags_NoTickLabels, ImPlotAxisFlags_AutoFit); + ImPlot::SetupAxisLimits(ImAxis_X1, 0, maxSamples, ImPlotCond_Always); + ImPlotSpec spec; + spec.Offset = ftOffset; + ImPlot::PlotLine("Frame Time", m_ft_buffer.data(), ftCount, 1.0, 0, spec); + ImPlot::EndPlot(); + } ImGui::SeparatorText("Current Editor"); m_editor->renderStats(); diff --git a/src/application.h b/src/application.h index 7fe8dbc..6901e4d 100644 --- a/src/application.h +++ b/src/application.h @@ -5,6 +5,7 @@ #include "editor/project_manager.h" #include "gfx/gl_texture.h" #include "version.h" +#include "util/circular_buffer.h" #include #include @@ -253,6 +254,13 @@ class Application { std::string m_extractedBinaryPath; float m_idleAccumulator = 0.0f; // accumulate time to occasionally update particles while unfocused + + // Performance Metrics + float m_history_secs = 10.0f; + float m_last_measurement = 0.0f; + float m_measurement_accum = 0.0f; + CircularBuffer m_fps_buffer; + CircularBuffer m_ft_buffer; }; inline Application* g_application = nullptr; diff --git a/src/util/circular_buffer.h b/src/util/circular_buffer.h new file mode 100644 index 0000000..d0c0f18 --- /dev/null +++ b/src/util/circular_buffer.h @@ -0,0 +1,70 @@ +#pragma once + +#include +#include +#include + +template +class CircularBuffer { +public: + using container_type = std::vector; + using value_type = T; + using size_type = std::size_t; + using difference_type = container_type::difference_type; + using reference = container_type::reference; + using const_reference = container_type::const_reference; + using pointer = container_type::pointer; + using const_pointer = container_type::const_pointer; + + CircularBuffer() = default; + CircularBuffer(std::initializer_list values) : m_data{ std::move(values) } {} + explicit CircularBuffer(size_type count, const_reference value = value_type{}) : m_data(count, value) {} + CircularBuffer(const CircularBuffer&) = default; + CircularBuffer(CircularBuffer&&) = default; + CircularBuffer& operator=(const CircularBuffer&) = default; + CircularBuffer& operator=(CircularBuffer&&) = default; + + void push(const_reference v) { + if (size() < capacity()) { + m_data.push_back(v); + } else { + m_data[m_offset] = v; + m_offset = (m_offset + 1) % capacity(); + } + } + + template + void push(X&& v) { + if (size() < capacity()) { + m_data.emplace_back(std::forward(v)); + } + else { + m_data[m_offset] = v; + m_offset = (m_offset + 1) % capacity(); + } + } + + size_type size() const { + return m_data.size(); + } + + static constexpr size_type capacity() { + return N; + } + + const_pointer data() const { + return m_data.data(); + } + + pointer data() { + return m_data.data(); + } + + size_type offset() const { + return m_offset; + } + +private: + std::vector m_data; + size_type m_offset = 0; +}; From c490fe9e8064d03eaa6b4d632a5c68bba2d1f377 Mon Sep 17 00:00:00 2001 From: Fexty12573 Date: Sun, 28 Jun 2026 19:04:04 +0200 Subject: [PATCH 6/7] More small animations --- src/editor/editor.cpp | 188 ++++++++++++++++++++------------------- src/editor/editor.h | 7 ++ src/imgui/extensions.cpp | 109 +++++++++++++++++++++++ src/imgui/extensions.h | 8 ++ 4 files changed, 219 insertions(+), 93 deletions(-) diff --git a/src/editor/editor.cpp b/src/editor/editor.cpp index 9547a2f..581310e 100644 --- a/src/editor/editor.cpp +++ b/src/editor/editor.cpp @@ -276,15 +276,44 @@ void Editor::renderStats() { const auto fraction = static_cast(activeParticles) / maxParticles; const auto particleText = fmt::format("Particles: {}/{}", activeParticles, maxParticles); - constexpr auto colorLow = glm::vec4(0.0f, 1.0f, 0.0f, 1.0f); - constexpr auto colorHigh = glm::vec4(1.0f, 0.0f, 0.0f, 1.0f); - const auto color = glm::mix(colorLow, colorHigh, fraction); + constexpr u32 colorLow = IM_COL32(0, 200, 0, 255); + constexpr u32 colorHigh = IM_COL32(220, 0, 0, 255); + ImGui::AnimatedStatBar("##particleBar", fraction, particleText.c_str(), colorLow, colorHigh); - ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4{ color.r, color.g, color.b, color.a }); - ImGui::ProgressBar(fraction, ImVec2(0.0f, 0.0f), particleText.c_str()); - ImGui::PopStyleColor(); - - ImGui::Text("Active Emitters: %" PRIu64, system.getEmitters().size()); + const auto emitterCount = system.getEmitters().size(); + const float dt = ImGui::GetIO().DeltaTime; + const ImGuiID flashId = ImGui::GetID("##emitterFlash"); + + if (emitterCount != m_lastEmitterCount) { + m_emitterFlashColor = emitterCount > m_lastEmitterCount ? AppColors::LightGreen : AppColors::LightRed; + iam_tween_float(flashId, 0, 1.0f, 0.0f, iam_ease_preset(iam_ease_linear), iam_policy_cut, dt, 0.0f); + m_lastEmitterCount = emitterCount; + } + + const float flash = iam_tween_float( + flashId, 0, 0.0f, 0.45f, + iam_ease_preset(iam_ease_out_cubic), + iam_policy_crossfade, dt, 0.0f + ); + + const auto emitterText = fmt::format("Active Emitters: {}", emitterCount); + if (flash > 0.001f && m_emitterFlashColor != 0) { + const auto& style = ImGui::GetStyle(); + const ImVec2 p = ImGui::GetCursorScreenPos(); + const ImVec2 ts = ImGui::CalcTextSize(emitterText.c_str()); + const ImVec2 pad = { style.FramePadding.x, style.FramePadding.y * 0.5f }; + + ImVec4 hl = ImGui::ColorConvertU32ToFloat4(m_emitterFlashColor); + hl.w = flash * 0.35f; + ImGui::GetWindowDrawList()->AddRectFilled( + p - pad, + { p.x + ts.x + pad.x, p.y + ts.y + pad.y }, + ImGui::ColorConvertFloat4ToU32(hl), + style.FrameRounding + ); + } + + ImGui::TextUnformatted(emitterText.c_str()); } void Editor::openPicker() { @@ -328,12 +357,14 @@ void Editor::openSettings() { m_settingsOpen = true; ImGui::PushOverrideID(m_settingsWindowId); ImGui::OpenPopup("Settings##Editor"); + ImGui::ResetPopupFade("Settings##Editor"); ImGui::PopID(); } void Editor::openTutorial() { ImGui::PushOverrideID(m_tutorialWindowId); ImGui::OpenPopup("##ViewportTutorial"); + ImGui::ResetPopupFade("##ViewportTutorial"); ImGui::PopID(); } @@ -627,6 +658,23 @@ void Editor::renderResourcePicker() { const float itemWidth = contentRegion.x - (style.ItemSpacing.x + style.WindowPadding.x) * 1.3f; const float dt = ImGui::GetIO().DeltaTime; + // Detect selection changes + const size_t selected = m_selectedResources[id]; + const ImGuiID accentId = ImGui::GetID("##pickerAccent"); + bool selectionChanged = false; + if (m_lastPickerSelection[id] != selected) { + m_lastPickerSelection[id] = selected; + selectionChanged = true; + iam_tween_float(accentId, 0, 0.0f, 0.0f, iam_ease_preset(iam_ease_linear), iam_policy_cut, dt, 0.0f); + } + + const float accentGrow = iam_tween_float( + accentId, 0, + selected != (size_t)-1 ? 1.0f : 0.0f, 0.3f, + iam_ease_preset(iam_ease_out_cubic), + iam_policy_crossfade, dt, 0.0f + ); + for (size_t i = 0; i < resources.size(); ++i) { const auto& resource = resources[i]; const auto& texture = textures[resource.header.misc.textureIndex]; @@ -689,10 +737,30 @@ void Editor::renderResourcePicker() { ImGui::ShadeVertsLinearColorGradientKeepAlpha(drawList, firstVtx, lastVtx, cursor, { cursor.x, cursor.y + itemHeight }, - ImGui::ColorConvertFloat4ToU32(bgColor), + ImGui::ColorConvertFloat4ToU32(bgColor), ImGui::ColorConvertFloat4ToU32(bgColor2) ); + // Selection accent bar on the left edge, eased in on selection change + if (m_selectedResources[id] == i && accentGrow > 0.001f) { + const float barHeight = itemHeight * accentGrow; + const float barTop = cursor.y + (itemHeight - barHeight) * 0.5f; + ImVec4 accent = ImGui::ColorConvertU32ToFloat4(AppColors::LightBlue); + accent.w = accentGrow; + drawList->AddRectFilled( + { cursor.x, barTop }, + { cursor.x + 3.0f, barTop + barHeight }, + ImGui::ColorConvertFloat4ToU32(accent), + 1.5f + ); + + if (selectionChanged) { + const float itemContentY = (cursor.y - ImGui::GetWindowPos().y) + ImGui::GetScrollY(); + const float targetScroll = itemContentY - ImGui::GetWindowHeight() * 0.5f + itemHeight * 0.5f; + iam_scroll_to_y(targetScroll, 0.25f); + } + } + const ImVec2 offset = { iam_tween_float( itemId, @@ -1298,6 +1366,7 @@ void Editor::renderResourceEditor() { void Editor::renderSettings() { ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f); ImGui::PushOverrideID(m_settingsWindowId); + ImGui::BeginPopupFade("Settings##Editor"); bool closedThroughButton = false; if (ImGui::BeginPopupModal("Settings##Editor", &m_settingsOpen)) { @@ -1312,6 +1381,10 @@ void Editor::renderSettings() { ImGuiWindowFlags_AlwaysVerticalScrollbar ); + if (ImGui::IsWindowAppearing()) { + iam_scroll_to_top(); + } + ImGui::SeparatorText("General"); ImGui::InputScalar("Max Particles", ImGuiDataType_U32, &m_settings.maxParticles); ImGui::SameLine(); @@ -1404,6 +1477,7 @@ void Editor::renderSettings() { m_settings = m_settingsBackup; } + ImGui::EndPopupFade(); ImGui::PopID(); ImGui::PopStyleVar(); } @@ -1411,6 +1485,7 @@ void Editor::renderSettings() { void Editor::renderTutorial() { ImGui::PushOverrideID(m_tutorialWindowId); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, { 16, 16 }); + ImGui::BeginPopupFade("##ViewportTutorial"); ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Appearing, { 0.5f, 0.5f }); ImGui::SetNextWindowSize({ 900, 0 }, ImGuiCond_Once); @@ -1537,6 +1612,7 @@ void Editor::renderTutorial() { ImGui::EndPopup(); } + ImGui::EndPopupFade(); ImGui::PopStyleVar(); ImGui::PopID(); } @@ -1908,16 +1984,9 @@ void Editor::renderBehaviorEditor(SPLResource& res) { bool Editor::renderGravityBehaviorEditor(const std::shared_ptr& gravity) { LOCK_EDITOR(); static bool hovered = false; - if (hovered) { - ImGui::PushStyleColor(ImGuiCol_Border, s_hoverAccentColor); - } - ImGui::BeginChild("##gravityEditor", {}, ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY); + ImGui::BeginHoverBorderChild("##gravityEditor", hovered, s_hoverAccentColor); ImGui::TextUnformatted("Gravity"); - if (hovered) { - ImGui::PopStyleColor(); - } - NOTIFY(ImGui::DragFloat3("Magnitude", glm::value_ptr(gravity->magnitude), 0.001f)); ImGui::EndChild(); @@ -1929,16 +1998,9 @@ bool Editor::renderGravityBehaviorEditor(const std::shared_ptr& random) { LOCK_EDITOR(); static bool hovered = false; - if (hovered) { - ImGui::PushStyleColor(ImGuiCol_Border, s_hoverAccentColor); - } - ImGui::BeginChild("##randomEditor", {}, ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY); + ImGui::BeginHoverBorderChild("##randomEditor", hovered, s_hoverAccentColor); ImGui::TextUnformatted("Random"); - if (hovered) { - ImGui::PopStyleColor(); - } - NOTIFY(ImGui::DragFloat3("Magnitude", glm::value_ptr(random->magnitude), 0.001f)); NOTIFY(ImGui::SliderFloat("Apply Interval", &random->applyInterval, 0, 5, "%.3fs", ImGuiSliderFlags_Logarithmic)); @@ -1951,16 +2013,9 @@ bool Editor::renderRandomBehaviorEditor(const std::shared_ptr bool Editor::renderMagnetBehaviorEditor(const std::shared_ptr& magnet) { LOCK_EDITOR(); static bool hovered = false; - if (hovered) { - ImGui::PushStyleColor(ImGuiCol_Border, s_hoverAccentColor); - } - ImGui::BeginChild("##magnetEditor", {}, ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY); + ImGui::BeginHoverBorderChild("##magnetEditor", hovered, s_hoverAccentColor); ImGui::TextUnformatted("Magnet"); - if (hovered) { - ImGui::PopStyleColor(); - } - NOTIFY(ImGui::DragFloat3("Target", glm::value_ptr(magnet->target), 0.05f, -5.0f, 5.0f)); NOTIFY(ImGui::SliderFloat("Force", &magnet->force, 0, 5, "%.3f", ImGuiSliderFlags_Logarithmic)); @@ -1973,16 +2028,9 @@ bool Editor::renderMagnetBehaviorEditor(const std::shared_ptr bool Editor::renderSpinBehaviorEditor(const std::shared_ptr& spin) { LOCK_EDITOR(); static bool hovered = false; - if (hovered) { - ImGui::PushStyleColor(ImGuiCol_Border, s_hoverAccentColor); - } - ImGui::BeginChild("##spinEditor", {}, ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY); + ImGui::BeginHoverBorderChild("##spinEditor", hovered, s_hoverAccentColor); ImGui::TextUnformatted("Spin"); - if (hovered) { - ImGui::PopStyleColor(); - } - NOTIFY(ImGui::SliderAngle("Angle", &spin->angle, -180.0f, 180.0f)); ImGui::TextUnformatted("Axis"); ImGui::Indent(); @@ -2000,16 +2048,9 @@ bool Editor::renderSpinBehaviorEditor(const std::shared_ptr& sp bool Editor::renderCollisionPlaneBehaviorEditor(const std::shared_ptr& collisionPlane) { LOCK_EDITOR(); static bool hovered = false; - if (hovered) { - ImGui::PushStyleColor(ImGuiCol_Border, s_hoverAccentColor); - } - ImGui::BeginChild("##collisionPlaneEditor", {}, ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY); + ImGui::BeginHoverBorderChild("##collisionPlaneEditor", hovered, s_hoverAccentColor); ImGui::TextUnformatted("Collision Plane"); - if (hovered) { - ImGui::PopStyleColor(); - } - NOTIFY(ImGui::DragFloat("Height", &collisionPlane->y, 0.05f)); NOTIFY(ImGui::SliderFloat("Elasticity", &collisionPlane->elasticity, 0, 2, "%.3f", ImGuiSliderFlags_Logarithmic)); ImGui::TextUnformatted("Collision Type"); @@ -2027,16 +2068,9 @@ bool Editor::renderCollisionPlaneBehaviorEditor(const std::shared_ptr& convergence) { LOCK_EDITOR(); static bool hovered = false; - if (hovered) { - ImGui::PushStyleColor(ImGuiCol_Border, s_hoverAccentColor); - } - ImGui::BeginChild("##convergenceEditor", {}, ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY); + ImGui::BeginHoverBorderChild("##convergenceEditor", hovered, s_hoverAccentColor); ImGui::TextUnformatted("Convergence"); - if (hovered) { - ImGui::PopStyleColor(); - } - NOTIFY(ImGui::DragFloat3("Target", glm::value_ptr(convergence->target), 0.05f, -5.0f, 5.0f)); NOTIFY(ImGui::SliderFloat("Force", &convergence->force, -5, 5, "%.3f", ImGuiSliderFlags_Logarithmic)); @@ -2115,15 +2149,7 @@ bool Editor::renderScaleAnimEditor(SPLScaleAnim& res) { return false; } - if (hovered) { - ImGui::PushStyleColor(ImGuiCol_Border, s_hoverAccentColor); - } - - ImGui::BeginChild("##scaleAnimEditor", {}, ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY); - - if (hovered) { - ImGui::PopStyleColor(); - } + ImGui::BeginHoverBorderChild("##scaleAnimEditor", hovered, s_hoverAccentColor); NOTIFY(ImGui::SliderFloat("Start Scale", &res.start, 0.01f, 10.0f, "%.3f", ImGuiSliderFlags_Logarithmic)); NOTIFY(ImGui::SliderFloat("Mid Scale", &res.mid, 0.01f, 10.0f, "%.3f", ImGuiSliderFlags_Logarithmic)); @@ -2167,15 +2193,7 @@ bool Editor::renderColorAnimEditor(const SPLResource& mainRes, SPLColorAnim& res return false; } - if (hovered) { - ImGui::PushStyleColor(ImGuiCol_Border, s_hoverAccentColor); - } - - ImGui::BeginChild("##colorAnimEditor", {}, ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY); - - if (hovered) { - ImGui::PopStyleColor(); - } + ImGui::BeginHoverBorderChild("##colorAnimEditor", hovered, s_hoverAccentColor); NOTIFY(ImGui::ColorEdit3("Start Color", glm::value_ptr(res.start))); NOTIFY(ImGui::ColorEdit3("End Color", glm::value_ptr(res.end))); @@ -2284,15 +2302,7 @@ bool Editor::renderAlphaAnimEditor(SPLAlphaAnim& res) { return false; } - if (hovered) { - ImGui::PushStyleColor(ImGuiCol_Border, s_hoverAccentColor); - } - - ImGui::BeginChild("##alphaAnimEditor", {}, ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY); - - if (hovered) { - ImGui::PopStyleColor(); - } + ImGui::BeginHoverBorderChild("##alphaAnimEditor", hovered, s_hoverAccentColor); NOTIFY(ImGui::SliderFloat("Start Alpha", &res.alpha.start, 0, 1)); NOTIFY(ImGui::SliderFloat("Mid Alpha", &res.alpha.mid, 0, 1)); @@ -2362,15 +2372,7 @@ bool Editor::renderTexAnimEditor(SPLTexAnim& res) { return false; } - if (hovered) { - ImGui::PushStyleColor(ImGuiCol_Border, s_hoverAccentColor); - } - - ImGui::BeginChild("##texAnimEditor", {}, ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY); - - if (hovered) { - ImGui::PopStyleColor(); - } + ImGui::BeginHoverBorderChild("##texAnimEditor", hovered, s_hoverAccentColor); NOTIFY(ImGui::SliderFloat("Step", &res.param.step, 0.01f, 1.0f)); HELP(texAnimStep); diff --git a/src/editor/editor.h b/src/editor/editor.h index 2f5da6c..d51bba5 100644 --- a/src/editor/editor.h +++ b/src/editor/editor.h @@ -151,6 +151,13 @@ class Editor { EmitterSpawnType m_emitterSpawnType = EmitterSpawnType::SingleShot; float m_emitterInterval = 1.0f; // seconds + // Stats panel feedback animation + u32 m_emitterFlashColor = 0; + size_t m_lastEmitterCount = 0; + + // Tracks last selection per editor to animate selection changes + std::unordered_map m_lastPickerSelection; + static inline const u32 s_hoverAccentColor = ImGui::ColorConvertFloat4ToU32({ 0.7f, 0.3f, 0.7f, 1.0f }); static constexpr glm::ivec2 s_gridDimensions = { 20, 20 }; static constexpr glm::vec2 s_gridSpacing = { 1.0f, 1.0f }; diff --git a/src/imgui/extensions.cpp b/src/imgui/extensions.cpp index 8184177..24efc9c 100644 --- a/src/imgui/extensions.cpp +++ b/src/imgui/extensions.cpp @@ -410,6 +410,115 @@ bool ImGui::IconButton(const char* icon, const char* text, ImU32 iconTint, bool return pressed; } +bool ImGui::BeginHoverBorderChild(const char* str_id, bool hovered, ImU32 accent, ImGuiChildFlags flags) { + const ImGuiID id = GetID(str_id); + const ImVec4 normal = GetStyleColorVec4(ImGuiCol_Border); + const ImVec4 target = hovered ? ColorConvertU32ToFloat4(accent) : normal; + + const ImVec4 col = iam_tween_color( + id, 0, target, 0.18f, + iam_ease_preset(iam_ease_out_cubic), + iam_policy_crossfade, iam_col_oklab, + GetIO().DeltaTime, normal + ); + + PushStyleColor(ImGuiCol_Border, col); + const bool open = BeginChild(str_id, ImVec2(0, 0), flags); + PopStyleColor(); + return open; +} + +void ImGui::AnimatedStatBar(const char* str_id, float fraction, const char* overlay, ImU32 lowColor, ImU32 highColor) { + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(str_id); + const float dt = g.IO.DeltaTime; + + const ImVec2 pos = window->DC.CursorPos; + const ImVec2 size = CalcItemSize(ImVec2(0.0f, 0.0f), CalcItemWidth(), g.FontSize + style.FramePadding.y * 2.0f); + const ImRect bb(pos, pos + size); + ItemSize(size, style.FramePadding.y); + if (!ItemAdd(bb, id)) + return; + + fraction = ImSaturate(fraction); + + // Ease the displayed fraction so the fill glides instead of snapping each frame + const float shown = ImSaturate(iam_tween_float( + id, 0, fraction, 0.25f, + iam_ease_preset(iam_ease_out_cubic), + iam_policy_crossfade, dt, fraction + )); + + // Blend low -> high in OKLAB based on the eased value + ImVec4 fill = iam_get_blended_color( + ColorConvertU32ToFloat4(lowColor), + ColorConvertU32ToFloat4(highColor), + shown, iam_col_oklab + ); + + // Gentle brightness pulse when near full + if (fraction > 0.9f) { + const float pulse = iam_oscillate(id, 0.10f, 2.0f, iam_wave_sine, 0.0f, dt); + fill.x = ImSaturate(fill.x + pulse); + fill.y = ImSaturate(fill.y + pulse); + fill.z = ImSaturate(fill.z + pulse); + } + + RenderFrame(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); + + const float fillWidth = shown * bb.GetWidth(); + if (fillWidth > 0.0f) { + const ImVec2 fillMax(bb.Min.x + fillWidth, bb.Max.y); + const ImU32 fillTop = ColorConvertFloat4ToU32(fill); + const ImU32 fillBot = ColorConvertFloat4ToU32(fill * ImVec4(0.8f, 0.8f, 0.8f, 1.0f)); + const int v0 = window->DrawList->VtxBuffer.Size; + window->DrawList->AddRectFilled(bb.Min, fillMax, fillTop, style.FrameRounding); + const int v1 = window->DrawList->VtxBuffer.Size; + ShadeVertsLinearColorGradientKeepAlpha(window->DrawList, v0, v1, bb.Min, bb.GetBL(), fillTop, fillBot); + } + + if (overlay && overlay[0]) { + const ImVec2 overlaySize = CalcTextSize(overlay, NULL); + RenderTextClipped(bb.Min, bb.Max, overlay, NULL, &overlaySize, ImVec2(0.5f, 0.5f), &bb); + } +} + +void ImGui::BeginPopupFade(const char* str_id) { + ImGuiContext& g = *GImGui; + const ImGuiID id = GetID(str_id); + const bool open = IsPopupOpen(str_id, ImGuiPopupFlags_None); + + float a = 1.0f; + if (open) { + a = iam_tween_float( + id, 0, 1.0f, 0.15f, + iam_ease_preset(iam_ease_out_cubic), + iam_policy_crossfade, g.IO.DeltaTime, 0.0f + ); + } + + PushStyleVar(ImGuiStyleVar_Alpha, a * g.Style.Alpha); +} + +void ImGui::EndPopupFade() { + PopStyleVar(); +} + +void ImGui::ResetPopupFade(const char* str_id) { + const ImGuiID id = GetID(str_id); + // Snap the channel to 0 so the next BeginPopupFade fades in from fully transparent + iam_tween_float( + id, 0, 0.0f, 0.0f, + iam_ease_preset(iam_ease_linear), + iam_policy_cut, GetIO().DeltaTime, 0.0f + ); +} + bool ImGui::CenteredButton(const char* label, float alignment) { const ImGuiStyle& style = GetStyle(); diff --git a/src/imgui/extensions.h b/src/imgui/extensions.h index d63320d..c51c810 100644 --- a/src/imgui/extensions.h +++ b/src/imgui/extensions.h @@ -20,6 +20,14 @@ bool PaddedTreeNode(const char* label, const ImVec2& padding, ImGuiTreeNodeFlags void VerticalSeparator(float height); +bool BeginHoverBorderChild(const char* str_id, bool hovered, ImU32 accent, ImGuiChildFlags flags = ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY); + +void AnimatedStatBar(const char* str_id, float fraction, const char* overlay, ImU32 lowColor, ImU32 highColor); + +void BeginPopupFade(const char* str_id); +void EndPopupFade(); +void ResetPopupFade(const char* str_id); + bool IconButton(const char* icon, const ImVec2& size = {}, ImU32 tint = 0, bool enabled = true); bool IconButton(const char* icon, const char* text, ImU32 iconTint = 0, bool enabled = true); From fd8e1205bd4ced8ccbc351f3f061f99abf151c7b Mon Sep 17 00:00:00 2001 From: Fexty12573 Date: Sun, 28 Jun 2026 19:56:52 +0200 Subject: [PATCH 7/7] Remove ImAnim demo files from build --- CMakeLists.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a559360..46c0297 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -46,9 +46,6 @@ set(EXTERNAL_SOURCES # ImAnim external/ImAnim/im_anim.cpp - external/ImAnim/im_anim_demo.cpp - external/ImAnim/im_anim_doc.cpp - external/ImAnim/im_anim_usecase.cpp # ImPlot external/implot/implot.cpp