fix(CT-GF1): review fix round — literal DrawHere clip shape, empty-clip cull, popup input routing
Applies all 11 items from the Opus dual-lens review of989f6652(0 blockers, 7 SHOULD-FIX, 4 NOTE): - S2: UiElement.DrawSelfAndChildren now pushes the ambient clip right after PushAlpha and wraps OnDraw + the children walk + OnDrawAfterChildren in ONE block — the literal UIRegion::DrawHere @0x0069FA30 shape, which clips an element's OWN DrawSelf too, not just its children (UIElement_Text::DrawSelf @0x00467AA0 locks glyph blits to its own clipped surface rect; UIRegion::DrawSelf @0x0069F1A0 blits per clip rect). Deleted the two now-redundant ad-hoc self-clips this supersedes: UiText.DrawText and UiField.DrawMultiLine both pushed their own (0,0,Width,Height) — exactly what the new ambient clip already provides one level up. Kept UiButton.DrawBlockLabel's clip: it clips to LabelBox/ValueBox, an authored INNER sub-rect that can be smaller than and offset from the button's own full rect — a genuine narrower viewport, not a redundant duplicate. - S3: deleted UiItemList's `ClipsChildren => CellWidth > 0f` override — correct under the old opt-in-false default, inverted under the new default-true (an unconfigured list would stop clipping instead of clipping like everything else). - S4: pinned the escaped-popup input path end to end. New UiAncestorClipTests test mounts a menu inside a short window on a real UiRoot, opens it, and proves a click in the escaped popup region reaches the menu through UiRoot.PopupHit (a plain top-down walk is proven to reject the same point first). UiRoot.WantsMouse now also checks PopupHit — it previously only checked Captured/ HitTestTopDown, so a game action could fire underneath an open dropdown's escaped region. OnMouseDown/OnScroll already routed through PopupHit first (#374); unchanged. - S5: strengthened the Titles-divider regression test's positive half. The old assertion only checked SOME quad's Y fell in a band — vacuously true given other same-band content. Now asserts the divider's exact rect (X and Y), then diffs against the same rect with the divider hidden (Visible=false) to prove the quad was actually attributable to it. - S1: added UiWindowDrawCaptureSweepTests — Character/Chat/Vendor/ Options mounted through their real production Bind entry points with a non-zero sprite resolver, drawn via RecordingGpuDevice, asserting a per-window vertex floor (~40-45% of this session's observed baseline: Character 588, Chat 162, Vendor 54, Options 240) plus one key sprite id read LIVE off the bound controller/element (never hardcoded). Character's key sprite (RetailChromeSprites. TopEdge) specifically exercises OnDrawAfterChildren, the exact path S2's caution note flagged. Inventory/Paperdoll/social/map-house skipped — no single fixture-driven top-level Bind entry point. - S6: added the CT-GF1 subsection to the campaign plan's ledger (989f6652+ this fix round; CT7 re-gate still owed). - S7: UiRenderContext.PushClipUnbounded now resets to the CANVAS rect (0,0,ScreenSize), not null — retail's own popup region is SCREEN-clipped (UIElement_Menu::MakePopup spawns a top-level region bounded by the screen), not truly unbounded. AD-113 amended. - N1: UiRoot overrides ClipsChildren => false — the root's own region IS the screen (the viewport already scissors it), so this is a safety net against a momentarily zero-sized root silently blanking the whole UI tree under the new ancestor-clip default. - N2: added the empty-clip subtree cull (retail's var_24 gate @0x0069FB8E) to DrawSelfAndChildren only — DrawOverlays is a wholly separate traversal untouched by this change. New test proves a menu inside a fully-clipped (zero-width) window still draws its open popup via the overlay pass while the main pass draws nothing. - N3: CT7 script §5 now names the collapsed-toolbar check and the four highest-overflow windows (combat/vitals bar, Options bottom-button row, map/house page, floaty chat) as explicit eyeball items for the re-gate. - N4: verification below covers both the working tree and the clean committed tree. Decomp anchors: UIRegion::DrawHere @0x0069FA30 (var_24 gate @0x0069FB8E); UIElement_Text::DrawSelf @0x00467AA0 (self-clip); UIRegion::DrawSelf @0x0069F1A0; UIElement_Menu::MakePopup (screen- clipped popup region). Verification (both runs green, --filter "Lane!=InstalledDat& Lane!=PreparedPackage&Lane!=Live&Lane!=Manual&Lane!=Timing& Lane!=Windows&Lane!=Linux&Lane!=SystemFont&Purpose!=Diagnostic& Status!=KnownFailure"): full Release solution build green; working tree 14,900+ tests across every project (one LandblockPresentation PipelineTests flake reproduced ONLY under full-solution parallel load, passes standalone and on rerun — unrelated to this change, streaming domain); InstalledDat lane green (ACDREAM_RUN_INSTALLED_DAT _TESTS=1, Status!=KnownFailure, 205+34+3+172 App/Content/Bake/Core tests). Clean committed tree (git stash push -u the uncommitted owner probe + docs files, rerun, stash pop) reported in the session summary. src/AcDream.App/UI/UiRoot.cs carries an unrelated, pre-existing uncommitted owner probe (ACDREAM_PROBE_UI_HOVER) — staged selectively (git add -p) so only this commit's own two hunks (ClipsChildren override, WantsMouse) landed; the probe hunk is untouched and stays uncommitted, same as before this fix round. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
989f665214
commit
025108a8aa
13 changed files with 717 additions and 120 deletions
|
|
@ -857,16 +857,20 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
|
|||
text, font.MeasureWidth, font.LineHeight,
|
||||
boxX, boxY, boxWidth, boxHeight, align, leftOffset);
|
||||
|
||||
// A multi-line result (an authored '\n') clips to its own box — the
|
||||
// button's normal draw has no ambient clip, and an oversized
|
||||
// wrapped caption (e.g. the Skills credits button's own tight 28px
|
||||
// height) should be cut off at the box edge rather than spill into
|
||||
// whatever sits below the button, matching every other clipped
|
||||
// Type-12 text box in this codebase (UiText.DrawText's own
|
||||
// PushClip). Single-line captions — the overwhelming majority,
|
||||
// and (post-R3-2) EVERY caption with no authored newline — never
|
||||
// pay this cost; see this method's own doc for why a single line
|
||||
// is deliberately left unclipped even when boxWidth was narrowed.
|
||||
// A multi-line result (an authored '\n') clips to its own box — an oversized
|
||||
// wrapped caption (e.g. the Skills credits button's own tight 28px height)
|
||||
// should be cut off at the box edge rather than spill into whatever sits
|
||||
// below the button. CT-GF1 fix round (S2) audit: this clip STAYS (unlike
|
||||
// UiText.DrawText's and UiField.DrawMultiLine's now-deleted self-clips) —
|
||||
// (boxX,boxY,boxWidth,boxHeight) is LabelBox/ValueBox, an authored INNER
|
||||
// sub-rect that can be smaller than and offset from the button's own full
|
||||
// (0,0,Width,Height) (see LabelBox's/ValueBox's own docs and the
|
||||
// ValueBox-narrows-boxWidth branch above), so it is not redundant with the
|
||||
// ambient (0,0,Width,Height) clip DrawSelfAndChildren now applies by default
|
||||
// one level up. Single-line captions — the overwhelming majority, and
|
||||
// (post-R3-2) EVERY caption with no authored newline — never pay this cost;
|
||||
// see this method's own doc for why a single line is deliberately left
|
||||
// unclipped even when boxWidth was narrowed.
|
||||
bool clip = lines.Count > 1;
|
||||
if (clip)
|
||||
ctx.PushClip(boxX, boxY, boxWidth, boxHeight);
|
||||
|
|
|
|||
|
|
@ -544,30 +544,39 @@ public abstract class UiElement
|
|||
protected virtual void OnDrawOverlay(UiRenderContext ctx) { }
|
||||
|
||||
/// <summary>
|
||||
/// Whether descendant drawing and hit-testing are clipped to this element's
|
||||
/// local bounds. THIS IS THE DEFAULT (true) FOR EVERY ELEMENT — CT-GF1 port
|
||||
/// of retail's ancestor-clip chain: <c>UIRegion::DrawHere @0x0069FA30</c> takes
|
||||
/// the element's screen <c>Box2D</c> plus a <c>SmartArray<Box2D></c> of
|
||||
/// inherited clip rects, intersects them (the min/max clamp loop
|
||||
/// @0x0069FAA7..0x0069FB82), and draws — <c>EraseSelf</c>/<c>DrawChildren</c>/
|
||||
/// <c>DrawSelf</c> all receive the intersected rect — ONLY when the intersection
|
||||
/// is non-empty (the <c>var_24</c> gate @0x0069FB8E). An element positioned
|
||||
/// outside its parent's box therefore silently disappears in retail, exactly
|
||||
/// like <see cref="UiRenderContext.PushClip"/>/<see cref="UiRenderContext.PopClip"/>
|
||||
/// (already wrapping the child-draw and child-hit-test walks below) now does for
|
||||
/// every element by default, not just the scrollable listboxes that opted in
|
||||
/// before this default flipped (owner gate finding: the Titles page's authored
|
||||
/// divider 0x10000530 escaped the Character window at the CT6-correct 372px
|
||||
/// mounted default — retail clips it away; acdream drew it floating above the
|
||||
/// window).
|
||||
/// Whether THIS element's own draw AND its descendants' drawing/hit-testing are
|
||||
/// clipped to this element's local bounds. THIS IS THE DEFAULT (true) FOR EVERY
|
||||
/// ELEMENT — CT-GF1 port of retail's ancestor-clip chain: <c>UIRegion::DrawHere
|
||||
/// @0x0069FA30</c> takes the element's screen <c>Box2D</c> plus a
|
||||
/// <c>SmartArray<Box2D></c> of inherited clip rects, intersects them (the
|
||||
/// min/max clamp loop @0x0069FAA7..0x0069FB82), and draws — <c>EraseSelf</c>/
|
||||
/// <c>DrawChildren</c>/<c>DrawSelf</c> ALL receive the intersected rect — ONLY
|
||||
/// when the intersection is non-empty (the <c>var_24</c> gate @0x0069FB8E). An
|
||||
/// element positioned outside its parent's box therefore silently disappears in
|
||||
/// retail, exactly like <see cref="UiRenderContext.PushClip"/>/
|
||||
/// <see cref="UiRenderContext.PopClip"/> now does for every element by default —
|
||||
/// <see cref="DrawSelfAndChildren"/>'s fix-round shape (S2) pushes right after
|
||||
/// <c>PushAlpha</c> and wraps <c>OnDraw</c> + the children walk +
|
||||
/// <c>OnDrawAfterChildren</c> in ONE block, the literal <c>DrawHere</c> shape
|
||||
/// (retail clips the element's OWN <c>DrawSelf</c> too, not just its children —
|
||||
/// <c>UIElement_Text::DrawSelf @0x00467AA0</c> locks glyph blits to its own
|
||||
/// clipped surface rect; <c>UIRegion::DrawSelf @0x0069F1A0</c> blits per clip
|
||||
/// rect). Not just the scrollable listboxes that opted in before this default
|
||||
/// flipped (owner gate finding: the Titles page's authored divider 0x10000530
|
||||
/// escaped the Character window at the CT6-correct 372px mounted default —
|
||||
/// retail clips it away; acdream drew it floating above the window).
|
||||
///
|
||||
/// <para>
|
||||
/// Override to <see langword="false"/> ONLY for a widget that must draw or accept
|
||||
/// input beyond its own bounds by deliberate design — today just
|
||||
/// <see cref="UiMenu"/>, whose popup (and its own out-of-bounds
|
||||
/// <c>OnHitTest</c> override) stands in for retail's separate top-level popup
|
||||
/// region; see <see cref="ExpandsClipForPopup"/> for the drawing half of that
|
||||
/// opt-out and the divergence register row it cites.
|
||||
/// <see cref="UiMenu"/> (whose popup, and its own out-of-bounds <c>OnHitTest</c>
|
||||
/// override, stands in for retail's separate top-level popup region — see
|
||||
/// <see cref="ExpandsClipForPopup"/> for the drawing half of that opt-out and the
|
||||
/// divergence register row it cites) and <see cref="UiRoot"/> (whose own region
|
||||
/// IS the screen — the viewport itself already scissors it, so narrowing to
|
||||
/// <c>(0,0,Width,Height)</c> here would blank the whole UI the moment the root's
|
||||
/// own tracked size is ever momentarily zero, e.g. before the first resize event
|
||||
/// lands).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
protected virtual bool ClipsChildren => true;
|
||||
|
|
@ -648,39 +657,49 @@ public abstract class UiElement
|
|||
// surface, chrome and glyphs together, not text-stays-sharp over a translucent panel).
|
||||
ctx.PushTransform(Left, Top);
|
||||
ctx.PushAlpha(Opacity);
|
||||
// CT-GF1 fix round (S2): the clip now wraps OnDraw + children +
|
||||
// OnDrawAfterChildren — the LITERAL UIRegion::DrawHere @0x0069FA30 shape,
|
||||
// which clips the element's OWN DrawSelf to the intersected rect, not just its
|
||||
// children (UIElement_Text::DrawSelf @0x00467AA0 locks glyph blits to arg3;
|
||||
// UIRegion::DrawSelf @0x0069F1A0 blits per clip rect). Pushed right after
|
||||
// PushAlpha, popped in the one finally below, so it is balanced regardless of
|
||||
// which branch below runs.
|
||||
bool clipsChildren = ClipsChildren;
|
||||
if (clipsChildren)
|
||||
ctx.PushClip(0f, 0f, Width, Height);
|
||||
try
|
||||
{
|
||||
OnDraw(ctx);
|
||||
|
||||
// Anchor layout: reflow children to this element's current size.
|
||||
for (int i = 0; i < _children.Count; i++)
|
||||
_children[i].ApplyAnchor(Width, Height);
|
||||
|
||||
// Children painted back-to-front (lowest ZOrder first).
|
||||
if (_children.Count > 0)
|
||||
// N2 fix round: retail's var_24 gate @0x0069FB8E — an EMPTY intersected
|
||||
// clip skips EraseSelf/DrawChildren/DrawSelf outright for the whole
|
||||
// subtree. DrawOverlays (the popup's SEPARATE second traversal) does not
|
||||
// share this walk or its clip-stack state, so an open UiMenu popup nested
|
||||
// here keeps drawing there regardless of this cull — see
|
||||
// UiAncestorClipTests' menu-inside-a-fully-clipped-window coverage.
|
||||
if (!ctx.CurrentClipIsEmpty)
|
||||
{
|
||||
bool clipsChildren = ClipsChildren;
|
||||
if (clipsChildren)
|
||||
ctx.PushClip(0f, 0f, Width, Height);
|
||||
try
|
||||
OnDraw(ctx);
|
||||
|
||||
// Anchor layout: reflow children to this element's current size.
|
||||
for (int i = 0; i < _children.Count; i++)
|
||||
_children[i].ApplyAnchor(Width, Height);
|
||||
|
||||
// Children painted back-to-front (lowest ZOrder first).
|
||||
if (_children.Count > 0)
|
||||
{
|
||||
UiElement[] ordered = ChildrenBackToFrontSnapshot();
|
||||
for (int i = 0; i < ordered.Length; i++)
|
||||
ordered[i].DrawSelfAndChildren(ctx);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (clipsChildren)
|
||||
ctx.PopClip();
|
||||
}
|
||||
}
|
||||
|
||||
// Foreground pass for this element (e.g. a window frame's border drawn
|
||||
// OVER its content's edges). Default no-op for ordinary elements.
|
||||
OnDrawAfterChildren(ctx);
|
||||
// Foreground pass for this element (e.g. a window frame's border drawn
|
||||
// OVER its content's edges). Default no-op for ordinary elements.
|
||||
OnDrawAfterChildren(ctx);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (clipsChildren)
|
||||
ctx.PopClip();
|
||||
ctx.PopAlpha();
|
||||
ctx.PopTransform();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -557,57 +557,56 @@ public sealed class UiField : UiElement
|
|||
Scroll.SetScrollY((int)MathF.Ceiling(caretBottom - visibleHeight));
|
||||
}
|
||||
|
||||
ctx.PushClip(0f, 0f, Width, Height);
|
||||
try
|
||||
// CT-GF1 fix round (S2): this used to push its OWN (0,0,Width,Height) clip
|
||||
// here. That is now REDUNDANT and deleted — UiElement.DrawSelfAndChildren
|
||||
// (DrawMultiLine is called from OnDraw) wraps this whole method in exactly
|
||||
// that same (0,0,Width,Height) ambient clip by default, matching retail's
|
||||
// DrawHere shape one level up instead of duplicating it here. The lines below
|
||||
// still rely on SOME clip being active (a partially visible row must still be
|
||||
// rejected, not drawn full-size) — that clip is now the ambient one.
|
||||
var (selectionLow, selectionHigh) = SelSpan();
|
||||
for (int i = 0; i < lines.Count; i++)
|
||||
{
|
||||
var (selectionLow, selectionHigh) = SelSpan();
|
||||
for (int i = 0; i < lines.Count; i++)
|
||||
WrappedLine line = lines[i];
|
||||
float y = Padding + (i * lineHeight) - Scroll.ScrollY;
|
||||
if (y + lineHeight <= Padding || y >= Height - Padding)
|
||||
continue;
|
||||
|
||||
int lineEnd = line.Start + line.Length;
|
||||
int highlightLow = Math.Max(selectionLow, line.Start);
|
||||
int highlightHigh = Math.Min(selectionHigh, lineEnd);
|
||||
if (HasSelection && highlightHigh > highlightLow)
|
||||
{
|
||||
WrappedLine line = lines[i];
|
||||
float y = Padding + (i * lineHeight) - Scroll.ScrollY;
|
||||
if (y + lineHeight <= Padding || y >= Height - Padding)
|
||||
continue;
|
||||
|
||||
int lineEnd = line.Start + line.Length;
|
||||
int highlightLow = Math.Max(selectionLow, line.Start);
|
||||
int highlightHigh = Math.Min(selectionHigh, lineEnd);
|
||||
if (HasSelection && highlightHigh > highlightLow)
|
||||
{
|
||||
float x0 = Padding + MeasureRange(
|
||||
line.Start,
|
||||
highlightLow - line.Start);
|
||||
float x1 = Padding + MeasureRange(
|
||||
line.Start,
|
||||
highlightHigh - line.Start);
|
||||
ctx.DrawFill(
|
||||
x0,
|
||||
y,
|
||||
MathF.Max(0f, x1 - x0),
|
||||
lineHeight,
|
||||
SelectionColor);
|
||||
}
|
||||
|
||||
if (DatFont is { } dat)
|
||||
ctx.DrawStringDat(dat, line.Text, Padding, y, TextColor, Outline, OutlineColor);
|
||||
else if (Font is { } bitmap)
|
||||
ctx.DrawString(line.Text, Padding, y, TextColor, bitmap);
|
||||
float x0 = Padding + MeasureRange(
|
||||
line.Start,
|
||||
highlightLow - line.Start);
|
||||
float x1 = Padding + MeasureRange(
|
||||
line.Start,
|
||||
highlightHigh - line.Start);
|
||||
ctx.DrawFill(
|
||||
x0,
|
||||
y,
|
||||
MathF.Max(0f, x1 - x0),
|
||||
lineHeight,
|
||||
SelectionColor);
|
||||
}
|
||||
|
||||
if (_focused && lines.Count > 0)
|
||||
{
|
||||
WrappedLine line = lines[caretLine];
|
||||
int lineColumn = Math.Clamp(
|
||||
_caret - line.Start,
|
||||
0,
|
||||
line.Length);
|
||||
float x = Padding + MeasureRange(line.Start, lineColumn);
|
||||
float y = Padding + (caretLine * lineHeight) - Scroll.ScrollY;
|
||||
ctx.DrawFill(x, y, 1f, lineHeight, TextColor);
|
||||
}
|
||||
if (DatFont is { } dat)
|
||||
ctx.DrawStringDat(dat, line.Text, Padding, y, TextColor, Outline, OutlineColor);
|
||||
else if (Font is { } bitmap)
|
||||
ctx.DrawString(line.Text, Padding, y, TextColor, bitmap);
|
||||
}
|
||||
finally
|
||||
|
||||
if (_focused && lines.Count > 0)
|
||||
{
|
||||
ctx.PopClip();
|
||||
WrappedLine line = lines[caretLine];
|
||||
int lineColumn = Math.Clamp(
|
||||
_caret - line.Start,
|
||||
0,
|
||||
line.Length);
|
||||
float x = Padding + MeasureRange(line.Start, lineColumn);
|
||||
float y = Padding + (caretLine * lineHeight) - Scroll.ScrollY;
|
||||
ctx.DrawFill(x, y, 1f, lineHeight, TextColor);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -358,7 +358,13 @@ public sealed class UiItemList : UiElement
|
|||
}
|
||||
}
|
||||
|
||||
protected override bool ClipsChildren => CellWidth > 0f;
|
||||
// CT-GF1 fix round (S3): the former `ClipsChildren => CellWidth > 0f` override is
|
||||
// DELETED. It made sense under the PRE-CT-GF1 opt-in default (false): clip only
|
||||
// once CellWidth is configured. Under the new client-wide default (true), that
|
||||
// same expression is INVERTED — an unconfigured list (CellWidth<=0, e.g. before
|
||||
// LayoutCells first runs) would evaluate to false and stop clipping, the opposite
|
||||
// of every other element's new default. Deleting the override restores the
|
||||
// uniform default (always clip to this element's own (0,0,Width,Height)).
|
||||
|
||||
public void Flush()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -116,18 +116,34 @@ public sealed class UiRenderContext
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discard every inherited clip rect for the duration of one overlay draw —
|
||||
/// the escape hatch <see cref="UiElement.ExpandsClipForPopup"/> uses so a popup
|
||||
/// drawn inline from its owning widget (see that property's doc comment for the
|
||||
/// retail-parity rationale) is not wrongly clipped by the ancestor chain the
|
||||
/// CT-GF1 default clip (<see cref="UiElement.ClipsChildren"/>) now threads through
|
||||
/// every other element. Shares <see cref="PopClip"/>'s stack, so pair the two
|
||||
/// exactly like <see cref="PushClip"/>.
|
||||
/// True when the current accumulated clip is non-null and has zero (or negative)
|
||||
/// area — CT-GF1 fix-round subtree cull, porting retail's
|
||||
/// <c>UIRegion::DrawHere</c> <c>var_24</c> gate @0x0069FB8E: an empty intersected
|
||||
/// clip skips <c>EraseSelf</c>/<c>DrawChildren</c>/<c>DrawSelf</c> for the whole
|
||||
/// subtree, not just individual draw calls (those already no-op against an empty
|
||||
/// clip via <see cref="ClipRect"/>/<see cref="UiClipRect.TryClipSprite"/> — this
|
||||
/// additionally skips the WALK). A null clip (nothing pushed yet, or reset via
|
||||
/// <see cref="PushClipUnbounded"/>) is NOT empty — it means unbounded, so this is
|
||||
/// false in that case.
|
||||
/// </summary>
|
||||
public bool CurrentClipIsEmpty => _clip is { } c && c.IsEmpty;
|
||||
|
||||
/// <summary>
|
||||
/// Reset the accumulated clip to the full CANVAS rect (0,0,ScreenSize) for the
|
||||
/// duration of one overlay draw — the escape hatch <see cref="UiElement.ExpandsClipForPopup"/>
|
||||
/// uses so a popup drawn inline from its owning widget (see that property's doc
|
||||
/// comment for the retail-parity rationale) is not wrongly clipped by the
|
||||
/// ancestor chain the CT-GF1 default clip (<see cref="UiElement.ClipsChildren"/>)
|
||||
/// now threads through every other element. Retail's own popup region is still
|
||||
/// SCREEN-clipped (<c>UIElement_Menu::MakePopup</c> spawns a top-level region
|
||||
/// bounded by the screen, not truly infinite) — this is the canvas rect, not
|
||||
/// <c>null</c>/unbounded, matching that. Shares <see cref="PopClip"/>'s stack, so
|
||||
/// pair the two exactly like <see cref="PushClip"/>.
|
||||
/// </summary>
|
||||
public void PushClipUnbounded()
|
||||
{
|
||||
_clipStack.Add(_clip);
|
||||
_clip = null;
|
||||
_clip = new UiClipRect(0f, 0f, ScreenSize.X, ScreenSize.Y);
|
||||
}
|
||||
|
||||
/// <summary>Route subsequent draws to the overlay layer (flushed on top of the whole
|
||||
|
|
|
|||
|
|
@ -33,6 +33,19 @@ public sealed class UiRoot : UiElement
|
|||
/// <summary>Single owner for named retained-window lifecycle and raise policy.</summary>
|
||||
public RetailWindowManager WindowManager { get; }
|
||||
|
||||
/// <summary>
|
||||
/// CT-GF1 fix round (N1): the root's own region IS the screen — the viewport
|
||||
/// itself already scissors everything drawn to it, so retail has no analog of
|
||||
/// clipping the root to its OWN tracked (Width,Height) the way
|
||||
/// <see cref="UiElement.ClipsChildren"/>'s new client-wide default would.
|
||||
/// Overriding false here is a safety net, not a cosmetic choice: without it, a
|
||||
/// root momentarily reporting a zero (or stale, pre-first-resize) size would
|
||||
/// silently blank the ENTIRE UI tree — every top-level window culled by the new
|
||||
/// ancestor-clip default's empty-intersection gate — rather than the intended
|
||||
/// "root passes its full extent through to its children uninterpreted."
|
||||
/// </summary>
|
||||
protected override bool ClipsChildren => false;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign LA gate round 2 (register AD-98): when set, the retained tree
|
||||
/// is laid out in this fixed authored canvas (the char-select screen's
|
||||
|
|
@ -189,8 +202,27 @@ public sealed class UiRoot : UiElement
|
|||
/// The host ORs this into the InputDispatcher's WantCaptureMouse gate so game
|
||||
/// actions (movement, world-pick) are suppressed while the user interacts with
|
||||
/// a retail window — mirrors ImGui's WantCaptureMouse.
|
||||
///
|
||||
/// <para>
|
||||
/// CT-GF1 fix round (S4): also checks <see cref="PopupHit"/> — an open UiMenu
|
||||
/// dropdown's escaped region (the part of the popup that extends outside its
|
||||
/// owning window's own ancestor-clipped bounds, e.g. a channel menu opened
|
||||
/// upward past a short chat window's top edge) is reachable by
|
||||
/// <see cref="OnMouseDown"/>/<see cref="OnScroll"/> through <see cref="PopupHit"/>
|
||||
/// specifically BECAUSE it bypasses the ordinary top-down <see cref="HitTest"/>
|
||||
/// walk's ancestor clip gate (CT-GF1's new client-wide
|
||||
/// <see cref="UiElement.ClipsChildren"/> default would otherwise reject that
|
||||
/// point before ever reaching the popup's own out-of-bounds <c>OnHitTest</c>
|
||||
/// union — see #374's popup routing doc above). Without this,
|
||||
/// <see cref="HitTestTopDown"/> alone would report "no widget here" for a point
|
||||
/// the pointer visibly sits over, letting a world click/movement action slip
|
||||
/// through underneath the open popup.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public bool WantsMouse => Captured is not null || HitTestTopDown(MouseX, MouseY).element is not null;
|
||||
public bool WantsMouse =>
|
||||
Captured is not null
|
||||
|| PopupHit(MouseX, MouseY) is not null
|
||||
|| HitTestTopDown(MouseX, MouseY).element is not null;
|
||||
|
||||
/// <summary>True when a widget holds keyboard focus (e.g. a focused chat input).</summary>
|
||||
public bool WantsKeyboard => KeyboardFocus is not null;
|
||||
|
|
@ -1386,6 +1418,23 @@ public sealed class UiRoot : UiElement
|
|||
Data1: (int)(x - screen.X),
|
||||
Data2: (int)(y - screen.Y));
|
||||
w.OnEvent(in enter);
|
||||
if (UiDiagnostics.ProbeHover)
|
||||
{
|
||||
string detail = w is UiScrollbar sb
|
||||
? $" start=0x{sb.ActiveStartSpriteForTest:X8}"
|
||||
+ $" end=0x{sb.ActiveEndSpriteForTest:X8}"
|
||||
+ $" thumb=0x{sb.ActiveThumbSpriteForTest:X8}"
|
||||
+ $" disabled={sb.IsModelDisabled}"
|
||||
: "";
|
||||
Console.WriteLine(
|
||||
$"[ui-hover] widget={w.GetType().Name}"
|
||||
+ $" dat=0x{w.DatElementId:X8}"
|
||||
+ $" local=({(int)(x - screen.X)},{(int)(y - screen.Y)}){detail}");
|
||||
}
|
||||
}
|
||||
else if (UiDiagnostics.ProbeHover)
|
||||
{
|
||||
Console.WriteLine("[ui-hover] widget=<none>");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -584,17 +584,17 @@ public sealed class UiText : UiElement, IUiDatStateful
|
|||
// visible surface as arg3 and clips each glyph blit to that rectangle. This is
|
||||
// observable in LayoutDesc 0x21000033: the owned component count is a 15px-high
|
||||
// text element using a 16px DAT font, so rejecting a partially visible line makes
|
||||
// the value disappear entirely. The shared render context clips both DAT and
|
||||
// bitmap glyph quads and composes this bound with any list/window ancestor clip.
|
||||
ctx.PushClip(0f, 0f, Width, Height);
|
||||
try
|
||||
{
|
||||
DrawClippedText(ctx);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ctx.PopClip();
|
||||
}
|
||||
// the value disappear entirely.
|
||||
//
|
||||
// CT-GF1 fix round (S2): this used to push its OWN (0,0,Width,Height) clip
|
||||
// here. That is now REDUNDANT and deleted — UiElement.DrawSelfAndChildren
|
||||
// wraps OnDraw/OnDrawAfterChildren (where DrawText is called from) in exactly
|
||||
// that same (0,0,Width,Height) ambient clip by default, intersected with any
|
||||
// list/window ancestor clip, matching retail's DrawHere shape one level up
|
||||
// instead of duplicating it here. DrawClippedText still relies on SOME clip
|
||||
// being active for correctness (a partially visible line must still be
|
||||
// rejected, not drawn full-size) — that clip is now the ambient one.
|
||||
DrawClippedText(ctx);
|
||||
}
|
||||
|
||||
private void DrawClippedText(UiRenderContext ctx)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue