using System.Collections.Generic;
using AcDream.App.UI.Layout;
namespace AcDream.App.UI;
///
/// Retail UIElement_TabControl (Type 8) — the Options panel's tab strip +
/// mounted-page switcher. Owns the authored tab table (dat property 0x2E,
/// see ): activating a tab shows exactly ONE page-slot
/// child and hides the rest, and updates the corresponding tab button's Open/Closed
/// visual state.
///
///
/// The tab buttons and page-slot children are ORDINARY imported dat elements — this
/// widget does not set , so
/// LayoutImporter builds them the normal recursive way (buttons build as
/// with Open/Closed states — retail's tab buttons are Type 0xC
/// text elements, not Type-1 buttons; page slots build as generic containers whose
/// OWN children are the mounted page's content via BaseLayoutId/
/// BaseElement inheritance). This widget only wires cross-references the tab
/// table names by id, which it can only resolve once that subtree exists — see
/// .
///
///
///
/// Retail anchors: docs/research/2026-08-10-options-panel-structure.md §1.3
/// (the tab table property 0x2E struct shape), §10.1 (structural inventory —
/// tab buttons `0x1000020D..0x1000020F`/`0x1000050B`, page slots
/// `0x10000211..0x10000213`/`0x1000050C`, Gameplay the authored default). Verified
/// byte-for-byte against the regenerated options_2100002B.json fixture: four
/// entries, Gameplay (`0x1000020D`/`0x10000212`) is the sole IsDefault row.
///
///
///
/// Retail's OnVisibilityChanged auto-apply/auto-revert semantics (research doc
/// §3.6 — hiding a page reverts uncommitted edits, showing one applies + commits) are
/// OUT of this widget's scope: they belong to the OptionPage/
/// PlayerOptionPage model a page controller owns (Campaign OP slice OP3+).
/// This widget only switches which page slot is .
///
///
public sealed class UiTabControl : UiElement, IUiChildrenAttachedListener
{
private readonly IReadOnlyList _tabs;
public UiTabControl(IReadOnlyList tabs)
{
_tabs = tabs;
}
/// The authored tab table this control was built from, in authored array order.
public IReadOnlyList Tabs => _tabs;
/// Dat element id of the currently active page slot. 0 before the first mount.
public uint ActivePageElementId { get; private set; }
///
/// Retail wires the tab table's cross-references (button ↔ page ↔ default) once
/// the imported subtree exists — see 's
/// doc for why this can't happen during DatWidgetFactory.Create. Every tab
/// button's click is bound to switch to its paired page, then the authored
/// default tab (the entry with true, or
/// the first entry if the dat authored none) is activated.
///
public void OnChildrenAttached()
{
UiTabTableEntry? defaultEntry = null;
foreach (UiTabTableEntry entry in _tabs)
{
UiElement? button = FindDirectChild(entry.ButtonElementId);
uint pageId = entry.PageElementId;
RetailTabBinding.SetClick(button, () => SwitchTo(pageId));
if (entry.IsDefault)
defaultEntry = entry;
}
defaultEntry ??= _tabs.Count > 0 ? _tabs[0] : null;
if (defaultEntry is { } def)
SwitchTo(def.PageElementId);
}
///
/// Activates the page slot named : shows it, hides
/// every other authored page slot, and sets each tab button's Open/Closed state to
/// match (). No-op if the page is already active.
///
public void SwitchTo(uint pageElementId)
{
if (ActivePageElementId == pageElementId) return;
foreach (UiTabTableEntry entry in _tabs)
{
bool active = entry.PageElementId == pageElementId;
UiElement? page = FindDirectChild(entry.PageElementId);
if (page is not null) page.Visible = active;
UiElement? button = FindDirectChild(entry.ButtonElementId);
RetailTabBinding.SetOpen(button, active);
}
ActivePageElementId = pageElementId;
}
private UiElement? FindDirectChild(uint datElementId)
{
foreach (UiElement child in Children)
if (child.DatElementId == datElementId)
return child;
return null;
}
}