Applies docs/research/2026-08-10-ch6ab-review-findings.md in full:
- BLOCKER 1: UiResizeGrip now carries its ElementInfo/resolve pair and
draws its own authored DirectState media (a synthetic parameterless
grip still draws nothing, preserving existing resize-drag tests).
DatWidgetFactory.BuildResizeGrip threads resolve through. All seven
live grips on the main chat window now resolve a non-zero sprite,
restoring the visible borders/corners CH6a silently dropped.
- SHOULD-FIX 2: ChatWindowState gains BroadcastTargetWindow, a sentinel
distinct from every real window id (0-4), fixing the bug where the
main window's explicit-addressing branch coincided with the broadcast
check (both were literal 0). SetFilter's main-window no-op is dropped
— the main window's filter is now genuinely settable. ChatWindowController
.Bind takes a ChatWindowState (the same canonical instance the floating
windows already share) and GetTranscriptLines builds a real accept
predicate instead of accept:null. Verified safe: ClientLocal (0x1A)
never reaches ChatLog (AddText routes it to the SpewBox and returns),
so nothing observable regresses.
- SHOULD-FIX 3: UiButton.SuppressSelfToggle stops the four chat-window
indicator buttons (DAT property 0x0B=true, no retail click handler)
from flipping their own Selected mirror on a stray click.
- SHOULD-FIX 4: generated and committed chat_floaty_2100005b.json from
the real installed dats; added the permanent RetailLayoutFixtureGenerator
entry. All three flagged FloatingChatWindowController assumptions
(input field, title bar, close button) are confirmed correct against
real data — no controller code changes needed. New finding: unlike the
main window, ALL EIGHT floaty border/corner elements are live Type-9
grips (the floaty's own title bar is its move handle), so a floaty
window resizes from every edge and corner.
- SHOULD-FIX 5: register row AP-189 documents the shared-500-entry/
200-line-tail vs retail's per-window 10,000-line scrollback depth gap.
- NITs 1-5: documented the filter-persistence-only-on-/saveautoui
asymmetry and the reconnect-preserves-filters intent; corrected the
research doc's modifier-mask mislabel and the "ONLY function" false
superlative; moved WrapText off ChatWindowController onto
ChatTranscriptRenderer, closing the circular dependency.
Full Release suite: 12,420 passed / 4 skipped / 0 failed (baseline
12,392/4/0 at 22020ef2; net +28 tests, zero regressions).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
149 lines
6.8 KiB
C#
149 lines
6.8 KiB
C#
using System;
|
||
using System.Numerics;
|
||
using AcDream.App.UI.Layout;
|
||
|
||
namespace AcDream.App.UI;
|
||
|
||
/// <summary>
|
||
/// Retail <c>UIElement_Resizebar</c> (element class 9,
|
||
/// <c>UIElement_Resizebar::Register @0x0046B920</c> registers class 9 with the
|
||
/// LayoutDesc element factory). A leaf grip authored at exactly one edge or
|
||
/// corner of a window; retail authors 4 edge grips + 4 corner grips per
|
||
/// resizable window (main chat: <c>0x1000069B</c>-<c>0x100006A2</c>, minus the
|
||
/// straight top strip which is a retail <c>UIElement_Dragbar</c> move handle
|
||
/// instead — see the class doc on <see cref="AcDream.App.UI.Layout.DatWidgetFactory"/>
|
||
/// and <c>docs/research/2026-08-09-chat-retail-window-shell.md</c> §2.1/§2.3).
|
||
///
|
||
/// <para>
|
||
/// <c>UIElement_Resizebar::StartMouseResizing @0x0046B7E0</c> reads four BOOL
|
||
/// attributes off the element — <c>0x2A</c>=bottom, <c>0x2B</c>=left,
|
||
/// <c>0x2C</c>=right, <c>0x2D</c>=top — and combines them into a
|
||
/// <c>BorderLocation</c> (<c>acclient.h:4327</c>). <see cref="DecodeBorderLocation"/>
|
||
/// is a verbatim port of that branch order. acdream's engine expresses resize
|
||
/// direction as an edge-flag bitmask (<see cref="ResizeEdges"/>) rather than
|
||
/// retail's nine-case enum; <see cref="Edges"/> is the lossless projection —
|
||
/// every <see cref="Border"/> value maps to exactly one <see cref="ResizeEdges"/>
|
||
/// combination and back.
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// A grip is a small (5px) leaf with no children; it exists primarily as a
|
||
/// precise hit region, but it IS authored with real border/corner media (the
|
||
/// <c>0x06006129</c>-family sprites — CH6a/b REJECT-review BLOCKER 1,
|
||
/// <c>docs/research/2026-08-10-ch6ab-review-findings.md</c>: every one of the
|
||
/// seven live grips on the main chat window carries a non-zero DirectState
|
||
/// sprite, and drawing nothing left the window with only its 400×5 top strip
|
||
/// visible). A factory-built grip (<see cref="AcDream.App.UI.Layout.DatWidgetFactory.Create"/>,
|
||
/// Type 9) therefore draws its own DirectState media exactly like
|
||
/// <see cref="AcDream.App.UI.Layout.UiDatElement"/> would; a synthetic grip
|
||
/// built with the parameterless constructor (unit tests exercising only the
|
||
/// resize-drag behavior) carries no media and draws nothing, matching its
|
||
/// prior behavior. <see cref="AcDream.App.UI.UiRoot"/> gives a directly-hit
|
||
/// grip's own <see cref="Edges"/> priority over the generic proximity-based
|
||
/// <see cref="AcDream.App.UI.UiRoot.HitEdges"/> heuristic, so e.g. the chat
|
||
/// window's top-left/top-right CORNER grips resize (including the Y/top axis)
|
||
/// while the plain top EDGE strip in between remains a pure move handle.
|
||
/// </para>
|
||
/// </summary>
|
||
public sealed class UiResizeGrip : UiElement
|
||
{
|
||
private readonly ElementInfo? _info;
|
||
private readonly Func<uint, (uint tex, int w, int h)>? _resolve;
|
||
|
||
/// <summary>Synthetic grip with no authored media — used by unit tests that
|
||
/// exercise only the resize-drag hit-testing/edge behavior.</summary>
|
||
public UiResizeGrip()
|
||
{
|
||
}
|
||
|
||
/// <summary>
|
||
/// Factory-built grip (CH6a/b REJECT-review BLOCKER 1): carries its resolved
|
||
/// <see cref="ElementInfo"/> and sprite resolver so it draws its own
|
||
/// authored DirectState media, the same way every other dat-imported leaf
|
||
/// does. <see cref="UiElement.ClickThrough"/> is left at the base
|
||
/// <see langword="false"/> default — unlike <see cref="UiDatElement"/>'s
|
||
/// decoration default of <see langword="true"/> — because a grip must stay
|
||
/// hit-testable to capture the resize drag.
|
||
/// </summary>
|
||
public UiResizeGrip(ElementInfo info, Func<uint, (uint tex, int w, int h)> resolve)
|
||
{
|
||
_info = info;
|
||
_resolve = resolve;
|
||
}
|
||
|
||
/// <summary>
|
||
/// This grip's authored DirectState sprite file id (0 for a synthetic grip,
|
||
/// or an authored grip with no media). Exposed for conformance tests —
|
||
/// BLOCKER 1's regression guard is that every one of the seven live Type-9
|
||
/// grips resolves a non-zero value here.
|
||
/// </summary>
|
||
public uint SpriteFile => _info is not null && _info.StateMedia.TryGetValue("", out var media)
|
||
? media.File
|
||
: 0u;
|
||
|
||
/// <summary>
|
||
/// Draws this grip's own DirectState media exactly like
|
||
/// <see cref="UiDatElement.OnDraw"/> — tiled at native size (Normal), same
|
||
/// blend for Overlay/Alphablend since the sprite shader already
|
||
/// alpha-blends. A synthetic (parameterless-constructed) grip has no
|
||
/// resolver and draws nothing, matching its pre-BLOCKER-1 behavior.
|
||
/// </summary>
|
||
protected override void OnDraw(UiRenderContext ctx)
|
||
{
|
||
if (_resolve is null) return;
|
||
uint file = SpriteFile;
|
||
if (file == 0u) return;
|
||
var (tex, tw, th) = _resolve(file);
|
||
if (tex == 0 || tw == 0 || th == 0) return;
|
||
ctx.DrawSprite(tex, 0, 0, Width, Height, 0, 0, Width / tw, Height / th, Vector4.One);
|
||
}
|
||
|
||
/// <summary>Retail <c>BorderLocation</c> (<c>acclient.h:4327</c>):
|
||
/// <c>BORDER_NONE=0, BORDER_UL=1, BORDER_TOP=2, BORDER_UR=3, BORDER_RIGHT=4,
|
||
/// BORDER_LR=5, BORDER_BOTTOM=6, BORDER_LL=7, BORDER_LEFT=8</c>.</summary>
|
||
public enum Border
|
||
{
|
||
None = 0,
|
||
UpperLeft = 1,
|
||
Top = 2,
|
||
UpperRight = 3,
|
||
Right = 4,
|
||
LowerRight = 5,
|
||
Bottom = 6,
|
||
LowerLeft = 7,
|
||
Left = 8,
|
||
}
|
||
|
||
/// <summary>Decoded retail BorderLocation for this grip.</summary>
|
||
public Border BorderLocation { get; set; }
|
||
|
||
/// <summary>
|
||
/// Exact port of <c>UIElement_Resizebar::StartMouseResizing @0x0046B7E0</c>'s
|
||
/// if/else-if chain (right beats left beats top beats bottom when more than
|
||
/// two bools are set, which retail's authored data never does — corners set
|
||
/// exactly two adjacent bools, edges set exactly one).
|
||
/// </summary>
|
||
public static Border DecodeBorderLocation(bool bottom, bool left, bool right, bool top)
|
||
{
|
||
if (right) return top ? Border.UpperRight : bottom ? Border.LowerRight : Border.Right;
|
||
if (left) return top ? Border.UpperLeft : bottom ? Border.LowerLeft : Border.Left;
|
||
if (top) return Border.Top;
|
||
if (bottom) return Border.Bottom;
|
||
return Border.None;
|
||
}
|
||
|
||
/// <summary>The <see cref="ResizeEdges"/> projection <see cref="UiRoot"/>'s resize
|
||
/// drag state machine consumes — a lossless re-encoding of <see cref="BorderLocation"/>.</summary>
|
||
public ResizeEdges Edges => BorderLocation switch
|
||
{
|
||
Border.UpperLeft => ResizeEdges.Left | ResizeEdges.Top,
|
||
Border.Top => ResizeEdges.Top,
|
||
Border.UpperRight => ResizeEdges.Right | ResizeEdges.Top,
|
||
Border.Right => ResizeEdges.Right,
|
||
Border.LowerRight => ResizeEdges.Right | ResizeEdges.Bottom,
|
||
Border.Bottom => ResizeEdges.Bottom,
|
||
Border.LowerLeft => ResizeEdges.Left | ResizeEdges.Bottom,
|
||
Border.Left => ResizeEdges.Left,
|
||
_ => ResizeEdges.None,
|
||
};
|
||
}
|