fix(ui): restore effect detail scrolling

Reflow selected Helpful/Harmful spell descriptions through the retained retail text shaper, bind their authored 0x10000127 information scrollbar independently from the list scrollbar, and allow display-only text to consume wheel scrolling without becoming selectable.

Format Vitae recovery experience through retail's grouped integer presentation. Release build succeeds and all 5,832 tests pass with five intentional skips.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-17 13:28:18 +02:00
parent 16c21e299c
commit 82789eea88
9 changed files with 117 additions and 18 deletions

View file

@ -16,8 +16,9 @@ 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 ListScrollbarId = 0x10000124u;
public const uint InfoTextId = 0x10000126u;
public const uint InfoScrollbarId = 0x10000127u;
public const uint RowTemplateId = 0x10000128u;
public const uint RowIconId = 0x10000129u;
public const uint RowLabelId = 0x1000012Au;
@ -32,7 +33,8 @@ public sealed class EffectsUiController : IRetainedPanelController
private readonly UiItemList _list;
private readonly UiText? _info;
private readonly UiButton? _close;
private readonly UiScrollbar? _scrollbar;
private readonly UiScrollbar? _listScrollbar;
private readonly UiScrollbar? _infoScrollbar;
private readonly Dictionary<uint, EffectRowTemplateFactory.EffectRow> _rows = new();
private uint? _selectedSpellId;
private double _lastDurationUpdate = double.NaN;
@ -60,13 +62,14 @@ public sealed class EffectsUiController : IRetainedPanelController
_list = list;
_info = layout.FindElement(InfoTextId) as UiText;
_close = layout.FindElement(CloseId) as UiButton;
_scrollbar = layout.FindElement(ScrollbarId) as UiScrollbar;
_listScrollbar = layout.FindElement(ListScrollbarId) as UiScrollbar;
_infoScrollbar = layout.FindElement(InfoScrollbarId) as UiScrollbar;
if (_close is not null) _close.OnClick = close;
_list.Columns = 1;
_list.CellWidth = templates.Width;
_list.CellHeight = templates.Height;
if (_scrollbar is not null)
_scrollbar.Model = _list.Scroll;
if (_listScrollbar is not null)
_listScrollbar.Model = _list.Scroll;
ConfigureInfo();
_spellbook.EnchantmentsChanged += Rebuild;
Rebuild();
@ -198,21 +201,25 @@ public sealed class EffectsUiController : IRetainedPanelController
{
if (_info is null) return;
_info.PreserveEndOnLayout = false;
_info.WheelScrollEnabled = true;
_info.ClickThrough = false;
if (_infoScrollbar is not null)
_infoScrollbar.Model = _info.Scroll;
_info.LinesProvider = () =>
{
if (_selectedSpellId is not uint spellId)
return [new UiText.Line(_selectPrompt, _info.DefaultColor)];
return IndicatorDetailText.Shape(_info, _selectPrompt);
ActiveEnchantmentRecord? selected = _spellbook.ActiveEnchantmentSnapshot
.Cast<ActiveEnchantmentRecord?>()
.FirstOrDefault(record => record?.SpellId == spellId);
if (selected is null || !_spellbook.TryGetMetadata(selected.Value.SpellId, out SpellMetadata metadata))
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),
];
return IndicatorDetailText.Shape(_info, _selectPrompt);
// gmEffectsUI::UpdateSelection @ 0x004B7F90 supplies one literal
// name + blank line + description to UIElement_Text::SetText. The
// text widget reflows it to the authored information width.
return IndicatorDetailText.Shape(
_info,
metadata.Name + "\n\n" + metadata.Description);
};
}
@ -224,6 +231,7 @@ 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;
if (_listScrollbar is not null) _listScrollbar.Model = null;
if (_infoScrollbar is not null) _infoScrollbar.Model = null;
}
}