fix(ui): restore retail magic shortcut bar
Port the retail horizontal ItemList empty-slot padding and share UIItem shortcut-number graphics with the status toolbar. Preserve all authored face children on compound DAT buttons so the three-piece Cast control reflows and renders as one complete button. Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
0527325b25
commit
e3605672bb
12 changed files with 466 additions and 62 deletions
|
|
@ -34,6 +34,7 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
|
|||
{
|
||||
private readonly ElementInfo _info;
|
||||
private readonly ElementInfo _mediaInfo;
|
||||
private readonly FaceSegment[] _faceSegments;
|
||||
private readonly Func<uint, (uint tex, int w, int h)> _resolve;
|
||||
private readonly HashSet<uint> _availableStates = new();
|
||||
private bool _pressed;
|
||||
|
|
@ -175,12 +176,12 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
|
|||
|
||||
if (stateId == UiStateInfo.DirectStateId)
|
||||
{
|
||||
if (!_mediaInfo.States.ContainsKey(stateId) && !_mediaInfo.StateMedia.ContainsKey(""))
|
||||
if (!TryFindState(stateId, out _) && !HasStateMedia(""))
|
||||
return false;
|
||||
ActiveState = "";
|
||||
return true;
|
||||
}
|
||||
if (_mediaInfo.States.TryGetValue(stateId, out var state))
|
||||
if (TryFindState(stateId, out var state))
|
||||
{
|
||||
ActiveState = state.Name;
|
||||
return true;
|
||||
|
|
@ -188,7 +189,7 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
|
|||
string stateName = UiButtonStateMachine.StateName(stateId);
|
||||
if (string.IsNullOrEmpty(stateName))
|
||||
stateName = RetailUiStateIds.StateName(stateId);
|
||||
if (!string.IsNullOrEmpty(stateName) && _mediaInfo.StateMedia.ContainsKey(stateName))
|
||||
if (!string.IsNullOrEmpty(stateName) && HasStateMedia(stateName))
|
||||
{
|
||||
ActiveState = stateName;
|
||||
return true;
|
||||
|
|
@ -202,10 +203,14 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
|
|||
public UiButton(
|
||||
ElementInfo info,
|
||||
Func<uint, (uint tex, int w, int h)> resolve,
|
||||
ElementInfo? mediaInfo = null)
|
||||
ElementInfo? mediaInfo = null,
|
||||
IReadOnlyList<ElementInfo>? faceSegments = null)
|
||||
{
|
||||
_info = info;
|
||||
_info = info;
|
||||
_mediaInfo = mediaInfo ?? info;
|
||||
_faceSegments = faceSegments is null
|
||||
? []
|
||||
: faceSegments.Select(static segment => new FaceSegment(segment)).ToArray();
|
||||
_resolve = resolve;
|
||||
ClickThrough = false; // buttons are interactive — opt OUT of click-through
|
||||
|
||||
|
|
@ -213,9 +218,11 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
|
|||
// Retail layouts commonly declare an empty Normal_pressed descriptor while
|
||||
// supplying art only for Normal/Highlight. Treating that property-only state
|
||||
// as drawable briefly blanks the button during mouse-down.
|
||||
foreach (string stateName in _mediaInfo.StateMedia.Keys)
|
||||
if (UiButtonStateMachine.TryStateId(stateName, out uint stateId))
|
||||
_availableStates.Add(stateId);
|
||||
if (_faceSegments.Length == 0)
|
||||
AddAvailableStates(_mediaInfo);
|
||||
else
|
||||
foreach (FaceSegment segment in _faceSegments)
|
||||
AddAvailableStates(segment.Info);
|
||||
|
||||
ToggleBehavior = info.TryGetEffectiveBool(0x0Bu, out bool toggle) && toggle;
|
||||
RolloverEnabled = info.TryGetEffectiveBool(0x13u, out bool rollover) && rollover;
|
||||
|
|
@ -233,7 +240,7 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
|
|||
// DefaultStateName wins; else "Normal" if that state has a sprite; else DirectState ("").
|
||||
if (!string.IsNullOrEmpty(info.DefaultStateName))
|
||||
ActiveState = info.DefaultStateName;
|
||||
else if (_mediaInfo.StateMedia.ContainsKey("Normal"))
|
||||
else if (HasStateMedia("Normal"))
|
||||
ActiveState = "Normal";
|
||||
// else ActiveState stays "" (DirectState)
|
||||
|
||||
|
|
@ -258,24 +265,32 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
|
|||
/// Returns 0 if neither exists.
|
||||
/// Mirrors <see cref="UiDatElement.ActiveMedia()"/>.
|
||||
/// </summary>
|
||||
private uint ActiveFile()
|
||||
=> _mediaInfo.StateMedia.TryGetValue(ActiveState, out var m) ? m.File
|
||||
: _mediaInfo.StateMedia.TryGetValue("", out var d) ? d.File : 0u;
|
||||
private uint ActiveFile(ElementInfo mediaInfo)
|
||||
=> mediaInfo.StateMedia.TryGetValue(ActiveState, out var m) ? m.File
|
||||
: mediaInfo.StateMedia.TryGetValue("", out var d) ? d.File : 0u;
|
||||
|
||||
protected override void OnDraw(UiRenderContext ctx)
|
||||
{
|
||||
uint file = ActiveFile();
|
||||
if (file != 0)
|
||||
if (_faceSegments.Length != 0)
|
||||
{
|
||||
var (tex, tw, th) = _resolve(file);
|
||||
if (tex != 0 && tw != 0 && th != 0)
|
||||
foreach (FaceSegment segment in _faceSegments)
|
||||
DrawFace(ctx, ActiveFile(segment.Info), segment.Rect(Width, Height));
|
||||
}
|
||||
else
|
||||
{
|
||||
uint file = ActiveFile(_mediaInfo);
|
||||
if (file != 0)
|
||||
{
|
||||
// Tiled draw — same call shape as UiDatElement.OnDraw (UV-repeat; GL_REPEAT-wrapped
|
||||
// UI texture). Matches ImgTex::TileCSI; no Stretch mode exists.
|
||||
float faceWidth = FaceWidth > 0f ? FaceWidth : Width;
|
||||
float faceHeight = FaceHeight > 0f ? FaceHeight : Height;
|
||||
ctx.DrawSprite(tex, FaceLeft, FaceTop, faceWidth, faceHeight,
|
||||
0, 0, faceWidth / tw, faceHeight / th, Vector4.One);
|
||||
var (tex, tw, th) = _resolve(file);
|
||||
if (tex != 0 && tw != 0 && th != 0)
|
||||
{
|
||||
// Tiled draw — same call shape as UiDatElement.OnDraw (UV-repeat; GL_REPEAT-wrapped
|
||||
// UI texture). Matches ImgTex::TileCSI; no Stretch mode exists.
|
||||
float faceWidth = FaceWidth > 0f ? FaceWidth : Width;
|
||||
float faceHeight = FaceHeight > 0f ? FaceHeight : Height;
|
||||
ctx.DrawSprite(tex, FaceLeft, FaceTop, faceWidth, faceHeight,
|
||||
0, 0, faceWidth / tw, faceHeight / th, Vector4.One);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -302,6 +317,79 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
|
|||
}
|
||||
}
|
||||
|
||||
private void DrawFace(UiRenderContext ctx, uint file, UiPixelRect rect)
|
||||
{
|
||||
if (file == 0 || rect.Width <= 0 || rect.Height <= 0)
|
||||
return;
|
||||
var (texture, textureWidth, textureHeight) = _resolve(file);
|
||||
if (texture == 0 || textureWidth == 0 || textureHeight == 0)
|
||||
return;
|
||||
|
||||
// Same tiled/cropped media path as UiDatElement. Segment geometry is
|
||||
// first reflowed by its own four-edge retail layout policy.
|
||||
ctx.DrawSprite(texture, rect.X0, rect.Y0, rect.Width, rect.Height,
|
||||
0f, 0f, (float)rect.Width / textureWidth, (float)rect.Height / textureHeight,
|
||||
Vector4.One);
|
||||
}
|
||||
|
||||
private void AddAvailableStates(ElementInfo mediaInfo)
|
||||
{
|
||||
foreach (string stateName in mediaInfo.StateMedia.Keys)
|
||||
if (UiButtonStateMachine.TryStateId(stateName, out uint stateId))
|
||||
_availableStates.Add(stateId);
|
||||
}
|
||||
|
||||
private bool HasStateMedia(string stateName)
|
||||
{
|
||||
if (_faceSegments.Length == 0)
|
||||
return _mediaInfo.StateMedia.ContainsKey(stateName);
|
||||
foreach (FaceSegment segment in _faceSegments)
|
||||
if (segment.Info.StateMedia.ContainsKey(stateName))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryFindState(uint stateId, out UiStateInfo state)
|
||||
{
|
||||
if (_faceSegments.Length == 0)
|
||||
return _mediaInfo.States.TryGetValue(stateId, out state!);
|
||||
foreach (FaceSegment segment in _faceSegments)
|
||||
if (segment.Info.States.TryGetValue(stateId, out state!))
|
||||
return true;
|
||||
state = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
private sealed class FaceSegment
|
||||
{
|
||||
private readonly UiPixelRect _original;
|
||||
private readonly UiLayoutPolicy? _layout;
|
||||
|
||||
public FaceSegment(ElementInfo info)
|
||||
{
|
||||
Info = info;
|
||||
_original = UiPixelRect.FromPositionAndSize(
|
||||
(int)info.X, (int)info.Y, (int)info.Width, (int)info.Height);
|
||||
if (info.HasOriginalParentSize)
|
||||
_layout = new UiLayoutPolicy(
|
||||
info.Left, info.Top, info.Right, info.Bottom,
|
||||
_original,
|
||||
UiPixelRect.FromPositionAndSize(
|
||||
0, 0, (int)info.OriginalParentWidth, (int)info.OriginalParentHeight));
|
||||
}
|
||||
|
||||
public ElementInfo Info { get; }
|
||||
|
||||
public UiPixelRect Rect(float parentWidth, float parentHeight)
|
||||
{
|
||||
if (_layout is null)
|
||||
return _original;
|
||||
return _layout.Apply(
|
||||
_original,
|
||||
UiPixelRect.FromPositionAndSize(0, 0, (int)parentWidth, (int)parentHeight));
|
||||
}
|
||||
}
|
||||
|
||||
public override bool OnEvent(in UiEvent e)
|
||||
{
|
||||
switch (e.Type)
|
||||
|
|
@ -393,6 +481,11 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
|
|||
UpdateVisualState();
|
||||
}
|
||||
|
||||
internal int FaceSegmentCount => _faceSegments.Length;
|
||||
|
||||
internal IReadOnlyList<UiPixelRect> FaceSegmentRectsForTest()
|
||||
=> _faceSegments.Select(segment => segment.Rect(Width, Height)).ToArray();
|
||||
|
||||
public void OnGlobalUiTime(double nowSeconds)
|
||||
{
|
||||
if (!_hotClicking || !HotClickEnabled)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue