engine: pin retained glyph text contract
This commit is contained in:
55
engine/Age.Engine.Tests/AdvRetainedTextContractTests.cs
Normal file
55
engine/Age.Engine.Tests/AdvRetainedTextContractTests.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using Age.Engine.Model;
|
||||
|
||||
public class AdvRetainedTextContractTests
|
||||
{
|
||||
[Fact]
|
||||
public void GlyphRecordUsesNativeEdgesAndExactChainFlag()
|
||||
{
|
||||
var chained = new AdvRetainedGlyphRecord(1, 10, 20, 34, 51);
|
||||
var nonChained = chained with { PublicationChainFlag = 2 };
|
||||
|
||||
Assert.Equal((24, 31), (chained.Width, chained.Height));
|
||||
Assert.True(chained.PublishesNextInSameTick);
|
||||
Assert.False(nonChained.PublishesNextInSameTick);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OverflowComparisonsAreStrictAndIndependentPerAxis()
|
||||
{
|
||||
Assert.Equal(
|
||||
AdvTextOverflowFlags.None,
|
||||
AdvRetainedTextContract.CheckOverflow(100, 50, 100, 50));
|
||||
Assert.Equal(
|
||||
AdvTextOverflowFlags.Horizontal,
|
||||
AdvRetainedTextContract.CheckOverflow(100, 50, 101, 50));
|
||||
Assert.Equal(
|
||||
AdvTextOverflowFlags.Vertical,
|
||||
AdvRetainedTextContract.CheckOverflow(100, 50, 100, 51));
|
||||
Assert.Equal(
|
||||
AdvTextOverflowFlags.Horizontal | AdvTextOverflowFlags.Vertical,
|
||||
AdvRetainedTextContract.CheckOverflow(100, 50, 101, 51));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0x8141)] // 、
|
||||
[InlineData(0x8142)] // 。
|
||||
[InlineData(0x8176)] // 」
|
||||
public void NativeClosingPunctuationPreventsHorizontalWrapBefore(int cp932)
|
||||
=> Assert.True(AdvRetainedTextContract.PreventsHorizontalWrapBefore((ushort)cp932));
|
||||
|
||||
[Fact]
|
||||
public void OtherCp932GlyphsMayWrapNormally()
|
||||
{
|
||||
Assert.False(AdvRetainedTextContract.PreventsHorizontalWrapBefore(0x8140)); // full-width space
|
||||
Assert.False(AdvRetainedTextContract.PreventsHorizontalWrapBefore(0x82a0)); // あ
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TimedPublicationStopsAtConfiguredObjectCapacity()
|
||||
{
|
||||
Assert.True(AdvRetainedTextContract.TimedPublicationHandleExists(0, 500));
|
||||
Assert.True(AdvRetainedTextContract.TimedPublicationHandleExists(499, 500));
|
||||
Assert.False(AdvRetainedTextContract.TimedPublicationHandleExists(500, 500));
|
||||
Assert.False(AdvRetainedTextContract.TimedPublicationHandleExists(-1, 500));
|
||||
}
|
||||
}
|
||||
@@ -289,6 +289,12 @@ public class AdvTextOpsTests
|
||||
Assert.Equal((10, 10, 250, 368), CursorAndBounds(history.GetLayoutSnapshot(9)));
|
||||
Assert.Equal(new[] { 1 }, history.LayoutsCoveredByTextObjectErase(0xd6d8, 0x1f4));
|
||||
Assert.Equal(new[] { 7 }, history.LayoutsCoveredByTextObjectErase(0x7d0, 0x1f4));
|
||||
Assert.All(Enumerable.Range(1, 9), slot =>
|
||||
{
|
||||
AdvTextLayoutPresentationBinding binding = history.GetPresentationBinding(slot);
|
||||
Assert.Equal(slot + 0x14, binding.SourceSurfaceSlot);
|
||||
Assert.Equal(0x1f4, binding.ObjectCapacity);
|
||||
});
|
||||
|
||||
static (int X, int Y, int Right, int Bottom) CursorAndBounds(AdvTextLayoutSnapshot layout)
|
||||
=> (layout.CursorX, layout.CursorY, layout.Right, layout.Bottom);
|
||||
|
||||
@@ -92,6 +92,34 @@ public class NativeNumberedSaveCodecTests
|
||||
Assert.Equal(AdvTextHistoryRecordKind.Metadata, restored.Records[1].Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HistoryTailRestorePreservesInitializedLiveLayoutBindings()
|
||||
{
|
||||
var savedHistory = new AdvTextHistory();
|
||||
savedHistory.DefineLayout(1, 400, 120, 75, 340);
|
||||
savedHistory.AppendText(1, 0x123, "保存履歴", AdvTextStyle.Default);
|
||||
|
||||
var liveHistory = new AdvTextHistory();
|
||||
liveHistory.DefineLayout(3, 320, 90, 20, 400);
|
||||
liveHistory.SetResetCursor(3, 45, 42);
|
||||
liveHistory.SetBounds(3, 300, 80);
|
||||
liveHistory.SetWaitIndicatorObjectHandle(3, 0x1234);
|
||||
liveHistory.SetTextObjectRange(3, 0x2000, 500);
|
||||
|
||||
NativeTextHistoryCodec.DecodeInto(NativeTextHistoryCodec.Encode(savedHistory), liveHistory);
|
||||
|
||||
Assert.Equal(savedHistory.Entries, liveHistory.Entries);
|
||||
Assert.Equal(savedHistory.Records.Select(record => record.Text),
|
||||
liveHistory.Records.Select(record => record.Text));
|
||||
Assert.Equal(3, liveHistory.CurrentLayoutSlot);
|
||||
Assert.Equal(
|
||||
new AdvTextLayoutSnapshot(3, 320, 90, 20, 400, 0, 0, 300, 80),
|
||||
liveHistory.GetLayoutSnapshot(3));
|
||||
Assert.Equal(
|
||||
new AdvTextLayoutPresentationBinding(3, 0x17, 0x2000, 500, 0x1234),
|
||||
liveHistory.GetPresentationBinding(3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DirectoryStorePreservesHistoryTailAfterNativeContainer()
|
||||
{
|
||||
|
||||
56
engine/Age.Engine/Model/AdvRetainedTextContract.cs
Normal file
56
engine/Age.Engine/Model/AdvRetainedTextContract.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
namespace Age.Engine.Model;
|
||||
|
||||
[Flags]
|
||||
public enum AdvTextOverflowFlags
|
||||
{
|
||||
None = 0,
|
||||
Horizontal = 1,
|
||||
Vertical = 2,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Native AGE's 20-byte live ADV glyph record. Rectangle values are edges, matching the
|
||||
/// retained-object bind ABI; they are not x/y/width/height values.
|
||||
/// </summary>
|
||||
public readonly record struct AdvRetainedGlyphRecord(
|
||||
int PublicationChainFlag,
|
||||
int Left,
|
||||
int Top,
|
||||
int Right,
|
||||
int Bottom)
|
||||
{
|
||||
public int Width => Right - Left;
|
||||
public int Height => Bottom - Top;
|
||||
public bool PublishesNextInSameTick => PublicationChainFlag == 1;
|
||||
}
|
||||
|
||||
/// <summary>The script-owned retained-object binding associated with one live ADV layout.</summary>
|
||||
public readonly record struct AdvTextLayoutPresentationBinding(
|
||||
int LayoutSlot,
|
||||
int SourceSurfaceSlot,
|
||||
long FirstObjectHandle,
|
||||
long ObjectCapacity,
|
||||
long WaitIndicatorObjectHandle);
|
||||
|
||||
/// <summary>Platform-neutral rules established from AGE's native retained-glyph workers.</summary>
|
||||
public static class AdvRetainedTextContract
|
||||
{
|
||||
public static AdvTextOverflowFlags CheckOverflow(
|
||||
int rightBound, int bottomBound, int glyphRight, int glyphBottom)
|
||||
{
|
||||
var result = AdvTextOverflowFlags.None;
|
||||
if (rightBound < glyphRight) result |= AdvTextOverflowFlags.Horizontal;
|
||||
if (bottomBound < glyphBottom) result |= AdvTextOverflowFlags.Vertical;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CP932 closing punctuation which AGE keeps on the preceding horizontal line even when the
|
||||
/// current glyph crosses the right bound.
|
||||
/// </summary>
|
||||
public static bool PreventsHorizontalWrapBefore(ushort cp932)
|
||||
=> cp932 is 0x8141 or 0x8142 or 0x8176; // 、 。 」
|
||||
|
||||
public static bool TimedPublicationHandleExists(int revealIndex, long objectCapacity)
|
||||
=> revealIndex >= 0 && revealIndex < objectCapacity;
|
||||
}
|
||||
@@ -195,6 +195,15 @@ public sealed class AdvTextHistory
|
||||
layout.TextObjectRangeCount = count;
|
||||
}
|
||||
|
||||
public AdvTextLayoutPresentationBinding GetPresentationBinding(int requestedSlot)
|
||||
{
|
||||
int slot = ResolveLayout(requestedSlot);
|
||||
var layout = GetOrCreateLayout(slot);
|
||||
return new AdvTextLayoutPresentationBinding(
|
||||
slot, checked(slot + 0x14), layout.TextObjectRangeFirst,
|
||||
layout.TextObjectRangeCount, layout.WaitIndicatorObjectHandle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve layouts whose complete native retained-glyph handle interval is covered by one op-0x1f7
|
||||
/// erase. The Label backend collapses those glyph objects into live text runs, so full native range
|
||||
@@ -275,20 +284,9 @@ public sealed class AdvTextHistory
|
||||
_entries.Clear();
|
||||
_entries.AddRange(entries);
|
||||
_pendingGroupStarts.Clear();
|
||||
_layouts.Clear();
|
||||
foreach (AdvTextHistoryRecord record in records)
|
||||
{
|
||||
var layout = GetOrCreateLayout(record.Layout.Slot);
|
||||
layout.Width = record.Layout.Width;
|
||||
layout.Height = record.Layout.Height;
|
||||
layout.OriginX = record.Layout.OriginX;
|
||||
layout.OriginY = record.Layout.OriginY;
|
||||
layout.CursorX = record.Layout.CursorX;
|
||||
layout.CursorY = record.Layout.CursorY;
|
||||
layout.Right = record.Layout.Right;
|
||||
layout.Bottom = record.Layout.Bottom;
|
||||
}
|
||||
CurrentLayoutSlot = entries.Count > 0 ? entries[^1].LayoutSlot : 0;
|
||||
// Native text_history_deserialize clears and reconstructs only the history entry/record vectors.
|
||||
// Live layout definitions, selected layout, surfaces, and retained-object bindings remain owned by
|
||||
// the initialized text manager and are rebuilt by the script restore path as needed.
|
||||
_navigationAnchorIndex = entries.Count - 1;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user