UiElement.MaxHeight + ResizableEdges mask; UiCollapsibleFrame snaps height to the nearer
of {collapsed,expanded} and toggles row-2 visibility; GameWindow computes the two heights
from the layout + top-anchors the content. Amends IA-17. UiNineSlicePanel unsealed to
allow subclassing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
112 lines
5.2 KiB
C#
112 lines
5.2 KiB
C#
using System.Numerics;
|
||
|
||
namespace AcDream.App.UI;
|
||
|
||
/// <summary>
|
||
/// A <see cref="UiPanel"/> whose background is the retail 8-piece window bevel
|
||
/// (<see cref="RetailChromeSprites"/>): 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:
|
||
/// <c>id => { var t = cache.GetOrUploadRenderSurface(id, out var w, out var h); return (t, w, h); }</c>.
|
||
/// </summary>
|
||
public class UiNineSlicePanel : UiPanel
|
||
{
|
||
/// <summary>A placed chrome piece: destination rect in local pixel space.</summary>
|
||
public readonly record struct Rect(float X, float Y, float W, float H);
|
||
|
||
/// <summary>The nine destination rects for an 8-piece border + center.</summary>
|
||
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<uint, (uint tex, int w, int h)> _resolve;
|
||
|
||
public UiNineSlicePanel(System.Func<uint, (uint, int, int)> resolve)
|
||
{
|
||
_resolve = resolve;
|
||
BackgroundColor = Vector4.Zero; // suppress the base flat-rect fill
|
||
BorderColor = Vector4.Zero;
|
||
Draggable = true; // retail windows are movable
|
||
Resizable = true; // retail windows are resizable
|
||
// A top-level window is USER-positioned: it must NOT be anchor-managed
|
||
// by its parent (UiRoot), or the per-frame anchor pass would reset its
|
||
// Left/Top/Width/Height every frame and undo move/resize. Children
|
||
// INSIDE the window still anchor to it (the bars stretch with width).
|
||
Anchors = AnchorEdges.None;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Destination rects (local px) for a frame of (<paramref name="w"/>,
|
||
/// <paramref name="h"/>) with border thickness <paramref name="b"/>:
|
||
/// 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.
|
||
/// </summary>
|
||
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)
|
||
{
|
||
// Center fill is the window BACKGROUND — it must sit UNDER the content, so it
|
||
// draws here (before children). The bevel border + grip is the OUTERMOST layer
|
||
// and draws in OnDrawAfterChildren (over the content's edges) so content can
|
||
// never poke through the frame (e.g. the toolbar's 2px bottom-right cap overhang).
|
||
var r = ComputeFrameRects(Width, Height, RetailChromeSprites.Border);
|
||
DrawTiled(ctx, RetailChromeSprites.CenterFill, r.Center);
|
||
}
|
||
|
||
protected override void OnDrawAfterChildren(UiRenderContext ctx)
|
||
{
|
||
var r = ComputeFrameRects(Width, Height, RetailChromeSprites.Border);
|
||
// 8-piece bevel: edges tile (UV repeat); corners stretch 1:1.
|
||
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);
|
||
|
||
// Resize-grip overlay (gold ridged edges + square corner studs) on top of the
|
||
// bevel — the second border layer the vitals LayoutDesc carries (0x1000063B–0x10000642).
|
||
DrawTiled(ctx, RetailChromeSprites.GripTop, r.Top);
|
||
DrawTiled(ctx, RetailChromeSprites.GripBottom, r.Bottom);
|
||
DrawTiled(ctx, RetailChromeSprites.GripLeft, r.Left);
|
||
DrawTiled(ctx, RetailChromeSprites.GripRight, r.Right);
|
||
DrawStretched(ctx, RetailChromeSprites.GripCorner, r.TL);
|
||
DrawStretched(ctx, RetailChromeSprites.GripCorner, r.TR);
|
||
DrawStretched(ctx, RetailChromeSprites.GripCorner, r.BL);
|
||
DrawStretched(ctx, RetailChromeSprites.GripCorner, 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);
|
||
}
|
||
}
|