diff --git a/src/AcDream.App/UI/UiNineSlicePanel.cs b/src/AcDream.App/UI/UiNineSlicePanel.cs new file mode 100644 index 00000000..2f04229a --- /dev/null +++ b/src/AcDream.App/UI/UiNineSlicePanel.cs @@ -0,0 +1,85 @@ +using System.Numerics; + +namespace AcDream.App.UI; + +/// +/// A whose background is the retail 8-piece window bevel +/// (): 4 corners + 4 edges around a tiled +/// center fill. Retires the flat translucent rect (divergence row TS-30). +/// Sprites resolve to (GL handle, width, height) via an injected delegate so +/// the widget is testable without GL. In production: +/// id => { var t = cache.GetOrUploadRenderSurface(id, out var w, out var h); return (t, w, h); }. +/// +public sealed class UiNineSlicePanel : UiPanel +{ + /// A placed chrome piece: destination rect in local pixel space. + public readonly record struct Rect(float X, float Y, float W, float H); + + /// The nine destination rects for an 8-piece border + center. + public readonly record struct FrameRects( + Rect Center, Rect Top, Rect Bottom, Rect Left, Rect Right, + Rect TL, Rect TR, Rect BL, Rect BR); + + private readonly System.Func _resolve; + + public UiNineSlicePanel(System.Func resolve) + { + _resolve = resolve; + BackgroundColor = Vector4.Zero; // suppress the base flat-rect fill + BorderColor = Vector4.Zero; + } + + /// + /// Destination rects (local px) for a frame of (, + /// ) with border thickness : + /// b×b corners, top/bottom edges spanning the interior width at height b, + /// left/right edges spanning the interior height at width b, center fills + /// the interior. + /// + public static FrameRects ComputeFrameRects(float w, float h, int b) + { + float innerW = w - 2 * b; + float innerH = h - 2 * b; + return new FrameRects( + Center: new Rect(b, b, innerW, innerH), + Top: new Rect(b, 0, innerW, b), + Bottom: new Rect(b, h - b, innerW, b), + Left: new Rect(0, b, b, innerH), + Right: new Rect(w - b, b, b, innerH), + TL: new Rect(0, 0, b, b), + TR: new Rect(w - b, 0, b, b), + BL: new Rect(0, h - b, b, b), + BR: new Rect(w - b, h - b, b, b)); + } + + protected override void OnDraw(UiRenderContext ctx) + { + var r = ComputeFrameRects(Width, Height, RetailChromeSprites.Border); + // center + edges tile (UV repeat); corners stretch 1:1. + DrawTiled(ctx, RetailChromeSprites.CenterFill, r.Center); + DrawTiled(ctx, RetailChromeSprites.TopEdge, r.Top); + DrawTiled(ctx, RetailChromeSprites.BottomEdge, r.Bottom); + DrawTiled(ctx, RetailChromeSprites.LeftEdge, r.Left); + DrawTiled(ctx, RetailChromeSprites.RightEdge, r.Right); + DrawStretched(ctx, RetailChromeSprites.CornerTL, r.TL); + DrawStretched(ctx, RetailChromeSprites.CornerTR, r.TR); + DrawStretched(ctx, RetailChromeSprites.CornerBL, r.BL); + DrawStretched(ctx, RetailChromeSprites.CornerBR, r.BR); + } + + private void DrawTiled(UiRenderContext ctx, uint id, Rect d) + { + if (d.W <= 0 || d.H <= 0) return; + var (tex, tw, th) = _resolve(id); + if (tex == 0 || tw == 0 || th == 0) return; + ctx.DrawSprite(tex, d.X, d.Y, d.W, d.H, 0, 0, d.W / tw, d.H / th, Vector4.One); + } + + private void DrawStretched(UiRenderContext ctx, uint id, Rect d) + { + if (d.W <= 0 || d.H <= 0) return; + var (tex, _, _) = _resolve(id); + if (tex == 0) return; + ctx.DrawSprite(tex, d.X, d.Y, d.W, d.H, 0, 0, 1, 1, Vector4.One); + } +} diff --git a/tests/AcDream.App.Tests/UI/UiNineSlicePanelTests.cs b/tests/AcDream.App.Tests/UI/UiNineSlicePanelTests.cs new file mode 100644 index 00000000..8a2b3d0a --- /dev/null +++ b/tests/AcDream.App.Tests/UI/UiNineSlicePanelTests.cs @@ -0,0 +1,27 @@ +using AcDream.App.UI; + +namespace AcDream.App.Tests.UI; + +public class UiNineSlicePanelTests +{ + [Fact] + public void ComputeFrameRects_PlacesCornersEdgesAndCenter() + { + var r = UiNineSlicePanel.ComputeFrameRects(100, 80, 5); + + // 5x5 corners at the four corners + Assert.Equal(new UiNineSlicePanel.Rect(0, 0, 5, 5), r.TL); + Assert.Equal(new UiNineSlicePanel.Rect(95, 0, 5, 5), r.TR); + Assert.Equal(new UiNineSlicePanel.Rect(0, 75, 5, 5), r.BL); + Assert.Equal(new UiNineSlicePanel.Rect(95, 75, 5, 5), r.BR); + + // edges span the interior (100-2*5 = 90 wide, 80-2*5 = 70 tall) + Assert.Equal(new UiNineSlicePanel.Rect(5, 0, 90, 5), r.Top); + Assert.Equal(new UiNineSlicePanel.Rect(5, 75, 90, 5), r.Bottom); + Assert.Equal(new UiNineSlicePanel.Rect(0, 5, 5, 70), r.Left); + Assert.Equal(new UiNineSlicePanel.Rect(95, 5, 5, 70), r.Right); + + // center fills the interior + Assert.Equal(new UiNineSlicePanel.Rect(5, 5, 90, 70), r.Center); + } +}