fix #490 (part 2): plugin panels adopt an authored size change instead of keeping a stale saved size forever
MountPlugins registered every plugin window with authoredGeometryRevision hard-coded to 0 (RetailUiRuntime.cs), so RetailWindowLayoutPersistence's MigrateAuthoredGeometry -- gated on "saved revision >= authored revision" -- never migrated a plugin window's saved size: 0 >= 0 forever. MossTank's panel went 856x236 -> 984x271 and every user with a stored layout stayed stuck at 856x236 with no way to see the new default. Two changes: 1. MountPlugins now passes RetailWindowManager.ComputeAuthoredGeometryRevision (added previous commit) as the plugin window's authoredGeometryRevision, derived from the panel's own authored width/height/minw/minh/resizable. 2. MigrateAuthoredGeometry now compares revisions for INEQUALITY (saved.Revision == authored.Revision) instead of ordering (saved.Revision >= authored.Revision). A hash is not an incrementing counter -- two different authored sizes can hash in either order -- so "the authored size changed" has to mean "the value differs", not "the value went up". Built-in windows' hand-picked incrementing literals (chat: authoredGeometryRevision = 1) are unaffected: no existing saved revision is ever equal to a later, different literal either way. Mutation shown to fail first: the two new PluginMarkupPanel_AuthoredSizeChanged_* tests in RetailWindowLayoutPersistenceTests.cs reproduce the exact bug with concrete literals (856x236/400/150/true -> 984x271/... and 200x100/100/80/true -> 220x110/...) chosen so ComputeAuthoredGeometryRevision's OLD hash is >= the NEW hash for each pair -- confirmed via a throwaway probe before writing the assertions, so the pre-fix run fails deterministically rather than by chance of hash ordering. Both failed before this commit (size stayed at the old authored extent) and pass after (PluginMarkupPanel_AuthoredSizeUnchanged_KeepsUserResizedSize, unaffected either way, is a regression-safety companion). Full RetailWindow/Markup/ PluginSidePanel filter: 249 passed (was 246), 0 failed, 0 skipped. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
b2e68c2343
commit
05f22d46ff
3 changed files with 188 additions and 2 deletions
|
|
@ -4767,11 +4767,20 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
// later registration/sidepanel failure then rolls the mounted
|
// later registration/sidepanel failure then rolls the mounted
|
||||||
// subtree back through FailMount instead of leaking it.
|
// subtree back through FailMount instead of leaking it.
|
||||||
_bindings.Plugins.CompleteMount(panel, Host.Root, element);
|
_bindings.Plugins.CompleteMount(panel, Host.Root, element);
|
||||||
|
// #490 part 2: derive the authored-geometry revision from
|
||||||
|
// the panel's own authored extent instead of a hard-coded 0
|
||||||
|
// — see RetailWindowLayoutPersistence's class doc and
|
||||||
|
// RetailWindowManager.ComputeAuthoredGeometryRevision's own
|
||||||
|
// doc for why a plugin window can't use the built-in
|
||||||
|
// windows' manual-literal scheme.
|
||||||
|
int authoredGeometryRevision = RetailWindowManager.ComputeAuthoredGeometryRevision(
|
||||||
|
element.Width, element.Height, element.MinWidth, element.MinHeight, element.Resizable);
|
||||||
RetailWindowHandle handle = Host.WindowManager.Register(
|
RetailWindowHandle handle = Host.WindowManager.Register(
|
||||||
panel.WindowName,
|
panel.WindowName,
|
||||||
element,
|
element,
|
||||||
element,
|
element,
|
||||||
visibility);
|
visibility,
|
||||||
|
authoredGeometryRevision: authoredGeometryRevision);
|
||||||
_bindings.Plugins.CompleteWindowMount(
|
_bindings.Plugins.CompleteWindowMount(
|
||||||
panel,
|
panel,
|
||||||
() => Host.WindowManager.Unregister(panel.WindowName));
|
() => Host.WindowManager.Unregister(panel.WindowName));
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,31 @@ namespace AcDream.App.UI;
|
||||||
/// per-resolution settings. It deliberately ignores the temporary pre-login
|
/// per-resolution settings. It deliberately ignores the temporary pre-login
|
||||||
/// <c>default</c> character key so startup layout cannot overwrite a real
|
/// <c>default</c> character key so startup layout cannot overwrite a real
|
||||||
/// character's state.
|
/// character's state.
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// <b>Authored-geometry revision (#490 part 2).</b> Every registered window
|
||||||
|
/// carries an <c>authoredGeometryRevision</c> (see
|
||||||
|
/// <see cref="RetailWindowHandle.AuthoredGeometryRevision"/>); a restore
|
||||||
|
/// whose saved revision differs from the handle's current one replaces only
|
||||||
|
/// the saved WIDTH/HEIGHT with the current authored size
|
||||||
|
/// (<see cref="MigrateAuthoredGeometry"/>) — position, visibility, and
|
||||||
|
/// collapsed/maximized state are untouched, and the clamp in
|
||||||
|
/// <see cref="Apply"/> still re-fits the kept position to the live screen.
|
||||||
|
/// Built-in retail-imported windows hand-pick that revision as a small
|
||||||
|
/// incrementing literal at their <c>Register</c> call site (chat windows:
|
||||||
|
/// <c>authoredGeometryRevision = 1</c>) — a deliberate author decision each
|
||||||
|
/// time their authored size changes. Plugin windows have no such call site
|
||||||
|
/// an author remembers to touch, so <c>MountPlugins</c> instead derives the
|
||||||
|
/// revision automatically from the authored geometry tuple itself via
|
||||||
|
/// <see cref="RetailWindowManager.ComputeAuthoredGeometryRevision"/>
|
||||||
|
/// (width, height, min width, min height, resizable): unchanged authored
|
||||||
|
/// geometry hashes to the same revision (a user's own resize survives
|
||||||
|
/// restore), and ANY authored geometry change hashes to a different one
|
||||||
|
/// (the stored size resets to the new default exactly once). Because a hash
|
||||||
|
/// is not an ordered counter, the comparison is for INEQUALITY — see
|
||||||
|
/// <see cref="MigrateAuthoredGeometry"/>'s own doc for why the original
|
||||||
|
/// "newer revision only" read was wrong for this case.
|
||||||
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class RetailWindowLayoutPersistence : IDisposable
|
public sealed class RetailWindowLayoutPersistence : IDisposable
|
||||||
{
|
{
|
||||||
|
|
@ -295,11 +320,26 @@ public sealed class RetailWindowLayoutPersistence : IDisposable
|
||||||
handle.AuthoredGeometryRevision);
|
handle.AuthoredGeometryRevision);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// #490 part 2: compares revisions for INEQUALITY, not ordering. Built-in
|
||||||
|
/// retail-imported windows hand-pick a small incrementing literal
|
||||||
|
/// (0, 1, 2…) that only ever grows, so the original "migrate only if
|
||||||
|
/// saved < authored" read fine for them. Plugin windows instead derive
|
||||||
|
/// their revision from a hash of the authored geometry itself
|
||||||
|
/// (<see cref="RetailWindowManager.ComputeAuthoredGeometryRevision"/>) so
|
||||||
|
/// their author never has to remember to bump a literal — but a hash is
|
||||||
|
/// not a counter, and two different authored sizes can hash in either
|
||||||
|
/// order. "The authored size changed" therefore means "the value
|
||||||
|
/// differs", not "the value went up"; treating it as ordered silently
|
||||||
|
/// dropped every size-decreasing (by hash value, not by pixels) plugin
|
||||||
|
/// update, which is exactly how MossTank's 856x236 -> 984x271 bump got
|
||||||
|
/// stuck at the old size for every user with a stored layout.
|
||||||
|
/// </summary>
|
||||||
private static UiWindowLayout MigrateAuthoredGeometry(
|
private static UiWindowLayout MigrateAuthoredGeometry(
|
||||||
UiWindowLayout saved,
|
UiWindowLayout saved,
|
||||||
UiWindowLayout authored)
|
UiWindowLayout authored)
|
||||||
{
|
{
|
||||||
if (saved.AuthoredGeometryRevision >= authored.AuthoredGeometryRevision)
|
if (saved.AuthoredGeometryRevision == authored.AuthoredGeometryRevision)
|
||||||
return saved;
|
return saved;
|
||||||
|
|
||||||
return saved with
|
return saved with
|
||||||
|
|
|
||||||
|
|
@ -499,6 +499,143 @@ public sealed class RetailWindowLayoutPersistenceTests : IDisposable
|
||||||
Assert.Equal((200f, 150f), (panel.Width, panel.Height));
|
Assert.Equal((200f, 150f), (panel.Width, panel.Height));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── #490 part 2: a plugin's authored panel size changes across a plugin
|
||||||
|
// update (MossTank went 856x236 -> 984x271) and every user's stored
|
||||||
|
// layout must adopt the new authored size rather than keep the old one
|
||||||
|
// forever. MountPlugins derives authoredGeometryRevision from the
|
||||||
|
// authored geometry tuple via RetailWindowManager.ComputeAuthoredGeometryRevision
|
||||||
|
// (#490 part 2) instead of a manual literal, so these tests register the
|
||||||
|
// way MountPlugins does: pass the SAME computed hash to RegisterWindow. ──
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PluginMarkupPanel_AuthoredSizeChanged_ReplacesStoredSizeButKeepsPosition()
|
||||||
|
{
|
||||||
|
const string oldXml =
|
||||||
|
"<panel x=\"0\" y=\"0\" w=\"856\" h=\"236\" resizable=\"true\" minw=\"400\" minh=\"150\"></panel>";
|
||||||
|
const string newXml =
|
||||||
|
"<panel x=\"0\" y=\"0\" w=\"984\" h=\"271\" resizable=\"true\" minw=\"400\" minh=\"150\"></panel>";
|
||||||
|
var store = new SettingsStore(PathName);
|
||||||
|
|
||||||
|
// "Old session": the plugin's previous authored size registers and
|
||||||
|
// the user drags the window.
|
||||||
|
var oldPanel = MarkupDocument.Build(oldXml, new object(), _ => (1u, 32, 32));
|
||||||
|
var oldRoot = new UiRoot { Width = 1280, Height = 720 };
|
||||||
|
oldRoot.AddChild(oldPanel);
|
||||||
|
RetailWindowHandle oldHandle = oldRoot.RegisterWindow(
|
||||||
|
"moss-tank",
|
||||||
|
oldPanel,
|
||||||
|
authoredGeometryRevision: RetailWindowManager.ComputeAuthoredGeometryRevision(
|
||||||
|
oldPanel.Width, oldPanel.Height, oldPanel.MinWidth, oldPanel.MinHeight, oldPanel.Resizable));
|
||||||
|
using (var oldPersistence = new RetailWindowLayoutPersistence(
|
||||||
|
oldRoot.WindowManager, store, () => "Alice", () => (1280, 720)))
|
||||||
|
{
|
||||||
|
oldHandle.MoveTo(120f, 90f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// "New session": the plugin ships its new authored 984x271 size.
|
||||||
|
var newPanel = MarkupDocument.Build(newXml, new object(), _ => (1u, 32, 32));
|
||||||
|
var newRoot = new UiRoot { Width = 1280, Height = 720 };
|
||||||
|
newRoot.AddChild(newPanel);
|
||||||
|
newRoot.RegisterWindow(
|
||||||
|
"moss-tank",
|
||||||
|
newPanel,
|
||||||
|
authoredGeometryRevision: RetailWindowManager.ComputeAuthoredGeometryRevision(
|
||||||
|
newPanel.Width, newPanel.Height, newPanel.MinWidth, newPanel.MinHeight, newPanel.Resizable));
|
||||||
|
using var persistence = new RetailWindowLayoutPersistence(
|
||||||
|
newRoot.WindowManager, store, () => "Alice", () => (1280, 720));
|
||||||
|
|
||||||
|
persistence.RestoreAll();
|
||||||
|
|
||||||
|
Assert.Equal((984f, 271f), (newPanel.Width, newPanel.Height));
|
||||||
|
Assert.Equal((120f, 90f), (newPanel.Left, newPanel.Top));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PluginMarkupPanel_AuthoredSizeUnchanged_KeepsUserResizedSize()
|
||||||
|
{
|
||||||
|
const string xml =
|
||||||
|
"<panel x=\"0\" y=\"0\" w=\"856\" h=\"236\" resizable=\"true\" minw=\"400\" minh=\"150\"></panel>";
|
||||||
|
var store = new SettingsStore(PathName);
|
||||||
|
|
||||||
|
var panel = MarkupDocument.Build(xml, new object(), _ => (1u, 32, 32));
|
||||||
|
var root = new UiRoot { Width = 1280, Height = 720 };
|
||||||
|
root.AddChild(panel);
|
||||||
|
RetailWindowHandle handle = root.RegisterWindow(
|
||||||
|
"moss-tank",
|
||||||
|
panel,
|
||||||
|
authoredGeometryRevision: RetailWindowManager.ComputeAuthoredGeometryRevision(
|
||||||
|
panel.Width, panel.Height, panel.MinWidth, panel.MinHeight, panel.Resizable));
|
||||||
|
using (var persistence = new RetailWindowLayoutPersistence(
|
||||||
|
root.WindowManager, store, () => "Alice", () => (1280, 720)))
|
||||||
|
{
|
||||||
|
handle.MoveTo(50f, 50f);
|
||||||
|
handle.ResizeTo(900f, 300f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fresh session: the SAME authored geometry (same markup) registers
|
||||||
|
// again — the derived revision is unchanged, so the user's own
|
||||||
|
// resize must survive.
|
||||||
|
var freshPanel = MarkupDocument.Build(xml, new object(), _ => (1u, 32, 32));
|
||||||
|
var freshRoot = new UiRoot { Width = 1280, Height = 720 };
|
||||||
|
freshRoot.AddChild(freshPanel);
|
||||||
|
freshRoot.RegisterWindow(
|
||||||
|
"moss-tank",
|
||||||
|
freshPanel,
|
||||||
|
authoredGeometryRevision: RetailWindowManager.ComputeAuthoredGeometryRevision(
|
||||||
|
freshPanel.Width, freshPanel.Height, freshPanel.MinWidth, freshPanel.MinHeight, freshPanel.Resizable));
|
||||||
|
using var freshPersistence = new RetailWindowLayoutPersistence(
|
||||||
|
freshRoot.WindowManager, store, () => "Alice", () => (1280, 720));
|
||||||
|
|
||||||
|
freshPersistence.RestoreAll();
|
||||||
|
|
||||||
|
Assert.Equal((900f, 300f), (freshPanel.Width, freshPanel.Height));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PluginMarkupPanel_AuthoredSizeChanged_ClampsPositionToNewScreenBounds()
|
||||||
|
{
|
||||||
|
// The authored-size migration keeps the saved POSITION, but that
|
||||||
|
// position still runs through the ordinary restore clamp (Apply's
|
||||||
|
// MoveTo clamp) — a larger authored size can push a near-edge saved
|
||||||
|
// X/Y off the live screen.
|
||||||
|
const string oldXml =
|
||||||
|
"<panel x=\"0\" y=\"0\" w=\"200\" h=\"100\" resizable=\"true\" minw=\"100\" minh=\"80\"></panel>";
|
||||||
|
const string newXml =
|
||||||
|
"<panel x=\"0\" y=\"0\" w=\"220\" h=\"110\" resizable=\"true\" minw=\"100\" minh=\"80\"></panel>";
|
||||||
|
var store = new SettingsStore(PathName);
|
||||||
|
|
||||||
|
var oldPanel = MarkupDocument.Build(oldXml, new object(), _ => (1u, 32, 32));
|
||||||
|
var oldRoot = new UiRoot { Width = 800, Height = 600 };
|
||||||
|
oldRoot.AddChild(oldPanel);
|
||||||
|
RetailWindowHandle oldHandle = oldRoot.RegisterWindow(
|
||||||
|
"moss-tank",
|
||||||
|
oldPanel,
|
||||||
|
authoredGeometryRevision: RetailWindowManager.ComputeAuthoredGeometryRevision(
|
||||||
|
oldPanel.Width, oldPanel.Height, oldPanel.MinWidth, oldPanel.MinHeight, oldPanel.Resizable));
|
||||||
|
using (var oldPersistence = new RetailWindowLayoutPersistence(
|
||||||
|
oldRoot.WindowManager, store, () => "Alice", () => (800, 600)))
|
||||||
|
{
|
||||||
|
oldHandle.MoveTo(590f, 490f); // fits the OLD 200x100 size exactly
|
||||||
|
}
|
||||||
|
|
||||||
|
var newPanel = MarkupDocument.Build(newXml, new object(), _ => (1u, 32, 32));
|
||||||
|
var newRoot = new UiRoot { Width = 800, Height = 600 };
|
||||||
|
newRoot.AddChild(newPanel);
|
||||||
|
newRoot.RegisterWindow(
|
||||||
|
"moss-tank",
|
||||||
|
newPanel,
|
||||||
|
authoredGeometryRevision: RetailWindowManager.ComputeAuthoredGeometryRevision(
|
||||||
|
newPanel.Width, newPanel.Height, newPanel.MinWidth, newPanel.MinHeight, newPanel.Resizable));
|
||||||
|
using var persistence = new RetailWindowLayoutPersistence(
|
||||||
|
newRoot.WindowManager, store, () => "Alice", () => (800, 600));
|
||||||
|
|
||||||
|
persistence.RestoreAll();
|
||||||
|
|
||||||
|
Assert.Equal((220f, 110f), (newPanel.Width, newPanel.Height));
|
||||||
|
// maxX = 800 - 220 = 580 (590 clamps down); maxY = 600 - 110 = 490 (already in bounds).
|
||||||
|
Assert.Equal((580f, 490f), (newPanel.Left, newPanel.Top));
|
||||||
|
}
|
||||||
|
|
||||||
private static RetailWindowHandle Mount(
|
private static RetailWindowHandle Mount(
|
||||||
UiRoot root,
|
UiRoot root,
|
||||||
string name,
|
string name,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue