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:
Erik 2026-07-15 19:11:07 +02:00
parent 0527325b25
commit e3605672bb
12 changed files with 466 additions and 62 deletions

View file

@ -82,6 +82,10 @@ public sealed class UiItemList : UiElement
public int GetNumUIItems() => _cells.Count;
/// <summary>Returns this cell's current retained-list position, matching
/// UIElement_ListBox::WhatNum. -1 means the cell is not owned by this list.</summary>
public int IndexOf(UiItemSlot cell) => _cells.IndexOf(cell);
public UiItemSlot? GetItem(int index)
=> index >= 0 && index < _cells.Count ? _cells[index] : null;
@ -128,6 +132,24 @@ public sealed class UiItemList : UiElement
/// <summary>Fixed cell height in grid mode (pairs with CellWidth).</summary>
public float CellHeight { get; set; }
/// <summary>
/// Lay cells left-to-right in one row. Retail favorite spell lists use the
/// horizontal UpdateEmptySlots branch (row-count attribute 0x5F is -1), so
/// the number of visible cells is determined by width rather than the nine
/// shortcut-number overlays.
/// </summary>
public bool SingleRow { get; set; }
/// <summary>
/// Maintain a tail of empty UIItems that fills the visible horizontal extent.
/// Port of UIElement_ItemList::UpdateEmptySlots @ 0x004E3700.
/// </summary>
public bool FillVisibleEmptySlots { get; set; }
/// <summary>Creates an empty cell for <see cref="FillVisibleEmptySlots"/>.
/// Null uses a plain physical UiItemSlot.</summary>
public Func<UiItemSlot>? EmptySlotFactory { get; set; }
/// <summary>Whole rows needed for <paramref name="cellCount"/> cells in a grid of
/// <paramref name="columns"/> columns.</summary>
public static int RowCount(int cellCount, int columns)
@ -177,7 +199,11 @@ public sealed class UiItemList : UiElement
return;
}
int cols = Columns < 1 ? 1 : Columns;
UpdateEmptySlots();
int cols = SingleRow
? Math.Max(1, _cells.Count)
: Columns < 1 ? 1 : Columns;
int cellH = (int)MathF.Round(CellHeight);
// Drive the shared scroll model from the current geometry, then re-clamp the offset to
@ -201,6 +227,35 @@ public sealed class UiItemList : UiElement
}
}
/// <summary>
/// Retail's horizontal branch pads to floor(listWidth / cellWidth), and when
/// the viewport shrinks removes only empty cells from the tail. Occupied cells
/// are never discarded merely because they extend beyond the viewport.
/// </summary>
private void UpdateEmptySlots()
{
if (!FillVisibleEmptySlots || !SingleRow || CellWidth <= 0f)
return;
int visibleCellCount = Math.Max(0, (int)MathF.Floor(Width / CellWidth));
while (_cells.Count < visibleCellCount)
{
UiItemSlot cell = EmptySlotFactory?.Invoke() ?? new UiItemSlot();
cell.SpriteResolve ??= SpriteResolve;
if (_cellEmptySprite != 0) cell.EmptySprite = _cellEmptySprite;
cell.Anchors = AnchorEdges.None;
_cells.Add(cell);
AddChild(cell);
}
while (_cells.Count > visibleCellCount && _cells[^1].IsEmptySlot)
{
UiItemSlot cell = _cells[^1];
_cells.RemoveAt(_cells.Count - 1);
RemoveChild(cell);
}
}
protected override bool ClipsChildren => CellWidth > 0f;
public void Flush()