From bb973a1007750e4af0ccc2c8a0f71cf6b8974d26 Mon Sep 17 00:00:00 2001 From: DanS Date: Sat, 10 Oct 2026 09:50:03 -0500 Subject: [PATCH] perf(ui): skip the scroll-edge mask when no edge fades ApplyScrollEdgeMask, which fades a scroll area's content at its top and bottom edges, walked every vertex the area emitted on every frame and tested both edges per vertex, even when the content fit without scrolling and no vertex could ever be written. It now returns before the walk when neither edge fades. The fade maths moved unchanged into scroll_edge_fade.h and is tested against the old inline expression, so rendering is identical. All six callers benefit without an edit: the Overview address list, Mining's recent blocks, Console, History, Explorer and Peers. Co-Authored-By: Claude Opus 5.5 --- src/ui/material/draw_helpers.h | 21 +++++-------- src/ui/material/scroll_edge_fade.h | 47 ++++++++++++++++++++++++++++++ tests/test_phase4.cpp | 35 ++++++++++++++++++++++ 3 files changed, 89 insertions(+), 14 deletions(-) create mode 100644 src/ui/material/scroll_edge_fade.h diff --git a/src/ui/material/draw_helpers.h b/src/ui/material/draw_helpers.h index 9dadfe4..900764e 100644 --- a/src/ui/material/draw_helpers.h +++ b/src/ui/material/draw_helpers.h @@ -7,6 +7,7 @@ #include "colors.h" #include "type.h" #include "tooltip_style.h" +#include "scroll_edge_fade.h" #include "../layout.h" #include "../schema/element_styles.h" #include "../schema/color_var_resolver.h" @@ -1149,23 +1150,15 @@ inline void ApplyScrollEdgeMask(ImDrawList* parentDL, int parentVtxStart, float fadeZone, float scrollY, float scrollMaxY) { + // Neither edge fades (not scrolled down and nothing below / not scrollable): every alpha would stay 1.0 + // and no vertex would be written, so skip the whole vertex walk. + const ScrollEdgeFade fade = ComputeScrollEdgeFade(scrollY, scrollMaxY); + if (!fade.any()) return; + auto mask = [&](ImDrawList* dl, int startIdx) { for (int vi = startIdx; vi < dl->VtxBuffer.Size; vi++) { ImDrawVert& v = dl->VtxBuffer[vi]; - float alpha = 1.0f; - - // Top fade — only when scrolled down - if (scrollY > 1.0f) { - float d = v.pos.y - topEdge; - if (d < fadeZone) - alpha = std::min(alpha, std::max(0.0f, d / fadeZone)); - } - // Bottom fade — only when not at scroll bottom - if (scrollMaxY > 0 && scrollY < scrollMaxY - 1.0f) { - float d = bottomEdge - v.pos.y; - if (d < fadeZone) - alpha = std::min(alpha, std::max(0.0f, d / fadeZone)); - } + const float alpha = ScrollEdgeAlpha(fade, v.pos.y, topEdge, bottomEdge, fadeZone); if (alpha < 1.0f) { int a = (v.col >> IM_COL32_A_SHIFT) & 0xFF; diff --git a/src/ui/material/scroll_edge_fade.h b/src/ui/material/scroll_edge_fade.h new file mode 100644 index 0000000..fe5cc6c --- /dev/null +++ b/src/ui/material/scroll_edge_fade.h @@ -0,0 +1,47 @@ +// DragonX Wallet - ImGui Edition +// Copyright 2024-2026 The Hush Developers +// Released under the GPLv3 +// +// Pure maths of ApplyScrollEdgeMask (draw_helpers.h), split out so it is unit-testable without ImGui. +#pragma once + +#include + +namespace dragonx { +namespace ui { +namespace material { + +struct ScrollEdgeFade { + bool top = false; // content scrolled off the top edge + bool bottom = false; // more content below the bottom edge + bool any() const { return top || bottom; } +}; + +inline ScrollEdgeFade ComputeScrollEdgeFade(float scrollY, float scrollMaxY) +{ + ScrollEdgeFade f; + f.top = scrollY > 1.0f; // only when scrolled down + f.bottom = scrollMaxY > 0 && scrollY < scrollMaxY - 1.0f; // only when not at the scroll bottom + return f; +} + +// Alpha multiplier (0..1) for a vertex at screen-y `posY`. Same maths as the former inline loop. +inline float ScrollEdgeAlpha(const ScrollEdgeFade& f, float posY, float topEdge, float bottomEdge, float fadeZone) +{ + float alpha = 1.0f; + if (f.top) { + float d = posY - topEdge; + if (d < fadeZone) + alpha = std::min(alpha, std::max(0.0f, d / fadeZone)); + } + if (f.bottom) { + float d = bottomEdge - posY; + if (d < fadeZone) + alpha = std::min(alpha, std::max(0.0f, d / fadeZone)); + } + return alpha; +} + +} // namespace material +} // namespace ui +} // namespace dragonx diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index 2c3b470..3dfb1c1 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -59,6 +59,7 @@ #include "ui/windows/mining_benchmark.h" #include "ui/windows/mining_pool_panel.h" #include "ui/windows/mining_tab_helpers.h" +#include "ui/material/scroll_edge_fade.h" #include "ui/node_status_banner.h" #include "ui/staleness_badge.h" #include "ui/notifications.h" @@ -6383,6 +6384,9 @@ void testMiningEarningsCache() EXPECT_FALSE(IntervalElapsed(1199, 200, 1000)); EXPECT_TRUE(IntervalElapsed(1200, 200, 1000)); } +// Local-midnight rollover with the tx list untouched (same tx_revision / count) and inside the 2 s max age: +// LocalDayStartMemo recomputes the day start as soon as `now` changes, the day start is part of the memo key, +// so "Today" empties into "Yesterday" at local midnight. Fixed POSIX TZ (UTC+3, no DST, no tzdata), restored. void testMiningEarningsMidnightRollover() { #ifndef _WIN32 @@ -6418,6 +6422,36 @@ void testMiningEarningsMidnightRollover() #endif } +void testScrollEdgeFade() +{ + using namespace dragonx::ui::material; + EXPECT_FALSE(ComputeScrollEdgeFade(0.0f, 0.0f).any()); // not scrollable + EXPECT_FALSE(ComputeScrollEdgeFade(1.0f, 0.0f).any()); // scrollY must be > 1 + EXPECT_TRUE(ComputeScrollEdgeFade(100.0f, 100.0f).top); // at the bottom: top fade only + EXPECT_FALSE(ComputeScrollEdgeFade(100.0f, 100.0f).bottom); + EXPECT_TRUE(ComputeScrollEdgeFade(0.0f, 200.0f).bottom); // top of a scrollable list: bottom fade only + EXPECT_FALSE(ComputeScrollEdgeFade(0.0f, 200.0f).top); + EXPECT_FALSE(ComputeScrollEdgeFade(199.5f, 200.0f).bottom); // within 1 px of the bottom + // Bit-equivalence with the former inline maths over a grid, and the early-out invariant. + auto oldAlpha = [](float scrollY, float scrollMaxY, float y, float topEdge, float bottomEdge, float fz) { + float alpha = 1.0f; + if (scrollY > 1.0f) { float d = y - topEdge; if (d < fz) alpha = std::min(alpha, std::max(0.0f, d / fz)); } + if (scrollMaxY > 0 && scrollY < scrollMaxY - 1.0f) { + float d = bottomEdge - y; if (d < fz) alpha = std::min(alpha, std::max(0.0f, d / fz)); + } + return alpha; + }; + const float states[][2] = {{0, 0}, {0, 300}, {0.5f, 300}, {1.0f, 300}, {1.5f, 300}, {150, 300}, + {298.9f, 300}, {299.5f, 300}, {300, 300}, {50, 0}}; + for (const auto& s : states) + for (float y = -20.0f; y <= 420.0f; y += 7.0f) { + const ScrollEdgeFade f = ComputeScrollEdgeFade(s[0], s[1]); + const float a = ScrollEdgeAlpha(f, y, 10.0f, 360.0f, 40.0f); + EXPECT_NEAR(a, oldAlpha(s[0], s[1], y, 10.0f, 360.0f, 40.0f), 0.0); + if (!f.any()) EXPECT_NEAR(a, 1.0, 0.0); + } +} + void testMiningBenchmarkModel() { dragonx::ui::ThreadBenchmark benchmark; @@ -14748,6 +14782,7 @@ int main() testMiningEarningsCore(); testMiningEarningsCache(); testMiningEarningsMidnightRollover(); + testScrollEdgeFade(); testBalanceAddressListModel(); testExplorerBlockCache(); testTransactionHistoryCache();