diff --git a/src/ui/effects/border_sampling.h b/src/ui/effects/border_sampling.h new file mode 100644 index 0000000..bff4e7e --- /dev/null +++ b/src/ui/effects/border_sampling.h @@ -0,0 +1,46 @@ +#pragma once +#include +#include + +namespace dragonx { namespace ui { namespace effects { + +/// Splits a rounded-rect border into line segments and writes the ascending perimeter fractions +/// out[0]=0 .. out[n]=1 (same parametrisation and piece order as ThemeEffects::perimeterPoint: +/// top, TR arc, right, BR arc, bottom, BL arc, left, TL arc). Straight edges get one segment per +/// `edgeStepPx` (<= 24 per edge); each corner arc gets clamp(ceil(r*0.4),3,8) chords, i.e. a +/// worst-case sag below 0.2 px. Returns n (>=1); needs maxSegs >= 128 + 1 entries in `out`. +inline int BorderSampleFractions(float w, float h, float r, float edgeStepPx, + float* out, int maxOut /* capacity of out, >= 129 */) { + constexpr float kPi = 3.14159265f; + out[0] = 0.0f; + if (w <= 0.0f || h <= 0.0f || maxOut < 129) { out[1] = 1.0f; return 1; } + r = std::min(r, std::min(w, h) * 0.5f); + if (r < 0.5f) r = 0.0f; // perimeterPoint treats tiny rounding as none + const float step = std::max(1.0f, edgeStepPx); + float len[8]; int segs[8]; int np; + auto edge = [&](float l) { return l <= 0.0f ? 0 : std::clamp((int)std::ceil(l / step), 1, 24); }; + if (r <= 0.0f) { + np = 4; + len[0] = w; len[1] = h; len[2] = w; len[3] = h; + for (int i = 0; i < 4; ++i) segs[i] = edge(len[i]); + } else { + np = 8; + const float arc = kPi * r * 0.5f, ew = w - 2.0f * r, eh = h - 2.0f * r; + const int arcSegs = std::clamp((int)std::ceil(r * 0.4f), 3, 8); + const float L[8] = { ew, arc, eh, arc, ew, arc, eh, arc }; + for (int i = 0; i < 8; ++i) { len[i] = L[i]; segs[i] = (i & 1) ? arcSegs : edge(L[i]); } + } + float perim = 0.0f; for (int i = 0; i < np; ++i) perim += len[i]; + int n = 0; float d0 = 0.0f; + for (int i = 0; i < np; ++i) { + for (int s = 1; s <= segs[i]; ++s) { + out[++n] = (d0 + len[i] * (float)s / (float)segs[i]) / perim; + } + d0 += len[i]; + } + if (n < 1) { out[1] = 1.0f; return 1; } + out[n] = 1.0f; + return n; // worst case 4*24 + 4*8 = 128 +} + +}}} // namespace diff --git a/src/ui/effects/theme_effects.cpp b/src/ui/effects/theme_effects.cpp index 63cb8ff..da257d6 100644 --- a/src/ui/effects/theme_effects.cpp +++ b/src/ui/effects/theme_effects.cpp @@ -4,6 +4,7 @@ #include "theme_effects.h" #include "low_spec.h" +#include "border_sampling.h" #include "effect_clock.h" #include "../schema/ui_schema.h" #include "../layout.h" @@ -294,31 +295,25 @@ void ThemeEffects::drawRainbowBorder(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax, float rounding, float thickness) const { if (!enabled_ || !rainbow_border_.enabled || rainbow_border_.stops.empty()) return; + // Final line alpha is rainbow_border_.alpha * bgOpacity_ (sampleGradient); under 1/255 nothing shows. + if (rainbow_border_.alpha * bgOpacity_ * 255.0f < 1.0f) return; + float t = cyclePhase(time_, rainbow_border_.speed); - // Draw the rainbow border as line segments around the rounded perimeter. - // Each segment gets a color sampled from the gradient at its position, - // giving a smoothly rotating hue-shift that follows rounded corners. - // - // We need enough segments so that the curved corners are smooth. - // Each corner arc ≈ π*r/2 pixels. We want ~1 segment per 2-3px of arc - // to look smooth, plus the straight edges. + // The border is drawn as line segments whose colour is sampled from the gradient at the segment + // midpoint. Straight edges change colour slowly (stops span a third of the perimeter at alpha + // ~0.08), so they get one segment per ~32 dp; corner arcs get a few chords each so they stay round. + const float dp = Layout::dpiScale(); float w = pMax.x - pMin.x; float h = pMax.y - pMin.y; - float r = std::min(rounding, std::min(w, h) * 0.5f); - float cornerSegs = (r > 0.5f) ? std::ceil(r * 1.5f) : 0.0f; // segments per corner arc - int segments = std::max(48, (int)(4 * cornerSegs + 2 * (w + h) / 8.0f)); - segments = std::min(segments, 256); // cap for perf + float fr[129]; + const int segments = BorderSampleFractions(w, h, std::min(rounding, std::min(w, h) * 0.5f), + 32.0f * dp, fr, 129); ImVec2 prev = perimeterPoint(pMin, pMax, 0.0f, rounding); for (int i = 1; i <= segments; i++) { - float frac = (float)i / (float)segments; - ImVec2 pt = perimeterPoint(pMin, pMax, frac, rounding); - - // Sample gradient at the midpoint of this segment for smooth color - float midFrac = (frac - 0.5f / segments); - ImU32 c = sampleGradient(t + midFrac); - + ImVec2 pt = perimeterPoint(pMin, pMax, fr[i], rounding); + ImU32 c = sampleGradient(t + 0.5f * (fr[i - 1] + fr[i])); // colour at the segment midpoint dl->AddLine(prev, pt, c, thickness); prev = pt; } diff --git a/src/ui/material/draw_helpers.h b/src/ui/material/draw_helpers.h index c22275e..c514ad3 100644 --- a/src/ui/material/draw_helpers.h +++ b/src/ui/material/draw_helpers.h @@ -8,6 +8,7 @@ #include "type.h" #include "tooltip_style.h" #include "scroll_edge_fade.h" +#include "rect_cull.h" #include "../layout.h" #include "../schema/element_styles.h" #include "../schema/color_var_resolver.h" @@ -694,10 +695,22 @@ inline void DrawCardDropShadow(ImDrawList* dl, const ImVec2& pMin, const ImVec2& dl->PopClipRect(); } +// True when a card lies wholly outside the draw list's current clip rect (a scrolled-out panel): +// everything it would draw is scissored away, except the theme effects, which go to the unclipped +// foreground list. The margin covers the light-theme drop shadow (reach 6 dp) and the border. +inline bool CardOutsideClip(const ImDrawList* dl, const ImVec2& pMin, const ImVec2& pMax) +{ + if (dl->_ClipRectStack.Size == 0) return false; + return RectOutsideRect(pMin, pMax, dl->GetClipRectMin(), dl->GetClipRectMax(), + 16.0f * Layout::dpiScale()); +} + inline void DrawGlassPanel(ImDrawList* dl, const ImVec2& pMin, const ImVec2& pMax, const GlassPanelSpec& spec = GlassPanelSpec()) { + if (CardOutsideClip(dl, pMin, pMax)) return; // scrolled out of view: nothing visible to draw + // Subtle drop shadow behind the card (light themes only). Drawn first so the card fill below // covers the interior, leaving only the outer soft blur — the card body is never contaminated. if (CardDropShadowWanted()) diff --git a/src/ui/material/rect_cull.h b/src/ui/material/rect_cull.h new file mode 100644 index 0000000..29e2440 --- /dev/null +++ b/src/ui/material/rect_cull.h @@ -0,0 +1,10 @@ +#pragma once +#include "imgui.h" +namespace dragonx { namespace ui { namespace material { +/// True when [aMin,aMax] grown by `margin` does not intersect [bMin,bMax]. +inline bool RectOutsideRect(const ImVec2& aMin, const ImVec2& aMax, + const ImVec2& bMin, const ImVec2& bMax, float margin) { + return aMax.x + margin < bMin.x || aMin.x - margin > bMax.x || + aMax.y + margin < bMin.y || aMin.y - margin > bMax.y; +} +}}} diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index 403eefb..822a943 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -70,6 +70,8 @@ #include "ui/node_status_banner.h" #include "ui/staleness_badge.h" #include "ui/effects/effect_clock.h" +#include "ui/effects/border_sampling.h" +#include "ui/material/rect_cull.h" #include "ui/notifications.h" #include "data/seed_migration_resume.h" #include "data/pin_vault_policy.h" @@ -13513,6 +13515,30 @@ void testEffectClock() { EXPECT_NEAR(radSin(1e6, 0.0, 1.5707963267948966), 1.0f, 1e-6f); } +void testBorderSampling() { + using namespace dragonx::ui::effects; float fr[129]; + int n = BorderSampleFractions(500, 150, 8, 32, fr, 129); + EXPECT_TRUE(n >= 40 && n <= 80); // old formula: ~210 + EXPECT_EQ(fr[0], 0.0f); EXPECT_EQ(fr[n], 1.0f); + for (int i = 1; i <= n; ++i) EXPECT_TRUE(fr[i] > fr[i - 1]); + n = BorderSampleFractions(3000, 800, 12, 32, fr, 129); EXPECT_TRUE(n <= 128); // old: 256 + n = BorderSampleFractions(300, 100, 0, 32, fr, 129); EXPECT_TRUE(n >= 4 && n <= 40); // r=0 rectangle + // each corner arc keeps >= 3 chords: top-right arc spans [s1/P, s2/P] + { float r = 12, w = 400, h = 120, P = 2*(w-2*r) + 2*(h-2*r) + 4*(3.14159265f*r*0.5f); + n = BorderSampleFractions(w, h, r, 32, fr, 129); + float lo = (w-2*r)/P, hi = lo + 3.14159265f*r*0.5f/P; int c = 0; + for (int i = 1; i <= n; ++i) if (fr[i] > lo + 1e-5f && fr[i] <= hi + 1e-5f) ++c; + EXPECT_TRUE(c >= 3); } + n = BorderSampleFractions(0, 50, 4, 32, fr, 129); EXPECT_EQ(n, 1); // degenerate +} +void testRectCull() { + using namespace dragonx::ui::material; ImVec2 a(0,0), b(100,100); + EXPECT_TRUE(!RectOutsideRect(ImVec2(10,10), ImVec2(50,50), a, b, 0)); + EXPECT_TRUE(RectOutsideRect(ImVec2(0,150), ImVec2(80,250), a, b, 16)); // below + EXPECT_TRUE(!RectOutsideRect(ImVec2(0,110), ImVec2(80,250), a, b, 16)); // inside margin + EXPECT_TRUE(RectOutsideRect(ImVec2(-90,0), ImVec2(-20,50), a, b, 16)); // left +} + // The effective pool list = official pools ∪ saved favorites ∪ current custom pool, // endpoint-deduped, with synthetic user rows flagged official=false. void testEffectivePools() @@ -18249,6 +18275,8 @@ int main() testPoolFeeParsing(); testEffectivePools(); testEffectClock(); + testBorderSampling(); + testRectCull(); testFormatFeePercent(); testPoolWeightedSelection(); testCoalescingWriter();