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