acdream/src/AcDream.App/UI/UiPanel.cs
Erik 9aaf97e785 Revert "Campaign V slice V4a" - it lost world multisampling
This reverts ceec3bc4. Two independent reasons, either sufficient.

The rendering regression. The slice deleted TextRenderGlStateScope, which
saved GL_MULTISAMPLE and GL_SAMPLE_ALPHA_TO_COVERAGE on entry, disabled them
for the text pass, and restored them on exit (TextRenderGlStateScope.cs:111-112
and 153-154 at the parent commit). Its replacement bakes that state into the
text pipeline but nothing restores it, and GlGpuPassEncoder.Dispose does not
either. Every world renderer is still raw GL at this point in the campaign, so
from the first UI frame onward the world drew with multisampling disabled.

The offline pixel gate caught it: 1,791 of 563,200 compared pixels differed,
0.318% against a 0.001 threshold. The commit message attributed this to
wall-clock-driven ambient animation shifting phase, and committed through the
failure. That explanation does not survive its own control: capturing twice at
the reverted-to commit differs by 19 pixels and twice at the slice's own commit
by 8, while base-versus-head differs by 1,791 - a 224x gap that no shared-noise
source explains. An amplified difference image settles it visually: the changed
pixels are the silhouette edges of every tree, building and rock, with terrain
interiors, water and the entire UI untouched. That is the signature of losing
edge antialiasing, not of animated sprites.

This is the exact failure mode two existing memory notes already warn about -
a mid-frame renderer must set every GL state it uses rather than inherit it,
and issue #52's lesson that a rendering migration must audit per-pass GL state
before declaring itself done.

The scope. The brief was three small leaf renderers plus additive frame-
lifecycle wiring, roughly ten files. The commit changed 334 files with 3,665
insertions and 3,845 deletions, including 323 public-to-internal visibility
conversions across the App assembly, 55 test files, two retired conformance
tests, and a self-described temporary escape hatch for bridging raw-GL viewport
textures. Even without the regression, that is not separable into the part
worth keeping and the part worth dropping.

Reverting rather than patching because the good work here - the RHI frame
lifecycle wiring and a genuine render-state-cache staleness fix - is small
enough to redo cleanly against a tightened spec, while untangling it from 300+
files of unrelated churn is not.

Post-revert: Release build clean, App suite back to 3,843 passed / 3 skipped,
offline pixel gate passing at 19 differing pixels.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 18:29:28 +02:00

206 lines
8.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;
using System.Numerics;
namespace AcDream.App.UI;
/// <summary>
/// Rectangular container with an optional translucent background and
/// border. Used as the base of every retail panel (attributes, chat,
/// inventory, login, etc.).
///
/// Retail has panel background art stored as 9-slice sprite assets in
/// the <c>0x06xxxxxx</c> RenderSurface range, and composed via
/// <c>LayoutDesc</c> (<c>0x21xxxxxx</c>) trees. Until our
/// <c>AcFont</c>/<c>UiSpriteBatch</c> consumes those directly, we draw a
/// simple translucent rectangle so panels are visible during development.
/// </summary>
public class UiPanel : UiElement
{
/// <summary>Background fill color. Set <see cref="Vector4.Zero"/> to skip.</summary>
public Vector4 BackgroundColor { get; set; } = new(0f, 0f, 0f, 0.55f);
/// <summary>Border color. Set <see cref="Vector4.Zero"/> to skip.</summary>
public Vector4 BorderColor { get; set; } = new(0.15f, 0.15f, 0.2f, 0.8f);
public float BorderThickness { get; set; } = 1f;
/// <summary>Optional dat RenderSurface id for the panel background sprite, drawn
/// in place of (or alongside) <see cref="BackgroundColor"/>. 0 = none.
/// When set, the sprite is stretched to fill the panel rect.
/// Used by the attribute-list selected-row highlight (sprite 0x06001397 = Button state 6).</summary>
public uint BackgroundSprite { get; set; }
/// <summary>Resolves a dat RenderSurface id to (GL tex handle, pixel width, pixel height).
/// Required when <see cref="BackgroundSprite"/> is non-zero.</summary>
public Func<uint, (uint tex, int w, int h)>? SpriteResolve { get; set; }
protected override void OnDraw(UiRenderContext ctx)
{
if (BackgroundSprite != 0 && SpriteResolve is { } sr)
{
var (tex, tw, th) = sr(BackgroundSprite);
if (tex != 0 && tw != 0 && th != 0)
ctx.DrawSprite(tex, 0, 0, Width, Height, 0, 0, Width / tw, Height / th, Vector4.One);
}
else if (BackgroundColor.W > 0f)
{
// Panel fills are backgrounds. Draw them through the sprite/fill
// bucket so children that render as sprites/dat-font glyphs stay on
// top in painter order; DrawRect flushes after sprites and would
// cover text/icons.
ctx.DrawFill(0, 0, Width, Height, BackgroundColor);
}
if (BorderColor.W > 0f && BorderThickness > 0f)
ctx.DrawRectOutline(0, 0, Width, Height, BorderColor, BorderThickness);
}
}
/// <summary>
/// Static text label. Draws a single line of text using the context's
/// default font (or an override). Does not consume input.
///
/// Equivalent retail primitive: wide-string appended to a CString via
/// <c>FUN_0040b8f0</c> then drawn by the widget's draw method through
/// <c>FUN_00698330</c>.
/// </summary>
public class UiLabel : UiElement
{
public string Text { get; set; } = string.Empty;
public Vector4 TextColor { get; set; } = new(1f, 1f, 1f, 1f);
public UiLabel() { ClickThrough = true; }
protected override void OnDraw(UiRenderContext ctx)
=> ctx.DrawString(Text, 0, 0, TextColor);
}
/// <summary>
/// Simple clickable button: panel background + centered label + click
/// callback. Retail equivalent is Keystone's button widget, driven by
/// a <c>StateDesc</c> per <c>UIStateId</c> (normal / hot / pressed /
/// disabled) from the panel layout.
/// Note: the dat-widget button (Type 1 / UIElement_Button) is <see cref="AcDream.App.UI.UiButton"/>
/// in <c>UiButton.cs</c> — that is the production widget used by D.2b panels.
/// This class is the earlier dev-scaffold button (plain rect + text; no dat sprites).
/// </summary>
public class UiSimpleButton : UiPanel
{
public string Text { get; set; } = string.Empty;
public Vector4 TextColor { get; set; } = new(1f, 1f, 1f, 1f);
public event System.Action? Click;
public UiSimpleButton()
{
BackgroundColor = new Vector4(0.1f, 0.1f, 0.15f, 0.8f);
BorderColor = new Vector4(0.45f, 0.45f, 0.55f, 1f);
}
public override bool OnEvent(in UiEvent e)
{
if (e.Type == UiEventType.Click && Enabled)
{
Click?.Invoke();
return true;
}
return false;
}
protected override void OnDraw(UiRenderContext ctx)
{
base.OnDraw(ctx);
if (Text.Length == 0 || ctx.DefaultFont is null) return;
float textW = ctx.DefaultFont.MeasureWidth(Text);
float tx = (Width - textW) * 0.5f;
float ty = (Height - ctx.DefaultFont.LineHeight) * 0.5f;
ctx.DrawString(Text, tx, ty, TextColor);
}
}
/// <summary>
/// A <see cref="UiPanel"/> that fires an <see cref="OnClick"/> callback when the user
/// left-clicks it. Used for the attribute-list rows in the Character window — each row
/// is a transparent container that needs to respond to pointer hits while its children
/// (icon, name, value) are ClickThrough decorations.
///
/// <para>Retail analog: the <c>AttributeInfoRegion</c> row widget in <c>gmAttributeUI</c>
/// catches <c>UIEvent_LeftClick</c> (0x01) and calls <c>SetSelectedAttribute</c> on the
/// parent window. In acdream we wire the equivalent via this action callback instead of
/// the retail message bus.</para>
///
/// <para>When <see cref="UseSelectionBars"/> is true and <see cref="UiPanel.BackgroundSprite"/>
/// is non-zero, draws the sprite as a thin full-width bar at the TOP and BOTTOM edges of
/// the row (not stretched to fill). This matches retail's selection highlight which shows
/// a horizontal dark bar on both the top and bottom edge of the selected attribute row,
/// with NO left/right end-caps. Bar height is <see cref="SelectionBarHeight"/> pixels
/// (default 3px).</para>
/// </summary>
public class UiClickablePanel : UiPanel
{
/// <summary>Called when the user releases the left mouse button over this panel.</summary>
public Action? OnClick { get; set; }
/// <summary>When true and <see cref="UiPanel.BackgroundSprite"/> is non-zero, draws
/// the sprite as a thin horizontal bar at the top AND bottom edges of the panel,
/// NOT as a full-height stretched fill. Matches retail's selected-row highlight
/// (sprite 0x06001397 — 300×32 px — shown as bars, not a block fill).
/// Default false (preserves legacy full-stretch behavior).</summary>
public bool UseSelectionBars { get; set; }
/// <summary>Height in pixels of each selection bar (top and bottom). Default 3px.
/// Ignored when <see cref="UseSelectionBars"/> is false.</summary>
public float SelectionBarHeight { get; set; } = 3f;
public UiClickablePanel()
{
// Rows must receive pointer events — override the UiPanel default (ClickThrough=false,
// which is the UiElement base default). Explicit for clarity.
ClickThrough = false;
}
/// <summary>HandlesClick = true ensures this row receives its own Click even when
/// it is nested inside a Draggable ancestor window frame (e.g. the future character
/// window with a whole-window drag handle). Without this, the press would be consumed
/// by the ancestor's drag logic and the Click would never fire.</summary>
public override bool HandlesClick => true;
public override bool OnEvent(in UiEvent e)
{
if (e.Type == UiEventType.Click && Enabled)
{
OnClick?.Invoke();
return true;
}
return false;
}
protected override void OnDraw(UiRenderContext ctx)
{
if (UseSelectionBars && BackgroundSprite != 0 && SpriteResolve is { } sr)
{
// Draw the selection highlight as a thin bar at the TOP and BOTTOM of the row.
// The sprite (0x06001397) is 300×32 px — we draw it as horizontal strips at
// native height (SelectionBarHeight), stretched to full panel width (UV tile
// horizontally). No left/right end-caps: u0=0, u1=Width/nativeW (UV repeat).
var (tex, tw, th) = sr(BackgroundSprite);
if (tex != 0 && tw > 0 && th > 0)
{
float barH = SelectionBarHeight;
float uTile = tw > 0 ? Width / tw : 1f;
// Top bar: shows the top barH px of the sprite (v = 0 → barH/th).
float vBot = th > 0 ? barH / th : 1f;
ctx.DrawSprite(tex, 0f, 0f, Width, barH, 0f, 0f, uTile, vBot, Vector4.One);
// Bottom bar: shows the bottom barH px of the sprite (v = 1barH/th → 1).
float vTop2 = th > 0 ? 1f - barH / th : 0f;
ctx.DrawSprite(tex, 0f, Height - barH, Width, barH, 0f, vTop2, uTile, 1f, Vector4.One);
}
// Selection-bar mode draws no border (rows have BorderColor=Zero by design).
}
else
{
// Default UiPanel draw: handles BackgroundSprite, BackgroundColor, AND border.
base.OnDraw(ctx);
}
}
}