feat(ui): port favorite spell bar overflow
Import the retail arrow-only spell bar scrollbar from LayoutDesc, preserve authored end-button extents and HideDisabled behavior in the shared retained widget, and bind each favorite list to its sole horizontal pixel-scroll model. Match retail selection exposure for off-screen spells and pin the behavior with real-fixture conformance tests. Document the six-slice world-interaction completion program as the active pre-M4 work order. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
20e2998adb
commit
02c29e67c8
12 changed files with 582 additions and 45 deletions
|
|
@ -21,6 +21,7 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
public const uint SpellNameId = 0x1000048Bu;
|
||||
public const uint EndowmentId = 0x100000B1u;
|
||||
public const uint CastButtonId = 0x100000B2u;
|
||||
public const uint FavoriteScrollbarId = 0x100000B5u;
|
||||
public const uint FavoriteListId = 0x100000B6u;
|
||||
|
||||
private static readonly uint[] TabIds =
|
||||
|
|
@ -50,6 +51,7 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
private readonly UiElement[] _tabs;
|
||||
private readonly UiElement[] _groups;
|
||||
private readonly UiItemList?[] _lists;
|
||||
private readonly UiScrollbar?[] _scrollbars;
|
||||
private readonly UiButton _cast;
|
||||
private readonly UiText? _spellName;
|
||||
private readonly UiElement _endowmentHost;
|
||||
|
|
@ -78,6 +80,7 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
UiElement[] tabs,
|
||||
UiElement[] groups,
|
||||
UiItemList?[] lists,
|
||||
UiScrollbar?[] scrollbars,
|
||||
UiButton cast,
|
||||
UiElement endowmentHost,
|
||||
UiShortcutDigitGraphics? shortcutDigits,
|
||||
|
|
@ -98,6 +101,7 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
_tabs = tabs;
|
||||
_groups = groups;
|
||||
_lists = lists;
|
||||
_scrollbars = scrollbars;
|
||||
_cast = cast;
|
||||
_spellName = layout.FindElement(SpellNameId) as UiText;
|
||||
_endowmentHost = endowmentHost;
|
||||
|
|
@ -114,6 +118,16 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
_endowmentSlot.DoubleClicked = () => { SelectEndowment(); CastSelected(); };
|
||||
_endowmentHost.AddChild(_endowmentSlot);
|
||||
|
||||
for (int i = 0; i < _lists.Length; i++)
|
||||
{
|
||||
if (_lists[i] is not { } list || _scrollbars[i] is not { } scrollbar)
|
||||
continue;
|
||||
list.HorizontalScroll = true;
|
||||
scrollbar.Horizontal = true;
|
||||
scrollbar.Model = list.Scroll;
|
||||
scrollbar.SpriteResolve ??= list.SpriteResolve;
|
||||
}
|
||||
|
||||
for (int i = 0; i < tabs.Length; i++)
|
||||
{
|
||||
int index = i;
|
||||
|
|
@ -156,6 +170,7 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
var tabs = new UiElement[8];
|
||||
var groups = new UiElement[8];
|
||||
var lists = new UiItemList?[8];
|
||||
var scrollbars = new UiScrollbar?[8];
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
if (layout.FindElement(TabIds[i]) is not { } tab
|
||||
|
|
@ -164,13 +179,15 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
tabs[i] = tab;
|
||||
groups[i] = group;
|
||||
lists[i] = Descendants(group).OfType<UiItemList>().FirstOrDefault();
|
||||
scrollbars[i] = Descendants(group).OfType<UiScrollbar>().FirstOrDefault(
|
||||
scrollbar => scrollbar.DatElementId == FavoriteScrollbarId);
|
||||
}
|
||||
|
||||
return new SpellcastingUiController(
|
||||
layout, spellbook, casting, objects, playerGuid, resolveSpellIcon,
|
||||
resolveItemDragIcon, useItem, selection,
|
||||
addFavorite, removeFavorite,
|
||||
tabs, groups, lists, cast, endowmentHost,
|
||||
tabs, groups, lists, scrollbars, cast, endowmentHost,
|
||||
shortcutDigits, emptySlotSprite);
|
||||
}
|
||||
|
||||
|
|
@ -234,6 +251,8 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
_endowmentSelected[_activeTab] = favorites.Count == 0 && _endowmentItemId != 0u;
|
||||
}
|
||||
SyncSelection();
|
||||
if (_selected[_activeTab] is uint visibleSpell)
|
||||
ScrollSelectedSpellIntoView(_activeTab, visibleSpell);
|
||||
UpdateCastAvailability();
|
||||
}
|
||||
|
||||
|
|
@ -256,9 +275,27 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
_endowmentSelected[_activeTab] = false;
|
||||
_selected[_activeTab] = spellId;
|
||||
SyncSelection();
|
||||
ScrollSelectedSpellIntoView(_activeTab, spellId);
|
||||
UpdateCastAvailability();
|
||||
}
|
||||
|
||||
private void ScrollSelectedSpellIntoView(int tab, uint spellId)
|
||||
{
|
||||
UiItemList? list = _lists[tab];
|
||||
if (list is null) return;
|
||||
for (int i = 0; i < list.GetNumUIItems(); i++)
|
||||
{
|
||||
if (list.GetItem(i) is UiCatalogSlot { EntryId: var entryId }
|
||||
&& entryId == spellId)
|
||||
{
|
||||
// SpellCastSubMenu::SetSelected @ 0x004C5B00 exposes the
|
||||
// selected UIItem through UIElement_ListBox::ScrollToView.
|
||||
list.ScrollItemIntoView(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CastSelected()
|
||||
{
|
||||
if (_endowmentSelected[_activeTab] && _endowmentItemId != 0u)
|
||||
|
|
@ -294,6 +331,7 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
{
|
||||
list.Flush();
|
||||
list.SingleRow = true;
|
||||
list.HorizontalScroll = true;
|
||||
list.CellWidth = 32f;
|
||||
list.CellHeight = 32f;
|
||||
list.CellEmptySprite = _emptySlotSprite;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue