feat(ui): port retail spellbook interactions

Resolve the authored spell shortcut row prototype so the spellbook presents the retail icon, name, separator, selected overlay, and scrollbar geometry. Port exact school and level filters, stable display ordering, selection exposure, and learned-spell drags into the open favorite bar.

Route deletion through the shared retail confirmation dialog and send CM_Magic::Event_RemoveSpell only after an affirmative answer, leaving the inbound server notice authoritative for list state.

Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
Erik 2026-07-15 16:26:30 +02:00
parent ad30c37a48
commit ac2ca8f965
17 changed files with 727 additions and 47 deletions

View file

@ -39,6 +39,13 @@ public sealed class UiItemList : UiElement
public Func<uint, (uint tex, int w, int h)>? SpriteResolve { get; set; }
/// <summary>
/// Optional non-weenie catalog drop target. Used by retail spell shortcuts;
/// physical-item drops continue through <see cref="DragHandler"/>.
/// Coordinates are local to this list.
/// </summary>
public Action<object, int, int>? CatalogDropped { get; set; }
private uint _cellEmptySprite;
/// <summary>Empty-slot sprite for THIS list's cells, resolved from the dat cell template
/// (retail attribute 0x1000000e -> catalog 0x21000037 prototype's ItemSlot_Empty; see
@ -204,6 +211,16 @@ public sealed class UiItemList : UiElement
public override bool OnEvent(in UiEvent e)
{
if (CatalogDropped is not null && e.Payload is not null)
{
if (e.Type is UiEventType.DragEnter or UiEventType.DragOver)
return true;
if (e.Type == UiEventType.DropReleased)
{
CatalogDropped(e.Payload, e.Data1, e.Data2);
return true;
}
}
if (e.Type == UiEventType.Scroll && CellWidth > 0f)
{
// Mirror UiText: Silk +Y wheel = up/older = decrease ScrollY; negate Data0.
@ -213,6 +230,24 @@ public sealed class UiItemList : UiElement
return base.OnEvent(e);
}
/// <summary>
/// Port of the listbox expose-item behavior called by
/// <c>gmSpellbookUI::SetSelected @ 0x0048B540</c>.
/// </summary>
public void ScrollItemIntoView(int index)
{
if (CellWidth <= 0f || index < 0 || index >= _cells.Count) return;
int columns = Math.Max(1, Columns);
int row = Flow == UiItemListFlow.ColumnMajor
? index % Math.Max(1, RowCount(_cells.Count, columns))
: index / columns;
float top = row * CellHeight;
float bottom = top + CellHeight;
if (top < Scroll.ScrollY) Scroll.SetScrollY((int)MathF.Floor(top));
else if (bottom > Scroll.ScrollY + Height)
Scroll.SetScrollY((int)MathF.Ceiling(bottom - Height));
}
protected override void OnDraw(UiRenderContext ctx)
{
// The factory sets Width/Height AFTER construction, so re-layout each frame: