fix(ui): preserve cropped chat and button faces
This commit is contained in:
parent
d825572e31
commit
accacecafe
7 changed files with 168 additions and 8 deletions
|
|
@ -2221,6 +2221,12 @@ public sealed class GameWindow : IDisposable
|
||||||
chatRoot.Width = contentW; chatRoot.Height = contentH;
|
chatRoot.Width = contentW; chatRoot.Height = contentH;
|
||||||
chatRoot.Anchors = AcDream.App.UI.AnchorEdges.Left | AcDream.App.UI.AnchorEdges.Top
|
chatRoot.Anchors = AcDream.App.UI.AnchorEdges.Left | AcDream.App.UI.AnchorEdges.Top
|
||||||
| AcDream.App.UI.AnchorEdges.Right | AcDream.App.UI.AnchorEdges.Bottom;
|
| AcDream.App.UI.AnchorEdges.Right | AcDream.App.UI.AnchorEdges.Bottom;
|
||||||
|
// The LayoutDesc root is 800px wide because it shares the display
|
||||||
|
// coordinate space with auxiliary/floaty chat elements. The docked
|
||||||
|
// gmMainChatUI content is the left 490px. Rebase its immediate children
|
||||||
|
// after that intentional crop so subsequent frame resizing grows the
|
||||||
|
// transcript/input across the whole mounted interior.
|
||||||
|
chatRoot.RebaseChildLayoutBaselines();
|
||||||
chatRoot.Draggable = false; chatRoot.Resizable = false;
|
chatRoot.Draggable = false; chatRoot.Resizable = false;
|
||||||
chatFrame.AddChild(chatRoot);
|
chatFrame.AddChild(chatRoot);
|
||||||
_uiHost.Root.AddChild(chatFrame);
|
_uiHost.Root.AddChild(chatFrame);
|
||||||
|
|
|
||||||
|
|
@ -232,6 +232,7 @@ public sealed class ChatWindowController
|
||||||
float oldTop = bar.Top;
|
float oldTop = bar.Top;
|
||||||
bar.Top = 0f; // pull up to the panel top (resize-bar reclaim)
|
bar.Top = 0f; // pull up to the panel top (resize-bar reclaim)
|
||||||
bar.Height = bar.Height + oldTop;
|
bar.Height = bar.Height + oldTop;
|
||||||
|
bar.ResetAnchorCapture();
|
||||||
bar.Model = c.Transcript.Scroll;
|
bar.Model = c.Transcript.Scroll;
|
||||||
bar.SpriteResolve = resolve;
|
bar.SpriteResolve = resolve;
|
||||||
bar.TrackSprite = TrackSprite;
|
bar.TrackSprite = TrackSprite;
|
||||||
|
|
@ -308,6 +309,7 @@ public sealed class ChatWindowController
|
||||||
// just LEFT of the scrollbar column — shift it one button-width left.
|
// just LEFT of the scrollbar column — shift it one button-width left.
|
||||||
if (track is not null)
|
if (track is not null)
|
||||||
maxMinEl.Left = track.Left - maxMinEl.Width;
|
maxMinEl.Left = track.Left - maxMinEl.Width;
|
||||||
|
maxMinEl.ResetAnchorCapture();
|
||||||
maxMinEl.OnClick = c.ToggleMaximize;
|
maxMinEl.OnClick = c.ToggleMaximize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -151,9 +151,10 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
|
||||||
_resolve = resolve;
|
_resolve = resolve;
|
||||||
ClickThrough = false; // buttons are interactive — opt OUT of click-through
|
ClickThrough = false; // buttons are interactive — opt OUT of click-through
|
||||||
|
|
||||||
foreach (uint stateId in info.States.Keys)
|
// Visual transitions can select only states with an actual button face.
|
||||||
if (stateId != UiStateInfo.DirectStateId)
|
// Retail layouts commonly declare an empty Normal_pressed descriptor while
|
||||||
_availableStates.Add(stateId);
|
// supplying art only for Normal/Highlight. Treating that property-only state
|
||||||
|
// as drawable briefly blanks the button during mouse-down.
|
||||||
foreach (string stateName in info.StateMedia.Keys)
|
foreach (string stateName in info.StateMedia.Keys)
|
||||||
if (UiButtonStateMachine.TryStateId(stateName, out uint stateId))
|
if (UiButtonStateMachine.TryStateId(stateName, out uint stateId))
|
||||||
_availableStates.Add(stateId);
|
_availableStates.Add(stateId);
|
||||||
|
|
|
||||||
|
|
@ -483,11 +483,41 @@ public abstract class UiElement
|
||||||
Left = x; Top = y; Width = w; Height = h;
|
Left = x; Top = y; Width = w; Height = h;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Forget the captured anchor margins so the next <see cref="ApplyAnchor"/>
|
/// <summary>
|
||||||
/// re-captures them from the CURRENT rect. Call after manually repositioning/resizing
|
/// Make the current geometry the new layout baseline after an intentional
|
||||||
/// an anchored element at runtime (e.g. reflowing the chat input when the channel
|
/// controller/runtime change. Compatibility anchors recapture their margins on
|
||||||
/// button width changes) so the new rect becomes the anchor baseline.</summary>
|
/// the next layout pass; imported raw-edge policies rebase immediately against
|
||||||
internal void ResetAnchorCapture() => _anchorCaptured = false;
|
/// the current parent rect.
|
||||||
|
/// </summary>
|
||||||
|
internal void ResetAnchorCapture()
|
||||||
|
{
|
||||||
|
_anchorCaptured = false;
|
||||||
|
if (LayoutPolicy is null || Parent is null) return;
|
||||||
|
|
||||||
|
LayoutPolicy.Rebase(
|
||||||
|
UiPixelRect.FromPositionAndSize(
|
||||||
|
(int)Left,
|
||||||
|
(int)Top,
|
||||||
|
(int)Width,
|
||||||
|
(int)Height),
|
||||||
|
UiPixelRect.FromPositionAndSize(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
(int)Parent.Width,
|
||||||
|
(int)Parent.Height));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rebase each immediate child after a host deliberately changes this element's
|
||||||
|
/// design extent. This is used when a production DAT root contains auxiliary
|
||||||
|
/// space that is cropped by its retail window mount; descendants must reflow
|
||||||
|
/// from the mounted content rect, not from the uncropped LayoutDesc display box.
|
||||||
|
/// </summary>
|
||||||
|
internal void RebaseChildLayoutBaselines()
|
||||||
|
{
|
||||||
|
foreach (var child in _children)
|
||||||
|
child.ResetAnchorCapture();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Walk up to the owning <see cref="UiRoot"/> (the top of the tree), or null
|
/// <summary>Walk up to the owning <see cref="UiRoot"/> (the top of the tree), or null
|
||||||
/// if this element is not attached. Lets a widget reach focus/capture services — e.g.
|
/// if this element is not attached. Lets a widget reach focus/capture services — e.g.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
using AcDream.App.UI;
|
using AcDream.App.UI;
|
||||||
using AcDream.App.UI.Layout;
|
using AcDream.App.UI.Layout;
|
||||||
|
using AcDream.Core.Chat;
|
||||||
|
using AcDream.UI.Abstractions;
|
||||||
|
using AcDream.UI.Abstractions.Panels.Chat;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.UI.Layout;
|
namespace AcDream.App.Tests.UI.Layout;
|
||||||
|
|
||||||
|
|
@ -10,6 +13,8 @@ namespace AcDream.App.Tests.UI.Layout;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ChatLayoutConformanceTests
|
public class ChatLayoutConformanceTests
|
||||||
{
|
{
|
||||||
|
private static (uint, int, int) NoTex(uint _) => (0, 0, 0);
|
||||||
|
|
||||||
private static ElementInfo? Find(ElementInfo n, uint id)
|
private static ElementInfo? Find(ElementInfo n, uint id)
|
||||||
{
|
{
|
||||||
if (n.Id == id) return n;
|
if (n.Id == id) return n;
|
||||||
|
|
@ -58,4 +63,58 @@ public class ChatLayoutConformanceTests
|
||||||
Assert.True(input.OneLine);
|
Assert.True(input.OneLine);
|
||||||
Assert.Equal(0x10000016u, input.DatElementId);
|
Assert.Equal(0x10000016u, input.DatElementId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ChatFixture_CroppedDockedRoot_ReflowsContentAcrossMountedWidth()
|
||||||
|
{
|
||||||
|
var infos = FixtureLoader.LoadChatInfos();
|
||||||
|
var layout = LayoutImporter.Build(infos, NoTex, null);
|
||||||
|
var controller = ChatWindowController.Bind(
|
||||||
|
infos,
|
||||||
|
layout,
|
||||||
|
new ChatVM(new ChatLog()),
|
||||||
|
() => NullCommandBus.Instance,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
NoTex);
|
||||||
|
Assert.NotNull(controller);
|
||||||
|
|
||||||
|
var root = controller!.Root;
|
||||||
|
root.Width = 490;
|
||||||
|
root.Height = 100;
|
||||||
|
root.RebaseChildLayoutBaselines();
|
||||||
|
root.Width = 615;
|
||||||
|
|
||||||
|
var transcriptPanel = layout.FindElement(0x10000010u)!;
|
||||||
|
var inputBar = layout.FindElement(0x10000013u)!;
|
||||||
|
transcriptPanel.ApplyAnchor(root.Width, root.Height);
|
||||||
|
inputBar.ApplyAnchor(root.Width, root.Height);
|
||||||
|
|
||||||
|
Assert.Equal(615f, transcriptPanel.Width);
|
||||||
|
Assert.Equal(615f, inputBar.Width);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ChatFixture_ChannelCaptionWidth_SurvivesImportedLayoutPass()
|
||||||
|
{
|
||||||
|
var infos = FixtureLoader.LoadChatInfos();
|
||||||
|
var layout = LayoutImporter.Build(infos, NoTex, null);
|
||||||
|
var controller = ChatWindowController.Bind(
|
||||||
|
infos,
|
||||||
|
layout,
|
||||||
|
new ChatVM(new ChatLog()),
|
||||||
|
() => NullCommandBus.Instance,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
NoTex);
|
||||||
|
Assert.NotNull(controller);
|
||||||
|
|
||||||
|
controller!.Menu.OnSelect!.Invoke(ChatChannelKind.General);
|
||||||
|
float fittedWidth = controller.Menu.Width;
|
||||||
|
controller.Menu.ApplyAnchor(controller.Menu.Parent!.Width, controller.Menu.Parent.Height);
|
||||||
|
|
||||||
|
Assert.True(fittedWidth > 46f);
|
||||||
|
Assert.Equal(fittedWidth, controller.Menu.Width);
|
||||||
|
Assert.Equal(controller.Menu.Left + fittedWidth, controller.Input.Left);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,22 @@ public class UiButtonTests
|
||||||
Assert.Equal("LockedUI", b.ActiveState);
|
Assert.Equal("LockedUI", b.ActiveState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PropertyOnlyPressedState_PreservesDrawableNormalFace()
|
||||||
|
{
|
||||||
|
var info = ButtonInfo("Normal", "Highlight");
|
||||||
|
info.States[UiButtonStateMachine.NormalPressed] = new UiStateInfo
|
||||||
|
{
|
||||||
|
Id = UiButtonStateMachine.NormalPressed,
|
||||||
|
Name = "Normal_pressed",
|
||||||
|
};
|
||||||
|
var b = CreateButton(info);
|
||||||
|
|
||||||
|
b.OnEvent(new UiEvent(0, b, UiEventType.MouseDown, Data1: 5, Data2: 5));
|
||||||
|
|
||||||
|
Assert.Equal("Normal", b.ActiveState);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void HotClick_FiresImmediatelyRepeatsAndSuppressesReleaseClick()
|
public void HotClick_FiresImmediatelyRepeatsAndSuppressesReleaseClick()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -129,4 +129,50 @@ public sealed class UiLayoutPolicyTests
|
||||||
Assert.Equal(400f, element.Width);
|
Assert.Equal(400f, element.Width);
|
||||||
Assert.Equal(200f, element.Height);
|
Assert.Equal(200f, element.Height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResetAnchorCapture_RebasesImportedPolicyAfterControllerResize()
|
||||||
|
{
|
||||||
|
var parent = new UiPanel { Width = 490, Height = 17 };
|
||||||
|
var child = new UiPanel
|
||||||
|
{
|
||||||
|
Left = 0,
|
||||||
|
Top = 0,
|
||||||
|
Width = 46,
|
||||||
|
Height = 17,
|
||||||
|
LayoutPolicy = new UiLayoutPolicy(
|
||||||
|
1, 1, 2, 1,
|
||||||
|
UiPixelRect.FromPositionAndSize(0, 0, 46, 17),
|
||||||
|
UiPixelRect.FromPositionAndSize(0, 0, 490, 17)),
|
||||||
|
};
|
||||||
|
parent.AddChild(child);
|
||||||
|
child.Width = 72;
|
||||||
|
|
||||||
|
child.ResetAnchorCapture();
|
||||||
|
child.ApplyAnchor(parent.Width, parent.Height);
|
||||||
|
|
||||||
|
Assert.Equal(72f, child.Width);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RebaseChildLayoutBaselines_CroppedChatRegionThenGrowsWithFrame()
|
||||||
|
{
|
||||||
|
var root = new UiPanel { Width = 490, Height = 100 };
|
||||||
|
var transcriptPanel = new UiPanel
|
||||||
|
{
|
||||||
|
Width = 490,
|
||||||
|
Height = 83,
|
||||||
|
LayoutPolicy = new UiLayoutPolicy(
|
||||||
|
1, 1, 1, 1,
|
||||||
|
UiPixelRect.FromPositionAndSize(0, 0, 490, 83),
|
||||||
|
UiPixelRect.FromPositionAndSize(0, 0, 800, 100)),
|
||||||
|
};
|
||||||
|
root.AddChild(transcriptPanel);
|
||||||
|
|
||||||
|
root.RebaseChildLayoutBaselines();
|
||||||
|
root.Width = 615;
|
||||||
|
transcriptPanel.ApplyAnchor(root.Width, root.Height);
|
||||||
|
|
||||||
|
Assert.Equal(615f, transcriptPanel.Width);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue