Skip to content

Commit

Permalink
feat(textbox): variable Tab widths i.e. Tab stops
Browse files Browse the repository at this point in the history
  • Loading branch information
ramezgerges committed May 29, 2024
1 parent 6d6b53d commit 8b7e727
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 18 deletions.
7 changes: 6 additions & 1 deletion src/Uno.UI/UI/Xaml/Documents/InlineCollection.skia.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,11 @@ internal Size Measure(Size availableSize, float defaultLineHeight)
goto MaxLinesHit;
}

if (segment.IsTab)
{
segment.AdjustTabWidth(x);
}

float remainingWidth = availableWidth - x;
(int segmentLengthWithoutTrailingSpaces, float widthWithoutTrailingSpaces) = GetSegmentRenderInfo(segment, start, characterSpacing);

Expand Down Expand Up @@ -1045,7 +1050,7 @@ internal Rect GetRectForIndex(int adjustedIndex)

/// <summary>
/// When we read the length of a span or segment from Skia/HarfBuzz, what we actually get is the
/// number of glyphs (i.e. Unicode runes), not the number of c# chars, so surrogate pairs will
/// number of glyphs, not the number of c# chars, so surrogate pairs will
/// only be counted as a single "unit". This method stretches the range to account for surrogate
/// pairs being 2 characters, not one.
/// </summary>
Expand Down
54 changes: 39 additions & 15 deletions src/Uno.UI/UI/Xaml/Documents/Run.skia.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using HarfBuzzSharp;
using SkiaSharp;
using Uno.Foundation.Logging;
Expand All @@ -14,8 +15,6 @@ namespace Microsoft.UI.Xaml.Documents
{
partial class Run
{
private const int SpacesPerTab = 12;

private List<Segment>? _segments;

internal IReadOnlyList<Segment> Segments => _segments ??= _segments = GetSegments();
Expand Down Expand Up @@ -71,7 +70,7 @@ private List<Segment> GetSegments()
else
{
// Count leading spaces
while (i < text.Length && char.IsWhiteSpace(text[i]) && !Unicode.IsLineBreak(text[i]))
while (i < text.Length && char.IsWhiteSpace(text[i]) && !Unicode.IsLineBreak(text[i]) && text[i] != '\t')
{
leadingSpaces++;
i++;
Expand All @@ -85,6 +84,26 @@ private List<Segment> GetSegments()
break;
}

// Since tabs require special handling, we put tabs in separate segments.
// Also, we don't consider tabs "spaces" since they don't get the general space treatment.
if (text[i] == '\t')
{
wordBreakAfter = true;
i++;
break;
}

if (i + 1 < text.Length && text[i + 1] == '\t')
{
if (char.IsWhiteSpace(text[i]))
{
trailingSpaces++;
}
wordBreakAfter = true;
i++;
break;
}

if (Unicode.HasWordBreakOpportunityAfter(text, i) || (i + 1 < text.Length && Unicode.HasWordBreakOpportunityBefore(text, i + 1)))
{
if (char.IsWhiteSpace(text[i]))
Expand Down Expand Up @@ -112,7 +131,7 @@ private List<Segment> GetSegments()
else
{
// Under some Linux systems, the symbol may not be found
// in the default font and
// in the default font and
// we have to skip the character and continue segments
// evaluation.

Expand All @@ -133,7 +152,7 @@ private List<Segment> GetSegments()
if (symbolTypeface is null)
{
// Under some Linux systems, the symbol may not be found
// in the default font and
// in the default font and
// we have to skip the character and continue segments
// evaluation.
if (this.Log().IsEnabled(LogLevel.Trace))
Expand All @@ -160,7 +179,7 @@ private List<Segment> GetSegments()
break;
}

if (char.IsWhiteSpace(text[i]))
if (char.IsWhiteSpace(text[i]) && text[i] != '\t')
{
trailingSpaces++;
i++;
Expand All @@ -176,7 +195,6 @@ private List<Segment> GetSegments()
int length = i - s;
if (length > 0)
{

if (lineBreakLength == 2)
{
buffer.AddUtf16(text.Slice(s, length - 1)); // Skip second line break char so that it is considered part of the same cluster as the first
Expand Down Expand Up @@ -211,7 +229,13 @@ private List<Segment> GetSegments()
buffer.ReverseClusters();
}

var glyphs = GetGlyphs(buffer, Text.AsSpan(s, length), fontInfo, s, textSizeX, textSizeY);
var glyphs = GetGlyphs(buffer, s, textSizeX, textSizeY);

Debug.Assert(!(Text.AsSpan(s, length).Contains('\t')) || length == 1);
if (length == 1 && text[s] == '\t')
{
glyphs[0] = glyphs[0] with { GlyphId = _getSpaceGlyph(fontInfo.Font) };
}

var segment = new Segment(this, direction, s, length, leadingSpaces, trailingSpaces, lineBreakLength, wordBreakAfter, glyphs, fallbackFont);

Expand Down Expand Up @@ -251,7 +275,7 @@ static bool ProcessLineBreak(ReadOnlySpan<char> text, ref int i, ref int lineBre
return false;
}

static List<GlyphInfo> GetGlyphs(Buffer buffer, ReadOnlySpan<char> textSpan, FontDetails fontInfo, int clusterStart, float textSizeX, float textSizeY)
static List<GlyphInfo> GetGlyphs(Buffer buffer, int clusterStart, float textSizeX, float textSizeY)
{
int length = buffer.Length;
var hbGlyphs = buffer.GetGlyphInfoSpan();
Expand All @@ -266,9 +290,9 @@ static List<GlyphInfo> GetGlyphs(Buffer buffer, ReadOnlySpan<char> textSpan, Fon

// We add special handling for tabs, which don't get rendered correctly, and treated as an unknown glyph
TextFormatting.GlyphInfo glyph = new(
textSpan[i] == '\t' ? _measureTab(fontInfo.Font).codepoint : (ushort)hbGlyph.Codepoint,
(ushort)hbGlyph.Codepoint,
clusterStart + (int)hbGlyph.Cluster,
(textSpan[i] == '\t' ? _measureTab(fontInfo.Font).width : hbPos.XAdvance) * textSizeX,
hbPos.XAdvance * textSizeX,
hbPos.XOffset * textSizeX,
hbPos.YOffset * textSizeY
);
Expand All @@ -282,14 +306,14 @@ static List<GlyphInfo> GetGlyphs(Buffer buffer, ReadOnlySpan<char> textSpan, Fon

partial void InvalidateSegmentsPartial() => _segments = null;

private static Func<Font, (ushort codepoint, float width)> _measureTab =
((Func<Font, (ushort codepoint, float width)>?)(font =>
private static readonly Func<Font, ushort> _getSpaceGlyph =
((Func<Font, ushort>?)(font =>
{
using var buffer = new Buffer();
using var buffer = new HarfBuzzSharp.Buffer();
buffer.AddUtf8(" ");
buffer.GuessSegmentProperties();
font.Shape(buffer);
return ((ushort)buffer.GlyphInfos[0].Codepoint, buffer.GlyphPositions[0].XAdvance * SpacesPerTab);
return (ushort)buffer.GlyphInfos[0].Codepoint;
}))
.AsMemoized();
}
Expand Down
19 changes: 17 additions & 2 deletions src/Uno.UI/UI/Xaml/Documents/TextFormatting/Segment.skia.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ namespace Microsoft.UI.Xaml.Documents.TextFormatting
[DebuggerDisplay("{DebugText}")]
internal sealed class Segment
{
private readonly IReadOnlyList<GlyphInfo>? _glyphs;
// Measured by hand from WinUI. Oddly enough, it doesn't depend on the font size.
private const float TabStopWidth = 50;

private readonly List<GlyphInfo>? _glyphs;
private readonly FontDetails? _fallbackFont;
// we cache the text as soon as we create the Segment in case a Run's text Updates
// before this (now outdated) Segment is discarded.
Expand Down Expand Up @@ -70,6 +73,8 @@ internal sealed class Segment
/// </summary>
public bool WordBreakAfter { get; }

public bool IsTab => Text.Length == 1 && Text[0] == '\t';

/// <summary>
/// Gets the section of text of the Run element this segment represents. Throws if this segment represents a LineBreak element.
/// </summary>
Expand All @@ -83,7 +88,7 @@ internal sealed class Segment

private string DebugText => Inline is Run ? Text.ToString() : "{LineBreak}";

public Segment(Run run, FlowDirection direction, int start, int length, int leadingSpaceCount, int trailingSpaceCount, int lineBreakLength, bool wordBreakAfter, IReadOnlyList<GlyphInfo> glyphs, FontDetails? fallbackFont)
public Segment(Run run, FlowDirection direction, int start, int length, int leadingSpaceCount, int trailingSpaceCount, int lineBreakLength, bool wordBreakAfter, List<GlyphInfo> glyphs, FontDetails? fallbackFont)
{
Inline = run;
Direction = direction;
Expand All @@ -107,5 +112,15 @@ public Segment(LineBreak lineBreak)
}

public FontDetails? FallbackFont => _fallbackFont;

/// <remarks>
/// This is the only form of impurity in this class. Unfortunately, we can't
/// calculate the width of the tab ahead of time.
/// </remarks>
public void AdjustTabWidth(float xOffset)
{
Debug.Assert(IsTab);
_glyphs![0] = _glyphs[0] with { AdvanceX = TabStopWidth - xOffset % TabStopWidth };
}
}
}

0 comments on commit 8b7e727

Please sign in to comment.