Skip to content

Commit

Permalink
Implement replacement entity
Browse files Browse the repository at this point in the history
  • Loading branch information
Soreepeong committed Aug 3, 2024
1 parent 018fe39 commit c7dc76a
Show file tree
Hide file tree
Showing 6 changed files with 375 additions and 52 deletions.
158 changes: 125 additions & 33 deletions Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,10 @@ public SeStringDrawResult Draw(
foreach (ref var f in fragmentSpan)
{
var data = state.Span[f.From..f.To];
this.DrawTextFragment(ref state, f.Offset, f.IsSoftHyphenVisible, data, lastRune, f.Link);
if (f.Entity)
f.Entity.Draw(state, f.From, f.Offset);
else
this.DrawTextFragment(ref state, f.Offset, f.IsSoftHyphenVisible, data, lastRune, f.Link);
lastRune = f.LastRune;
}

Expand Down Expand Up @@ -376,58 +379,125 @@ private void CreateTextFragments(ref SeStringDrawState state, float baseY)
var link = -1;
foreach (var (breakAt, mandatory) in new LineBreakEnumerator(state.Span, UtfEnumeratorFlags.Utf8SeString))
{
// Might have happened if custom entity was longer than the previous break unit.
if (prev > breakAt)
continue;

var nextLink = link;
for (var first = true; prev < breakAt; first = false)
{
var curr = breakAt;
var entity = default(SeStringReplacementEntity);

// Try to split by link payloads.
// Try to split by link payloads and custom entities.
foreach (var p in new ReadOnlySeStringSpan(state.Span[prev..breakAt]).GetOffsetEnumerator())
{
if (p.Payload.MacroCode == MacroCode.Link)
var break2 = false;
switch (p.Payload.Type)
{
nextLink =
p.Payload.TryGetExpression(out var e) &&
e.TryGetUInt(out var u) &&
u == (uint)LinkMacroPayloadType.Terminator
? -1
: prev + p.Offset;

// Split only if we're not splitting at the beginning.
if (p.Offset != 0)
case ReadOnlySePayloadType.Text when state.GetEntity is { } getEntity:
foreach (var oe in UtfEnumerator.From(p.Payload.Body, UtfEnumeratorFlags.Utf8))
{
var entityOffset = prev + p.Offset + oe.ByteOffset;
entity = getEntity(state, entityOffset);
if (!entity)
continue;

if (prev == entityOffset)
{
curr = entityOffset + entity.ByteLength;
}
else
{
entity = default;
curr = entityOffset;
}

break2 = true;
break;
}

break;

case ReadOnlySePayloadType.Macro when
state.GetEntity is { } getEntity &&
getEntity(state, prev + p.Offset) is { ByteLength: > 0 } entity1:
entity = entity1;
if (p.Offset == 0)
{
curr = prev + p.Offset + entity.ByteLength;
}
else
{
entity = default;
curr = prev + p.Offset;
}

break2 = true;
break;

case ReadOnlySePayloadType.Macro when p.Payload.MacroCode == MacroCode.Link:
{
curr = prev + p.Offset;
nextLink =
p.Payload.TryGetExpression(out var e) &&
e.TryGetUInt(out var u) &&
u == (uint)LinkMacroPayloadType.Terminator
? -1
: prev + p.Offset;

// Split only if we're not splitting at the beginning.
if (p.Offset != 0)
{
curr = prev + p.Offset;
break2 = true;
break;
}

link = nextLink;

break;
}

link = nextLink;
case ReadOnlySePayloadType.Invalid:
default:
break;
}

if (break2) break;
}

// Create a text fragment without applying wrap width limits for testing.
var fragment = this.CreateFragment(state, prev, curr, curr == breakAt && mandatory, xy, link);
var fragment = this.CreateFragment(state, prev, curr, curr == breakAt && mandatory, xy, link, entity);
var overflows = Math.Max(w, xy.X + fragment.VisibleWidth) > state.WrapWidth;

// Test if the fragment does not fit into the current line and the current line is not empty,
// if this is the first time testing the current break unit.
if (first && xy.X != 0 && this.fragments.Count > 0 && !this.fragments[^1].BreakAfter && overflows)
// Test if the fragment does not fit into the current line and the current line is not empty.
if (xy.X != 0 && this.fragments.Count > 0 && !this.fragments[^1].BreakAfter && overflows)
{
// The break unit as a whole does not fit into the current line. Advance to the next line.
xy.X = 0;
xy.Y += state.LineHeight;
w = 0;
CollectionsMarshal.AsSpan(this.fragments)[^1].BreakAfter = true;
fragment.Offset = xy;

// Now that the fragment is given its own line, test if it overflows again.
overflows = fragment.VisibleWidth > state.WrapWidth;
// Introduce break if this is the first time testing the current break unit or the current fragment
// is an entity.
if (first || entity)
{
// The break unit as a whole does not fit into the current line. Advance to the next line.
xy.X = 0;
xy.Y += state.LineHeight;
w = 0;
CollectionsMarshal.AsSpan(this.fragments)[^1].BreakAfter = true;
fragment.Offset = xy;

// Now that the fragment is given its own line, test if it overflows again.
overflows = fragment.VisibleWidth > state.WrapWidth;
}
}

if (overflows)
{
// Create a fragment again that fits into the given width limit.
var remainingWidth = state.WrapWidth - xy.X;
fragment = this.CreateFragment(state, prev, curr, true, xy, link, remainingWidth);
// A replacement entity may not be broken down further.
if (!entity)
{
// Create a fragment again that fits into the given width limit.
var remainingWidth = state.WrapWidth - xy.X;
fragment = this.CreateFragment(state, prev, curr, true, xy, link, entity, remainingWidth);
}
}
else if (this.fragments.Count > 0 && xy.X != 0)
{
Expand Down Expand Up @@ -630,8 +700,9 @@ when payload.TryGetExpression(out var icon) && icon.TryGetInt(out var iconId):
/// with.</param>
/// <param name="breakAfter">Whether to break line after this fragment.</param>
/// <param name="offset">Offset in pixels w.r.t. <see cref="SeStringDrawParams.ScreenOffset"/>.</param>
/// <param name="activeLinkOffset">Byte offset of the link payload in <see cref="SeStringDrawState.Span"/> that
/// <param name="link">Byte offset of the link payload in <see cref="SeStringDrawState.Span"/> that
/// decorates this text fragment.</param>
/// <param name="entity">Entity to display in place of this fragment.</param>
/// <param name="wrapWidth">Optional wrap width to stop at while creating this text fragment. Note that at least
/// one visible character needs to be there in a single text fragment, in which case it is allowed to exceed
/// the wrap width.</param>
Expand All @@ -642,9 +713,27 @@ private TextFragment CreateFragment(
int to,
bool breakAfter,
Vector2 offset,
int activeLinkOffset,
int link,
SeStringReplacementEntity entity,
float wrapWidth = float.MaxValue)
{
if (entity)
{
return new(
from,
to,
link,
offset,
entity,
entity.Size.X,
entity.Size.X,
entity.Size.X,
false,
false,
default,
default);
}

var x = 0f;
var w = 0f;
var visibleWidth = 0f;
Expand Down Expand Up @@ -717,8 +806,9 @@ private TextFragment CreateFragment(
return new(
from,
to,
activeLinkOffset,
link,
offset,
entity,
visibleWidth,
advanceWidth,
advanceWidthWithoutSoftHyphen,
Expand All @@ -733,6 +823,7 @@ private TextFragment CreateFragment(
/// <param name="To">Ending byte offset (exclusive) in a SeString.</param>
/// <param name="Link">Byte offset of the link that decorates this text fragment, or <c>-1</c> if none.</param>
/// <param name="Offset">Offset in pixels w.r.t. <see cref="SeStringDrawParams.ScreenOffset"/>.</param>
/// <param name="Entity">Replacement entity, if any.</param>
/// <param name="VisibleWidth">Visible width of this text fragment. This is the width required to draw everything
/// without clipping.</param>
/// <param name="AdvanceWidth">Advance width of this text fragment. This is the width required to add to the cursor
Expand All @@ -749,6 +840,7 @@ private record struct TextFragment(
int To,
int Link,
Vector2 Offset,
SeStringReplacementEntity Entity,
float VisibleWidth,
float AdvanceWidth,
float AdvanceWidthWithoutSoftHyphen,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal.TextProcessing;
internal ref struct LineBreakEnumerator
{
private readonly UtfEnumeratorFlags enumeratorFlags;
private readonly int dataLength;

private UtfEnumerator enumerator;
private int dataLength;
private int currentByteOffsetDelta;

private Entry class1;
private Entry class2;

Expand All @@ -24,8 +26,6 @@ internal ref struct LineBreakEnumerator

private int consecutiveRegionalIndicators;

private bool finished;

/// <summary>Initializes a new instance of the <see cref="LineBreakEnumerator"/> struct.</summary>
/// <param name="data">UTF-N byte sequence.</param>
/// <param name="enumeratorFlags">Flags to pass to sub-enumerator.</param>
Expand Down Expand Up @@ -58,11 +58,25 @@ private enum LineBreakMode : byte
/// <inheritdoc cref="IEnumerator{T}.Current"/>
public (int ByteOffset, bool Mandatory) Current { get; private set; }

/// <summary>Gets a value indicating whether the end of the underlying span has been reached.</summary>
public bool Finished { get; private set; }

/// <summary>Resumes enumeration with the given data.</summary>
/// <param name="data">The data.</param>
/// <param name="offsetDelta">Offset to add to <see cref="Current"/>.<c>ByteOffset</c>.</param>
public void ResumeWith(ReadOnlySpan<byte> data, int offsetDelta)
{
this.enumerator = UtfEnumerator.From(data, this.enumeratorFlags);
this.dataLength = data.Length;
this.currentByteOffsetDelta = offsetDelta;
this.Finished = false;
}

/// <inheritdoc cref="IEnumerator.MoveNext"/>
[SuppressMessage("ReSharper", "ConvertIfStatementToSwitchStatement", Justification = "No")]
public bool MoveNext()
{
if (this.finished)
if (this.Finished)
return false;

while (this.enumerator.MoveNext())
Expand All @@ -77,10 +91,10 @@ public bool MoveNext()
switch (this.HandleCharacter(effectiveInt))
{
case LineBreakMode.Mandatory:
this.Current = (this.enumerator.Current.ByteOffset, true);
this.Current = (this.enumerator.Current.ByteOffset + this.currentByteOffsetDelta, true);
return true;
case LineBreakMode.Optional:
this.Current = (this.enumerator.Current.ByteOffset, false);
this.Current = (this.enumerator.Current.ByteOffset + this.currentByteOffsetDelta, false);
return true;
case LineBreakMode.Prohibited:
default:
Expand All @@ -90,8 +104,8 @@ public bool MoveNext()

// Start and end of text:
// LB3 Always break at the end of text.
this.Current = (this.dataLength, true);
this.finished = true;
this.Current = (this.dataLength + this.currentByteOffsetDelta, true);
this.Finished = true;
return true;
}

Expand Down
4 changes: 4 additions & 0 deletions Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ public record struct SeStringDrawParams
/// </remarks>
public ImDrawListPtr? TargetDrawList { get; set; }

/// <summary>Gets or sets the function to be called on every codepoint and payload for the purpose of offering
/// chances to draw something else instead of glyphs or SeString payload entities.</summary>
public SeStringReplacementEntity.GetEntityDelegate? GetEntity { get; set; }

/// <summary>Gets or sets the screen offset of the left top corner.</summary>
/// <value>Screen offset to draw at, or <c>null</c> to use <see cref="ImGui.GetCursorScreenPos"/>.</value>
public Vector2? ScreenOffset { get; set; }
Expand Down
10 changes: 7 additions & 3 deletions Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ internal SeStringDrawState(
this.splitter = splitter;
this.drawList = ssdp.TargetDrawList ?? ImGui.GetWindowDrawList();
this.Span = span;
this.GetEntity = ssdp.GetEntity;
this.ScreenOffset = ssdp.ScreenOffset ?? ImGui.GetCursorScreenPos();
this.Font = ssdp.EffectiveFont;
this.FontSize = ssdp.FontSize ?? ImGui.GetFontSize();
Expand Down Expand Up @@ -65,6 +66,9 @@ internal SeStringDrawState(
/// <summary>Gets the raw SeString byte span.</summary>
public ReadOnlySpan<byte> Span { get; }

/// <inheritdoc cref="SeStringDrawParams.GetEntity"/>
public SeStringReplacementEntity.GetEntityDelegate? GetEntity { get; }

/// <inheritdoc cref="SeStringDrawParams.ScreenOffset"/>
public Vector2 ScreenOffset { get; }

Expand Down Expand Up @@ -212,12 +216,12 @@ internal readonly void DrawGlyph(scoped in ImGuiHelpers.ImFontGlyphReal g, Vecto
var dyItalic = this.Italic
? (new Vector2(this.FontSize - g.Y0, this.FontSize - g.Y1) / 6)
: Vector2.Zero;

offset.Y += MathF.Round(((this.LineHeight - this.Font->FontSize) * this.FontSizeScale) / 2f);

var xy0 = g.XY0 * this.FontSizeScale;
var xy1 = g.XY1 * this.FontSizeScale;

if (this.ShouldDrawShadow)
{
this.SetCurrentChannel(SeStringDrawChannel.Shadow);
Expand All @@ -228,7 +232,7 @@ internal readonly void DrawGlyph(scoped in ImGuiHelpers.ImFontGlyphReal g, Vecto
if (this.ShouldDrawEdge)
{
this.SetCurrentChannel(SeStringDrawChannel.Edge);

// Top & Bottom
for (var i = -1; i <= dxBold; i++)
{
Expand Down
Loading

0 comments on commit c7dc76a

Please sign in to comment.