acdream/src/AcDream.App/UI/UiNineSlicePanel.cs
Erik 1bd2b30291
All checks were successful
CI / linux-portable (push) Successful in 3m16s
CI / windows-gate (push) Successful in 5m41s
CI / release (push) Successful in 2m5s
fix(ui): restore retail vitals and window interactions
2026-08-20 13:26:35 +02:00

130 lines
5.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
/// <summary>
/// Whether this wrapper paints the shared center surface. Some retail child
/// panels already author that same full-height surface; their standalone
/// wrapper supplies only the border so translucency is not compounded.
/// </summary>
public bool DrawCenterFill { get; set; } = true;
/// <summary>
/// Whether to paint the live gold resize edges and corner studs over the
/// ordinary retail bevel. UI lock disables only this affordance layer;
/// the frame and optional center fill remain visible.
/// </summary>
public bool DrawResizeAffordances { get; set; } = true;
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);
if (DrawCenterFill)
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);
if (!DrawResizeAffordances)
return;
// Resize-grip overlay (gold ridged edges + square corner studs) on top of the
// bevel — the second border layer the vitals LayoutDesc carries (0x1000063B0x10000642).
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);
}
}