674 lines
34 KiB
C#
674 lines
34 KiB
C#
// Copyright (c) Six Labors.
|
|
// Licensed under the Six Labors Split License.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Numerics;
|
|
using SixLabors.Fonts.Unicode;
|
|
|
|
namespace SixLabors.Fonts {
|
|
/// <content>
|
|
/// Line break candidate collection and layout-level tailoring.
|
|
/// </content>
|
|
internal static partial class TextLayout
|
|
{
|
|
private const int SoftHyphen = 0x00AD;
|
|
private const int StandardHyphen = 0x2010;
|
|
private const int StandardEllipsis = 0x2026;
|
|
|
|
/// <summary>
|
|
/// Composes the logical <see cref="TextLine"/> from shaped glyph data before width-dependent line breaking.
|
|
/// </summary>
|
|
/// <param name="shapedText">The width-independent shaping state.</param>
|
|
/// <param name="text">The original source text.</param>
|
|
/// <param name="options">The text shaping and layout options.</param>
|
|
/// <returns>The logical text line and line break opportunities before line breaking.</returns>
|
|
public static LogicalTextLine ComposeLogicalLine(
|
|
in ShapedText shapedText,
|
|
ReadOnlySpan<char> text,
|
|
TextOptions options)
|
|
{
|
|
bool isHorizontalLayout = shapedText.LayoutMode.IsHorizontal();
|
|
bool isVerticalLayout = shapedText.LayoutMode.IsVertical();
|
|
bool isVerticalMixedLayout = shapedText.LayoutMode.IsVerticalMixed();
|
|
|
|
int graphemeIndex = 0;
|
|
int codePointIndex = 0;
|
|
int glyphSearchIndex = 0;
|
|
TextLine textLine = new();
|
|
int stringIndex = 0;
|
|
List<WordSegmentRun> wordSegments = [];
|
|
List<GlyphLayoutData> hyphenationMarkers = [];
|
|
CodePoint? hyphenationMarkerCodePoint = GetHyphenationMarkerCodePoint(options);
|
|
|
|
// No glyph should contain more than 64 metrics.
|
|
// We do a sanity check below just in case.
|
|
Span<float> decomposedAdvancesBuffer = stackalloc float[64];
|
|
|
|
// Word-boundary segments are prepared with the logical line, while grapheme
|
|
// and codepoint enumeration still own shaping data creation.
|
|
SpanWordEnumerator wordEnumerator = new(text);
|
|
while (wordEnumerator.MoveNext())
|
|
{
|
|
WordSegment wordSegment = wordEnumerator.Current;
|
|
int wordSegmentGraphemeStart = graphemeIndex;
|
|
|
|
SpanGraphemeEnumerator graphemeEnumerator = new(wordSegment.Span);
|
|
while (graphemeEnumerator.MoveNext())
|
|
{
|
|
// Now enumerate through each codepoint in the grapheme.
|
|
ReadOnlySpan<char> grapheme = graphemeEnumerator.Current.Span;
|
|
int graphemeCodePointIndex = 0;
|
|
SpanCodePointEnumerator codePointEnumerator = new(grapheme);
|
|
while (codePointEnumerator.MoveNext())
|
|
{
|
|
if (!shapedText.Positionings.TryGetGlyphMetricsAtOffset(
|
|
codePointIndex,
|
|
ref glyphSearchIndex,
|
|
out float pointSize,
|
|
out bool isSubstituted,
|
|
out bool isVerticalSubstitution,
|
|
out bool isDecomposed,
|
|
out IReadOnlyList<GlyphPositioningCollection.GlyphPositioningData>? glyphData))
|
|
{
|
|
// Codepoint was skipped during original enumeration.
|
|
codePointIndex++;
|
|
graphemeCodePointIndex++;
|
|
continue;
|
|
}
|
|
|
|
List<FontGlyphMetrics> metrics = [];
|
|
for (int i = 0; i < glyphData.Count; i++)
|
|
{
|
|
GlyphPositioningCollection.GlyphPositioningData data = glyphData[i];
|
|
if (data.Data.IsPlaceholder)
|
|
{
|
|
textLine.AddPlaceholder(
|
|
data,
|
|
graphemeIndex,
|
|
stringIndex,
|
|
isHorizontalLayout,
|
|
isVerticalMixedLayout,
|
|
options.LineSpacing);
|
|
|
|
continue;
|
|
}
|
|
|
|
metrics.Add(data.Metrics);
|
|
}
|
|
|
|
if (metrics.Count == 0)
|
|
{
|
|
// This source codepoint was skipped during shaping; any placeholder
|
|
// sharing the same source offset has already been added above.
|
|
codePointIndex++;
|
|
graphemeCodePointIndex++;
|
|
continue;
|
|
}
|
|
|
|
FontGlyphMetrics glyph = metrics[0];
|
|
|
|
// Retrieve the current codepoint from the enumerator.
|
|
// If the glyph represents a substituted codepoint and the substitution is a single codepoint substitution,
|
|
// or composite glyph, then the codepoint should be updated to the substitution value so we can read its properties.
|
|
// Substitutions that are decomposed glyphs will have multiple metrics and any layout should be based on the
|
|
// original codepoint.
|
|
//
|
|
// Note: Not all glyphs in a font will have a codepoint associated with them. e.g. most compositions, ligatures, etc.
|
|
CodePoint codePoint = codePointEnumerator.Current;
|
|
if (isSubstituted && metrics.Count == 1)
|
|
{
|
|
codePoint = glyph.CodePoint;
|
|
}
|
|
|
|
// Determine whether the glyph advance should be calculated using vertical or horizontal metrics
|
|
// For vertical mixed layout we will rotate glyphs with the vertical orientation type R or TR
|
|
// which do not already have a vertical substitution.
|
|
bool shouldRotate = isVerticalMixedLayout &&
|
|
!isVerticalSubstitution &&
|
|
CodePoint.GetVerticalOrientationType(codePoint) is
|
|
VerticalOrientationType.Rotate or
|
|
VerticalOrientationType.TransformRotate;
|
|
|
|
// Determine whether the glyph advance should be offset for vertical layout.
|
|
bool shouldOffset = isVerticalLayout &&
|
|
!isVerticalSubstitution &&
|
|
CodePoint.GetVerticalOrientationType(codePoint) is
|
|
VerticalOrientationType.Rotate or
|
|
VerticalOrientationType.TransformRotate;
|
|
|
|
if (CodePoint.IsVariationSelector(codePoint))
|
|
{
|
|
codePointIndex++;
|
|
graphemeCodePointIndex++;
|
|
continue;
|
|
}
|
|
|
|
// Calculate the advance for the current codepoint.
|
|
|
|
// This should never happen, but we need to ensure that the buffer is large enough
|
|
// if, for some crazy reason, a glyph does contain more than 64 metrics.
|
|
Span<float> decomposedAdvances = metrics.Count > decomposedAdvancesBuffer.Length
|
|
? new float[metrics.Count]
|
|
: decomposedAdvancesBuffer[..(isDecomposed ? metrics.Count : 1)];
|
|
|
|
float glyphAdvance;
|
|
if (isHorizontalLayout || shouldRotate)
|
|
{
|
|
glyphAdvance = glyph.AdvanceWidth;
|
|
}
|
|
else
|
|
{
|
|
glyphAdvance = glyph.AdvanceHeight;
|
|
}
|
|
|
|
decomposedAdvances[0] = glyphAdvance;
|
|
|
|
bool isSoftHyphen = codePoint.Value == SoftHyphen;
|
|
if (isSoftHyphen)
|
|
{
|
|
glyphAdvance = 0;
|
|
decomposedAdvances[0] = 0;
|
|
}
|
|
else if (CodePoint.IsTabulation(codePoint))
|
|
{
|
|
if (options.TabWidth > -1F)
|
|
{
|
|
// Do not use the default font tab width. Instead find the advance for the space glyph
|
|
// and multiply that by the options value.
|
|
CodePoint space = new(0x0020);
|
|
if (glyph.FontMetrics.TryGetGlyphId(space, out ushort spaceGlyphId))
|
|
{
|
|
FontGlyphMetrics spaceMetrics = glyph.FontMetrics.GetGlyphMetrics(
|
|
space,
|
|
spaceGlyphId,
|
|
glyph.TextAttributes,
|
|
glyph.TextDecorations,
|
|
shapedText.LayoutMode,
|
|
options.ColorFontSupport);
|
|
|
|
if (isHorizontalLayout || shouldRotate)
|
|
{
|
|
glyphAdvance = spaceMetrics.AdvanceWidth * options.TabWidth;
|
|
glyph.SetAdvanceWidth((ushort)glyphAdvance);
|
|
}
|
|
else
|
|
{
|
|
glyphAdvance = spaceMetrics.AdvanceHeight * options.TabWidth;
|
|
glyph.SetAdvanceHeight((ushort)glyphAdvance);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (metrics.Count == 1 && (CodePoint.IsZeroWidthJoiner(codePoint) || CodePoint.IsZeroWidthNonJoiner(codePoint)))
|
|
{
|
|
// The zero-width joiner characters should be ignored when determining word or
|
|
// line break boundaries so are safe to skip here. Any existing instances are the result of font error
|
|
// unless multiple metrics are associated with code point. In this case they are most likely the result
|
|
// of a substitution and shouldn't be ignored.
|
|
glyphAdvance = 0;
|
|
decomposedAdvances[0] = 0;
|
|
}
|
|
else if (!CodePoint.IsNewLine(codePoint))
|
|
{
|
|
// Standard text.
|
|
// If decomposed we need to add the advance; otherwise, use the largest advance for the metrics.
|
|
if (isHorizontalLayout || shouldRotate)
|
|
{
|
|
for (int i = 1; i < metrics.Count; i++)
|
|
{
|
|
float a = metrics[i].AdvanceWidth;
|
|
if (isDecomposed)
|
|
{
|
|
glyphAdvance += a;
|
|
decomposedAdvances[i] = a;
|
|
}
|
|
else if (a > glyphAdvance)
|
|
{
|
|
glyphAdvance = a;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (int i = 1; i < metrics.Count; i++)
|
|
{
|
|
float a = metrics[i].AdvanceHeight;
|
|
if (isDecomposed)
|
|
{
|
|
glyphAdvance += a;
|
|
decomposedAdvances[i] = a;
|
|
}
|
|
else if (a > glyphAdvance)
|
|
{
|
|
glyphAdvance = a;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Now scale the advance. We use inches for comparison.
|
|
if (isHorizontalLayout || shouldRotate)
|
|
{
|
|
float scaleAX = pointSize / glyph.ScaleFactor.X;
|
|
glyphAdvance *= scaleAX;
|
|
for (int i = 0; i < decomposedAdvances.Length; i++)
|
|
{
|
|
decomposedAdvances[i] *= scaleAX;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
float scaleAY = pointSize / glyph.ScaleFactor.Y;
|
|
glyphAdvance *= scaleAY;
|
|
for (int i = 0; i < decomposedAdvances.Length; i++)
|
|
{
|
|
decomposedAdvances[i] *= scaleAY;
|
|
}
|
|
}
|
|
|
|
int graphemeCodePointMax = CodePoint.GetCodePointCount(grapheme) - 1;
|
|
|
|
// For non-decomposed glyphs the length is always 1.
|
|
int glyphDataIndex = 0;
|
|
|
|
for (int i = 0; i < decomposedAdvances.Length; i++)
|
|
{
|
|
// Determine if this is the last codepoint in the grapheme.
|
|
bool isLastInGrapheme = graphemeCodePointIndex == graphemeCodePointMax && i == decomposedAdvances.Length - 1;
|
|
|
|
float decomposedAdvance = decomposedAdvances[i];
|
|
|
|
// Work out the scaled metrics for the glyph.
|
|
while (glyphData[glyphDataIndex].Data.IsPlaceholder)
|
|
{
|
|
glyphDataIndex++;
|
|
}
|
|
|
|
GlyphPositioningCollection.GlyphPositioningData positionedGlyph = glyphData[glyphDataIndex];
|
|
FontGlyphMetrics metric = positionedGlyph.Metrics;
|
|
|
|
// Adjust the advance for the last decomposed glyph to add tracking if applicable.
|
|
// Tracking should only be added once per grapheme, so only on the last codepoint of the grapheme.
|
|
if (isLastInGrapheme && options.Tracking != 0 && i == decomposedAdvances.Length - 1)
|
|
{
|
|
// Tracking should not be applied to tab characters or non-rendered codepoints.
|
|
if (!CodePoint.IsTabulation(codePoint) && !UnicodeUtility.ShouldNotBeRendered(codePoint))
|
|
{
|
|
if (isHorizontalLayout || shouldRotate)
|
|
{
|
|
float scaleAX = pointSize / glyph.ScaleFactor.X;
|
|
decomposedAdvance += options.Tracking * metric.FontMetrics.UnitsPerEm * scaleAX;
|
|
}
|
|
else
|
|
{
|
|
float scaleAY = pointSize / glyph.ScaleFactor.Y;
|
|
decomposedAdvance += options.Tracking * metric.FontMetrics.UnitsPerEm * scaleAY;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Convert design-space units to pixels based on the target point size.
|
|
// ScaleFactor.Y represents the vertical UPEM scaling factor for this glyph.
|
|
float scaleY = pointSize / metric.ScaleFactor.Y;
|
|
|
|
// Choose which metrics table to use based on layout orientation.
|
|
// Horizontal is the default; vertical fonts use VMTX if available.
|
|
IMetricsHeader metricsHeader = isHorizontalLayout || shouldRotate
|
|
? metric.FontMetrics.HorizontalMetrics
|
|
: metric.FontMetrics.VerticalMetrics;
|
|
|
|
// Ascender and descender are stored in font design units, so scale them to pixels.
|
|
float ascender = metricsHeader.Ascender * scaleY;
|
|
|
|
// Match browser line-height calculation logic.
|
|
// Reference: https://www.w3.org/TR/CSS2/visudet.html#propdef-line-height
|
|
// The line height in CSS is based on a multiple of the font-size (pointSize),
|
|
// but fonts may define a custom LineHeight in their metrics that differs from UPEM.
|
|
float descender = Math.Abs(metricsHeader.Descender * scaleY);
|
|
float lineHeight = metric.UnitsPerEm * scaleY;
|
|
|
|
// The delta centers the font's line box within the CSS line box when
|
|
// LineHeight differs from the nominal font size.
|
|
float delta = ((metricsHeader.LineHeight * scaleY) - lineHeight) * 0.5F;
|
|
|
|
// Adjust ascender and descender symmetrically by delta to preserve visual balance.
|
|
ascender -= delta;
|
|
descender -= delta;
|
|
|
|
GlyphLayoutMode mode = GlyphLayoutMode.Horizontal;
|
|
if (isVerticalLayout)
|
|
{
|
|
mode = GlyphLayoutMode.Vertical;
|
|
}
|
|
else if (isVerticalMixedLayout)
|
|
{
|
|
mode = shouldRotate ? GlyphLayoutMode.VerticalRotated : GlyphLayoutMode.Vertical;
|
|
}
|
|
|
|
int hyphenationMarkerIndex = -1;
|
|
if (isSoftHyphen && hyphenationMarkerCodePoint.HasValue)
|
|
{
|
|
// U+00AD is shaped as an invisible source entry, but if this exact
|
|
// discretionary break is later selected we need a visible marker with
|
|
// the same run, font attributes, bidi mapping, and source mapping. Build
|
|
// that marker here while those values are already in hand; BreakLines can
|
|
// then account for its advance without rescanning or reshaping the line.
|
|
hyphenationMarkerIndex = hyphenationMarkers.Count;
|
|
hyphenationMarkers.Add(CreateGeneratedMarker(
|
|
glyph,
|
|
pointSize,
|
|
shapedText.BidiRuns[shapedText.BidiMap[codePointIndex]],
|
|
graphemeIndex,
|
|
isLastInGrapheme,
|
|
codePointIndex,
|
|
graphemeCodePointIndex,
|
|
stringIndex,
|
|
hyphenationMarkerCodePoint.Value,
|
|
shapedText.LayoutMode,
|
|
positionedGlyph.Font,
|
|
options));
|
|
}
|
|
|
|
// Add our metrics to the line.
|
|
textLine.Add(
|
|
isDecomposed ? new FontGlyphMetrics[] { metric } : metrics,
|
|
positionedGlyph.Font,
|
|
pointSize,
|
|
decomposedAdvance,
|
|
lineHeight,
|
|
ascender,
|
|
descender,
|
|
delta,
|
|
shapedText.BidiRuns[shapedText.BidiMap[codePointIndex]],
|
|
graphemeIndex,
|
|
isLastInGrapheme,
|
|
codePointIndex,
|
|
graphemeCodePointIndex,
|
|
shouldRotate || shouldOffset,
|
|
isDecomposed,
|
|
stringIndex,
|
|
mode,
|
|
options.LineSpacing,
|
|
hyphenationMarkerIndex);
|
|
|
|
glyphDataIndex++;
|
|
}
|
|
|
|
codePointIndex++;
|
|
graphemeCodePointIndex++;
|
|
}
|
|
|
|
stringIndex += grapheme.Length;
|
|
graphemeIndex++;
|
|
}
|
|
|
|
wordSegments.Add(new WordSegmentRun(
|
|
wordSegmentGraphemeStart,
|
|
graphemeIndex,
|
|
wordSegment.Utf16Offset,
|
|
wordSegment.Utf16Offset + wordSegment.Utf16Length));
|
|
}
|
|
|
|
// Placeholders do not consume source text. A placeholder inserted at
|
|
// the final source position has no following codepoint to visit in
|
|
// the main loop, so we add those trailing placeholder entries here.
|
|
if (shapedText.Positionings.TryGetGlyphMetricsAtOffset(
|
|
codePointIndex,
|
|
ref glyphSearchIndex,
|
|
out _,
|
|
out _,
|
|
out _,
|
|
out _,
|
|
out IReadOnlyList<GlyphPositioningCollection.GlyphPositioningData>? endGlyphData))
|
|
{
|
|
for (int i = 0; i < endGlyphData.Count; i++)
|
|
{
|
|
GlyphPositioningCollection.GlyphPositioningData data = endGlyphData[i];
|
|
if (data.Data.IsPlaceholder)
|
|
{
|
|
textLine.AddPlaceholder(
|
|
data,
|
|
graphemeIndex,
|
|
stringIndex,
|
|
isHorizontalLayout,
|
|
isVerticalMixedLayout,
|
|
options.LineSpacing);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Line break candidates are width-independent and belong with the composed logical line.
|
|
List<LineBreak> lineBreaks = CollectLineBreaks(text, hyphenationMarkerCodePoint.HasValue);
|
|
|
|
return new LogicalTextLine(textLine, lineBreaks, wordSegments, hyphenationMarkers);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies line-break opportunities to a shaped <see cref="TextLine"/> using the configured
|
|
/// <see cref="TextOptions.WordBreaking"/> behavior and supplied wrapping length.
|
|
/// Finalizes each line (trimming trailing whitespace and applying bidi reordering) and applies
|
|
/// justification where requested.
|
|
/// </summary>
|
|
/// <param name="logicalLine">The logical text line and line break opportunities to break.</param>
|
|
/// <param name="options">The text shaping and layout options.</param>
|
|
/// <param name="wrappingLength">The wrapping length in pixels.</param>
|
|
/// <returns>The shaped, line-broken, finalized text box ready for glyph placement.</returns>
|
|
public static TextBox BreakLines(
|
|
in LogicalTextLine logicalLine,
|
|
TextOptions options,
|
|
float wrappingLength)
|
|
{
|
|
int maxLines = options.MaxLines;
|
|
|
|
if (maxLines == 0)
|
|
{
|
|
TextDirection emptyTextDirection = options.TextDirection == TextDirection.RightToLeft
|
|
? TextDirection.RightToLeft
|
|
: TextDirection.LeftToRight;
|
|
|
|
return new TextBox([], emptyTextDirection);
|
|
}
|
|
|
|
TextDirection textDirection = GetTextDirection(logicalLine, options);
|
|
|
|
List<TextLine> textLines = [];
|
|
TextLineBreakEnumerator lineEnumerator = new(logicalLine, options);
|
|
|
|
while (lineEnumerator.MoveNext(wrappingLength))
|
|
{
|
|
textLines.Add(lineEnumerator.Current);
|
|
}
|
|
|
|
return new TextBox(textLines, textDirection);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the block-level text direction for a prepared logical line.
|
|
/// </summary>
|
|
/// <param name="logicalLine">The prepared logical line.</param>
|
|
/// <param name="options">The text options used for layout.</param>
|
|
/// <returns>The block-level text direction.</returns>
|
|
public static TextDirection GetTextDirection(in LogicalTextLine logicalLine, TextOptions options)
|
|
=> options.TextDirection == TextDirection.Auto && logicalLine.TextLine.Count > 0
|
|
? logicalLine.TextLine[0].TextDirection
|
|
: options.TextDirection;
|
|
|
|
/// <summary>
|
|
/// Collects the line break opportunities used by the wrapping loop.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <see cref="LineBreakEnumerator"/> is the Unicode-conforming default line breaker. Its default
|
|
/// constructor remains independent from layout policy so the Unicode conformance tests continue to
|
|
/// describe only the default UAX #14 behavior. This method is the boundary where layout-specific
|
|
/// tailoring is requested.
|
|
/// </para>
|
|
/// <para>
|
|
/// The line breaker itself is streaming and does not allocate. Layout materializes the resulting
|
|
/// break opportunities because the line fitting loop scans the same candidates repeatedly while it
|
|
/// removes finalized lines from the front of the shaped text line.
|
|
/// </para>
|
|
/// <para>
|
|
/// Solidus handling is intentionally conservative. UAX #14 classifies U+002F SOLIDUS as SY, which
|
|
/// gives ordinary text a break opportunity after a slash. That is valid for the default algorithm,
|
|
/// but it produced undesirable layout in issue 448 for ordinary slash-separated text. At the same
|
|
/// time, UAX #14 section 8 explicitly calls out URL tailoring that can allow breaks after slash
|
|
/// separated URL segments even when the next segment starts with a digit. The result here is:
|
|
/// keep default slash behavior for standard enumeration, suppress ordinary slash breaks for layout,
|
|
/// and reintroduce the narrow URL numeric-segment break only for URL-like runs.
|
|
/// </para>
|
|
/// </remarks>
|
|
/// <param name="text">The original source text being laid out.</param>
|
|
/// <param name="includeHyphenationBreaks">Whether soft-hyphen break opportunities should be included.</param>
|
|
/// <returns>The ordered line break opportunities after layout-level tailoring.</returns>
|
|
private static List<LineBreak> CollectLineBreaks(ReadOnlySpan<char> text, bool includeHyphenationBreaks)
|
|
{
|
|
LineBreakEnumerator lineBreakEnumerator = new(text, tailorUrls: true);
|
|
List<LineBreak> lineBreaks = [];
|
|
while (lineBreakEnumerator.MoveNext())
|
|
{
|
|
LineBreak lineBreak = lineBreakEnumerator.Current;
|
|
if (lineBreak.IsHyphenationBreak && !includeHyphenationBreaks)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
lineBreaks.Add(lineBreak);
|
|
}
|
|
|
|
return lineBreaks;
|
|
}
|
|
|
|
private static CodePoint? GetHyphenationMarkerCodePoint(TextOptions options)
|
|
=> options.TextHyphenation switch
|
|
{
|
|
TextHyphenation.Standard => new CodePoint(StandardHyphen),
|
|
TextHyphenation.Custom => options.CustomHyphen,
|
|
_ => null
|
|
};
|
|
|
|
/// <summary>
|
|
/// Creates a visible generated marker that matches the layout style of the anchor entry.
|
|
/// </summary>
|
|
/// <param name="anchorMetric">The glyph metric that supplies font, run, attributes, and decorations.</param>
|
|
/// <param name="pointSize">The point size at which the marker is rendered.</param>
|
|
/// <param name="bidiRun">The bidi run that the marker belongs to.</param>
|
|
/// <param name="graphemeIndex">The source grapheme index to map the marker to.</param>
|
|
/// <param name="isLastInGrapheme">Whether the marker maps to the last entry in its grapheme.</param>
|
|
/// <param name="codePointIndex">The source codepoint index to map the marker to.</param>
|
|
/// <param name="graphemeCodePointIndex">The source codepoint-in-grapheme index to map the marker to.</param>
|
|
/// <param name="stringIndex">The UTF-16 source index to map the marker to.</param>
|
|
/// <param name="markerCodePoint">The marker codepoint to create.</param>
|
|
/// <param name="layoutMode">The layout mode used to calculate marker orientation.</param>
|
|
/// <param name="font">The font used to shape and render the marker.</param>
|
|
/// <param name="options">The text options used for layout.</param>
|
|
/// <returns>The generated marker entry.</returns>
|
|
internal static GlyphLayoutData CreateGeneratedMarker(
|
|
FontGlyphMetrics anchorMetric,
|
|
float pointSize,
|
|
BidiRun bidiRun,
|
|
int graphemeIndex,
|
|
bool isLastInGrapheme,
|
|
int codePointIndex,
|
|
int graphemeCodePointIndex,
|
|
int stringIndex,
|
|
CodePoint markerCodePoint,
|
|
LayoutMode layoutMode,
|
|
Font font,
|
|
TextOptions options)
|
|
{
|
|
anchorMetric.FontMetrics.TryGetGlyphId(markerCodePoint, out ushort markerGlyphId);
|
|
|
|
FontGlyphMetrics markerMetric = anchorMetric.FontMetrics.GetGlyphMetrics(
|
|
markerCodePoint,
|
|
markerGlyphId,
|
|
anchorMetric.TextAttributes,
|
|
anchorMetric.TextDecorations,
|
|
layoutMode,
|
|
options.ColorFontSupport);
|
|
|
|
markerMetric = markerMetric.CloneForRendering(anchorMetric.TextRun);
|
|
|
|
bool isHorizontalLayout = layoutMode.IsHorizontal();
|
|
bool isVerticalLayout = layoutMode.IsVertical();
|
|
bool isVerticalMixedLayout = layoutMode.IsVerticalMixed();
|
|
bool shouldRotate = isVerticalMixedLayout &&
|
|
CodePoint.GetVerticalOrientationType(markerCodePoint) is
|
|
VerticalOrientationType.Rotate or
|
|
VerticalOrientationType.TransformRotate;
|
|
|
|
bool shouldOffset = isVerticalLayout &&
|
|
CodePoint.GetVerticalOrientationType(markerCodePoint) is
|
|
VerticalOrientationType.Rotate or
|
|
VerticalOrientationType.TransformRotate;
|
|
|
|
GlyphLayoutMode markerMode = GlyphLayoutMode.Horizontal;
|
|
if (isVerticalLayout)
|
|
{
|
|
markerMode = GlyphLayoutMode.Vertical;
|
|
}
|
|
else if (isVerticalMixedLayout)
|
|
{
|
|
markerMode = shouldRotate ? GlyphLayoutMode.VerticalRotated : GlyphLayoutMode.Vertical;
|
|
}
|
|
|
|
float markerAdvance = isHorizontalLayout || shouldRotate
|
|
? markerMetric.AdvanceWidth * (pointSize / markerMetric.ScaleFactor.X)
|
|
: markerMetric.AdvanceHeight * (pointSize / markerMetric.ScaleFactor.Y);
|
|
|
|
// Generated markers must reserve the same CSS line box as ordinary glyphs
|
|
// from the same run so truncation and discretionary hyphens do not collapse
|
|
// or expand line spacing.
|
|
float markerScaleY = pointSize / markerMetric.ScaleFactor.Y;
|
|
IMetricsHeader markerMetricsHeader = isHorizontalLayout || shouldRotate
|
|
? markerMetric.FontMetrics.HorizontalMetrics
|
|
: markerMetric.FontMetrics.VerticalMetrics;
|
|
|
|
float markerAscender = markerMetricsHeader.Ascender * markerScaleY;
|
|
float markerDescender = Math.Abs(markerMetricsHeader.Descender * markerScaleY);
|
|
float markerLineHeight = markerMetric.UnitsPerEm * markerScaleY;
|
|
float markerDelta = ((markerMetricsHeader.LineHeight * markerScaleY) - markerLineHeight) * 0.5F;
|
|
|
|
markerAscender -= markerDelta;
|
|
markerDescender -= markerDelta;
|
|
|
|
FontRectangle markerBox = FontGlyphMetrics.ShouldSkipGlyphRendering(markerMetric.CodePoint)
|
|
? FontRectangle.Empty
|
|
: markerMetric.GetBoundingBox(markerMode, Vector2.Zero, pointSize);
|
|
|
|
return new GlyphLayoutData(
|
|
new FontGlyphMetrics[] { markerMetric },
|
|
font,
|
|
pointSize,
|
|
markerAdvance,
|
|
markerLineHeight * options.LineSpacing,
|
|
markerAscender,
|
|
markerDescender,
|
|
markerDelta,
|
|
MathF.Min(0, markerBox.Y),
|
|
bidiRun,
|
|
graphemeIndex,
|
|
isLastInGrapheme,
|
|
codePointIndex,
|
|
graphemeCodePointIndex,
|
|
shouldRotate || shouldOffset,
|
|
false,
|
|
stringIndex);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the configured ellipsis marker codepoint.
|
|
/// </summary>
|
|
/// <param name="options">The text options used for layout.</param>
|
|
/// <returns>The configured ellipsis marker codepoint, or <see langword="null"/> when ellipsis is disabled.</returns>
|
|
public static CodePoint? GetEllipsisMarkerCodePoint(TextOptions options)
|
|
=> options.TextEllipsis switch
|
|
{
|
|
TextEllipsis.Standard => new CodePoint(StandardEllipsis),
|
|
TextEllipsis.Custom => options.CustomEllipsis,
|
|
_ => null
|
|
};
|
|
}
|
|
}
|