fix(ui): match retail spell bar controls
Place favorite-bar arrows by their authored sides, import rollover and pressed media through the shared scrollbar, and preserve manual offsets across passive refreshes. Carry the mixed-parent DAT anchor chain to a fixed 18-cell favorite viewport so overflow controls and the Cast button remain inside the retail-sized combat frame. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
02c29e67c8
commit
0134122c28
17 changed files with 424 additions and 59 deletions
|
|
@ -116,7 +116,7 @@ user-gated. Deterministic world-lifecycle automation protects fresh login,
|
||||||
outdoor/world-edge/dungeon travel, same-location revisit, exact graceful
|
outdoor/world-edge/dungeon travel, same-location revisit, exact graceful
|
||||||
disconnect, and uncapped fresh-process reconnect with JSON/PNG artifacts.
|
disconnect, and uncapped fresh-process reconnect with JSON/PNG artifacts.
|
||||||
Release builds; the structural closeout plus interaction-completion Slice 1
|
Release builds; the structural closeout plus interaction-completion Slice 1
|
||||||
pass 293 focused tests, 3,466 App tests / 3 skips, and 7,839 complete-solution
|
pass 293 focused tests, 3,472 App tests / 3 skips, and 7,845 complete-solution
|
||||||
tests / 5 skips.
|
tests / 5 skips.
|
||||||
|
|
||||||
**Current work order:** the six-slice pre-M4 world-interaction completion
|
**Current work order:** the six-slice pre-M4 world-interaction completion
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@ user-gated. Deterministic world-lifecycle automation protects fresh login,
|
||||||
outdoor/world-edge/dungeon travel, same-location revisit, exact graceful
|
outdoor/world-edge/dungeon travel, same-location revisit, exact graceful
|
||||||
disconnect, and uncapped fresh-process reconnect with JSON/PNG artifacts.
|
disconnect, and uncapped fresh-process reconnect with JSON/PNG artifacts.
|
||||||
Release builds; the structural closeout plus interaction-completion Slice 1
|
Release builds; the structural closeout plus interaction-completion Slice 1
|
||||||
pass 293 focused tests, 3,466 App tests / 3 skips, and 7,839 complete-solution
|
pass 293 focused tests, 3,472 App tests / 3 skips, and 7,845 complete-solution
|
||||||
tests / 5 skips.
|
tests / 5 skips.
|
||||||
|
|
||||||
**Current work order:** the six-slice pre-M4 world-interaction completion
|
**Current work order:** the six-slice pre-M4 world-interaction completion
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,12 @@ bodies begin: spell-bar overflow controls, status-bar Use/Assess commands,
|
||||||
assessment information, equipped-child picking, vendor browsing, and
|
assessment information, equipped-child picking, vendor browsing, and
|
||||||
server-authoritative vendor transactions. Slice 1 is active and ports the
|
server-authoritative vendor transactions. Slice 1 is active and ports the
|
||||||
DAT-authored arrow-only favorite-spell scrollbar through the shared retained
|
DAT-authored arrow-only favorite-spell scrollbar through the shared retained
|
||||||
scrollbar/list model. Its implementation and complete Release suite pass;
|
scrollbar/list model. The corrected import preserves authored arrow sides and
|
||||||
connected overflow-arrow visual acceptance is pending.
|
rollover/pressed media, passive refresh preserves manual offsets, and the
|
||||||
|
mixed-parent DAT anchor chain produces the fixed retail capacity of 18 visible
|
||||||
|
favorite cells (nine numbered plus nine unnumbered). Its
|
||||||
|
implementation and complete Release suite pass; connected overflow-arrow
|
||||||
|
visual acceptance is pending.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,12 +43,18 @@ Each slice begins with named-retail research, produces pseudocode and
|
||||||
conformance tests, updates the divergence register if required, and lands as a
|
conformance tests, updates the divergence register if required, and lands as a
|
||||||
separate bisectable commit. A visual gate follows each UI-bearing slice.
|
separate bisectable commit. A visual gate follows each UI-bearing slice.
|
||||||
|
|
||||||
Slice 1 now imports both 23-pixel arrow buttons and HideDisabled from the real
|
Slice 1 now imports both 23-pixel arrow buttons, their rollover/pressed media,
|
||||||
combat fixture, shares the list's single pixel-scroll model, advances one
|
and HideDisabled from the real combat fixture; places their artwork by the
|
||||||
32-pixel cell per press, and exposes programmatically selected spells. The
|
authored leading/trailing positions; shares the list's single pixel-scroll
|
||||||
focused 57-test set, all 3,466 App tests / 3 skips, the Release solution build,
|
model; advances one 32-pixel cell per press; and exposes only actively selected
|
||||||
and all 7,839 solution tests / 5 skips pass. Only the connected visual gate
|
spells. Passive object/endowment refresh preserves a manual offset. The mixed
|
||||||
remains before Slice 2 starts.
|
610/800-pixel DAT anchor chain is solved to a fixed 747-pixel combat root,
|
||||||
|
producing exactly 18 visible 32-pixel favorite cells (nine numbered plus nine
|
||||||
|
unnumbered) without consuming the overflow. The initial 57-test/full-suite gate passed;
|
||||||
|
the corrective 104-test focus set, 3,472 App tests / 3 skips, Release solution
|
||||||
|
build, and 7,845 complete-solution tests / 5 skips pass. The connected gate is
|
||||||
|
pending after user feedback on arrow sides, rollover, width, and offset
|
||||||
|
persistence.
|
||||||
|
|
||||||
## Slice 1 — spell-bar overflow arrows
|
## Slice 1 — spell-bar overflow arrows
|
||||||
|
|
||||||
|
|
@ -74,7 +80,8 @@ Named retail references and executable pseudocode are recorded in
|
||||||
|
|
||||||
1. Generalize `DatWidgetFactory.BuildScrollbar` so horizontal and vertical
|
1. Generalize `DatWidgetFactory.BuildScrollbar` so horizontal and vertical
|
||||||
scrollbars both import the referenced decrement/increment children,
|
scrollbars both import the referenced decrement/increment children,
|
||||||
including their authored dimensions and HideDisabled property.
|
including their authored positions, dimensions, Normal/rollover/pressed
|
||||||
|
media, and HideDisabled property.
|
||||||
2. Generalize `UiScrollbar` to use distinct authored decrement/increment
|
2. Generalize `UiScrollbar` to use distinct authored decrement/increment
|
||||||
extents for rendering, hit-testing, track geometry, and dragging. Preserve
|
extents for rendering, hit-testing, track geometry, and dragging. Preserve
|
||||||
the existing 16-pixel default for layouts whose button children are absent.
|
the existing 16-pixel default for layouts whose button children are absent.
|
||||||
|
|
@ -85,10 +92,14 @@ Named retail references and executable pseudocode are recorded in
|
||||||
`UiItemList.Scroll`, enable horizontal scrolling, and retain one independent
|
`UiItemList.Scroll`, enable horizontal scrolling, and retain one independent
|
||||||
pixel offset per favorite tab.
|
pixel offset per favorite tab.
|
||||||
5. Match `SpellCastSubMenu::SetSelected`: selecting a spell through keyboard,
|
5. Match `SpellCastSubMenu::SetSelected`: selecting a spell through keyboard,
|
||||||
shortcut, or code scrolls that item into view.
|
shortcut, or code scrolls that item into view; passive state refresh does
|
||||||
6. Pin the importer, arrow hit extents, 32-pixel step, no-overflow behavior,
|
not re-expose it.
|
||||||
|
6. Carry the combat root's mixed-parent raw-edge policies through the complete
|
||||||
|
imported tree and solve for the retail 18-cell favorite viewport; keep that
|
||||||
|
HUD capacity fixed across desktop resizes.
|
||||||
|
7. Pin the importer, arrow hit extents, 32-pixel step, no-overflow behavior,
|
||||||
controller binding, and selection exposure with focused App tests.
|
controller binding, and selection exposure with focused App tests.
|
||||||
7. Run the App Release suite, solution Release build, and complete Release
|
8. Run the App Release suite, solution Release build, and complete Release
|
||||||
suite. Then update the roadmap/memory and request the connected visual gate:
|
suite. Then update the roadmap/memory and request the connected visual gate:
|
||||||
place more favorites than fit, scroll both directions, change tabs, and
|
place more favorites than fit, scroll both directions, change tabs, and
|
||||||
verify arrows disappear on a non-overflowing tab.
|
verify arrows disappear on a non-overflowing tab.
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,12 @@ Each of the eight `SpellCastSubMenu` groups contains:
|
||||||
```text
|
```text
|
||||||
favorite group
|
favorite group
|
||||||
scrollbar 0x100000B5, 685x36
|
scrollbar 0x100000B5, 685x36
|
||||||
decrement button 0x10000071, 23x36
|
increment reference 0x10000072, authored x=0, 23x36
|
||||||
|
Normal 0x06004CDE (left-facing arrow)
|
||||||
|
Rollover 0x06004CDF
|
||||||
|
decrement reference 0x10000071, authored x=63, 23x36
|
||||||
Normal 0x06004CDC
|
Normal 0x06004CDC
|
||||||
Rollover 0x06004CDD
|
Rollover 0x06004CDD
|
||||||
increment button 0x10000072, 23x36
|
|
||||||
Normal 0x06004CDE
|
|
||||||
Rollover 0x06004CDF
|
|
||||||
item list 0x100000B6, x=23, y=2, 639x32
|
item list 0x100000B6, x=23, y=2, 639x32
|
||||||
|
|
||||||
scrollbar property 0x77 = increment button 0x10000072
|
scrollbar property 0x77 = increment button 0x10000072
|
||||||
|
|
@ -28,7 +28,12 @@ item-list property 0x71 = horizontal scrollbar 0x100000B5
|
||||||
```
|
```
|
||||||
|
|
||||||
The spell-bar scrollbar has no authored track or thumb. Its 23-pixel buttons
|
The spell-bar scrollbar has no authored track or thumb. Its 23-pixel buttons
|
||||||
occupy the space immediately outside the 639-pixel item-list viewport.
|
occupy the space immediately outside the 639-pixel item-list viewport. The
|
||||||
|
property names do not determine their drawn side:
|
||||||
|
`UIElement_Scrollbar::UpdateScrollingArea @ 0x00470AA0` places the 0x77
|
||||||
|
increment reference at the leading/top-left edge and the 0x78 decrement
|
||||||
|
reference at the trailing/bottom-right edge. For this horizontal fixture that
|
||||||
|
means `0x06004CDE` on the left and `0x06004CDC` on the right.
|
||||||
|
|
||||||
## Initialization and ownership
|
## Initialization and ownership
|
||||||
|
|
||||||
|
|
@ -85,8 +90,8 @@ UpdateScrollbarSize(horizontal):
|
||||||
|
|
||||||
Scrollbar.UpdateLayout():
|
Scrollbar.UpdateLayout():
|
||||||
scrollingArea = own rectangle
|
scrollingArea = own rectangle
|
||||||
position decrement button at the start and subtract its authored width
|
position property-0x77 button at the start and subtract its authored width
|
||||||
position increment button at the end and subtract its authored width
|
position property-0x78 button at the end and subtract its authored width
|
||||||
update thumb/track geometry, when those children exist
|
update thumb/track geometry, when those children exist
|
||||||
Visible = NOT (Disabled AND HideDisabled)
|
Visible = NOT (Disabled AND HideDisabled)
|
||||||
```
|
```
|
||||||
|
|
@ -95,6 +100,31 @@ For the favorite bar, disabled plus HideDisabled removes both arrows when every
|
||||||
favorite/empty cell fits. With overflow, the arrows remain visible and the
|
favorite/empty cell fits. With overflow, the arrows remain visible and the
|
||||||
item list clips the horizontally translated cells.
|
item list clips the horizontally translated cells.
|
||||||
|
|
||||||
|
The combat LayoutDesc deliberately mixes design extents: its outer combat root
|
||||||
|
is 610 pixels wide, while the magic page's children are authored against an
|
||||||
|
800-pixel parent. Applying retail
|
||||||
|
`UIElement::UpdateForParentSizeChange @ 0x00462640` through that complete
|
||||||
|
raw-edge chain gives a 439-pixel favorite viewport at the 610-pixel root—the
|
||||||
|
13-slot short bar seen before this slice.
|
||||||
|
|
||||||
|
The retail presentation exposes 18 favorite cells: nine shortcut-numbered
|
||||||
|
cells plus nine unnumbered cells. At 32 pixels per cell the target viewport is
|
||||||
|
576 pixels. Because every horizontal raw-edge stage carries the parent delta
|
||||||
|
one-for-one, solving the imported tree for that viewport yields:
|
||||||
|
|
||||||
|
```text
|
||||||
|
outer combat root = 747 px
|
||||||
|
magic page = 737 px (5 px inset on each side)
|
||||||
|
favorite group = 622 px (x=40, followed by Cast)
|
||||||
|
scrollbar = 622 px (23 + 576 + 23)
|
||||||
|
favorite list = 576 px (18 * 32)
|
||||||
|
Cast = x=662, width=75 inside the magic page
|
||||||
|
```
|
||||||
|
|
||||||
|
This is a fixed visible-capacity HUD, not a desktop-wide strip. Stretching it
|
||||||
|
to the display width makes the favorite content fit, which legitimately
|
||||||
|
disables and hides the `HideDisabled` overflow arrows.
|
||||||
|
|
||||||
## Scroll delta and selection exposure
|
## Scroll delta and selection exposure
|
||||||
|
|
||||||
Retail:
|
Retail:
|
||||||
|
|
@ -127,15 +157,23 @@ SpellCastSubMenu.SetSelected(spellId):
|
||||||
```
|
```
|
||||||
|
|
||||||
The favorite cells are 32 pixels wide, so a line arrow moves exactly 32
|
The favorite cells are 32 pixels wide, so a line arrow moves exactly 32
|
||||||
pixels. Programmatic or keyboard selection exposes the selected cell even when
|
pixels. An actual programmatic, keyboard, or pointer selection exposes the
|
||||||
it was outside the prior viewport.
|
selected cell even when it was outside the prior viewport. Passive endowment
|
||||||
|
or object-table refresh does not re-run `SetSelected`; doing so would undo a
|
||||||
|
manual scrollbar offset.
|
||||||
|
|
||||||
## Port constraints
|
## Port constraints
|
||||||
|
|
||||||
- Preserve the imported `0x71`/`0x77`/`0x78` relationships.
|
- Preserve the imported `0x71`/`0x77`/`0x78` relationships.
|
||||||
- Preserve each authored button's dimension; do not assume the 16-pixel chat
|
- Preserve each authored button's dimension; do not assume the 16-pixel chat
|
||||||
scrollbar button size.
|
scrollbar button size.
|
||||||
|
- Preserve each authored button's Normal, Normal_rollover, and Normal_pressed
|
||||||
|
media through the shared scrollbar presentation path.
|
||||||
- Do not fabricate a track/thumb for this arrow-only control.
|
- Do not fabricate a track/thumb for this arrow-only control.
|
||||||
- Derive disabled state from the shared model's content and viewport extents.
|
- Derive disabled state from the shared model's content and viewport extents.
|
||||||
- Hide disabled controls from both drawing and hit-testing.
|
- Hide disabled controls from both drawing and hit-testing.
|
||||||
- Keep every favorite tab's scroll offset independent.
|
- Keep every favorite tab's scroll offset independent.
|
||||||
|
- Carry the DAT raw-edge policies through the full imported combat tree and
|
||||||
|
solve the root width for an 18-cell (576-pixel) favorite viewport.
|
||||||
|
- Keep the resulting 747-pixel HUD width fixed across desktop resizes; do not
|
||||||
|
turn the favorite bar into a desktop-wide strip.
|
||||||
|
|
|
||||||
|
|
@ -126,8 +126,11 @@ public static class DatWidgetFactory
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Bind inherited scrollbar media structurally. Property 0x77 names the
|
/// Bind inherited scrollbar media structurally. Property 0x77 names the
|
||||||
/// increment button and 0x78 the decrement button; the remaining Type-1
|
/// increment button and 0x78 the decrement button; retail
|
||||||
/// child is the thumb with ordered top/middle/bottom image slices.
|
/// <c>UIElement_Scrollbar::UpdateScrollingArea @ 0x00470AA0</c> then places
|
||||||
|
/// those referenced child buttons by their authored leading/trailing
|
||||||
|
/// positions. The remaining Type-1 child is the thumb with ordered
|
||||||
|
/// top/middle/bottom image slices.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static UiScrollbar BuildScrollbar(
|
private static UiScrollbar BuildScrollbar(
|
||||||
ElementInfo info,
|
ElementInfo info,
|
||||||
|
|
@ -144,17 +147,31 @@ public static class DatWidgetFactory
|
||||||
uint decrementId = ReferencedElementId(info, 0x78u);
|
uint decrementId = ReferencedElementId(info, 0x78u);
|
||||||
ElementInfo? increment = info.Children.FirstOrDefault(child => child.Id == incrementId);
|
ElementInfo? increment = info.Children.FirstOrDefault(child => child.Id == incrementId);
|
||||||
ElementInfo? decrement = info.Children.FirstOrDefault(child => child.Id == decrementId);
|
ElementInfo? decrement = info.Children.FirstOrDefault(child => child.Id == decrementId);
|
||||||
bar.UpSprite = decrement is null ? 0u : DefaultImage(decrement);
|
ElementInfo? leadingButton = new[] { increment, decrement }
|
||||||
bar.DownSprite = increment is null ? 0u : DefaultImage(increment);
|
.Where(child => child is not null)
|
||||||
|
.OrderBy(child => bar.Horizontal ? child!.X : child!.Y)
|
||||||
|
.ThenBy(child => child!.ReadOrder)
|
||||||
|
.FirstOrDefault();
|
||||||
|
ElementInfo? trailingButton = new[] { increment, decrement }
|
||||||
|
.Where(child => child is not null)
|
||||||
|
.OrderByDescending(child => bar.Horizontal ? child!.X : child!.Y)
|
||||||
|
.ThenByDescending(child => child!.ReadOrder)
|
||||||
|
.FirstOrDefault();
|
||||||
|
bar.UpSprite = ButtonStateImage(leadingButton, "Normal");
|
||||||
|
bar.UpRolloverSprite = ButtonStateImage(leadingButton, "Normal_rollover");
|
||||||
|
bar.UpPressedSprite = ButtonStateImage(leadingButton, "Normal_pressed");
|
||||||
|
bar.DownSprite = ButtonStateImage(trailingButton, "Normal");
|
||||||
|
bar.DownRolloverSprite = ButtonStateImage(trailingButton, "Normal_rollover");
|
||||||
|
bar.DownPressedSprite = ButtonStateImage(trailingButton, "Normal_pressed");
|
||||||
if (info.TryGetEffectiveBool(0x79u, out bool hideDisabled))
|
if (info.TryGetEffectiveBool(0x79u, out bool hideDisabled))
|
||||||
bar.HideWhenDisabled = hideDisabled;
|
bar.HideWhenDisabled = hideDisabled;
|
||||||
|
|
||||||
if (bar.Horizontal)
|
if (bar.Horizontal)
|
||||||
{
|
{
|
||||||
if (decrement is { Width: > 0f })
|
if (leadingButton is { Width: > 0f })
|
||||||
bar.DecrementButtonExtent = decrement.Width;
|
bar.DecrementButtonExtent = leadingButton.Width;
|
||||||
if (increment is { Width: > 0f })
|
if (trailingButton is { Width: > 0f })
|
||||||
bar.IncrementButtonExtent = increment.Width;
|
bar.IncrementButtonExtent = trailingButton.Width;
|
||||||
|
|
||||||
// Retail horizontal scrollbars use structural child ids: element 1 is
|
// Retail horizontal scrollbars use structural child ids: element 1 is
|
||||||
// the thumb and element 4 is the optional child-authored track.
|
// the thumb and element 4 is the optional child-authored track.
|
||||||
|
|
@ -207,10 +224,10 @@ public static class DatWidgetFactory
|
||||||
return bar;
|
return bar;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (decrement is { Height: > 0f })
|
if (leadingButton is { Height: > 0f })
|
||||||
bar.DecrementButtonExtent = decrement.Height;
|
bar.DecrementButtonExtent = leadingButton.Height;
|
||||||
if (increment is { Height: > 0f })
|
if (trailingButton is { Height: > 0f })
|
||||||
bar.IncrementButtonExtent = increment.Height;
|
bar.IncrementButtonExtent = trailingButton.Height;
|
||||||
|
|
||||||
ElementInfo? thumb = info.Children.FirstOrDefault(child =>
|
ElementInfo? thumb = info.Children.FirstOrDefault(child =>
|
||||||
child.Type == 1u && child.Id != incrementId && child.Id != decrementId);
|
child.Type == 1u && child.Id != incrementId && child.Id != decrementId);
|
||||||
|
|
@ -272,6 +289,19 @@ public static class DatWidgetFactory
|
||||||
return 0u;
|
return 0u;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static uint ButtonStateImage(ElementInfo? info, string stateName)
|
||||||
|
{
|
||||||
|
if (info is null)
|
||||||
|
return 0u;
|
||||||
|
if (info.StateMedia.TryGetValue(stateName, out var media))
|
||||||
|
return media.File;
|
||||||
|
UiStateInfo? state = info.States.Values.FirstOrDefault(
|
||||||
|
candidate => string.Equals(candidate.Name, stateName, StringComparison.Ordinal));
|
||||||
|
if (state?.Image is { } image)
|
||||||
|
return image.File;
|
||||||
|
return stateName == "Normal" ? DefaultImage(info) : 0u;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Meter ────────────────────────────────────────────────────────────────
|
// ── Meter ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
49
src/AcDream.App/UI/Layout/RetailCombatLayout.cs
Normal file
49
src/AcDream.App/UI/Layout/RetailCombatLayout.cs
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
namespace AcDream.App.UI.Layout;
|
||||||
|
|
||||||
|
/// <summary>Retail geometry policy for the shared melee/missile/magic combat root.</summary>
|
||||||
|
internal static class RetailCombatLayout
|
||||||
|
{
|
||||||
|
internal const int VisibleFavoriteSlots = 18;
|
||||||
|
internal const float FavoriteCellWidth = 32f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resize the imported combat tree so its favorite list exposes exactly the
|
||||||
|
/// requested number of cells. The DAT deliberately authors the outer combat
|
||||||
|
/// root at 610 pixels while the magic page's descendants use an 800-pixel
|
||||||
|
/// design parent. Retail <c>UIElement::UpdateForParentSizeChange @
|
||||||
|
/// 0x00462640</c> carries that width delta through every raw-edge policy.
|
||||||
|
/// Applying the same chain and solving for the list viewport keeps the
|
||||||
|
/// endowment, arrows, list, Cast button, and outer frame in one coherent
|
||||||
|
/// retained layout instead of resizing individual controls ad hoc.
|
||||||
|
/// </summary>
|
||||||
|
internal static float FitFavoriteSlots(
|
||||||
|
ImportedLayout layout,
|
||||||
|
int visibleSlots = VisibleFavoriteSlots,
|
||||||
|
float cellWidth = FavoriteCellWidth)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(layout);
|
||||||
|
if (visibleSlots < 1)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(visibleSlots));
|
||||||
|
if (!(cellWidth > 0f) || !float.IsFinite(cellWidth))
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(cellWidth));
|
||||||
|
|
||||||
|
UiElement root = layout.Root;
|
||||||
|
ApplyDescendantLayout(root);
|
||||||
|
if (layout.FindElement(SpellcastingUiController.FavoriteListId) is not UiItemList list)
|
||||||
|
throw new InvalidOperationException("Retail combat layout has no favorite spell list.");
|
||||||
|
|
||||||
|
float targetViewport = visibleSlots * cellWidth;
|
||||||
|
root.Width = MathF.Max(0f, root.Width + targetViewport - list.Width);
|
||||||
|
ApplyDescendantLayout(root);
|
||||||
|
return root.Width;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ApplyDescendantLayout(UiElement parent)
|
||||||
|
{
|
||||||
|
foreach (UiElement child in parent.Children)
|
||||||
|
{
|
||||||
|
child.ApplyAnchor(parent.Width, parent.Height);
|
||||||
|
ApplyDescendantLayout(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -64,6 +64,12 @@ public static class RetailWindowFrame
|
||||||
public bool DrawChromeCenter { get; init; } = true;
|
public bool DrawChromeCenter { get; init; } = true;
|
||||||
public bool Visible { get; init; } = true;
|
public bool Visible { get; init; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Top-level desktop anchors. Most movable windows are unanchored; fixed
|
||||||
|
/// retail HUD roots can preserve their authored screen-edge margins.
|
||||||
|
/// </summary>
|
||||||
|
public AnchorEdges OuterAnchors { get; init; } = AnchorEdges.None;
|
||||||
|
|
||||||
public AnchorEdges ContentAnchors { get; init; } =
|
public AnchorEdges ContentAnchors { get; init; } =
|
||||||
AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Right | AnchorEdges.Bottom;
|
AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Right | AnchorEdges.Bottom;
|
||||||
public bool? ContentClickThrough { get; init; }
|
public bool? ContentClickThrough { get; init; }
|
||||||
|
|
@ -133,7 +139,7 @@ public static class RetailWindowFrame
|
||||||
outerFrame.Top = options.Top;
|
outerFrame.Top = options.Top;
|
||||||
outerFrame.Width = outerWidth;
|
outerFrame.Width = outerWidth;
|
||||||
outerFrame.Height = outerHeight;
|
outerFrame.Height = outerHeight;
|
||||||
outerFrame.Anchors = AnchorEdges.None;
|
outerFrame.Anchors = options.OuterAnchors;
|
||||||
outerFrame.Draggable = options.Draggable;
|
outerFrame.Draggable = options.Draggable;
|
||||||
outerFrame.Resizable = options.Resizable;
|
outerFrame.Resizable = options.Resizable;
|
||||||
outerFrame.ResizeX = options.ResizeX;
|
outerFrame.ResizeX = options.ResizeX;
|
||||||
|
|
|
||||||
|
|
@ -251,8 +251,6 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
||||||
_endowmentSelected[_activeTab] = favorites.Count == 0 && _endowmentItemId != 0u;
|
_endowmentSelected[_activeTab] = favorites.Count == 0 && _endowmentItemId != 0u;
|
||||||
}
|
}
|
||||||
SyncSelection();
|
SyncSelection();
|
||||||
if (_selected[_activeTab] is uint visibleSpell)
|
|
||||||
ScrollSelectedSpellIntoView(_activeTab, visibleSpell);
|
|
||||||
UpdateCastAvailability();
|
UpdateCastAvailability();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -764,11 +764,12 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
strings.Resolve);
|
strings.Resolve);
|
||||||
labels = info is null ? null : CombatUiLabels.Resolve(info, strings);
|
labels = info is null ? null : CombatUiLabels.Resolve(info, strings);
|
||||||
}
|
}
|
||||||
if (layout is null || labels is null)
|
if (info is null || layout is null || labels is null)
|
||||||
{
|
{
|
||||||
Console.WriteLine("[M2] combat: LayoutDesc 0x21000073 not found.");
|
Console.WriteLine("[M2] combat: LayoutDesc 0x21000073 not found.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
float combatWidth = RetailCombatLayout.FitFavoriteSlots(layout);
|
||||||
|
|
||||||
CombatUiController? controller = Layout.CombatUiController.Bind(
|
CombatUiController? controller = Layout.CombatUiController.Bind(
|
||||||
layout,
|
layout,
|
||||||
|
|
@ -821,9 +822,13 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
{
|
{
|
||||||
WindowName = WindowNames.Combat,
|
WindowName = WindowNames.Combat,
|
||||||
Chrome = RetailWindowChrome.Imported,
|
Chrome = RetailWindowChrome.Imported,
|
||||||
Left = Math.Max(0f, (Host.Root.Width - root.Width) * 0.5f),
|
Left = 0f,
|
||||||
Top = Math.Max(0f, Host.Root.Height - root.Height - 10f),
|
Top = Math.Max(0f, Host.Root.Height - root.Height - 10f),
|
||||||
|
ContentWidth = combatWidth,
|
||||||
|
DatConstraintSource = info,
|
||||||
|
OuterAnchors = AnchorEdges.Left | AnchorEdges.Bottom,
|
||||||
Visible = false,
|
Visible = false,
|
||||||
|
Draggable = false,
|
||||||
Resizable = false,
|
Resizable = false,
|
||||||
ResizeX = false,
|
ResizeX = false,
|
||||||
ResizeY = false,
|
ResizeY = false,
|
||||||
|
|
|
||||||
|
|
@ -233,6 +233,14 @@ public abstract class UiElement
|
||||||
/// self-driven interior drag such as text selection). Default false.</summary>
|
/// self-driven interior drag such as text selection). Default false.</summary>
|
||||||
public virtual bool HandlesClick => false;
|
public virtual bool HandlesClick => false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether pointer movement should continue reaching this element while it remains
|
||||||
|
/// hovered and no mouse capture is active. Composite controls use this to update
|
||||||
|
/// procedural sub-region hover state without broadening mouse-move delivery to every
|
||||||
|
/// retained widget.
|
||||||
|
/// </summary>
|
||||||
|
public virtual bool ReceivesHoverMouseMove => false;
|
||||||
|
|
||||||
/// <summary>Minimum size enforced while resizing.</summary>
|
/// <summary>Minimum size enforced while resizing.</summary>
|
||||||
public float MinWidth { get; set; } = 40f;
|
public float MinWidth { get; set; } = 40f;
|
||||||
public float MinHeight { get; set; } = 40f;
|
public float MinHeight { get; set; } = 40f;
|
||||||
|
|
|
||||||
|
|
@ -851,7 +851,12 @@ public sealed class UiRoot : UiElement
|
||||||
private void UpdateHover(int x, int y)
|
private void UpdateHover(int x, int y)
|
||||||
{
|
{
|
||||||
var (w, _, _) = HitTestTopDown(x, y);
|
var (w, _, _) = HitTestTopDown(x, y);
|
||||||
if (ReferenceEquals(w, _hoverWidget)) return;
|
if (ReferenceEquals(w, _hoverWidget))
|
||||||
|
{
|
||||||
|
if (w?.ReceivesHoverMouseMove == true)
|
||||||
|
DispatchMouseMove(w, x, y);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (_hoverWidget is not null)
|
if (_hoverWidget is not null)
|
||||||
{
|
{
|
||||||
|
|
@ -863,7 +868,13 @@ public sealed class UiRoot : UiElement
|
||||||
_tooltipFired = false;
|
_tooltipFired = false;
|
||||||
if (w is not null)
|
if (w is not null)
|
||||||
{
|
{
|
||||||
var enter = new UiEvent(w.EventId, w, UiEventType.HoverEnter);
|
var screen = w.ScreenPosition;
|
||||||
|
var enter = new UiEvent(
|
||||||
|
w.EventId,
|
||||||
|
w,
|
||||||
|
UiEventType.HoverEnter,
|
||||||
|
Data1: (int)(x - screen.X),
|
||||||
|
Data2: (int)(y - screen.Y));
|
||||||
w.OnEvent(in enter);
|
w.OnEvent(in enter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ namespace AcDream.App.UI;
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public sealed class UiScrollbar : UiElement
|
public sealed class UiScrollbar : UiElement
|
||||||
{
|
{
|
||||||
|
public override bool ReceivesHoverMouseMove => true;
|
||||||
|
|
||||||
/// <summary>The scroll model this bar reflects + drives (shared with the transcript).</summary>
|
/// <summary>The scroll model this bar reflects + drives (shared with the transcript).</summary>
|
||||||
public UiScrollable? Model { get; set; }
|
public UiScrollable? Model { get; set; }
|
||||||
|
|
||||||
|
|
@ -83,6 +85,14 @@ public sealed class UiScrollbar : UiElement
|
||||||
/// <summary>Down-arrow button sprite id (0x06004C6C Normal state, element 0x10000072).</summary>
|
/// <summary>Down-arrow button sprite id (0x06004C6C Normal state, element 0x10000072).</summary>
|
||||||
public uint DownSprite { get; set; }
|
public uint DownSprite { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Rollover and pressed media for the start/decrement button.</summary>
|
||||||
|
public uint UpRolloverSprite { get; set; }
|
||||||
|
public uint UpPressedSprite { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Rollover and pressed media for the end/increment button.</summary>
|
||||||
|
public uint DownRolloverSprite { get; set; }
|
||||||
|
public uint DownPressedSprite { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Authored extent of the decrement button along the scrollbar axis. Retail
|
/// Authored extent of the decrement button along the scrollbar axis. Retail
|
||||||
/// positions and subtracts the referenced button's real size; chat uses
|
/// positions and subtracts the referenced button's real size; chat uses
|
||||||
|
|
@ -108,6 +118,15 @@ public sealed class UiScrollbar : UiElement
|
||||||
private bool _draggingThumb;
|
private bool _draggingThumb;
|
||||||
private float _dragOffsetY;
|
private float _dragOffsetY;
|
||||||
private float _dragOffsetX;
|
private float _dragOffsetX;
|
||||||
|
private EndButton _hoveredButton;
|
||||||
|
private EndButton _pressedButton;
|
||||||
|
|
||||||
|
private enum EndButton
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Decrement,
|
||||||
|
Increment,
|
||||||
|
}
|
||||||
|
|
||||||
public UiScrollbar() { CapturesPointerDrag = true; }
|
public UiScrollbar() { CapturesPointerDrag = true; }
|
||||||
|
|
||||||
|
|
@ -201,8 +220,8 @@ public sealed class UiScrollbar : UiElement
|
||||||
float incrementExtent = AxisExtent(IncrementButtonExtent, Height);
|
float incrementExtent = AxisExtent(IncrementButtonExtent, Height);
|
||||||
|
|
||||||
// Decrement/up and increment/down use their authored button heights.
|
// Decrement/up and increment/down use their authored button heights.
|
||||||
DrawSprite(ctx, resolve, UpSprite, 0f, 0f, Width, decrementExtent);
|
DrawSprite(ctx, resolve, ActiveStartSprite, 0f, 0f, Width, decrementExtent);
|
||||||
DrawSprite(ctx, resolve, DownSprite,
|
DrawSprite(ctx, resolve, ActiveEndSprite,
|
||||||
0f, Height - incrementExtent, Width, incrementExtent);
|
0f, Height - incrementExtent, Width, incrementExtent);
|
||||||
|
|
||||||
// Thumb — only when content overflows the view. Retail 3-slice: top cap +
|
// Thumb — only when content overflows the view. Retail 3-slice: top cap +
|
||||||
|
|
@ -235,8 +254,8 @@ public sealed class UiScrollbar : UiElement
|
||||||
float decrementExtent = AxisExtent(DecrementButtonExtent, Width);
|
float decrementExtent = AxisExtent(DecrementButtonExtent, Width);
|
||||||
float incrementExtent = AxisExtent(IncrementButtonExtent, Width);
|
float incrementExtent = AxisExtent(IncrementButtonExtent, Width);
|
||||||
DrawTiled(ctx, resolve, TrackSprite, 0f, 0f, Width, Height);
|
DrawTiled(ctx, resolve, TrackSprite, 0f, 0f, Width, Height);
|
||||||
DrawSprite(ctx, resolve, UpSprite, 0f, 0f, decrementExtent, Height);
|
DrawSprite(ctx, resolve, ActiveStartSprite, 0f, 0f, decrementExtent, Height);
|
||||||
DrawSprite(ctx, resolve, DownSprite,
|
DrawSprite(ctx, resolve, ActiveEndSprite,
|
||||||
Width - incrementExtent, 0f, incrementExtent, Height);
|
Width - incrementExtent, 0f, incrementExtent, Height);
|
||||||
if (!model.HasOverflow) return;
|
if (!model.HasOverflow) return;
|
||||||
|
|
||||||
|
|
@ -322,9 +341,24 @@ public sealed class UiScrollbar : UiElement
|
||||||
if (IsModelDisabled)
|
if (IsModelDisabled)
|
||||||
{
|
{
|
||||||
_draggingThumb = false;
|
_draggingThumb = false;
|
||||||
|
_hoveredButton = EndButton.None;
|
||||||
|
_pressedButton = EndButton.None;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (e.Type == UiEventType.HoverEnter)
|
||||||
|
{
|
||||||
|
_hoveredButton = ButtonAt(e.Data1, e.Data2);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (e.Type == UiEventType.HoverLeave)
|
||||||
|
{
|
||||||
|
_hoveredButton = EndButton.None;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (e.Type == UiEventType.MouseMove)
|
||||||
|
_hoveredButton = ButtonAt(e.Data1, e.Data2);
|
||||||
|
|
||||||
if (Horizontal && ScalarChanged is not null)
|
if (Horizontal && ScalarChanged is not null)
|
||||||
return OnScalarEvent(e);
|
return OnScalarEvent(e);
|
||||||
|
|
||||||
|
|
@ -339,6 +373,7 @@ public sealed class UiScrollbar : UiElement
|
||||||
{
|
{
|
||||||
// e.Data1 = local X, e.Data2 = local Y (int pixel coords, see UiRoot hit dispatch).
|
// e.Data1 = local X, e.Data2 = local Y (int pixel coords, see UiRoot hit dispatch).
|
||||||
float ly = e.Data2;
|
float ly = e.Data2;
|
||||||
|
_pressedButton = ButtonAt(e.Data1, e.Data2);
|
||||||
float decrementExtent = AxisExtent(DecrementButtonExtent, Height);
|
float decrementExtent = AxisExtent(DecrementButtonExtent, Height);
|
||||||
float incrementExtent = AxisExtent(IncrementButtonExtent, Height);
|
float incrementExtent = AxisExtent(IncrementButtonExtent, Height);
|
||||||
|
|
||||||
|
|
@ -386,6 +421,7 @@ public sealed class UiScrollbar : UiElement
|
||||||
|
|
||||||
case UiEventType.MouseUp:
|
case UiEventType.MouseUp:
|
||||||
_draggingThumb = false;
|
_draggingThumb = false;
|
||||||
|
_pressedButton = EndButton.None;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -400,6 +436,7 @@ public sealed class UiScrollbar : UiElement
|
||||||
case UiEventType.MouseDown:
|
case UiEventType.MouseDown:
|
||||||
{
|
{
|
||||||
float x = e.Data1;
|
float x = e.Data1;
|
||||||
|
_pressedButton = ButtonAt(e.Data1, e.Data2);
|
||||||
float decrementExtent = AxisExtent(DecrementButtonExtent, Width);
|
float decrementExtent = AxisExtent(DecrementButtonExtent, Width);
|
||||||
float incrementExtent = AxisExtent(IncrementButtonExtent, Width);
|
float incrementExtent = AxisExtent(IncrementButtonExtent, Width);
|
||||||
if (x < decrementExtent) { m.ScrollByLines(-1); return true; }
|
if (x < decrementExtent) { m.ScrollByLines(-1); return true; }
|
||||||
|
|
@ -437,6 +474,7 @@ public sealed class UiScrollbar : UiElement
|
||||||
|
|
||||||
case UiEventType.MouseUp:
|
case UiEventType.MouseUp:
|
||||||
_draggingThumb = false;
|
_draggingThumb = false;
|
||||||
|
_pressedButton = EndButton.None;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -475,6 +513,7 @@ public sealed class UiScrollbar : UiElement
|
||||||
|
|
||||||
case UiEventType.MouseUp:
|
case UiEventType.MouseUp:
|
||||||
_draggingThumb = false;
|
_draggingThumb = false;
|
||||||
|
_pressedButton = EndButton.None;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -497,6 +536,48 @@ public sealed class UiScrollbar : UiElement
|
||||||
ScalarChanged?.Invoke(ScalarPosition);
|
ScalarChanged?.Invoke(ScalarPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private EndButton ButtonAt(float x, float y)
|
||||||
|
{
|
||||||
|
if (x < 0f || x >= Width || y < 0f || y >= Height)
|
||||||
|
return EndButton.None;
|
||||||
|
|
||||||
|
if (Horizontal)
|
||||||
|
{
|
||||||
|
if (x < AxisExtent(DecrementButtonExtent, Width))
|
||||||
|
return EndButton.Decrement;
|
||||||
|
if (x >= Width - AxisExtent(IncrementButtonExtent, Width))
|
||||||
|
return EndButton.Increment;
|
||||||
|
return EndButton.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (y < AxisExtent(DecrementButtonExtent, Height))
|
||||||
|
return EndButton.Decrement;
|
||||||
|
if (y >= Height - AxisExtent(IncrementButtonExtent, Height))
|
||||||
|
return EndButton.Increment;
|
||||||
|
return EndButton.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
private uint ActiveStartSprite
|
||||||
|
=> _pressedButton == EndButton.Decrement
|
||||||
|
&& _hoveredButton == EndButton.Decrement
|
||||||
|
&& UpPressedSprite != 0u
|
||||||
|
? UpPressedSprite
|
||||||
|
: _hoveredButton == EndButton.Decrement && UpRolloverSprite != 0u
|
||||||
|
? UpRolloverSprite
|
||||||
|
: UpSprite;
|
||||||
|
|
||||||
|
private uint ActiveEndSprite
|
||||||
|
=> _pressedButton == EndButton.Increment
|
||||||
|
&& _hoveredButton == EndButton.Increment
|
||||||
|
&& DownPressedSprite != 0u
|
||||||
|
? DownPressedSprite
|
||||||
|
: _hoveredButton == EndButton.Increment && DownRolloverSprite != 0u
|
||||||
|
? DownRolloverSprite
|
||||||
|
: DownSprite;
|
||||||
|
|
||||||
|
internal uint ActiveStartSpriteForTest => ActiveStartSprite;
|
||||||
|
internal uint ActiveEndSpriteForTest => ActiveEndSprite;
|
||||||
|
|
||||||
private static float AxisExtent(float authoredExtent, float axisLength)
|
private static float AxisExtent(float authoredExtent, float axisLength)
|
||||||
=> Math.Clamp(authoredExtent, 0f, MathF.Max(0f, axisLength));
|
=> Math.Clamp(authoredExtent, 0f, MathF.Max(0f, axisLength));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -230,24 +230,32 @@ public class DatWidgetFactoryTests
|
||||||
const uint IncrementId = 0x10000072u;
|
const uint IncrementId = 0x10000072u;
|
||||||
const uint DecrementSprite = 0x06004CDCu;
|
const uint DecrementSprite = 0x06004CDCu;
|
||||||
const uint IncrementSprite = 0x06004CDEu;
|
const uint IncrementSprite = 0x06004CDEu;
|
||||||
|
const uint DecrementRollover = 0x06004CDDu;
|
||||||
|
const uint IncrementRollover = 0x06004CDFu;
|
||||||
var decrement = new ElementInfo
|
var decrement = new ElementInfo
|
||||||
{
|
{
|
||||||
Id = DecrementId, Type = 1u, Width = 23f, Height = 36f,
|
Id = DecrementId, Type = 1u, X = 63f, Width = 23f, Height = 36f,
|
||||||
};
|
};
|
||||||
decrement.States[UiStateInfo.DirectStateId] = new UiStateInfo
|
decrement.States[UiStateInfo.DirectStateId] = new UiStateInfo
|
||||||
{
|
{
|
||||||
Id = UiStateInfo.DirectStateId,
|
Id = UiStateInfo.DirectStateId,
|
||||||
Image = new UiImageMedia(DecrementSprite, 1),
|
Image = new UiImageMedia(DecrementSprite, 1),
|
||||||
};
|
};
|
||||||
|
decrement.StateMedia["Normal"] = (DecrementSprite, 1);
|
||||||
|
decrement.StateMedia["Normal_rollover"] = (DecrementRollover, 1);
|
||||||
|
decrement.StateMedia["Normal_pressed"] = (DecrementSprite, 1);
|
||||||
var increment = new ElementInfo
|
var increment = new ElementInfo
|
||||||
{
|
{
|
||||||
Id = IncrementId, Type = 1u, Width = 23f, Height = 36f,
|
Id = IncrementId, Type = 1u, X = 0f, Width = 23f, Height = 36f,
|
||||||
};
|
};
|
||||||
increment.States[UiStateInfo.DirectStateId] = new UiStateInfo
|
increment.States[UiStateInfo.DirectStateId] = new UiStateInfo
|
||||||
{
|
{
|
||||||
Id = UiStateInfo.DirectStateId,
|
Id = UiStateInfo.DirectStateId,
|
||||||
Image = new UiImageMedia(IncrementSprite, 1),
|
Image = new UiImageMedia(IncrementSprite, 1),
|
||||||
};
|
};
|
||||||
|
increment.StateMedia["Normal"] = (IncrementSprite, 1);
|
||||||
|
increment.StateMedia["Normal_rollover"] = (IncrementRollover, 1);
|
||||||
|
increment.StateMedia["Normal_pressed"] = (IncrementSprite, 1);
|
||||||
var info = new ElementInfo
|
var info = new ElementInfo
|
||||||
{
|
{
|
||||||
Type = 11u,
|
Type = 11u,
|
||||||
|
|
@ -271,8 +279,12 @@ public class DatWidgetFactoryTests
|
||||||
Assert.True(bar.HideWhenDisabled);
|
Assert.True(bar.HideWhenDisabled);
|
||||||
Assert.Equal(23f, bar.DecrementButtonExtent);
|
Assert.Equal(23f, bar.DecrementButtonExtent);
|
||||||
Assert.Equal(23f, bar.IncrementButtonExtent);
|
Assert.Equal(23f, bar.IncrementButtonExtent);
|
||||||
Assert.Equal(DecrementSprite, bar.UpSprite);
|
Assert.Equal(IncrementSprite, bar.UpSprite);
|
||||||
Assert.Equal(IncrementSprite, bar.DownSprite);
|
Assert.Equal(IncrementRollover, bar.UpRolloverSprite);
|
||||||
|
Assert.Equal(IncrementSprite, bar.UpPressedSprite);
|
||||||
|
Assert.Equal(DecrementSprite, bar.DownSprite);
|
||||||
|
Assert.Equal(DecrementRollover, bar.DownRolloverSprite);
|
||||||
|
Assert.Equal(DecrementSprite, bar.DownPressedSprite);
|
||||||
Assert.Equal(0u, bar.TrackSprite);
|
Assert.Equal(0u, bar.TrackSprite);
|
||||||
Assert.Equal(0u, bar.ThumbSprite);
|
Assert.Equal(0u, bar.ThumbSprite);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,43 @@ public sealed class RetailWindowFrameTests
|
||||||
{
|
{
|
||||||
private static (uint, int, int) NoTex(uint _) => (0u, 0, 0);
|
private static (uint, int, int) NoTex(uint _) => (0u, 0, 0);
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(9, 459f, 288f)]
|
||||||
|
[InlineData(13, 587f, 416f)]
|
||||||
|
[InlineData(18, 747f, 576f)]
|
||||||
|
public void CombatRoot_FitsRequestedFavoriteSlotCapacity(
|
||||||
|
int visibleSlots,
|
||||||
|
float expectedRootWidth,
|
||||||
|
float expectedListWidth)
|
||||||
|
{
|
||||||
|
ImportedLayout combat = LayoutImporter.Build(
|
||||||
|
FixtureLoader.LoadCombatInfos(), NoTex, datFont: null);
|
||||||
|
|
||||||
|
float width = RetailCombatLayout.FitFavoriteSlots(combat, visibleSlots);
|
||||||
|
|
||||||
|
Assert.Equal(expectedRootWidth, width);
|
||||||
|
Assert.Equal(expectedListWidth, combat.FindElement(
|
||||||
|
SpellcastingUiController.FavoriteListId)!.Width);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CombatRoot_EighteenSlotsKeepArrowsAndCastInsideFrame()
|
||||||
|
{
|
||||||
|
ImportedLayout combat = LayoutImporter.Build(
|
||||||
|
FixtureLoader.LoadCombatInfos(), NoTex, datFont: null);
|
||||||
|
|
||||||
|
RetailCombatLayout.FitFavoriteSlots(combat);
|
||||||
|
|
||||||
|
UiElement group = combat.FindElement(0x100000AAu)!;
|
||||||
|
UiElement scrollbar = combat.FindElement(SpellcastingUiController.FavoriteScrollbarId)!;
|
||||||
|
UiElement cast = combat.FindElement(SpellcastingUiController.CastButtonId)!;
|
||||||
|
Assert.Equal(747f, combat.Root.Width);
|
||||||
|
Assert.Equal((40f, 622f), (group.Left, group.Width));
|
||||||
|
Assert.Equal(622f, scrollbar.Width);
|
||||||
|
Assert.Equal((662f, 75f), (cast.Left, cast.Width));
|
||||||
|
Assert.Equal(737f, cast.Left + cast.Width);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ImportedChrome_MountsRootWithoutASecondFrame()
|
public void ImportedChrome_MountsRootWithoutASecondFrame()
|
||||||
{
|
{
|
||||||
|
|
@ -41,6 +78,30 @@ public sealed class RetailWindowFrameTests
|
||||||
Assert.Equal(40f, content.MinWidth);
|
Assert.Equal(40f, content.MinWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImportedChrome_CanPreserveFixedHudDesktopAnchors()
|
||||||
|
{
|
||||||
|
var root = new UiRoot { Width = 1920, Height = 1080 };
|
||||||
|
var content = new UiPanel { Width = 1730, Height = 90 };
|
||||||
|
|
||||||
|
RetailWindowHandle handle = RetailWindowFrame.Mount(
|
||||||
|
root,
|
||||||
|
content,
|
||||||
|
NoTex,
|
||||||
|
new RetailWindowFrame.Options
|
||||||
|
{
|
||||||
|
WindowName = "combat",
|
||||||
|
Chrome = RetailWindowChrome.Imported,
|
||||||
|
Left = 0f,
|
||||||
|
Top = 980f,
|
||||||
|
OuterAnchors = AnchorEdges.Left | AnchorEdges.Bottom,
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
AnchorEdges.Left | AnchorEdges.Bottom,
|
||||||
|
handle.OuterFrame.Anchors);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void NineSlice_UsesCroppedContentAndDatHeightConstraints()
|
public void NineSlice_UsesCroppedContentAndDatHeightConstraints()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ public sealed class SpellcastingUiControllerTests
|
||||||
{
|
{
|
||||||
ImportedLayout layout = LayoutImporter.Build(
|
ImportedLayout layout = LayoutImporter.Build(
|
||||||
FixtureLoader.LoadCombatInfos(), NoTex, datFont: null);
|
FixtureLoader.LoadCombatInfos(), NoTex, datFont: null);
|
||||||
ApplyAnchors(layout.Root);
|
RetailCombatLayout.FitFavoriteSlots(layout);
|
||||||
var spellbook = new Spellbook();
|
var spellbook = new Spellbook();
|
||||||
spellbook.OnSpellLearned(42u, 1f);
|
spellbook.OnSpellLearned(42u, 1f);
|
||||||
spellbook.SetFavorite(0, 0, 42u);
|
spellbook.SetFavorite(0, 0, 42u);
|
||||||
|
|
@ -44,11 +44,13 @@ public sealed class SpellcastingUiControllerTests
|
||||||
Assert.True(scrollbar.HideWhenDisabled);
|
Assert.True(scrollbar.HideWhenDisabled);
|
||||||
Assert.Equal(23f, scrollbar.DecrementButtonExtent);
|
Assert.Equal(23f, scrollbar.DecrementButtonExtent);
|
||||||
Assert.Equal(23f, scrollbar.IncrementButtonExtent);
|
Assert.Equal(23f, scrollbar.IncrementButtonExtent);
|
||||||
Assert.Equal(0x06004CDCu, scrollbar.UpSprite);
|
Assert.Equal(0x06004CDEu, scrollbar.UpSprite);
|
||||||
Assert.Equal(0x06004CDEu, scrollbar.DownSprite);
|
Assert.Equal(0x06004CDFu, scrollbar.UpRolloverSprite);
|
||||||
|
Assert.Equal(0x06004CDCu, scrollbar.DownSprite);
|
||||||
|
Assert.Equal(0x06004CDDu, scrollbar.DownRolloverSprite);
|
||||||
Assert.False(scrollbar.IsPresentationVisible);
|
Assert.False(scrollbar.IsPresentationVisible);
|
||||||
Assert.Equal(439f, list.Width);
|
Assert.Equal(576f, list.Width);
|
||||||
Assert.Equal(13, list.GetNumUIItems());
|
Assert.Equal(18, list.GetNumUIItems());
|
||||||
|
|
||||||
UiCatalogSlot favorite = Assert.IsType<UiCatalogSlot>(list.GetItem(0));
|
UiCatalogSlot favorite = Assert.IsType<UiCatalogSlot>(list.GetItem(0));
|
||||||
Assert.Equal(42u, favorite.EntryId);
|
Assert.Equal(42u, favorite.EntryId);
|
||||||
|
|
@ -63,7 +65,7 @@ public sealed class SpellcastingUiControllerTests
|
||||||
Assert.Equal(i, slot.ShortcutNum);
|
Assert.Equal(i, slot.ShortcutNum);
|
||||||
Assert.Same(empty, slot.ActiveDigitArray());
|
Assert.Same(empty, slot.ActiveDigitArray());
|
||||||
}
|
}
|
||||||
for (int i = 9; i < 13; i++)
|
for (int i = 9; i < 18; i++)
|
||||||
{
|
{
|
||||||
Assert.Equal(0x06001A97u, list.GetItem(i)!.EmptySprite);
|
Assert.Equal(0x06001A97u, list.GetItem(i)!.EmptySprite);
|
||||||
Assert.Equal(-1, list.GetItem(i)!.ShortcutNum);
|
Assert.Equal(-1, list.GetItem(i)!.ShortcutNum);
|
||||||
|
|
@ -71,7 +73,7 @@ public sealed class SpellcastingUiControllerTests
|
||||||
|
|
||||||
var cast = Assert.IsType<UiButton>(
|
var cast = Assert.IsType<UiButton>(
|
||||||
layout.FindElement(SpellcastingUiController.CastButtonId));
|
layout.FindElement(SpellcastingUiController.CastButtonId));
|
||||||
Assert.Equal((525f, 75f), (cast.Left, cast.Width));
|
Assert.Equal((662f, 75f), (cast.Left, cast.Width));
|
||||||
Assert.Equal(3, cast.FaceSegmentCount);
|
Assert.Equal(3, cast.FaceSegmentCount);
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
[
|
[
|
||||||
|
|
@ -87,7 +89,7 @@ public sealed class SpellcastingUiControllerTests
|
||||||
{
|
{
|
||||||
ImportedLayout layout = LayoutImporter.Build(
|
ImportedLayout layout = LayoutImporter.Build(
|
||||||
FixtureLoader.LoadCombatInfos(), NoTex, datFont: null);
|
FixtureLoader.LoadCombatInfos(), NoTex, datFont: null);
|
||||||
ApplyAnchors(layout.Root);
|
RetailCombatLayout.FitFavoriteSlots(layout);
|
||||||
var spellbook = new Spellbook();
|
var spellbook = new Spellbook();
|
||||||
for (uint spellId = 1u; spellId <= 20u; spellId++)
|
for (uint spellId = 1u; spellId <= 20u; spellId++)
|
||||||
{
|
{
|
||||||
|
|
@ -105,7 +107,7 @@ public sealed class SpellcastingUiControllerTests
|
||||||
UiScrollbar scrollbar = Descendants(group).OfType<UiScrollbar>().Single(
|
UiScrollbar scrollbar = Descendants(group).OfType<UiScrollbar>().Single(
|
||||||
candidate => candidate.DatElementId == SpellcastingUiController.FavoriteScrollbarId);
|
candidate => candidate.DatElementId == SpellcastingUiController.FavoriteScrollbarId);
|
||||||
Assert.True(scrollbar.IsPresentationVisible);
|
Assert.True(scrollbar.IsPresentationVisible);
|
||||||
Assert.Equal(201, list.Scroll.MaxScroll);
|
Assert.Equal(64, list.Scroll.MaxScroll);
|
||||||
|
|
||||||
Assert.True(scrollbar.OnEvent(new UiEvent(
|
Assert.True(scrollbar.OnEvent(new UiEvent(
|
||||||
0u, scrollbar, UiEventType.MouseDown, Data1: (int)scrollbar.Width - 1)));
|
0u, scrollbar, UiEventType.MouseDown, Data1: (int)scrollbar.Width - 1)));
|
||||||
|
|
@ -117,6 +119,14 @@ public sealed class SpellcastingUiControllerTests
|
||||||
controller.AddFavorite(20u);
|
controller.AddFavorite(20u);
|
||||||
|
|
||||||
Assert.Equal(list.Scroll.MaxScroll, list.Scroll.ScrollY);
|
Assert.Equal(list.Scroll.MaxScroll, list.Scroll.ScrollY);
|
||||||
|
|
||||||
|
Assert.True(scrollbar.OnEvent(new UiEvent(
|
||||||
|
0u, scrollbar, UiEventType.MouseDown, Data1: 0)));
|
||||||
|
int manualOffset = list.Scroll.ScrollY;
|
||||||
|
objects.AddOrUpdate(new ClientObject { ObjectId = 99u, Name = "Unrelated" });
|
||||||
|
controller.Tick();
|
||||||
|
|
||||||
|
Assert.Equal(manualOffset, list.Scroll.ScrollY);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -157,6 +157,47 @@ public class UiScrollbarTests
|
||||||
Assert.Equal(64, model.ScrollY);
|
Assert.Equal(64, model.ScrollY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HorizontalModel_UsesIndependentRolloverAndPressedArrowMedia()
|
||||||
|
{
|
||||||
|
var root = new UiRoot { Width = 300f, Height = 100f };
|
||||||
|
var model = new UiScrollable
|
||||||
|
{
|
||||||
|
ContentHeight = 640,
|
||||||
|
ViewHeight = 320,
|
||||||
|
LineHeight = 32,
|
||||||
|
};
|
||||||
|
var bar = new UiScrollbar
|
||||||
|
{
|
||||||
|
Width = 160f,
|
||||||
|
Height = 36f,
|
||||||
|
Horizontal = true,
|
||||||
|
Model = model,
|
||||||
|
UpSprite = 1u,
|
||||||
|
UpRolloverSprite = 2u,
|
||||||
|
UpPressedSprite = 3u,
|
||||||
|
DownSprite = 4u,
|
||||||
|
DownRolloverSprite = 5u,
|
||||||
|
DownPressedSprite = 6u,
|
||||||
|
};
|
||||||
|
root.AddChild(bar);
|
||||||
|
|
||||||
|
root.OnMouseMove(5, 10);
|
||||||
|
Assert.Equal(2u, bar.ActiveStartSpriteForTest);
|
||||||
|
Assert.Equal(4u, bar.ActiveEndSpriteForTest);
|
||||||
|
|
||||||
|
// Moving between two regions of the same procedural scrollbar must
|
||||||
|
// refresh its sub-control hover, not wait for a whole-widget leave.
|
||||||
|
root.OnMouseMove(155, 10);
|
||||||
|
Assert.Equal(1u, bar.ActiveStartSpriteForTest);
|
||||||
|
Assert.Equal(5u, bar.ActiveEndSpriteForTest);
|
||||||
|
|
||||||
|
root.OnMouseDown(UiMouseButton.Left, 155, 10);
|
||||||
|
Assert.Equal(6u, bar.ActiveEndSpriteForTest);
|
||||||
|
root.OnMouseUp(UiMouseButton.Left, 155, 10);
|
||||||
|
Assert.Equal(5u, bar.ActiveEndSpriteForTest);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ModelWithoutOverflow_IsDisabledAndHideDisabledSuppressesPresentation()
|
public void ModelWithoutOverflow_IsDisabledAndHideDisabledSuppressesPresentation()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue