fix(ui): complete retail indicator detail panels
Port the authored effect row template, remaining-time and selection details, synchronize the full gmPanelUI child geometry, and route the burden indicator to Character Information panel 3. Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
a96767ba6d
commit
d1d603105f
24 changed files with 3696 additions and 147 deletions
|
|
@ -6,7 +6,8 @@ using AcDream.App.UI;
|
|||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Per-window controller for the Character window (LayoutDesc 0x2100002E, gmCharacterInfoUI).
|
||||
/// Per-window controller for the Character Information child in the production
|
||||
/// gmPanelUI layout (LayoutDesc 0x2100006E, root 0x10000183).
|
||||
///
|
||||
/// <para>Retail fills a single scrollable <c>UIElement_Text</c> element (id 0x1000011d,
|
||||
/// <c>m_pMainText</c>) by calling a sequence of Update* methods that each
|
||||
|
|
@ -32,9 +33,14 @@ namespace AcDream.App.UI.Layout;
|
|||
/// </summary>
|
||||
public static class CharacterController
|
||||
{
|
||||
public const uint LayoutId = 0x2100006Eu;
|
||||
public const uint RootId = 0x10000183u;
|
||||
|
||||
/// <summary>Dat element id for the main report text (m_pMainText). Confirmed in
|
||||
/// <c>gmCharacterInfoUI::PostInit</c> 0x004b86f0: <c>GetChildRecursive(this, 0x1000011d)</c>.</summary>
|
||||
public const uint MainTextId = 0x1000011du;
|
||||
public const uint ScrollbarId = 0x1000011Eu;
|
||||
public const uint CloseId = 0x100000FCu;
|
||||
|
||||
/// <summary>White body text — matches retail's default UIElement_Text foreground.</summary>
|
||||
private static readonly Vector4 BodyColor = new(1f, 1f, 1f, 1f);
|
||||
|
|
@ -48,27 +54,27 @@ public static class CharacterController
|
|||
/// <see cref="UiText.LinesProvider"/> so the Studio or a live session can push a fresh
|
||||
/// <see cref="CharacterSheet"/> without re-binding.
|
||||
///
|
||||
/// <para>The element must resolve as a <see cref="UiText"/> (Type 12 in the dat).
|
||||
/// If 0x1000011d is absent from the layout the bind is a no-op — partial layouts
|
||||
/// (e.g. test fakes) don't cause errors.</para>
|
||||
/// <para>Production data resolves the element as a <see cref="UiText"/>
|
||||
/// (Type 12). A synthetic fallback is created only for deliberately reduced
|
||||
/// test layouts.</para>
|
||||
/// </summary>
|
||||
/// <param name="layout">Imported 0x2100002E layout tree.</param>
|
||||
/// <param name="layout">Imported 0x2100006E / 0x10000183 layout tree.</param>
|
||||
/// <param name="data">Provider returning the current <see cref="CharacterSheet"/>.</param>
|
||||
/// <param name="datFont">Retail dat font forwarded from the render stack. May be null
|
||||
/// (falls back to the BitmapFont debug path).</param>
|
||||
public static void Bind(
|
||||
ImportedLayout layout,
|
||||
Func<CharacterSheet> data,
|
||||
UiDatFont? datFont = null)
|
||||
UiDatFont? datFont = null,
|
||||
Action? close = null)
|
||||
{
|
||||
// Retail's gmCharacterInfoUI CREATES m_pMainText (0x1000011d) at RUNTIME — it is NOT a static
|
||||
// element in any LayoutDesc (confirmed: acdream's dat-import of 0x2100002E AND 0x2100006E both
|
||||
// lack it; only a runtime UI-tree capture has it). So replicate that: build the report text
|
||||
// element ourselves and place it in the panel body, exactly as the gm*UI does at runtime.
|
||||
// End-of-retail data authors m_pMainText under the Character Information
|
||||
// child. The fallback exists only so focused controller tests can bind a
|
||||
// deliberately reduced tree without fabricating production structure.
|
||||
var root = layout.Root;
|
||||
if (root is null) return;
|
||||
|
||||
var text = new UiText
|
||||
UiText text = layout.FindElement(MainTextId) as UiText ?? new UiText
|
||||
{
|
||||
EventId = MainTextId,
|
||||
Name = "m_pMainText",
|
||||
|
|
@ -81,8 +87,17 @@ public static class CharacterController
|
|||
DatFont = datFont,
|
||||
ClickThrough = false,
|
||||
};
|
||||
if (text.Parent is null)
|
||||
root.AddChild(text);
|
||||
else if (text.DatFont is null && datFont is not null)
|
||||
text.DatFont = datFont;
|
||||
text.PreserveEndOnLayout = false;
|
||||
if (layout.FindElement(ScrollbarId) is UiScrollbar scrollbar)
|
||||
scrollbar.Model = text.Scroll;
|
||||
text.LinesProvider = () => BuildReport(data());
|
||||
root.AddChild(text);
|
||||
|
||||
if (layout.FindElement(CloseId) is UiButton closeButton)
|
||||
closeButton.OnClick = close;
|
||||
}
|
||||
|
||||
// ── Report builder ────────────────────────────────────────────────────────
|
||||
|
|
|
|||
148
src/AcDream.App/UI/Layout/EffectRowTemplateFactory.cs
Normal file
148
src/AcDream.App/UI/Layout/EffectRowTemplateFactory.cs
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
using DatReaderWriter;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Instantiates retail's effect-token template <c>0x10000128</c>. This is the
|
||||
/// retained equivalent of <c>InfoRegion::InfoRegion @ 0x004F1450</c> calling
|
||||
/// <c>UIElement_ListBox::AddItemFromTemplateList</c> for
|
||||
/// <c>EffectInfoRegion</c>.
|
||||
/// </summary>
|
||||
public sealed class EffectRowTemplateFactory
|
||||
{
|
||||
private readonly ElementInfo _template;
|
||||
private readonly Func<uint, (uint tex, int w, int h)> _resolveSprite;
|
||||
private readonly UiDatFont? _defaultFont;
|
||||
private readonly IReadOnlyDictionary<uint, UiDatFont?> _fonts;
|
||||
|
||||
public EffectRowTemplateFactory(
|
||||
ElementInfo template,
|
||||
Func<uint, (uint tex, int w, int h)> resolveSprite,
|
||||
UiDatFont? defaultFont,
|
||||
IReadOnlyDictionary<uint, UiDatFont?>? fonts = null)
|
||||
{
|
||||
_template = template ?? throw new ArgumentNullException(nameof(template));
|
||||
_resolveSprite = resolveSprite ?? throw new ArgumentNullException(nameof(resolveSprite));
|
||||
_defaultFont = defaultFont;
|
||||
_fonts = fonts ?? new Dictionary<uint, UiDatFont?>();
|
||||
}
|
||||
|
||||
public float Width => _template.Width;
|
||||
public float Height => _template.Height;
|
||||
|
||||
public static EffectRowTemplateFactory? TryLoad(
|
||||
DatCollection dats,
|
||||
Func<uint, (uint tex, int w, int h)> resolveSprite,
|
||||
UiDatFont? defaultFont,
|
||||
Func<uint, UiDatFont?>? resolveFont)
|
||||
{
|
||||
ElementInfo? template = LayoutImporter.ImportInfos(
|
||||
dats,
|
||||
EffectsUiController.LayoutId,
|
||||
EffectsUiController.RowTemplateId);
|
||||
if (template is null
|
||||
|| Find(template, EffectsUiController.RowIconId) is null
|
||||
|| Find(template, EffectsUiController.RowLabelId) is null
|
||||
|| Find(template, EffectsUiController.RowDurationId) is null
|
||||
|| template.Width <= 0f
|
||||
|| template.Height <= 0f)
|
||||
return null;
|
||||
|
||||
var fonts = new Dictionary<uint, UiDatFont?>();
|
||||
CaptureFonts(template, resolveFont, fonts);
|
||||
return new EffectRowTemplateFactory(
|
||||
template, resolveSprite, defaultFont, fonts);
|
||||
}
|
||||
|
||||
public EffectRow Create(
|
||||
uint spellId,
|
||||
uint iconTexture,
|
||||
string name,
|
||||
string remaining)
|
||||
{
|
||||
ImportedLayout content = LayoutImporter.Build(
|
||||
_template,
|
||||
_resolveSprite,
|
||||
_defaultFont,
|
||||
did => _fonts.TryGetValue(did, out UiDatFont? font)
|
||||
? font
|
||||
: _defaultFont);
|
||||
UiElement iconHost = Required(content, EffectsUiController.RowIconId);
|
||||
UiText label = Required<UiText>(content, EffectsUiController.RowLabelId);
|
||||
UiText duration = Required<UiText>(content, EffectsUiController.RowDurationId);
|
||||
|
||||
iconHost.AddChild(new UiTextureElement
|
||||
{
|
||||
Width = iconHost.Width,
|
||||
Height = iconHost.Height,
|
||||
Anchors = AnchorEdges.Left | AnchorEdges.Top
|
||||
| AnchorEdges.Right | AnchorEdges.Bottom,
|
||||
Texture = iconTexture,
|
||||
});
|
||||
label.LinesProvider = () => [new UiText.Line(name, label.DefaultColor)];
|
||||
|
||||
var slot = new UiTemplateListSlot(
|
||||
content,
|
||||
spellId,
|
||||
UiButtonStateMachine.Normal,
|
||||
UiButtonStateMachine.Highlight);
|
||||
return new EffectRow(slot, duration, remaining);
|
||||
}
|
||||
|
||||
public sealed class EffectRow
|
||||
{
|
||||
private readonly UiText _duration;
|
||||
private string _remaining;
|
||||
|
||||
internal EffectRow(
|
||||
UiTemplateListSlot slot,
|
||||
UiText duration,
|
||||
string remaining)
|
||||
{
|
||||
Slot = slot;
|
||||
_duration = duration;
|
||||
_remaining = remaining;
|
||||
_duration.LinesProvider = () =>
|
||||
[new UiText.Line(_remaining, _duration.DefaultColor)];
|
||||
}
|
||||
|
||||
public UiTemplateListSlot Slot { get; }
|
||||
|
||||
public string Remaining
|
||||
{
|
||||
get => _remaining;
|
||||
set => _remaining = value;
|
||||
}
|
||||
}
|
||||
|
||||
private static T Required<T>(ImportedLayout content, uint id)
|
||||
where T : UiElement
|
||||
=> content.FindElement(id) as T
|
||||
?? throw new InvalidOperationException(
|
||||
$"Retail effect template element 0x{id:X8} did not resolve to {typeof(T).Name}.");
|
||||
|
||||
private static UiElement Required(ImportedLayout content, uint id)
|
||||
=> content.FindElement(id)
|
||||
?? throw new InvalidOperationException(
|
||||
$"Retail effect template element 0x{id:X8} is missing.");
|
||||
|
||||
private static ElementInfo? Find(ElementInfo root, uint id)
|
||||
{
|
||||
if (root.Id == id) return root;
|
||||
foreach (ElementInfo child in root.Children)
|
||||
if (Find(child, id) is { } found)
|
||||
return found;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void CaptureFonts(
|
||||
ElementInfo info,
|
||||
Func<uint, UiDatFont?>? resolveFont,
|
||||
Dictionary<uint, UiDatFont?> fonts)
|
||||
{
|
||||
if (info.FontDid != 0u && !fonts.ContainsKey(info.FontDid))
|
||||
fonts[info.FontDid] = resolveFont?.Invoke(info.FontDid);
|
||||
foreach (ElementInfo child in info.Children)
|
||||
CaptureFonts(child, resolveFont, fonts);
|
||||
}
|
||||
}
|
||||
|
|
@ -16,16 +16,24 @@ public sealed class EffectsUiController : IRetainedPanelController
|
|||
public const uint NegativeRootId = 0x10000121u;
|
||||
public const uint CloseId = 0x100000FCu;
|
||||
public const uint ListId = 0x10000123u;
|
||||
public const uint ScrollbarId = 0x10000124u;
|
||||
public const uint InfoTextId = 0x10000126u;
|
||||
public const uint RowTemplateId = 0x10000128u;
|
||||
public const uint RowIconId = 0x10000129u;
|
||||
public const uint RowLabelId = 0x1000012Au;
|
||||
public const uint RowDurationId = 0x1000012Bu;
|
||||
|
||||
private readonly Spellbook _spellbook;
|
||||
private readonly bool _positive;
|
||||
private readonly Func<double> _serverTime;
|
||||
private readonly Func<uint, uint> _resolveSpellIcon;
|
||||
private readonly EffectRowTemplateFactory _templates;
|
||||
private readonly string _selectPrompt;
|
||||
private readonly UiItemList _list;
|
||||
private readonly UiText? _info;
|
||||
private readonly UiButton? _close;
|
||||
private readonly Dictionary<uint, UiCatalogSlot> _rows = new();
|
||||
private readonly UiScrollbar? _scrollbar;
|
||||
private readonly Dictionary<uint, EffectRowTemplateFactory.EffectRow> _rows = new();
|
||||
private uint? _selectedSpellId;
|
||||
private double _lastDurationUpdate = double.NaN;
|
||||
private bool _disposed;
|
||||
|
|
@ -38,6 +46,8 @@ public sealed class EffectsUiController : IRetainedPanelController
|
|||
bool positive,
|
||||
Func<double> serverTime,
|
||||
Func<uint, uint> resolveSpellIcon,
|
||||
EffectRowTemplateFactory templates,
|
||||
string selectPrompt,
|
||||
UiItemList list,
|
||||
Action? close)
|
||||
{
|
||||
|
|
@ -45,13 +55,18 @@ public sealed class EffectsUiController : IRetainedPanelController
|
|||
_positive = positive;
|
||||
_serverTime = serverTime;
|
||||
_resolveSpellIcon = resolveSpellIcon;
|
||||
_templates = templates;
|
||||
_selectPrompt = selectPrompt;
|
||||
_list = list;
|
||||
_info = layout.FindElement(InfoTextId) as UiText;
|
||||
_close = layout.FindElement(CloseId) as UiButton;
|
||||
_scrollbar = layout.FindElement(ScrollbarId) as UiScrollbar;
|
||||
if (_close is not null) _close.OnClick = close;
|
||||
_list.Columns = 1;
|
||||
_list.CellWidth = Math.Max(1f, _list.Width);
|
||||
_list.CellHeight = 32f;
|
||||
_list.CellWidth = templates.Width;
|
||||
_list.CellHeight = templates.Height;
|
||||
if (_scrollbar is not null)
|
||||
_scrollbar.Model = _list.Scroll;
|
||||
ConfigureInfo();
|
||||
_spellbook.EnchantmentsChanged += Rebuild;
|
||||
Rebuild();
|
||||
|
|
@ -64,6 +79,8 @@ public sealed class EffectsUiController : IRetainedPanelController
|
|||
Func<double> serverTime,
|
||||
Func<uint, (uint Texture, int Width, int Height)> spriteResolve,
|
||||
Func<uint, uint> resolveSpellIcon,
|
||||
EffectRowTemplateFactory templates,
|
||||
string selectPrompt,
|
||||
Action? close = null)
|
||||
{
|
||||
UiElement? host = layout.FindElement(ListId);
|
||||
|
|
@ -82,7 +99,15 @@ public sealed class EffectsUiController : IRetainedPanelController
|
|||
host.AddChild(list);
|
||||
}
|
||||
return new EffectsUiController(
|
||||
layout, spellbook, positive, serverTime, resolveSpellIcon, list, close);
|
||||
layout,
|
||||
spellbook,
|
||||
positive,
|
||||
serverTime,
|
||||
resolveSpellIcon,
|
||||
templates,
|
||||
selectPrompt,
|
||||
list,
|
||||
close);
|
||||
}
|
||||
|
||||
public void Tick()
|
||||
|
|
@ -95,8 +120,10 @@ public sealed class EffectsUiController : IRetainedPanelController
|
|||
return;
|
||||
_lastDurationUpdate = now;
|
||||
foreach (ActiveEnchantmentRecord enchantment in VisibleEnchantments())
|
||||
if (_rows.TryGetValue(enchantment.Identity, out UiCatalogSlot? row))
|
||||
row.Detail = FormatRemaining(enchantment, now);
|
||||
if (_rows.TryGetValue(
|
||||
enchantment.Identity,
|
||||
out EffectRowTemplateFactory.EffectRow? row))
|
||||
row.Remaining = FormatRemaining(enchantment, now);
|
||||
}
|
||||
|
||||
private void Rebuild()
|
||||
|
|
@ -111,22 +138,18 @@ public sealed class EffectsUiController : IRetainedPanelController
|
|||
{
|
||||
_spellbook.TryGetMetadata(enchantment.SpellId, out SpellMetadata? metadata);
|
||||
uint identity = enchantment.Identity;
|
||||
var row = new UiCatalogSlot
|
||||
{
|
||||
EntryId = enchantment.SpellId,
|
||||
CatalogIconTexture = metadata is null ? 0u : _resolveSpellIcon(enchantment.SpellId),
|
||||
Label = metadata?.Name ?? $"Spell {enchantment.SpellId}",
|
||||
Detail = FormatRemaining(enchantment, _serverTime()),
|
||||
ShowLabel = true,
|
||||
SpriteResolve = _list.SpriteResolve,
|
||||
};
|
||||
row.Clicked = () => Select(enchantment.SpellId);
|
||||
EffectRowTemplateFactory.EffectRow row = _templates.Create(
|
||||
enchantment.SpellId,
|
||||
metadata is null ? 0u : _resolveSpellIcon(enchantment.SpellId),
|
||||
metadata?.Name ?? $"Spell {enchantment.SpellId}",
|
||||
FormatRemaining(enchantment, _serverTime()));
|
||||
row.Slot.Clicked = () => Select(enchantment.SpellId);
|
||||
_rows[identity] = row;
|
||||
_list.AddItem(row);
|
||||
_list.AddItem(row.Slot);
|
||||
}
|
||||
}
|
||||
if (_selectedSpellId is uint selected
|
||||
&& !_rows.Values.Any(row => row.EntryId == selected))
|
||||
&& !_rows.Values.Any(row => row.Slot.EntryId == selected))
|
||||
_selectedSpellId = null;
|
||||
SyncSelection();
|
||||
}
|
||||
|
|
@ -145,7 +168,9 @@ public sealed class EffectsUiController : IRetainedPanelController
|
|||
|
||||
private static string FormatRemaining(ActiveEnchantmentRecord enchantment, double now)
|
||||
{
|
||||
if (enchantment.Duration < 0) return "Permanent";
|
||||
// EffectInfoRegion::Update @ 0x004F1C00 starts from retail's empty
|
||||
// PString and only formats it when the remaining duration is >= 0.
|
||||
if (enchantment.Duration < 0) return string.Empty;
|
||||
double remaining = Math.Max(0, enchantment.StartTime + enchantment.Duration - now);
|
||||
if (!double.IsFinite(remaining)) return "--:--";
|
||||
remaining = Math.Min(remaining, TimeSpan.MaxValue.TotalSeconds);
|
||||
|
|
@ -165,25 +190,27 @@ public sealed class EffectsUiController : IRetainedPanelController
|
|||
|
||||
private void SyncSelection()
|
||||
{
|
||||
foreach (UiCatalogSlot row in _rows.Values)
|
||||
row.Selected = row.EntryId == _selectedSpellId;
|
||||
foreach (EffectRowTemplateFactory.EffectRow row in _rows.Values)
|
||||
row.Slot.SetSelected(row.Slot.EntryId == _selectedSpellId);
|
||||
}
|
||||
|
||||
private void ConfigureInfo()
|
||||
{
|
||||
if (_info is null) return;
|
||||
_info.PreserveEndOnLayout = false;
|
||||
_info.LinesProvider = () =>
|
||||
{
|
||||
if (_selectedSpellId is not uint spellId)
|
||||
return Array.Empty<UiText.Line>();
|
||||
return [new UiText.Line(_selectPrompt, _info.DefaultColor)];
|
||||
ActiveEnchantmentRecord? selected = _spellbook.ActiveEnchantmentSnapshot
|
||||
.Cast<ActiveEnchantmentRecord?>()
|
||||
.FirstOrDefault(record => record?.SpellId == spellId);
|
||||
if (selected is null || !_spellbook.TryGetMetadata(selected.Value.SpellId, out SpellMetadata metadata))
|
||||
return Array.Empty<UiText.Line>();
|
||||
return [new UiText.Line(_selectPrompt, _info.DefaultColor)];
|
||||
return
|
||||
[
|
||||
new UiText.Line(metadata.Name, _info.DefaultColor),
|
||||
new UiText.Line(string.Empty, _info.DefaultColor),
|
||||
new UiText.Line(metadata.Description, _info.DefaultColor),
|
||||
];
|
||||
};
|
||||
|
|
@ -197,5 +224,6 @@ public sealed class EffectsUiController : IRetainedPanelController
|
|||
_disposed = true;
|
||||
_spellbook.EnchantmentsChanged -= Rebuild;
|
||||
if (_close is not null) _close.OnClick = null;
|
||||
if (_scrollbar is not null) _scrollbar.Model = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ public sealed class IndicatorBarController : IRetainedPanelController
|
|||
_harmful.OnClick = () => bindings.TogglePanel(RetailPanelCatalog.NegativeEffects);
|
||||
_link.OnClick = () => bindings.TogglePanel(RetailPanelCatalog.LinkStatus);
|
||||
_vitae.OnClick = () => bindings.TogglePanel(RetailPanelCatalog.Vitae);
|
||||
_burden.OnClick = () => bindings.TogglePanel(RetailPanelCatalog.Character);
|
||||
_burden.OnClick = () => bindings.TogglePanel(RetailPanelCatalog.CharacterInformation);
|
||||
_miniGame.OnClick = () => bindings.TogglePanel(RetailPanelCatalog.MiniGame);
|
||||
_endCharacterSession.OnClick = bindings.RequestEndCharacterSession;
|
||||
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ public sealed class RetailPanelUiController : IDisposable
|
|||
private uint? _activePanel;
|
||||
private uint? _deferredPanel;
|
||||
private bool _applying;
|
||||
private bool _synchronizingPlacement;
|
||||
private PanelPlacement? _mainPanelPlacement;
|
||||
private bool _synchronizingGeometry;
|
||||
private PanelGeometry? _mainPanelGeometry;
|
||||
private bool _disposed;
|
||||
|
||||
public RetailPanelUiController(
|
||||
|
|
@ -38,11 +38,11 @@ public sealed class RetailPanelUiController : IDisposable
|
|||
public uint? ActivePanelId => _activePanel;
|
||||
|
||||
/// <summary>
|
||||
/// Registers one of retail <c>gmPanelUI</c>'s primary toolbar children.
|
||||
/// Inventory, Character, and Magic retain panel-specific content/resize
|
||||
/// policy but share the one parent placement. A drag of any registered
|
||||
/// child updates every hidden sibling through typed window handles, so
|
||||
/// persistence observes the same canonical position too.
|
||||
/// Registers one of retail <c>gmPanelUI</c>'s toolbar or detail children.
|
||||
/// All primary and detail children retain panel-specific content but share
|
||||
/// one parent geometry. A move or resize of any registered child updates
|
||||
/// every hidden sibling through typed window handles, so persistence sees
|
||||
/// the same canonical rectangle too.
|
||||
/// </summary>
|
||||
public void RegisterMainPanel(
|
||||
uint panelId,
|
||||
|
|
@ -61,7 +61,7 @@ public sealed class RetailPanelUiController : IDisposable
|
|||
windowName,
|
||||
restorePrevious,
|
||||
window,
|
||||
sharesMainPanelPlacement: true);
|
||||
sharesMainPanelGeometry: true);
|
||||
}
|
||||
|
||||
/// <param name="restorePrevious">
|
||||
|
|
@ -75,14 +75,14 @@ public sealed class RetailPanelUiController : IDisposable
|
|||
windowName,
|
||||
restorePrevious,
|
||||
window: null,
|
||||
sharesMainPanelPlacement: false);
|
||||
sharesMainPanelGeometry: false);
|
||||
|
||||
private void RegisterCore(
|
||||
uint panelId,
|
||||
string windowName,
|
||||
bool restorePrevious,
|
||||
RetailWindowHandle? window,
|
||||
bool sharesMainPanelPlacement)
|
||||
bool sharesMainPanelGeometry)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (panelId == 0) throw new ArgumentOutOfRangeException(nameof(panelId));
|
||||
|
|
@ -95,14 +95,17 @@ public sealed class RetailPanelUiController : IDisposable
|
|||
windowName,
|
||||
restorePrevious,
|
||||
window,
|
||||
sharesMainPanelPlacement);
|
||||
sharesMainPanelGeometry);
|
||||
_byPanel.Add(panelId, entry);
|
||||
_byWindow.Add(windowName, panelId);
|
||||
if (window is not null)
|
||||
{
|
||||
window.Moved += OnWindowMoved;
|
||||
window.Resized += OnWindowResized;
|
||||
}
|
||||
|
||||
if (sharesMainPanelPlacement && _mainPanelPlacement is { } placement)
|
||||
MoveWindowTo(entry, placement);
|
||||
if (sharesMainPanelGeometry && _mainPanelGeometry is { } geometry)
|
||||
ApplyWindowGeometry(entry, geometry);
|
||||
if (_isVisible(windowName))
|
||||
ObserveWindowVisibility(windowName, visible: true);
|
||||
}
|
||||
|
|
@ -144,7 +147,7 @@ public sealed class RetailPanelUiController : IDisposable
|
|||
{
|
||||
if (_activePanel == panelId)
|
||||
{
|
||||
PrepareMainPanelPlacement(requested);
|
||||
PrepareMainPanelGeometry(requested);
|
||||
return _show(requested.WindowName);
|
||||
}
|
||||
|
||||
|
|
@ -156,7 +159,7 @@ public sealed class RetailPanelUiController : IDisposable
|
|||
: null;
|
||||
|
||||
if (previous is { } previousEntry)
|
||||
CaptureMainPanelPlacement(previousEntry, synchronizeSiblings: true);
|
||||
CaptureMainPanelGeometry(previousEntry, synchronizeSiblings: true);
|
||||
|
||||
_deferredPanel = requested.RestorePrevious
|
||||
&& previous is { RestorePrevious: false }
|
||||
|
|
@ -165,7 +168,7 @@ public sealed class RetailPanelUiController : IDisposable
|
|||
_activePanel = panelId;
|
||||
if (previous is not null)
|
||||
_hide(previous.Value.WindowName);
|
||||
PrepareMainPanelPlacement(requested);
|
||||
PrepareMainPanelGeometry(requested);
|
||||
return _show(requested.WindowName);
|
||||
}
|
||||
|
||||
|
|
@ -175,7 +178,7 @@ public sealed class RetailPanelUiController : IDisposable
|
|||
return _hide(requested.WindowName);
|
||||
}
|
||||
|
||||
CaptureMainPanelPlacement(requested, synchronizeSiblings: true);
|
||||
CaptureMainPanelGeometry(requested, synchronizeSiblings: true);
|
||||
bool hidden = _hide(requested.WindowName);
|
||||
_activePanel = null;
|
||||
if (_deferredPanel is not uint deferredId
|
||||
|
|
@ -184,7 +187,7 @@ public sealed class RetailPanelUiController : IDisposable
|
|||
|
||||
_deferredPanel = null;
|
||||
_activePanel = deferredId;
|
||||
PrepareMainPanelPlacement(deferred);
|
||||
PrepareMainPanelGeometry(deferred);
|
||||
return _show(deferred.WindowName) || hidden;
|
||||
}
|
||||
finally
|
||||
|
|
@ -205,76 +208,109 @@ public sealed class RetailPanelUiController : IDisposable
|
|||
|
||||
private void OnWindowMoved(RetailWindowHandle window)
|
||||
{
|
||||
if (_disposed || _synchronizingPlacement) return;
|
||||
if (_disposed || _synchronizingGeometry) return;
|
||||
|
||||
foreach (PanelEntry entry in _byPanel.Values)
|
||||
{
|
||||
if (!entry.SharesMainPanelPlacement
|
||||
if (!entry.SharesMainPanelGeometry
|
||||
|| !ReferenceEquals(entry.Window, window))
|
||||
continue;
|
||||
|
||||
_mainPanelPlacement = new PanelPlacement(window.Left, window.Top);
|
||||
SynchronizeMainPanelSiblings(window);
|
||||
CaptureAndSynchronizeMainPanelGeometry(window);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void CaptureMainPanelPlacement(
|
||||
private void OnWindowResized(RetailWindowHandle window)
|
||||
{
|
||||
if (_disposed || _synchronizingGeometry) return;
|
||||
|
||||
foreach (PanelEntry entry in _byPanel.Values)
|
||||
{
|
||||
if (!entry.SharesMainPanelGeometry
|
||||
|| !ReferenceEquals(entry.Window, window))
|
||||
continue;
|
||||
|
||||
CaptureAndSynchronizeMainPanelGeometry(window);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void CaptureAndSynchronizeMainPanelGeometry(RetailWindowHandle source)
|
||||
{
|
||||
_mainPanelGeometry = new PanelGeometry(
|
||||
source.Left,
|
||||
source.Top,
|
||||
source.Width,
|
||||
source.Height);
|
||||
SynchronizeMainPanelSiblings(source);
|
||||
}
|
||||
|
||||
private void CaptureMainPanelGeometry(
|
||||
PanelEntry entry,
|
||||
bool synchronizeSiblings)
|
||||
{
|
||||
if (!entry.SharesMainPanelPlacement || entry.Window is not { } window)
|
||||
if (!entry.SharesMainPanelGeometry || entry.Window is not { } window)
|
||||
return;
|
||||
|
||||
_mainPanelPlacement = new PanelPlacement(window.Left, window.Top);
|
||||
_mainPanelGeometry = new PanelGeometry(
|
||||
window.Left,
|
||||
window.Top,
|
||||
window.Width,
|
||||
window.Height);
|
||||
if (synchronizeSiblings)
|
||||
SynchronizeMainPanelSiblings(window);
|
||||
}
|
||||
|
||||
private void PrepareMainPanelPlacement(PanelEntry entry)
|
||||
private void PrepareMainPanelGeometry(PanelEntry entry)
|
||||
{
|
||||
if (!entry.SharesMainPanelPlacement || entry.Window is not { } window)
|
||||
if (!entry.SharesMainPanelGeometry || entry.Window is not { } window)
|
||||
return;
|
||||
|
||||
if (_mainPanelPlacement is not { } placement)
|
||||
if (_mainPanelGeometry is not { } geometry)
|
||||
{
|
||||
_mainPanelPlacement = new PanelPlacement(window.Left, window.Top);
|
||||
_mainPanelGeometry = new PanelGeometry(
|
||||
window.Left,
|
||||
window.Top,
|
||||
window.Width,
|
||||
window.Height);
|
||||
SynchronizeMainPanelSiblings(window);
|
||||
return;
|
||||
}
|
||||
|
||||
MoveWindowTo(entry, placement);
|
||||
ApplyWindowGeometry(entry, geometry);
|
||||
}
|
||||
|
||||
private void SynchronizeMainPanelSiblings(RetailWindowHandle source)
|
||||
{
|
||||
if (_mainPanelPlacement is not { } placement) return;
|
||||
if (_mainPanelGeometry is not { } geometry) return;
|
||||
|
||||
_synchronizingPlacement = true;
|
||||
_synchronizingGeometry = true;
|
||||
try
|
||||
{
|
||||
foreach (PanelEntry sibling in _byPanel.Values)
|
||||
{
|
||||
if (!sibling.SharesMainPanelPlacement
|
||||
if (!sibling.SharesMainPanelGeometry
|
||||
|| sibling.Window is not { } window
|
||||
|| ReferenceEquals(window, source))
|
||||
continue;
|
||||
MoveWindowTo(sibling, placement);
|
||||
ApplyWindowGeometry(sibling, geometry);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_synchronizingPlacement = false;
|
||||
_synchronizingGeometry = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void MoveWindowTo(PanelEntry entry, PanelPlacement placement)
|
||||
private static void ApplyWindowGeometry(PanelEntry entry, PanelGeometry geometry)
|
||||
{
|
||||
if (entry.Window is not { } window
|
||||
|| (window.Left == placement.Left && window.Top == placement.Top))
|
||||
return;
|
||||
if (entry.Window is not { } window) return;
|
||||
|
||||
window.MoveTo(placement.Left, placement.Top);
|
||||
if (window.Left != geometry.Left || window.Top != geometry.Top)
|
||||
window.MoveTo(geometry.Left, geometry.Top);
|
||||
if (window.Width != geometry.Width || window.Height != geometry.Height)
|
||||
window.ResizeTo(geometry.Width, geometry.Height);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
|
@ -283,14 +319,21 @@ public sealed class RetailPanelUiController : IDisposable
|
|||
_disposed = true;
|
||||
foreach (PanelEntry entry in _byPanel.Values)
|
||||
if (entry.Window is { } window)
|
||||
{
|
||||
window.Moved -= OnWindowMoved;
|
||||
window.Resized -= OnWindowResized;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly record struct PanelPlacement(float Left, float Top);
|
||||
private readonly record struct PanelGeometry(
|
||||
float Left,
|
||||
float Top,
|
||||
float Width,
|
||||
float Height);
|
||||
|
||||
private readonly record struct PanelEntry(
|
||||
string WindowName,
|
||||
bool RestorePrevious,
|
||||
RetailWindowHandle? Window,
|
||||
bool SharesMainPanelPlacement);
|
||||
bool SharesMainPanelGeometry);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue