fix(vendor): gate-findings pass — the X button HIDES like retail, clicks return, the dropdown scrolls, pyreal suffix, staged-tab slots
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
The user's connected gate found five issues; each fixed at the root: G4 (the discovery): retail's vendor X button calls only SetVisible(0) (pc:204147-204182) — the SESSION stays open and re-using the vendor lands on the same-session refresh; the range watcher remains the sole real close. Our port invented a full teardown on X, which is exactly why reopening died. The Runtime fixture proves the wire dispatch was never the problem; ACE has no already-open short-circuit. G3 (regression from the drag-suppression fix): denying IsDragSource also dropped press capture, so clicks fell through to window-drag. UiItemSlot.HandlesClick now claims presses for any occupied cell independent of drag eligibility — clickable and draggable are separate concerns. G5: the authored popup 0x21000043 is ONE scrollable column with a real scrollbar subtree (live-dat scan: ListBox 0x10000350 + scrollbar 0x10000351), not a 3x6 grid. UiMenu gains an authored-driven Scrollable mode (wheel, thumb drag, track paging, up/down buttons); chat's menu is untouched and its ten tests prove it. G1: retail's cost format is "%s %hsp (you have %hsp)" — the p after each %hs is a LITERAL pyreal suffix the port swallowed as part of the specifier. Restored. G2: the Buying/Selling pages' authored lists (same cell template as Items) get the empty-slot fill, presentation-only until staging. Clean-room complete solution: 11,390 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
58c8de264e
commit
5224e43890
8 changed files with 1061 additions and 51 deletions
|
|
@ -48,10 +48,16 @@ namespace AcDream.App.UI.Layout;
|
|||
/// <para>
|
||||
/// <b>Lifecycle: pure state projection.</b> This controller owns no
|
||||
/// session/transaction state of its own. It opens/repopulates/closes purely
|
||||
/// by observing <see cref="VendorState.Changed"/>; the one mutation it may
|
||||
/// trigger is the close button calling <see cref="VendorState.Close"/> —
|
||||
/// going through the owner (client-local per research §A.4, nothing on the
|
||||
/// wire), never writing fields directly.
|
||||
/// by observing <see cref="VendorState.Changed"/>. The close button (G4,
|
||||
/// vendor gate finding) does NOT mutate <see cref="VendorState"/> at all —
|
||||
/// see <see cref="CloseButtonPressed"/> for the retail citation
|
||||
/// (<c>gmVendorUI::HandleButtonClicks</c>'s <c>0x100000d6</c> case,
|
||||
/// <c>pc:204147-204182</c>): with nothing staged it is a plain window hide,
|
||||
/// leaving the session open in the background exactly like retail's
|
||||
/// still-registered range watcher. <see cref="VendorState.Close"/> —
|
||||
/// client-local per research §A.4, nothing on the wire — is reached only via
|
||||
/// <see cref="RuntimeVendorRangeQuery.EnforceRange"/>'s distance check, never
|
||||
/// from a direct field write here.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class VendorUiController : IRetainedPanelController
|
||||
|
|
@ -79,6 +85,19 @@ public sealed class VendorUiController : IRetainedPanelController
|
|||
public const uint BuyingPageId = 0x100000C4u;
|
||||
public const uint SellingPageId = 0x100000CDu;
|
||||
|
||||
// G2 (vendor gate finding): the Buying/Selling pages author their OWN
|
||||
// item strip + scrollbar pair, geometrically identical to the Items
|
||||
// tab's (same X/Y/Width/Height, same cell-template attribute 0x1000000e
|
||||
// -> prototype 0x1000033A, verified against the fixture). Staging
|
||||
// (populating these lists with a player's held-for-sale / to-buy items)
|
||||
// is still deferred (Slice 6 territory) — these ids exist ONLY so the
|
||||
// authored empty-slot fill can be wired, matching the Items list's
|
||||
// treatment instead of leaving a bare blue background.
|
||||
public const uint BuyingListId = 0x100000C5u;
|
||||
public const uint BuyingScrollbarId = 0x100000C6u;
|
||||
public const uint SellingListId = 0x100000CEu;
|
||||
public const uint SellingScrollbarId = 0x100000CFu;
|
||||
|
||||
/// <summary>
|
||||
/// F1 (Slice 5.4 review): the category dropdown's authored popup.
|
||||
/// Retail <c>UIElement_Menu::MakePopup</c> (<c>0x0046D310</c>,
|
||||
|
|
@ -124,6 +143,35 @@ public sealed class VendorUiController : IRetainedPanelController
|
|||
/// the absent authored sprite exactly rather than inventing one.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>G5 correction (vendor gate finding): it is a SCROLLABLE single
|
||||
/// column, not a 3-column grid.</b> The F1 review's "column-major grid"
|
||||
/// framing was wrong — a live-dat scan (<c>tools/VendorLayoutScan</c>,
|
||||
/// <c>dump</c>/<c>resolved 0x21000043 0x1000034F</c>) shows
|
||||
/// <c>0x1000034F</c> has TWO children, not one: the ListBox
|
||||
/// <c>0x10000350</c> (100x108, resolved attribute <c>0x5E=6</c>/
|
||||
/// <c>0x5F=1</c> — six rows, ONE column) AND a SIBLING
|
||||
/// <c>UIElement_Scrollbar</c> (class <c>0xB</c>, element
|
||||
/// <c>0x10000351</c>, 16x108, docked at X=100 immediately right of the
|
||||
/// list, with a real thumb/up/down-button subtree matching
|
||||
/// <see cref="UiScrollbar"/>'s own shape exactly: thumb caps
|
||||
/// <c>0x06004C60</c>/<c>63</c>/<c>66</c>, up button (element
|
||||
/// <c>0x10000071</c>) <c>0x06004C69</c>/<c>6A</c>/<c>6B</c>, down button
|
||||
/// (element <c>0x10000072</c>) <c>0x06004C6C</c>/<c>6D</c>/<c>6E</c>,
|
||||
/// track <c>0x06004C5F</c>). With 18 authored categories and only 6
|
||||
/// visible rows, retail's actual rendering is a single scrolling column
|
||||
/// (matching the user's reference screenshot: ~visible rows + scrollbar +
|
||||
/// highlight — not our earlier 3-column x 6-row grid showing all 18 at
|
||||
/// once). <see cref="UiMenu.Scrollable"/> switches the popup to this
|
||||
/// shape; <see cref="UiMenu.RowsPerColumn"/> keeps its existing meaning
|
||||
/// as the authored visible-row count (still 6 — 108px ListBox height /
|
||||
/// 18px row height, now interpreted as "rows before scrolling" instead
|
||||
/// of "rows before wrapping to a new column"). Chat's own popup
|
||||
/// (LayoutDesc <c>0x21000006</c>, element <c>0x1000001C</c>) has NO
|
||||
/// sibling scrollbar element and is unaffected —
|
||||
/// <see cref="ChatWindowController"/> never sets <c>Scrollable</c>, so
|
||||
/// it keeps the original grid path byte-identical.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The button FACE reuses the same two sprites: <c>0x060012B3</c> is
|
||||
/// literally what vendor's OWN button-face child (<c>0x1000034D</c>)
|
||||
/// resolves to in the fixture, and <c>0x060012B4</c> (the row
|
||||
|
|
@ -150,6 +198,19 @@ public sealed class VendorUiController : IRetainedPanelController
|
|||
private const uint TypeMenuNormalSprite = 0x060012B3u;
|
||||
private const uint TypeMenuPressedSprite = 0x060012B4u;
|
||||
|
||||
// G5 (vendor gate finding): the popup's docked scrollbar (element
|
||||
// 0x10000351, verified via tools/VendorLayoutScan against the live dat —
|
||||
// see the class doc's "G5 correction" paragraph above). Width/button
|
||||
// extent both 16px matching the authored element/child sizes exactly.
|
||||
private const float TypeMenuScrollbarWidth = 16f;
|
||||
private const float TypeMenuScrollButtonExtent = 16f;
|
||||
private const uint TypeMenuScrollTrackSprite = 0x06004C5Fu;
|
||||
private const uint TypeMenuScrollThumbTopSprite = 0x06004C60u;
|
||||
private const uint TypeMenuScrollThumbSprite = 0x06004C63u;
|
||||
private const uint TypeMenuScrollThumbBottomSprite = 0x06004C66u;
|
||||
private const uint TypeMenuScrollUpSprite = 0x06004C69u;
|
||||
private const uint TypeMenuScrollDownSprite = 0x06004C6Cu;
|
||||
|
||||
/// <summary>
|
||||
/// Retail's ordered category table, transcribed verbatim from
|
||||
/// <c>VendorItemsUI::OpenVendor</c>'s <c>AddTypeFilter</c> call chain
|
||||
|
|
@ -195,6 +256,10 @@ public sealed class VendorUiController : IRetainedPanelController
|
|||
private readonly UiElement _buyingTab;
|
||||
private readonly UiElement _sellingTab;
|
||||
private readonly UiItemList _itemList;
|
||||
// G2: presentation-only strips (empty-slot fill only, never populated —
|
||||
// see the BuyingListId/SellingListId doc comments).
|
||||
private readonly UiItemList? _buyingList;
|
||||
private readonly UiItemList? _sellingList;
|
||||
private readonly UiMenu _typeMenu;
|
||||
private readonly UiText _itemNameText;
|
||||
private readonly UiText _itemCostText;
|
||||
|
|
@ -228,6 +293,10 @@ public sealed class VendorUiController : IRetainedPanelController
|
|||
UiElement sellingTab,
|
||||
UiItemList itemList,
|
||||
UiScrollbar? itemScrollbar,
|
||||
UiItemList? buyingList,
|
||||
UiScrollbar? buyingScrollbar,
|
||||
UiItemList? sellingList,
|
||||
UiScrollbar? sellingScrollbar,
|
||||
UiMenu typeMenu,
|
||||
UiText itemNameText,
|
||||
UiText itemCostText,
|
||||
|
|
@ -237,7 +306,9 @@ public sealed class VendorUiController : IRetainedPanelController
|
|||
UiDatFont? datFont,
|
||||
BitmapFont? debugFont,
|
||||
Func<uint, (uint tex, int w, int h)> resolveSprite,
|
||||
uint emptySlotSprite)
|
||||
uint emptySlotSprite,
|
||||
uint buyingEmptySlotSprite,
|
||||
uint sellingEmptySlotSprite)
|
||||
{
|
||||
_vendor = vendor;
|
||||
_window = window;
|
||||
|
|
@ -254,6 +325,8 @@ public sealed class VendorUiController : IRetainedPanelController
|
|||
_buyingTab = buyingTab;
|
||||
_sellingTab = sellingTab;
|
||||
_itemList = itemList;
|
||||
_buyingList = buyingList;
|
||||
_sellingList = sellingList;
|
||||
_typeMenu = typeMenu;
|
||||
_itemNameText = itemNameText;
|
||||
_itemCostText = itemCostText;
|
||||
|
|
@ -295,6 +368,25 @@ public sealed class VendorUiController : IRetainedPanelController
|
|||
itemScrollbar.Horizontal = true;
|
||||
}
|
||||
|
||||
// G2 (vendor gate finding): Buying/Selling get the SAME empty-slot
|
||||
// fill treatment as the Items strip above — presentation only, the
|
||||
// lists are never populated (staging into these tabs stays Slice 6
|
||||
// territory, unimplemented). Mounting here (rather than skipping
|
||||
// entirely) is what replaces the bare blue authored background with
|
||||
// the correct empty-cell art the instant the panel opens.
|
||||
ConfigureEmptyStrip(_buyingList, buyingEmptySlotSprite);
|
||||
if (buyingScrollbar is not null && _buyingList is not null)
|
||||
{
|
||||
buyingScrollbar.Model = _buyingList.Scroll;
|
||||
buyingScrollbar.Horizontal = true;
|
||||
}
|
||||
ConfigureEmptyStrip(_sellingList, sellingEmptySlotSprite);
|
||||
if (sellingScrollbar is not null && _sellingList is not null)
|
||||
{
|
||||
sellingScrollbar.Model = _sellingList.Scroll;
|
||||
sellingScrollbar.Horizontal = true;
|
||||
}
|
||||
|
||||
// F1 (Slice 5.4 review): wire the dropdown's font/sprite resolvers
|
||||
// (UiMenu draws nothing without SpriteResolve — see the popup
|
||||
// geometry class doc above) and the vendor-authored popup geometry
|
||||
|
|
@ -309,6 +401,18 @@ public sealed class VendorUiController : IRetainedPanelController
|
|||
_typeMenu.RowsPerColumn = TypeMenuRowsPerColumn;
|
||||
_typeMenu.RowHeight = TypeMenuRowHeight;
|
||||
_typeMenu.ColumnWidth = TypeMenuColumnWidth;
|
||||
// G5: the authored popup is a scrollable single column with a docked
|
||||
// scrollbar, not a column-major grid — see the class doc's "G5
|
||||
// correction" paragraph above.
|
||||
_typeMenu.Scrollable = true;
|
||||
_typeMenu.ScrollbarWidth = TypeMenuScrollbarWidth;
|
||||
_typeMenu.ScrollButtonExtent = TypeMenuScrollButtonExtent;
|
||||
_typeMenu.ScrollTrackSprite = TypeMenuScrollTrackSprite;
|
||||
_typeMenu.ScrollThumbTopSprite = TypeMenuScrollThumbTopSprite;
|
||||
_typeMenu.ScrollThumbSprite = TypeMenuScrollThumbSprite;
|
||||
_typeMenu.ScrollThumbBottomSprite = TypeMenuScrollThumbBottomSprite;
|
||||
_typeMenu.ScrollUpSprite = TypeMenuScrollUpSprite;
|
||||
_typeMenu.ScrollDownSprite = TypeMenuScrollDownSprite;
|
||||
_typeMenu.OnSelect = payload =>
|
||||
{
|
||||
if (payload is uint mask) SelectCategory(mask);
|
||||
|
|
@ -322,7 +426,7 @@ public sealed class VendorUiController : IRetainedPanelController
|
|||
RetailTabBinding.SetClick(_buyingTab, () => ShowTab(VendorPanelTab.Buying));
|
||||
RetailTabBinding.SetClick(_sellingTab, () => ShowTab(VendorPanelTab.Selling));
|
||||
if (_close is not null)
|
||||
_close.OnClick = () => _vendor.Close();
|
||||
_close.OnClick = CloseButtonPressed;
|
||||
// Slice 6.3: retail gmVendorUI::HandleButtonClicks' 0x100000C2 case —
|
||||
// BuySingleItem(selectedID) — an immediate single-item purchase, no
|
||||
// staging list required (research doc §B.1).
|
||||
|
|
@ -398,6 +502,14 @@ public sealed class VendorUiController : IRetainedPanelController
|
|||
/// <param name="debugFont">Fallback debug bitmap font (used when <paramref name="datFont"/> is null).</param>
|
||||
/// <param name="resolveSprite">Dat RenderSurface id → (GL tex handle, px width, px height).</param>
|
||||
/// <param name="emptySlotSprite">Authored empty-slot background for the item strip, or 0 for none.</param>
|
||||
/// <param name="buyingEmptySlotSprite">
|
||||
/// G2: authored empty-slot background for the Buying tab's own item
|
||||
/// strip (<see cref="BuyingListId"/>), or 0 for none.
|
||||
/// </param>
|
||||
/// <param name="sellingEmptySlotSprite">
|
||||
/// G2: authored empty-slot background for the Selling tab's own item
|
||||
/// strip (<see cref="SellingListId"/>), or 0 for none.
|
||||
/// </param>
|
||||
public static VendorUiController? Bind(
|
||||
ImportedLayout layout,
|
||||
VendorState vendor,
|
||||
|
|
@ -411,7 +523,9 @@ public sealed class VendorUiController : IRetainedPanelController
|
|||
UiDatFont? datFont,
|
||||
BitmapFont? debugFont,
|
||||
Func<uint, (uint tex, int w, int h)> resolveSprite,
|
||||
uint emptySlotSprite = 0u)
|
||||
uint emptySlotSprite = 0u,
|
||||
uint buyingEmptySlotSprite = 0u,
|
||||
uint sellingEmptySlotSprite = 0u)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(layout);
|
||||
ArgumentNullException.ThrowIfNull(vendor);
|
||||
|
|
@ -442,6 +556,12 @@ public sealed class VendorUiController : IRetainedPanelController
|
|||
UiScrollbar? itemScrollbar = layout.FindElement(ItemScrollbarId) as UiScrollbar;
|
||||
UiButton? buyButton = layout.FindElement(BuyButtonId) as UiButton;
|
||||
UiButton? addButton = layout.FindElement(AddButtonId) as UiButton;
|
||||
// G2: optional — presentation-only strips, absent gracefully no-ops
|
||||
// (see the class-level BuyingListId/SellingListId doc comments).
|
||||
UiItemList? buyingList = layout.FindElement(BuyingListId) as UiItemList;
|
||||
UiScrollbar? buyingScrollbar = layout.FindElement(BuyingScrollbarId) as UiScrollbar;
|
||||
UiItemList? sellingList = layout.FindElement(SellingListId) as UiItemList;
|
||||
UiScrollbar? sellingScrollbar = layout.FindElement(SellingScrollbarId) as UiScrollbar;
|
||||
|
||||
return new VendorUiController(
|
||||
vendor,
|
||||
|
|
@ -460,6 +580,10 @@ public sealed class VendorUiController : IRetainedPanelController
|
|||
sellingTab,
|
||||
itemList,
|
||||
itemScrollbar,
|
||||
buyingList,
|
||||
buyingScrollbar,
|
||||
sellingList,
|
||||
sellingScrollbar,
|
||||
typeMenu,
|
||||
itemNameText,
|
||||
itemCostText,
|
||||
|
|
@ -469,7 +593,9 @@ public sealed class VendorUiController : IRetainedPanelController
|
|||
datFont,
|
||||
debugFont,
|
||||
resolveSprite,
|
||||
emptySlotSprite);
|
||||
emptySlotSprite,
|
||||
buyingEmptySlotSprite,
|
||||
sellingEmptySlotSprite);
|
||||
}
|
||||
|
||||
private enum VendorPanelTab { Items, Buying, Selling }
|
||||
|
|
@ -906,9 +1032,14 @@ public sealed class VendorUiController : IRetainedPanelController
|
|||
|
||||
int playerTotal = _objects.Get(_playerGuid())?.Properties.GetInt((uint)PropertyInt.CoinValue) ?? 0;
|
||||
string verb = quantity <= 1 ? "costs" : "cost";
|
||||
// G1 (vendor gate finding): retail's literal format is
|
||||
// "%s %hsp (you have %hsp)" (pc:202769) — the trailing "p" after
|
||||
// each %hs is a LITERAL pyreal-currency suffix character, not part
|
||||
// of the specifier. The prior port read "%hsp" as one token and
|
||||
// dropped both literal p's.
|
||||
return string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0} {1} (you have {2})",
|
||||
"{0} {1}p (you have {2}p)",
|
||||
verb,
|
||||
price.ToString("N0", CultureInfo.InvariantCulture),
|
||||
playerTotal.ToString("N0", CultureInfo.InvariantCulture));
|
||||
|
|
@ -1002,6 +1133,52 @@ public sealed class VendorUiController : IRetainedPanelController
|
|||
_vendor.Profile.AlternateCurrencyWcid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// G4 (vendor gate finding): port of retail's close/pushpin button
|
||||
/// handler — <c>gmVendorUI::HandleButtonClicks</c>'s <c>0x100000d6</c>
|
||||
/// case (<c>pc:204147-204182</c>). Retail branches on whether the
|
||||
/// Buying/Selling staging lists (<c>m_buyList</c>/<c>m_sellList</c>) hold
|
||||
/// anything uncommitted: with nothing pending it calls ONLY
|
||||
/// <c>this->vtable->SetVisible(0)</c> — a plain window hide, NOT
|
||||
/// <c>gmVendorUI::CloseVendor</c> (<c>pc:202080</c>, the range-watcher-
|
||||
/// unregister/session-teardown function <see cref="VendorState.Close"/>
|
||||
/// ports). Only when something IS pending does retail show a
|
||||
/// confirmation dialog ("You have not completed all transactions...")
|
||||
/// whose Yes callback (<c>gmVendorUI::CloseVendorDialogCallback</c>,
|
||||
/// <c>pc:202104-202166</c>) is what actually reaches
|
||||
/// <c>CM_Vendor::SendNotice_CloseVendor</c> — itself an internal
|
||||
/// notice-bus fanout to local UI listeners, not a network send (see the
|
||||
/// class doc's A.4 citation: retail's close path never puts anything on
|
||||
/// the wire either way).
|
||||
/// <para>
|
||||
/// This controller's staging lists are ALWAYS empty (Slice 6 territory —
|
||||
/// the "Buying"/"Selling" tabs render but are never populated, see the
|
||||
/// class doc's "Three tabs, not two" note), so retail's
|
||||
/// <c>m_buyList.head == 0 && m_sellList.head == 0</c> condition
|
||||
/// is vacuously true for every close today — the confirmation-dialog
|
||||
/// branch has no reachable case yet and is deliberately not ported;
|
||||
/// revisit once staging lands.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Behavior change from the prior port.</b> This button used to call
|
||||
/// <see cref="VendorState.Close"/> directly — a full session teardown
|
||||
/// (VendorId/Profile/Items cleared, every materialized shop item
|
||||
/// retired) on every ordinary close, which retail does NOT do. The
|
||||
/// session now stays open in the background exactly like retail's
|
||||
/// hidden-but-still-registered range watcher:
|
||||
/// <see cref="RuntimeVendorRangeQuery.EnforceRange"/> is evaluated
|
||||
/// unconditionally every frame regardless of window visibility (it reads
|
||||
/// only <see cref="VendorState.VendorId"/>, never this window's
|
||||
/// <c>IsVisible</c>), so leaving <c>UseRadius</c> still converges to a
|
||||
/// full <see cref="VendorState.Close"/> exactly as before. Re-approaching
|
||||
/// the SAME vendor before then now lands on retail's <c>sameVendor==1</c>
|
||||
/// refresh-in-place path (<see cref="VendorStateTransitionKind.Refreshed"/>,
|
||||
/// which preserves the player's category selection) instead of a full
|
||||
/// from-scratch <see cref="VendorStateTransitionKind.Opened"/> reopen.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private void CloseButtonPressed() => _window.Hide();
|
||||
|
||||
private void ClearContent()
|
||||
{
|
||||
_presentCategories.Clear();
|
||||
|
|
@ -1018,6 +1195,39 @@ public sealed class VendorUiController : IRetainedPanelController
|
|||
ClearSelectionDisplay();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// G2 (vendor gate finding): mirrors the Items strip's empty-slot-fill
|
||||
/// configuration (see the constructor's <c>_itemList</c> block) for the
|
||||
/// Buying/Selling pages' own authored lists — presentation only. The
|
||||
/// list is flushed once (dropping the single default cell every
|
||||
/// <see cref="UiItemList"/> constructs itself with) so every visible
|
||||
/// cell comes from <paramref name="list"/>'s own
|
||||
/// <see cref="UiItemList.EmptySlotFactory"/> with consistent styling
|
||||
/// (non-drag-source), then left alone: nothing ever calls
|
||||
/// <see cref="UiItemList.AddItem"/> on it, so
|
||||
/// <see cref="UiItemList.LayoutCells"/>'s empty-slot padding
|
||||
/// (<c>UpdateEmptySlots</c>) is the ONLY thing that ever populates it.
|
||||
/// </summary>
|
||||
private static void ConfigureEmptyStrip(UiItemList? list, uint emptySlotSprite)
|
||||
{
|
||||
if (list is null) return;
|
||||
|
||||
list.Flush();
|
||||
list.Columns = 1;
|
||||
list.SingleRow = true;
|
||||
list.HorizontalScroll = true;
|
||||
list.CellWidth = 32f;
|
||||
list.CellHeight = 32f;
|
||||
list.FillVisibleEmptySlots = true;
|
||||
if (emptySlotSprite != 0u)
|
||||
list.CellEmptySprite = emptySlotSprite;
|
||||
list.EmptySlotFactory = () => new UiItemSlot
|
||||
{
|
||||
SpriteResolve = list.SpriteResolve,
|
||||
AllowDragSource = false,
|
||||
};
|
||||
}
|
||||
|
||||
private static void SetPlainText(UiText text, string value)
|
||||
{
|
||||
IReadOnlyList<UiText.Line> lines = string.IsNullOrEmpty(value)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue