mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-08-01 16:28:36 -04:00
ADDED css min(), max() and clamp() for lengths css-wide keywords inherit, initial, unset, revert (incl. shorthands) added currentColor keyword to css colors can parse oklch(), lab() and hwb() added css text-align: justify css white-space: pre-wrap and break-spaces added css word-break: break-word parses css image-rendering: crisp-edges (point sampling) added css inset shorthand now parses 1–4 values in the css border-width shorthand added css transition-duration, -delay, -property and -timing-function longhands css :has() now supports descendant selectors added css opacity percentages added css overflow: auto (maps to scroll) added css gap: normal added css filter: none css transform: scale() now takes comma-separated args css animations now accept ms units added more css justify-content, align-* and text-align keywords (start, end, …) parses css dvh/svh/lvh/dvw viewport units (treated as vh/vw) parses css object-fit: scale-down (treated as contain) added css margin-block / margin-inline (+ the logical margin sides) added css padding-block / padding-inline (+ the logical padding sides) added css inset-block / inset-inline (+ the logical inset sides) added the css flex-flow shorthand added the css font shorthand added css font-size keywords (xx-small … xxx-large) css letter-spacing and word-spacing now accept normal added css font-smooth: none css aspect-ratio now accepts the auto form css font-family now maps generic families (serif, sans-serif, monospace) added css flex-flow added css font FIXED fixed css !important dropping the whole declaration css line-height without units is now a multiplier fixed css calc() division and full-expression parsing fixed the css flex shorthand for single-number and three-value forms fixed the css transition shorthand when the property is omitted css font-family now uses the first family in a comma stack colors now parse fully in the css background shorthand an unknown css @-rule no longer drops the whole stylesheet css variables no longer match on partial tokens injected css variables are kept across stylesheet hotloads fixed css selector specificity overflowing when comparing rules css background: none now resets the background (image and colour) css filter: none now overrides a filter set by a base class css transform: none now overrides a transform set by a base class IMPROVED css rule matching is ~3–5× faster on large stylesheets (rules indexed by class) ~2× fewer allocations in css transitions (snapshot styles once, not per property) finished css animations no longer re-layout every frame less GC in css sibling/child selector queries (no list copies or wrappers) less GC diffing active css rules (no LINQ or hashsets) faster, lower-GC Yoga layout wrapper css stylesheet hotload now watches newly imported files
2350 lines
62 KiB
C#
2350 lines
62 KiB
C#
|
|
// RichTextKit
|
|
// Copyright © 2019-2020 Topten Software. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
// not use this product except in compliance with the License. You may obtain
|
|
// a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
// License for the specific language governing permissions and limitations
|
|
// under the License.
|
|
|
|
using SkiaSharp;
|
|
using Topten.RichTextKit.Utils;
|
|
|
|
namespace Topten.RichTextKit
|
|
{
|
|
/// <summary>
|
|
/// Represents a block of formatted, laid out and measurable text
|
|
/// </summary>
|
|
public class TextBlock : StyledText
|
|
{
|
|
/// <summary>
|
|
/// Constructor
|
|
/// </summary>
|
|
public TextBlock()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// The max width property sets the maximum width of a line, after which
|
|
/// the line will be wrapped onto the next line.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This property can be set to null, in which case lines won't be wrapped.
|
|
/// </remarks>
|
|
public float? MaxWidth
|
|
{
|
|
get => _maxWidth;
|
|
set
|
|
{
|
|
if ( value.HasValue && value.Value < 0 )
|
|
value = 0;
|
|
if ( _maxWidth != value )
|
|
{
|
|
_maxWidth = value;
|
|
InvalidateLayout();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The maximum height of the TextBlock after which lines will be
|
|
/// truncated and the final line will be appended with an
|
|
/// ellipsis (`...`) character.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This property can be set to null, in which case the vertical height of the text block
|
|
/// won't be capped.
|
|
/// </remarks>
|
|
public float? MaxHeight
|
|
{
|
|
get => _maxHeight;
|
|
set
|
|
{
|
|
if ( value.HasValue && value.Value < 0 )
|
|
value = 0;
|
|
|
|
if ( value != _maxHeight )
|
|
{
|
|
_maxHeight = value;
|
|
InvalidateLayout();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The maximum number of lines after which lines will be
|
|
/// truncated and the final line will be appended with an
|
|
/// ellipsis (`...`) character.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This property can be set to null, in which case the vertical height of
|
|
/// the text block won't be capped.
|
|
/// </remarks>
|
|
public int? MaxLines
|
|
{
|
|
get => _maxLines;
|
|
set
|
|
{
|
|
if ( value.HasValue && value.Value < 0 )
|
|
value = 0;
|
|
|
|
if ( value != _maxLines )
|
|
{
|
|
_maxLines = value;
|
|
InvalidateLayout();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Prevents wrapping to a new line when reaching max width
|
|
/// </summary>
|
|
public bool NoWrap
|
|
{
|
|
get => _noWrap;
|
|
set
|
|
{
|
|
if ( value == _noWrap )
|
|
return;
|
|
|
|
_noWrap = value;
|
|
InvalidateLayout();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the left, right or center alignment of the text block.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Set this property to <see cref="TextAlignment.Auto"/> to align
|
|
/// the paragraph according to the <see cref="BaseDirection"/>.
|
|
///
|
|
/// * If the <see cref="MaxWidth"/> property has been set this will
|
|
/// be used for alignment calculations.
|
|
/// * If the <see cref="MaxWidth"/> property has not been set, the
|
|
/// width of the longest line will be used.
|
|
/// </remarks>
|
|
public TextAlignment Alignment
|
|
{
|
|
get => _textAlignment;
|
|
set
|
|
{
|
|
if ( _textAlignment != value )
|
|
{
|
|
_textAlignment = value;
|
|
InvalidateLayout();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets how a text block will overflow.
|
|
/// <remarks>
|
|
/// <see cref="TextOverflow.Ellipsis"/> will append an ellipsis.
|
|
/// <see cref="TextOverflow.Clip"/> will clip without appending anything.
|
|
/// </remarks>
|
|
/// </summary>
|
|
public TextOverflow Overflow
|
|
{
|
|
get => _overflow;
|
|
set
|
|
{
|
|
if ( _overflow != value )
|
|
{
|
|
_overflow = value;
|
|
InvalidateLayout();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets how a text block will break on overflow.
|
|
/// </summary>
|
|
public WordBreakMode WordBreak
|
|
{
|
|
get => _breakMode;
|
|
set
|
|
{
|
|
if ( _breakMode != value )
|
|
{
|
|
_breakMode = value;
|
|
InvalidateLayout();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The base directionality of this text block (whether text is laid out
|
|
/// left to right, or right to left)
|
|
/// </summary>
|
|
public TextDirection BaseDirection
|
|
{
|
|
get => _baseDirection;
|
|
set
|
|
{
|
|
if ( _baseDirection != value )
|
|
{
|
|
_baseDirection = value;
|
|
InvalidateLayout();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clear the content of this text block
|
|
/// </summary>
|
|
public override void Clear()
|
|
{
|
|
// Reset everything
|
|
FontRun.Pool.Value.ReturnAndClear( _fontRuns );
|
|
TextLine.Pool.Value.ReturnAndClear( _lines );
|
|
_textShapingBuffers.Clear();
|
|
base.Clear();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Split this text block at the specified code point index
|
|
/// </summary>
|
|
/// <param name="from">The code point index to copy from</param>
|
|
/// <param name="length">The number of code points to copy</param>
|
|
/// <returns>A new text block with the RHS split part of the text</returns>
|
|
public TextBlock Copy( int from, int length )
|
|
{
|
|
// Create a new text block with the same attributes as this one
|
|
var other = new TextBlock();
|
|
other.Alignment = this.Alignment;
|
|
other.BaseDirection = this.BaseDirection;
|
|
other.MaxWidth = this.MaxWidth;
|
|
other.MaxHeight = this.MaxHeight;
|
|
other.MaxLines = this.MaxLines;
|
|
|
|
// Copy text to the new paragraph
|
|
foreach ( var subRun in _styleRuns.GetInterectingRuns( from, length ) )
|
|
{
|
|
var sr = _styleRuns[subRun.Index];
|
|
other.AddText( sr.CodePoints.SubSlice( subRun.Offset, subRun.Length ), sr.Style );
|
|
}
|
|
|
|
return other;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override void OnChanged()
|
|
{
|
|
InvalidateLayout();
|
|
base.OnChanged();
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Appends an ellipsis to this text block
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This method checks if the text block has already been truncated and if
|
|
/// not appends an ellipsis without changing the measured vertical layout of the
|
|
/// text block. The ellipsis only remains in effect until the block's layout
|
|
/// is recalculated.
|
|
///
|
|
/// The text block must have at least one line. If the block contains no text,
|
|
/// then use AddText("\n", style) to create a single line with an attached style
|
|
/// but no text.
|
|
///
|
|
/// The intended purpose of this is to included an ellipsis on this text block
|
|
/// when a following text block doesn't fit.
|
|
/// </remarks>
|
|
public void AddEllipsis()
|
|
{
|
|
// Make sure laid out
|
|
Layout();
|
|
|
|
// Already truncated?
|
|
if ( _truncated )
|
|
return;
|
|
|
|
if ( _lines.Count == 0 )
|
|
throw new InvalidOperationException( "Ellipsis can't be appended to a text block with no lines" );
|
|
|
|
// Append the ellipsis
|
|
var line = _lines[_lines.Count - 1];
|
|
|
|
// Because adorning the line with ellipsis resets the XCoord of each font run
|
|
// to be left aligned, we need to move the glyphs to be left aligned too
|
|
for ( int frIndex = 0; frIndex < line.Runs.Count; frIndex++ )
|
|
{
|
|
var fr = line.Runs[frIndex];
|
|
fr.MoveGlyphs( -fr.XCoord, 0 );
|
|
}
|
|
|
|
// Append the ellipsis to the line and relayout theline
|
|
AdornLineWithOverflowContent( line, true );
|
|
|
|
// Work out the new x-alignment
|
|
var ta = ResolveTextAlignment();
|
|
float xAdjust = 0;
|
|
switch ( ta )
|
|
{
|
|
case TextAlignment.Right:
|
|
xAdjust = (_maxWidth ?? _measuredWidth) - line.Width;
|
|
break;
|
|
|
|
case TextAlignment.Center:
|
|
xAdjust = ((_maxWidth ?? _measuredWidth) - line.Width) / 2;
|
|
break;
|
|
}
|
|
|
|
// Adjust the measured width if the adorned line is the widest line
|
|
if ( line.Width > _measuredWidth )
|
|
_measuredWidth = line.Width;
|
|
|
|
// Reposition each run to the correct location
|
|
for ( int frIndex = 0; frIndex < line.Runs.Count; frIndex++ )
|
|
{
|
|
var fr = line.Runs[frIndex];
|
|
fr.Line = line;
|
|
fr.XCoord += xAdjust;
|
|
if ( fr.RunKind == FontRunKind.Ellipsis )
|
|
{
|
|
// The ellipsis has it's xcoord setup, but it's glyphs have never
|
|
// been positioned so need to handle this a little differently
|
|
fr.MoveGlyphs( fr.XCoord, line.YCoord + line.BaseLine );
|
|
}
|
|
else
|
|
{
|
|
// Move the glyphs back to corectly aligned position
|
|
fr.MoveGlyphs( fr.XCoord, 0 );
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the internal layout of the text block
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Generally you don't need to call this method as the layout
|
|
/// will be automatically updated as needed.
|
|
/// </remarks>
|
|
public void Layout( bool force = false )
|
|
{
|
|
// Needed?
|
|
if ( !force && !_needsLayout )
|
|
return;
|
|
_needsLayout = false;
|
|
|
|
// Resolve max width/height
|
|
_maxWidthResolved = _maxWidth ?? float.MaxValue;
|
|
_maxHeightResolved = _maxHeight ?? float.MaxValue;
|
|
_maxLinesResolved = _maxLines ?? int.MaxValue;
|
|
|
|
if ( _noWrap && _overflow == TextOverflow.None )
|
|
{
|
|
_maxWidthResolved = float.MaxValue;
|
|
}
|
|
|
|
// Reset layout state
|
|
_textShapingBuffers.Clear();
|
|
_fontRuns.Clear();
|
|
_lines.Clear();
|
|
_caretIndicies.Clear();
|
|
_wordBoundaryIndicies.Clear();
|
|
_measuredHeight = 0;
|
|
_measuredWidth = 0;
|
|
_leftOverhang = null;
|
|
_rightOverhang = null;
|
|
_truncated = false;
|
|
|
|
// Only layout if actually have some text
|
|
if ( _codePoints.Length != 0 )
|
|
{
|
|
// Build font runs
|
|
BuildFontRuns();
|
|
|
|
// Break font runs into lines
|
|
BreakLines();
|
|
|
|
// Finalize lines
|
|
FinalizeLines();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get all font runs for this text block
|
|
/// </summary>
|
|
public IReadOnlyList<FontRun> FontRuns
|
|
{
|
|
get
|
|
{
|
|
Layout();
|
|
return _fontRuns;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get all the lines for this text block
|
|
/// </summary>
|
|
public IReadOnlyList<TextLine> Lines
|
|
{
|
|
get
|
|
{
|
|
Layout();
|
|
return _lines;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Paint this text block
|
|
/// </summary>
|
|
/// <param name="canvas">The Skia canvas to paint to</param>
|
|
/// <param name="options">Options controlling the paint operation</param>
|
|
public void Paint( SKCanvas canvas, TextPaintOptions options = null )
|
|
{
|
|
// Ensure have options
|
|
if ( options == null )
|
|
options = TextPaintOptions.Default;
|
|
|
|
// Ensure layout done
|
|
Layout();
|
|
|
|
// Create context
|
|
var ctx = new PaintTextContext()
|
|
{
|
|
Canvas = canvas,
|
|
Options = options,
|
|
};
|
|
|
|
ctx.Shader = options.TextGradient?.CreateShader( MeasuredWidth, MeasuredHeight, MeasuredPadding.Left );
|
|
|
|
// Prepare selection
|
|
if ( options.Selection.HasValue )
|
|
{
|
|
ctx.SelectionStart = options.Selection.Value.Minimum;
|
|
ctx.SelectionEnd = options.Selection.Value.Maximum;
|
|
ctx.PaintSelectionBackground = new SKPaint()
|
|
{
|
|
Color = options.SelectionColor,
|
|
IsStroke = false,
|
|
IsAntialias = false,
|
|
};
|
|
if ( options.SelectionHandleScale != 0 && options.SelectionHandleColor.Alpha > 0 )
|
|
{
|
|
ctx.SelectionHandleScale = options.SelectionHandleScale;
|
|
ctx.PaintSelectionHandle = new SKPaint()
|
|
{
|
|
Color = options.SelectionHandleColor,
|
|
IsStroke = false,
|
|
IsAntialias = true,
|
|
};
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ctx.SelectionStart = -1;
|
|
ctx.SelectionEnd = -1;
|
|
}
|
|
|
|
foreach ( var l in _lines )
|
|
{
|
|
l.PaintBackground( ctx );
|
|
}
|
|
|
|
// Paint each line
|
|
foreach ( var l in _lines )
|
|
{
|
|
l.Paint( ctx );
|
|
}
|
|
|
|
// Clean up
|
|
ctx.PaintSelectionBackground?.Dispose();
|
|
ctx.Shader?.Dispose();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Paint this text block
|
|
/// </summary>
|
|
/// <param name="canvas">The Skia canvas to paint to</param>
|
|
/// <param name="position">The top left position within the canvas to draw at</param>
|
|
/// <param name="options">Options controlling the paint operation</param>
|
|
public void Paint( SKCanvas canvas, SKPoint position, TextPaintOptions options = null )
|
|
{
|
|
// Translate
|
|
canvas.Save();
|
|
canvas.Translate( position.X, position.Y );
|
|
|
|
// Paint it
|
|
Paint( canvas, options );
|
|
|
|
// Restore and done!
|
|
canvas.Restore();
|
|
}
|
|
|
|
/// <summary>
|
|
/// The total height of all lines.
|
|
/// </summary>
|
|
public float MeasuredHeight
|
|
{
|
|
get
|
|
{
|
|
Layout();
|
|
return _measuredHeight;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The length of the displayed text (in code points)
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If the text is truncated, this is the index of the point
|
|
/// at which the ellipsis was inserted. If the text it not
|
|
/// truncated, is the length of all added text.
|
|
/// </remarks>
|
|
public int MeasuredLength
|
|
{
|
|
get
|
|
{
|
|
Layout();
|
|
if ( _lines.Count == 0 )
|
|
return 0;
|
|
return _lines[_lines.Count - 1].End;
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// The number of lines in the text
|
|
/// </summary>
|
|
public int LineCount
|
|
{
|
|
get
|
|
{
|
|
Layout();
|
|
return _lines.Count;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The width of the widest line of text.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The returned width does not include any overhang.
|
|
/// </remarks>
|
|
public float MeasuredWidth
|
|
{
|
|
get
|
|
{
|
|
Layout();
|
|
return _measuredWidth;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Indicates if the text was truncated due to max height or max lines
|
|
/// constraints
|
|
/// </summary>
|
|
public bool Truncated
|
|
{
|
|
get
|
|
{
|
|
Layout();
|
|
return _truncated;
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Gets the size of any unused space around the text.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If MaxWidth is not set, the left and right padding will always be zero.
|
|
///
|
|
/// This property also returns a bottom padding amount if MaxHeight is set.
|
|
///
|
|
/// The returned top padding is always zero.
|
|
///
|
|
/// The return rectangle describes padding amounts for each edge - not
|
|
/// rectangle co-ordinates.
|
|
/// </remarks>
|
|
public SKRect MeasuredPadding
|
|
{
|
|
get
|
|
{
|
|
var r = new SKRect();
|
|
|
|
// Bottom padding?
|
|
if ( _maxHeight.HasValue )
|
|
{
|
|
r.Bottom = _maxHeight.Value - _measuredHeight;
|
|
}
|
|
|
|
if ( !_maxWidth.HasValue )
|
|
return r;
|
|
|
|
Layout();
|
|
|
|
switch ( ResolveTextAlignment() )
|
|
{
|
|
case TextAlignment.Left:
|
|
case TextAlignment.Justify: // justified text is anchored left and fills the width
|
|
r.Left = 0;
|
|
r.Right = _maxWidthResolved - _measuredWidth;
|
|
return r;
|
|
|
|
case TextAlignment.Right:
|
|
r.Left = _maxWidthResolved - _measuredWidth;
|
|
r.Right = 0;
|
|
return r;
|
|
|
|
case TextAlignment.Center:
|
|
r.Left = (_maxWidthResolved - _measuredWidth) / 2;
|
|
r.Right = (_maxWidthResolved - _measuredWidth) / 2;
|
|
return r;
|
|
}
|
|
|
|
throw new InvalidOperationException();
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Gets the actual measured overhang in each direction based on the
|
|
/// fonts used, and the supplied text.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The return rectangle describes overhang amounts for each edge - not
|
|
/// rectangle co-ordinates.
|
|
/// </remarks>
|
|
public SKRect MeasuredOverhang
|
|
{
|
|
get
|
|
{
|
|
Layout();
|
|
if ( !_leftOverhang.HasValue )
|
|
{
|
|
var right = _maxWidth ?? MeasuredWidth;
|
|
float leftOverhang = 0;
|
|
float rightOverhang = 0;
|
|
foreach ( var l in _lines )
|
|
{
|
|
l.UpdateOverhang( right, ref leftOverhang, ref rightOverhang );
|
|
}
|
|
_leftOverhang = leftOverhang;
|
|
_rightOverhang = rightOverhang;
|
|
}
|
|
return new SKRect( _leftOverhang.Value, 0, _rightOverhang.Value, 0 );
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Hit test this block of text
|
|
/// </summary>
|
|
/// <param name="lineIndex">The line to be hit test</param>
|
|
/// <param name="x">The x-coordinate relative to top left of the block</param>
|
|
/// <returns>A HitTestResult</returns>
|
|
public HitTestResult HitTestLine( int lineIndex, float x )
|
|
{
|
|
return _lines[lineIndex].HitTest( x );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Hit test this block of text
|
|
/// </summary>
|
|
/// <param name="x">The x-coordinate relative to top left of the block</param>
|
|
/// <param name="y">The x-coordinate relative to top left of the block</param>
|
|
/// <returns>A HitTestResult</returns>
|
|
public HitTestResult HitTest( float x, float y )
|
|
{
|
|
Layout();
|
|
|
|
var htr = new HitTestResult();
|
|
|
|
// Work out which line number we're over
|
|
htr.OverLine = -1;
|
|
htr.OverCodePointIndex = -1;
|
|
for ( int i = 0; i < _lines.Count; i++ )
|
|
{
|
|
var l = _lines[i];
|
|
if ( y >= l.YCoord && y < l.YCoord + l.Height )
|
|
{
|
|
htr.OverLine = i;
|
|
}
|
|
}
|
|
|
|
// Work out the closest line
|
|
if ( htr.OverLine >= 0 )
|
|
{
|
|
htr.ClosestLine = htr.OverLine;
|
|
}
|
|
else if ( y < 0 )
|
|
{
|
|
htr.ClosestLine = 0;
|
|
}
|
|
else
|
|
{
|
|
htr.ClosestLine = _lines.Count - 1;
|
|
}
|
|
|
|
// Hit test each cluster
|
|
if ( htr.ClosestLine >= 0 && htr.ClosestLine < _lines.Count )
|
|
{
|
|
// Hit test the line
|
|
var l = _lines[htr.ClosestLine];
|
|
l.HitTest( x, ref htr );
|
|
}
|
|
|
|
// If we're not over the line, we're also not over the character
|
|
if ( htr.OverLine < 0 )
|
|
htr.OverCodePointIndex = -1;
|
|
|
|
if ( htr.ClosestCodePointIndex < 0 )
|
|
{
|
|
if ( htr.ClosestLine == _lines.Count - 1 && _lines.Count > 0 )
|
|
htr.ClosestCodePointIndex = _lines[_lines.Count - 1].End - 1;
|
|
else
|
|
htr.ClosestCodePointIndex = 0;
|
|
}
|
|
|
|
return htr;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Build map of all caret positions
|
|
/// </summary>
|
|
void BuildCaretIndicies()
|
|
{
|
|
Layout();
|
|
if ( _caretIndicies.Count == 0 )
|
|
{
|
|
foreach ( var r in _lines.SelectMany( x => x.Runs ) )
|
|
{
|
|
for ( int i = 0; i < r.Clusters.Length; i++ )
|
|
{
|
|
_caretIndicies.Add( r.Clusters[i] );
|
|
}
|
|
}
|
|
_caretIndicies.Add( MeasuredLength );
|
|
_caretIndicies = _caretIndicies.OrderBy( x => x ).Distinct().ToList();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves a list of all valid caret positions
|
|
/// </summary>
|
|
public IReadOnlyList<int> CaretIndicies
|
|
{
|
|
get
|
|
{
|
|
BuildCaretIndicies();
|
|
return _caretIndicies;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves a list of all valid caret positions
|
|
/// </summary>
|
|
public IReadOnlyList<int> WordBoundaryIndicies
|
|
{
|
|
get
|
|
{
|
|
// Find word boundaries (if not already done)
|
|
if ( _wordBoundaryIndicies.Count == 0 )
|
|
{
|
|
_wordBoundaryIndicies = WordBoundaryAlgorithm.FindWordBoundaries( _codePoints.AsSlice() ).ToList();
|
|
}
|
|
return _wordBoundaryIndicies;
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Retrieves a list of the indicies of the first code point in each line
|
|
/// </summary>
|
|
public IReadOnlyList<int> LineIndicies
|
|
{
|
|
get
|
|
{
|
|
return _lines.Select( x => x.Start ).ToList();
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Given a code point index, find the index in the CaretIndicies
|
|
/// </summary>
|
|
/// <param name="codePointIndex">The code point index to lookup</param>
|
|
/// <returns>The index in the code point idnex in the CaretIndicies array</returns>
|
|
public int LookupCaretIndex( int codePointIndex )
|
|
{
|
|
BuildCaretIndicies();
|
|
int index = _caretIndicies.BinarySearch( codePointIndex );
|
|
if ( index < 0 )
|
|
index = ~index;
|
|
return index;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates useful information for displaying a caret
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// When altPosition is true, if the code point index indicates the first
|
|
/// code point after a line break, the returned caret position will be the
|
|
/// end of the previous line (instead of the start of the next line)
|
|
/// </remarks>
|
|
/// <param name="position">The caret position</param>
|
|
/// <returns>A CaretInfo struct</returns>
|
|
public CaretInfo GetCaretInfo( CaretPosition position )
|
|
{
|
|
// Empty text block?
|
|
if ( _codePoints.Length == 0 || position.CodePointIndex < 0 || _lines.Count == 0 )
|
|
{
|
|
return CaretInfo.None;
|
|
}
|
|
|
|
// Past the measured length?
|
|
if ( position.CodePointIndex > MeasuredLength )
|
|
{
|
|
return CaretInfo.None;
|
|
}
|
|
|
|
// Look up the caret index
|
|
int cpii = LookupCaretIndex( position.CodePointIndex );
|
|
|
|
// Create caret info
|
|
var ci = new CaretInfo();
|
|
ci.CodePointIndex = _caretIndicies[cpii];
|
|
|
|
var frIndex = FindFontRunForCodePointIndex( position.CodePointIndex );
|
|
FontRun fr = null;
|
|
if ( frIndex >= 0 )
|
|
{
|
|
fr = _fontRuns[frIndex];
|
|
|
|
if ( fr.Start == position.CodePointIndex && frIndex > 0 )
|
|
{
|
|
var frPrior = _fontRuns[frIndex - 1];
|
|
if ( frPrior.End == position.CodePointIndex )
|
|
{
|
|
if ( position.AltPosition ||
|
|
(frPrior.Direction == TextDirection.RTL &&
|
|
frPrior.RunKind != FontRunKind.TrailingWhitespace) )
|
|
{
|
|
fr = frPrior;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var lastLine = _lines[_lines.Count - 1];
|
|
if ( lastLine.RunsInternal.Count > 0 )
|
|
fr = lastLine.RunsInternal[lastLine.RunsInternal.Count - 1];
|
|
}
|
|
|
|
if ( fr == null )
|
|
return CaretInfo.None;
|
|
|
|
// Setup caret coordinates
|
|
ci.CaretXCoord = ci.CodePointIndex < 0 ? 0 : fr.GetXCoordOfCodePointIndex( ci.CodePointIndex );
|
|
ci.CaretRectangle = CalculateCaretRectangle( ci, fr );
|
|
ci.LineIndex = _lines.IndexOf( fr.Line );
|
|
|
|
return ci;
|
|
}
|
|
|
|
SKRect CalculateCaretRectangle( CaretInfo ci, FontRun fr )
|
|
{
|
|
if ( ci.CodePointIndex < 0 )
|
|
return SKRect.Empty;
|
|
|
|
// Get the font run to be used for caret metrics
|
|
fr = GetFontRunForCaretMetrics( ci, fr );
|
|
|
|
// Setup the basic rectangle
|
|
var rect = new SKRect();
|
|
rect.Left = ci.CaretXCoord;
|
|
rect.Top = fr.Line.YCoord + fr.Line.BaseLine + fr.Ascent;
|
|
rect.Right = rect.Left;
|
|
rect.Bottom = fr.Line.YCoord + fr.Line.BaseLine + fr.Descent;
|
|
|
|
// Apply slant if italic
|
|
if ( fr.Style.FontItalic )
|
|
{
|
|
rect.Left -= rect.Height / 14;
|
|
rect.Right = rect.Left + rect.Height / 5;
|
|
}
|
|
|
|
return rect;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Internal helper to get the font run that should
|
|
/// be used for caret metrics.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The returned font run is the font run of the previous
|
|
/// character, or the same character if the first font run
|
|
/// on the line.
|
|
/// </remarks>
|
|
/// <returns>The determined font run</returns>
|
|
FontRun GetFontRunForCaretMetrics( CaretInfo ci, FontRun fr )
|
|
{
|
|
// Same font run?
|
|
if ( ci.CodePointIndex > fr.Start )
|
|
return fr;
|
|
|
|
// Try to get the previous font run in this line
|
|
var lineRuns = fr.Line.Runs as List<FontRun>;
|
|
int index = lineRuns.IndexOf( fr );
|
|
if ( index <= 0 )
|
|
return fr;
|
|
|
|
// Use the previous font run
|
|
return lineRuns[index - 1];
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Find the font run holding a code point index
|
|
/// </summary>
|
|
/// <param name="codePointIndex"></param>
|
|
/// <returns></returns>
|
|
public int FindFontRunForCodePointIndex( int codePointIndex )
|
|
{
|
|
// Past end of text?
|
|
if ( codePointIndex > MeasuredLength )
|
|
return -1;
|
|
|
|
// Look up font run
|
|
int frIndex = FontRuns.BinarySearch( codePointIndex, ( run, value ) =>
|
|
{
|
|
return (run.End - 1) - codePointIndex;
|
|
} );
|
|
if ( frIndex < 0 )
|
|
frIndex = ~frIndex;
|
|
|
|
if ( frIndex == _fontRuns.Count )
|
|
frIndex = _fontRuns.Count - 1;
|
|
|
|
if ( frIndex < 0 )
|
|
return -1;
|
|
|
|
// Return the font run
|
|
var fr = _fontRuns[frIndex];
|
|
System.Diagnostics.Debug.Assert( codePointIndex >= fr.Start );
|
|
System.Diagnostics.Debug.Assert( codePointIndex <= fr.End );
|
|
return frIndex;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invalidate the layout
|
|
/// </summary>
|
|
void InvalidateLayout()
|
|
{
|
|
// Make sure style runs are valid (debug only)
|
|
_styleRuns.CheckValid( _codePoints.Length );
|
|
|
|
// Set layout flag
|
|
_needsLayout = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set if the current layout is dirty
|
|
/// </summary>
|
|
bool _needsLayout = true;
|
|
|
|
/// <summary>
|
|
/// Maximum width (wrap point, or null for no wrapping)
|
|
/// </summary>
|
|
float? _maxWidth;
|
|
|
|
/// <summary>
|
|
/// Width at which to wrap content
|
|
/// </summary>
|
|
float _maxWidthResolved = float.MaxValue;
|
|
|
|
/// <summary>
|
|
/// Maximum height (crop lines after this)
|
|
/// </summary>
|
|
float? _maxHeight;
|
|
|
|
/// <summary>
|
|
/// Maximum layout height
|
|
/// </summary>
|
|
float _maxHeightResolved = float.MaxValue;
|
|
|
|
/// <summary>
|
|
/// Prevents wrapping to a new line when reaching max width
|
|
/// </summary>
|
|
bool _noWrap;
|
|
|
|
/// <summary>
|
|
/// Maximum number of lines
|
|
/// </summary>
|
|
int? _maxLines;
|
|
|
|
/// <summary>
|
|
/// Maximum number of lines
|
|
/// </summary>
|
|
int _maxLinesResolved = int.MaxValue;
|
|
|
|
/// <summary>
|
|
/// Text alignment
|
|
/// </summary>
|
|
TextAlignment _textAlignment = TextAlignment.Auto;
|
|
|
|
/// <summary>
|
|
/// Base direction as set by user
|
|
/// </summary>
|
|
TextDirection _baseDirection = TextDirection.Auto;
|
|
|
|
/// <summary>
|
|
/// Base direction as resolved if auto
|
|
/// </summary>
|
|
TextDirection _resolvedBaseDirection;
|
|
|
|
/// <summary>
|
|
/// How text blocks will overflow
|
|
/// </summary>
|
|
TextOverflow _overflow;
|
|
|
|
/// <summary>
|
|
/// How text blocks will overflow
|
|
/// </summary>
|
|
WordBreakMode _breakMode;
|
|
|
|
/// <summary>
|
|
/// Re-usable buffers for text shaping results
|
|
/// </summary>
|
|
TextShaper.ResultBufferSet _textShapingBuffers = new TextShaper.ResultBufferSet();
|
|
|
|
/// <summary>
|
|
/// Reusable buffer for bidi data
|
|
/// </summary>
|
|
BidiData _bidiData = new BidiData();
|
|
|
|
/// <summary>
|
|
/// A list of font runs, after splitting by directionality, user styles and font fallback
|
|
/// </summary>
|
|
List<FontRun> _fontRuns = new List<FontRun>();
|
|
|
|
/// <summary>
|
|
/// Helper for splitting code into linebreaks
|
|
/// </summary>
|
|
LineBreaker _lineBreaker = new LineBreaker();
|
|
|
|
/// <summary>
|
|
/// The measured height
|
|
/// </summary>
|
|
float _measuredHeight;
|
|
|
|
/// <summary>
|
|
/// The measured width
|
|
/// </summary>
|
|
float _measuredWidth;
|
|
|
|
/// <summary>
|
|
/// The required left overhang
|
|
/// </summary>
|
|
float? _leftOverhang = null;
|
|
|
|
/// <summary>
|
|
/// The required left overhang
|
|
/// </summary>
|
|
float? _rightOverhang = null;
|
|
|
|
/// <summary>
|
|
/// Indicates if the text was truncated by max height/max lines limitations
|
|
/// </summary>
|
|
bool _truncated;
|
|
|
|
/// <summary>
|
|
/// The final laid out set of lines
|
|
/// </summary>
|
|
List<TextLine> _lines = new List<TextLine>();
|
|
|
|
/// <summary>
|
|
/// Calculated valid caret indicies
|
|
/// </summary>
|
|
List<int> _caretIndicies = new List<int>();
|
|
|
|
/// <summary>
|
|
/// Calculated word boundary caret indicies
|
|
/// </summary>
|
|
List<int> _wordBoundaryIndicies = new List<int>();
|
|
|
|
/// <summary>
|
|
/// Resolve the text alignment when set to Auto
|
|
/// </summary>
|
|
/// <returns>Resolved text alignment (left, right or center)</returns>
|
|
internal TextAlignment ResolveTextAlignment()
|
|
{
|
|
if ( _textAlignment == TextAlignment.Auto )
|
|
return _resolvedBaseDirection == TextDirection.LTR ? TextAlignment.Left : TextAlignment.Right;
|
|
else
|
|
return _textAlignment;
|
|
}
|
|
|
|
// Use the shared Bidi algo instance
|
|
Bidi _bidi = Bidi.Instance.Value;
|
|
|
|
/// <summary>
|
|
/// Split into runs based on directionality and style switch points
|
|
/// </summary>
|
|
void BuildFontRuns()
|
|
{
|
|
var originalLength = _codePoints.Length;
|
|
try
|
|
{
|
|
// Clearn unshaped run buffer
|
|
_unshapedRuns.Clear();
|
|
|
|
// Break supplied text into directionality runs
|
|
_bidiData.Init( _codePoints.AsSlice(), (sbyte)_baseDirection );
|
|
|
|
// If we have embedded directional overrides then change those
|
|
// ranges to neutral
|
|
if ( _hasTextDirectionOverrides )
|
|
{
|
|
// Save types
|
|
_bidiData.SaveTypes();
|
|
|
|
for ( int i = 0; i < _styleRuns.Count; i++ )
|
|
{
|
|
// Get the run
|
|
var sr = _styleRuns[i];
|
|
|
|
// Does it have a direction override?
|
|
if ( sr.Style.TextDirection == TextDirection.Auto )
|
|
continue;
|
|
|
|
// Change the range to neutral with no brackets
|
|
_bidiData.Types.SubSlice( sr.Start, sr.Length ).Fill( Directionality.ON );
|
|
_bidiData.PairedBracketTypes.SubSlice( sr.Start, sr.Length ).Fill( PairedBracketType.n );
|
|
}
|
|
}
|
|
|
|
// Process bidi
|
|
_bidi.Process( _bidiData );
|
|
|
|
var resolvedLevels = _bidi.ResolvedLevels;
|
|
|
|
// Get resolved direction
|
|
_resolvedBaseDirection = (TextDirection)_bidi.ResolvedParagraphEmbeddingLevel;
|
|
|
|
// Now process the embedded runs
|
|
if ( _hasTextDirectionOverrides )
|
|
{
|
|
// Restore types
|
|
_bidiData.RestoreTypes();
|
|
|
|
// Process each run individually
|
|
for ( int i = 0; i < _styleRuns.Count; i++ )
|
|
{
|
|
// Get the run
|
|
var sr = _styleRuns[i];
|
|
|
|
// Does it have a direction override?
|
|
if ( sr.Style.TextDirection == TextDirection.Auto )
|
|
continue;
|
|
|
|
// Get the style run bidi data
|
|
var types = _bidiData.Types.SubSlice( sr.Start, sr.Length );
|
|
var pbts = _bidiData.PairedBracketTypes.SubSlice( sr.Start, sr.Length );
|
|
var pbvs = _bidiData.PairedBracketValues.SubSlice( sr.Start, sr.Length );
|
|
|
|
// Get a temp buffer to store the results
|
|
// (We can't use the Bidi's built in buffer because we're about to patch it)
|
|
var levels = _bidiData.GetTempLevelBuffer( sr.Length );
|
|
|
|
// Process this style run
|
|
_bidi.Process( types, pbts, pbvs, (sbyte)sr.Style.TextDirection, _bidiData.HasBrackets, _bidiData.HasEmbeddings, _bidiData.HasIsolates, levels );
|
|
|
|
// Copy result levels back to the full level set
|
|
resolvedLevels.SubSlice( sr.Start, sr.Length ).Set( levels );
|
|
}
|
|
}
|
|
|
|
// Get the list of directional runs
|
|
var bidiRuns = BidiRun.CoalescLevels( resolvedLevels ).ToList();
|
|
|
|
// Split...
|
|
var pos = 0;
|
|
int bidiRun = 0;
|
|
int styleRun = 0;
|
|
while ( pos < _codePoints.Length )
|
|
{
|
|
// Move to next bidi/style run
|
|
if ( pos == bidiRuns[bidiRun].End )
|
|
bidiRun++;
|
|
if ( pos == _styleRuns[styleRun].End )
|
|
styleRun++;
|
|
|
|
// Work out where this run ends
|
|
int nextPos = Math.Min( bidiRuns[bidiRun].End, _styleRuns[styleRun].End );
|
|
|
|
// Add the run
|
|
var dir = bidiRuns[bidiRun].Direction == Directionality.L ? TextDirection.LTR : TextDirection.RTL;
|
|
AddDirectionalRun( _styleRuns[styleRun], pos, nextPos - pos, dir, _styleRuns[styleRun].Style );
|
|
|
|
// Move to next position
|
|
pos = nextPos;
|
|
}
|
|
|
|
System.Diagnostics.Debug.Assert( bidiRun == bidiRuns.Count - 1 );
|
|
System.Diagnostics.Debug.Assert( styleRun == _styleRuns.Count - 1 );
|
|
|
|
// Add the final run
|
|
var dir2 = bidiRuns[bidiRun].Direction == Directionality.L ? TextDirection.LTR : TextDirection.RTL;
|
|
AddDirectionalRun( _styleRuns[_styleRuns.Count - 1], pos, _codePoints.Length - pos, dir2, _styleRuns[styleRun].Style );
|
|
|
|
// Flush runs
|
|
FlushUnshapedRuns();
|
|
|
|
// Split tabs
|
|
SplitTabs();
|
|
}
|
|
catch ( Exception x )
|
|
{
|
|
throw new InvalidOperationException( $"Exception in BuildFontRuns() with original length of {originalLength} now {_codePoints.Length}, style run count {_styleRuns.Count}, font run count {_fontRuns.Count}, direction overrides: {_hasTextDirectionOverrides}", x );
|
|
}
|
|
}
|
|
|
|
void SplitTabs()
|
|
{
|
|
int i = 0;
|
|
while ( i < _fontRuns.Count )
|
|
{
|
|
var fr = _fontRuns[i];
|
|
|
|
if ( FindFirstTabRun( fr, out var start, out var length ) )
|
|
{
|
|
var end = start + length;
|
|
|
|
if ( start == fr.Start && end < fr.End )
|
|
{
|
|
var trailingRun = fr.Split( start + length );
|
|
fr.RunKind = FontRunKind.Tabs;
|
|
|
|
_fontRuns.Insert( ++i, trailingRun );
|
|
}
|
|
else if ( start > fr.Start && end < fr.End )
|
|
{
|
|
var tabRun = fr.Split( start );
|
|
var trailingRun = tabRun.Split( start + length );
|
|
tabRun.RunKind = FontRunKind.Tabs;
|
|
|
|
_fontRuns.Insert( ++i, tabRun );
|
|
_fontRuns.Insert( ++i, trailingRun );
|
|
}
|
|
else if ( start != fr.Start && end == fr.End )
|
|
{
|
|
var tabRun = fr.Split( start );
|
|
tabRun.RunKind = FontRunKind.Tabs;
|
|
|
|
_fontRuns.Insert( ++i, tabRun );
|
|
}
|
|
else
|
|
{
|
|
if ( start == fr.Start && end == fr.End )
|
|
{
|
|
fr.RunKind = FontRunKind.Tabs;
|
|
}
|
|
i++;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
i++;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool FindFirstTabRun( FontRun fr, out int start, out int length )
|
|
{
|
|
start = 0;
|
|
length = 0;
|
|
|
|
for ( int i = fr.Start; i < fr.Start + fr.Length; i++ )
|
|
{
|
|
if ( _codePoints[i] == '\t' )
|
|
{
|
|
start = i;
|
|
for ( int j = i; j < fr.Start + fr.Length; j++ )
|
|
{
|
|
if ( _codePoints[j] != '\t' ) break;
|
|
length++;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
return length > 0;
|
|
}
|
|
|
|
float CalculateTabWidth( FontRun fontRun )
|
|
{
|
|
const int tabSpaceCount = 4;
|
|
|
|
var xcoord = fontRun.XCoord;
|
|
var tabStopWidth = tabSpaceCount * fontRun.SpaceWidth;
|
|
var tabStopsPassed = (int)(xcoord / tabStopWidth);
|
|
var endingTabStopXPosition = (tabStopsPassed + fontRun.Length) * tabStopWidth;
|
|
var requiredWidth = endingTabStopXPosition - xcoord;
|
|
|
|
return requiredWidth;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the Skia type face for a IStyle
|
|
/// </summary>
|
|
/// <param name="style">The style</param>
|
|
/// <param name="ignoreFontVariants">When true, doesn't embolden super/sub scripts</param>
|
|
/// <returns>The Skia typeface</returns>
|
|
SKTypeface TypefaceFromStyle( IStyle style, bool ignoreFontVariants = false )
|
|
{
|
|
return (FontMapper ?? FontMapper.Default).TypefaceFromStyle( style, ignoreFontVariants );
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Gets or sets the font mapper to be used by this TextBlock instance
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// When null, the default font mapper (FontMapper.Default) is used.
|
|
/// </remarks>
|
|
public FontMapper FontMapper
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a run of directional text
|
|
/// </summary>
|
|
/// <param name="styleRun">The style run the directional run was created from</param>
|
|
/// <param name="start">Index of the first code point _codePoints buffer</param>
|
|
/// <param name="length">Number of code points in this run</param>
|
|
/// <param name="direction">The direction of the text</param>
|
|
/// <param name="style">The user supplied style for this run</param>
|
|
void AddDirectionalRun( StyleRun styleRun, int start, int length, TextDirection direction, IStyle style )
|
|
{
|
|
// Quit if redundant...
|
|
if ( length == 0 )
|
|
return;
|
|
|
|
// Get the typeface
|
|
var typeface = TypefaceFromStyle( style );
|
|
|
|
// Get the slice of code points
|
|
var codePointsSlice = _codePoints.SubSlice( start, length );
|
|
|
|
// Split into font fallback runs
|
|
foreach ( var fontRun in FontFallback.GetFontRuns( codePointsSlice, typeface, style.ReplacementCharacter ) )
|
|
{
|
|
AddFontRun( styleRun, start + fontRun.Start, fontRun.Length, direction, style, fontRun.Typeface, typeface );
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a run of single font text
|
|
/// </summary>
|
|
/// <param name="styleRun">The style run the directional run was created from</param>
|
|
/// <param name="start">Index of the first code point _codePoints buffer</param>
|
|
/// <param name="length">Number of code points in this run</param>
|
|
/// <param name="direction">The direction of the text</param>
|
|
/// <param name="style">The user supplied style for this run</param>
|
|
/// <param name="typeface">The typeface of the run</param>
|
|
/// <param name="asFallbackFor">The typeface this is a fallback for</param>
|
|
void AddFontRun( StyleRun styleRun, int start, int length, TextDirection direction, IStyle style, SKTypeface typeface, SKTypeface asFallbackFor )
|
|
{
|
|
// Coalesc added font runs such that those with the exact same font, direction etc... are
|
|
// all shaped together such that kerning is correct for the entire run - regardless of
|
|
// non-typographic style changes (eg: underline, color etc...).
|
|
var usr = new UnshapedRun()
|
|
{
|
|
styleRun = styleRun,
|
|
start = start,
|
|
length = length,
|
|
direction = direction,
|
|
style = style,
|
|
typeface = typeface,
|
|
asFallbackFor = asFallbackFor,
|
|
};
|
|
|
|
// If this run can't be shaped with the previous one then flush those accumlated so far
|
|
if ( _unshapedRuns.Count > 0 && !_unshapedRuns[_unshapedRuns.Count - 1].CanShapeWith( usr ) )
|
|
{
|
|
FlushUnshapedRuns();
|
|
}
|
|
|
|
// Store it for now, actually shape it once we determine the next run is really
|
|
// a different font
|
|
_unshapedRuns.Add( usr );
|
|
}
|
|
|
|
void FlushUnshapedRuns()
|
|
{
|
|
// Quit if nothing to do?
|
|
if ( _unshapedRuns.Count == 0 )
|
|
return;
|
|
|
|
if ( _unshapedRuns.Count == 1 )
|
|
{
|
|
// If there's only one accumulated shape run then we can shape and add it directly
|
|
var usr = _unshapedRuns[0];
|
|
_fontRuns.Add( CreateFontRun( usr.styleRun, _codePoints.SubSlice( usr.start, usr.length ), usr.direction, usr.style, usr.typeface, usr.asFallbackFor ) );
|
|
}
|
|
else
|
|
{
|
|
// There are multiple styled runs, but they're all using the exact same type face
|
|
// so shape them all together to maintain correct kerning, but then break them
|
|
// apart so they're rendered separately with correct style for each piece.
|
|
var first = _unshapedRuns[0];
|
|
var last = _unshapedRuns[_unshapedRuns.Count - 1];
|
|
var spanningRun = CreateFontRun( first.styleRun, _codePoints.SubSlice( first.start, last.start + last.length - first.start ), first.direction, first.style, first.typeface, first.asFallbackFor );
|
|
|
|
for ( int i = 1; i < _unshapedRuns.Count; i++ )
|
|
{
|
|
var newRun = spanningRun.Split( _unshapedRuns[i].start );
|
|
|
|
spanningRun.StyleRun = _unshapedRuns[i - 1].styleRun;
|
|
spanningRun.Style = _unshapedRuns[i - 1].style;
|
|
|
|
_fontRuns.Add( spanningRun );
|
|
spanningRun = newRun;
|
|
}
|
|
spanningRun.StyleRun = _unshapedRuns[_unshapedRuns.Count - 1].styleRun;
|
|
spanningRun.Style = _unshapedRuns[_unshapedRuns.Count - 1].style;
|
|
_fontRuns.Add( spanningRun );
|
|
}
|
|
|
|
_unshapedRuns.Clear();
|
|
}
|
|
|
|
List<UnshapedRun> _unshapedRuns = new List<UnshapedRun>();
|
|
|
|
class UnshapedRun
|
|
{
|
|
public StyleRun styleRun;
|
|
public int start;
|
|
public int length;
|
|
public TextDirection direction;
|
|
public IStyle style;
|
|
public SKTypeface typeface;
|
|
public SKTypeface asFallbackFor;
|
|
|
|
// Check if this unshaped run can be shaped with the next one
|
|
public bool CanShapeWith( UnshapedRun next )
|
|
{
|
|
return typeface == next.typeface &&
|
|
style.FontSize == next.style.FontSize &&
|
|
asFallbackFor == next.asFallbackFor &&
|
|
direction == next.direction &&
|
|
start + length == next.start;
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Helper to create a font run
|
|
/// </summary>
|
|
/// <param name="styleRun">The style run owning this font run</param>
|
|
/// <param name="codePoints">The code points of the run</param>
|
|
/// <param name="direction">The run direction</param>
|
|
/// <param name="style">The user supplied style for this run</param>
|
|
/// <param name="typeface">The typeface of the run</param>
|
|
/// <param name="asFallbackFor">The original typeface this is a fallback for</param>
|
|
/// <returns>A FontRun</returns>
|
|
FontRun CreateFontRun( StyleRun styleRun, Slice<int> codePoints, TextDirection direction, IStyle style, SKTypeface typeface, SKTypeface asFallbackFor )
|
|
{
|
|
// Shape the text
|
|
var shaper = TextShaper.ForTypeface( typeface );
|
|
TextShaper.Result shaped;
|
|
if ( style.ReplacementCharacter == '\0' )
|
|
shaped = shaper.Shape( _textShapingBuffers, codePoints, style, direction, codePoints.Start, asFallbackFor, ResolveTextAlignment() );
|
|
else
|
|
shaped = shaper.ShapeReplacement( _textShapingBuffers, codePoints, style, codePoints.Start );
|
|
|
|
// Create the run
|
|
var fontRun = FontRun.Pool.Value.Get();
|
|
fontRun.StyleRun = styleRun;
|
|
fontRun.CodePointBuffer = _codePoints;
|
|
fontRun.Start = codePoints.Start;
|
|
fontRun.Length = codePoints.Length;
|
|
fontRun.Style = style;
|
|
fontRun.Direction = direction;
|
|
fontRun.Typeface = typeface;
|
|
fontRun.Glyphs = shaped.GlyphIndicies;
|
|
fontRun.GlyphPositions = shaped.GlyphPositions;
|
|
fontRun.RelativeCodePointXCoords = shaped.CodePointXCoords;
|
|
fontRun.Clusters = shaped.Clusters;
|
|
fontRun.Ascent = shaped.Ascent;
|
|
fontRun.Descent = shaped.Descent;
|
|
fontRun.Leading = shaped.Leading;
|
|
fontRun.Width = shaped.EndXCoord.X;
|
|
fontRun.SpaceWidth = typeface.ToFont( fontRun.Style.FontSize ).MeasureText( spaceCharacter );
|
|
|
|
return fontRun;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Break the list of font runs into lines
|
|
/// </summary>
|
|
void BreakLines()
|
|
{
|
|
// Work out possible line break positions
|
|
_lineBreaker.Reset( _codePoints.AsSlice() );
|
|
|
|
var mandatoryOnly = !_maxWidth.HasValue || _breakMode == WordBreakMode.Character;
|
|
var lineBreakPositions = _lineBreaker.GetBreaks( mandatoryOnly );
|
|
|
|
int frIndexStartOfLine = 0; // Index of the first font run in the current line
|
|
int frIndex = 0; // Index of the current font run
|
|
int lbrIndex = 0; // Index of the next unconsumed linebreak
|
|
float consumedWidth = 0; // Total width of all font runs placed on this line
|
|
int frSplitIndex = -1; // Index of the last font run that contained fitting break point
|
|
int codePointIndexSplit = -1; // Code point index of the last fitting break point measure position
|
|
int codePointIndexWrap = -1; // Code point index of the last fitting breaj point wrap position
|
|
|
|
if ( !CheckHeightConstraints() )
|
|
return;
|
|
|
|
while ( frIndex < _fontRuns.Count )
|
|
{
|
|
// Get the font run, update it's position
|
|
// and move to next
|
|
var fr = _fontRuns[frIndex];
|
|
fr.XCoord = consumedWidth;
|
|
|
|
if ( fr.RunKind == FontRunKind.Tabs )
|
|
{
|
|
fr.Width = CalculateTabWidth( fr );
|
|
}
|
|
|
|
consumedWidth += fr.Width;
|
|
|
|
float totalWidthToThisBreakPoint = 0;
|
|
|
|
// Skip line breaks
|
|
bool breakLine = false;
|
|
while ( lbrIndex < lineBreakPositions.Count )
|
|
{
|
|
// Past this run?
|
|
var lbr = lineBreakPositions[lbrIndex];
|
|
if ( lbr.PositionMeasure < fr.Start )
|
|
{
|
|
lbrIndex++;
|
|
continue;
|
|
}
|
|
if ( lbr.PositionMeasure >= fr.End )
|
|
break;
|
|
|
|
// Do we need to break
|
|
totalWidthToThisBreakPoint = fr.XCoord + fr.LeadingWidth( lbr.PositionMeasure );
|
|
if ( totalWidthToThisBreakPoint > _maxWidthResolved )
|
|
{
|
|
breakLine = true;
|
|
break;
|
|
}
|
|
|
|
// It fits, remember that
|
|
lbrIndex++;
|
|
|
|
// Only mark that we have something that fits if we actually know something has (or will be) been consumed
|
|
if ( totalWidthToThisBreakPoint > 0 || lbr.PositionWrap > lbr.PositionMeasure )
|
|
{
|
|
frSplitIndex = frIndex;
|
|
codePointIndexSplit = lbr.PositionMeasure;
|
|
codePointIndexWrap = lbr.PositionWrap;
|
|
}
|
|
|
|
if ( lbr.Required )
|
|
{
|
|
breakLine = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If we're on the last run and we've exceeded the width limit then force a break
|
|
if ( !breakLine && frIndex + 1 == _fontRuns.Count && consumedWidth > _maxWidthResolved )
|
|
breakLine = true;
|
|
|
|
// Break the line here?
|
|
if ( !breakLine )
|
|
{
|
|
frIndex++;
|
|
continue;
|
|
}
|
|
|
|
// If there wasn't a line break anywhere in the line, then we need to force one
|
|
// on a character boundary. Also do this is we know we're on the last available line.
|
|
var onLastLine = _maxLines.HasValue && _lines.Count == _maxLines.Value - 1;
|
|
if ( frSplitIndex < 0 || onLastLine )
|
|
{
|
|
// Jake: Pulled from upstream repo after spending the whole
|
|
// day trying to comprehend this big filthy function
|
|
while ( frIndex > frIndexStartOfLine && _fontRuns[frIndex].XCoord > _maxWidthResolved )
|
|
{
|
|
frIndex--;
|
|
}
|
|
|
|
fr = _fontRuns[frIndex];
|
|
var room = _maxWidthResolved - fr.XCoord;
|
|
frSplitIndex = frIndex;
|
|
codePointIndexSplit = fr.FindBreakPosition( room, frSplitIndex == frIndexStartOfLine );
|
|
codePointIndexWrap = codePointIndexSplit;
|
|
while ( codePointIndexWrap < _codePoints.Length && UnicodeClasses.LineBreakClass( _codePoints[codePointIndexWrap] ) == LineBreakClass.SP )
|
|
codePointIndexWrap++;
|
|
}
|
|
|
|
// Split it
|
|
fr = _fontRuns[frSplitIndex];
|
|
if ( codePointIndexSplit == fr.Start )
|
|
{
|
|
// Split exactly before the run
|
|
}
|
|
else if ( codePointIndexSplit == fr.End )
|
|
{
|
|
// Split exactly after the run
|
|
frSplitIndex++;
|
|
}
|
|
else
|
|
{
|
|
// Split in the middle of the run
|
|
frSplitIndex++;
|
|
_fontRuns.Insert( frSplitIndex, fr.Split( codePointIndexSplit ) );
|
|
}
|
|
|
|
// Trailing whitespace
|
|
int frTrailingWhiteSpaceIndex = frSplitIndex;
|
|
while ( frTrailingWhiteSpaceIndex < _fontRuns.Count && _fontRuns[frTrailingWhiteSpaceIndex].Start < codePointIndexWrap )
|
|
{
|
|
if ( codePointIndexWrap < _fontRuns[frTrailingWhiteSpaceIndex].End )
|
|
{
|
|
var fr2 = _fontRuns[frTrailingWhiteSpaceIndex].Split( codePointIndexWrap );
|
|
_fontRuns.Insert( frTrailingWhiteSpaceIndex + 1, fr2 );
|
|
}
|
|
|
|
frTrailingWhiteSpaceIndex++;
|
|
}
|
|
|
|
// Build the final line
|
|
BuildLine( frIndexStartOfLine, frSplitIndex, frTrailingWhiteSpaceIndex );
|
|
|
|
// Reset for the next line
|
|
frSplitIndex = -1;
|
|
frIndex = frTrailingWhiteSpaceIndex;
|
|
frIndexStartOfLine = frIndex;
|
|
consumedWidth = 0;
|
|
|
|
// Check height constraints and quit if finished
|
|
if ( !CheckHeightConstraints() )
|
|
return;
|
|
}
|
|
|
|
// Build the final line
|
|
if ( frIndexStartOfLine < _fontRuns.Count )
|
|
{
|
|
BuildLine( frIndexStartOfLine, _fontRuns.Count, _fontRuns.Count );
|
|
CheckHeightConstraints();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Construct a single line from the specified font run indicies
|
|
/// </summary>
|
|
/// <param name="frIndexStartOfLine">Index of the first font run in the line</param>
|
|
/// <param name="frSplitIndex">Index of the last type</param>
|
|
/// <param name="frTrailingWhiteSpaceIndex"></param>
|
|
void BuildLine( int frIndexStartOfLine, int frSplitIndex, int frTrailingWhiteSpaceIndex )
|
|
{
|
|
// Create the line
|
|
|
|
var line = TextLine.Pool.Value.Get();
|
|
line.TextBlock = this;
|
|
line.YCoord = _measuredHeight;
|
|
|
|
// Add runs
|
|
for ( int i = frIndexStartOfLine; i < frTrailingWhiteSpaceIndex; i++ )
|
|
{
|
|
// Tag trailing whitespace appropriately
|
|
if ( i >= frSplitIndex )
|
|
{
|
|
var fr = _fontRuns[i];
|
|
fr.RunKind = FontRunKind.TrailingWhitespace;
|
|
if ( fr.Direction != _resolvedBaseDirection )
|
|
{
|
|
// What is this a fallback for?
|
|
var asFallbackFor = TypefaceFromStyle( fr.Style, false );
|
|
|
|
// Create a new font run over the same text span but using the base direction
|
|
_fontRuns[i] = CreateFontRun( fr.StyleRun, fr.CodePoints, _resolvedBaseDirection, fr.Style, fr.Typeface, asFallbackFor );
|
|
}
|
|
}
|
|
|
|
line.RunsInternal.Add( _fontRuns[i] );
|
|
}
|
|
|
|
// Add the line to the collection
|
|
_lines.Add( line );
|
|
|
|
// Lay it out
|
|
LayoutLine( line );
|
|
|
|
// Update y position
|
|
_measuredHeight += line.Height;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Layout the font runs within a line
|
|
/// </summary>
|
|
/// <param name="line">The line to layout</param>
|
|
void LayoutLine( TextLine line )
|
|
{
|
|
// How much to x-adjust all runs by to position correctly
|
|
float xAdjust;
|
|
|
|
// Which direction?
|
|
if ( _resolvedBaseDirection == TextDirection.LTR )
|
|
{
|
|
xAdjust = LayoutLineLTR( line );
|
|
}
|
|
else
|
|
{
|
|
/*
|
|
if (_useMSWordStyleRtlLayout)
|
|
{
|
|
xAdjust = LayoutLineRTLWordStyle(line);
|
|
}
|
|
else
|
|
*/
|
|
{
|
|
xAdjust = LayoutLineRTL( line );
|
|
}
|
|
}
|
|
|
|
// Process all runs, setting their final left-aligned x-position and
|
|
// calculating metrics for the line as a whole
|
|
float maxAbove = 0;
|
|
float maxBelow = 0;
|
|
float maxAscent = 0;
|
|
float maxDescent = 0;
|
|
for ( int frIndex = 0; frIndex < line.Runs.Count; frIndex++ )
|
|
{
|
|
// Get the run
|
|
var fr = line.Runs[frIndex];
|
|
|
|
// Adjust run position
|
|
fr.XCoord += xAdjust;
|
|
|
|
var above = -fr.Ascent + fr.HalfLeading;
|
|
var below = fr.Descent + fr.HalfLeading;
|
|
|
|
if ( above > maxAbove )
|
|
maxAbove = above;
|
|
if ( below > maxBelow )
|
|
maxBelow = below;
|
|
|
|
if ( fr.Ascent < maxAscent )
|
|
maxAscent = fr.Ascent;
|
|
if ( fr.Descent > maxDescent )
|
|
maxDescent = fr.Descent;
|
|
}
|
|
|
|
// Store line metrics
|
|
line.MaxAscent = maxAscent;
|
|
line.MaxDescent = maxDescent;
|
|
line.Height = maxAbove + maxBelow;
|
|
line.BaseLine = maxAbove;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Layout a line (LTR edition)
|
|
/// </summary>
|
|
/// <param name="line">The line to be laid out</param>
|
|
/// <returns>A final x-adjustment to be applied to the line's font runs</returns>
|
|
private float LayoutLineLTR( TextLine line )
|
|
{
|
|
float x = 0;
|
|
float trailingWhitespaceWidth = 0;
|
|
for ( int i = 0; i < line.Runs.Count; i++ )
|
|
{
|
|
var fr = line.Runs[i];
|
|
if ( fr.Direction == TextDirection.LTR )
|
|
{
|
|
fr.XCoord = x;
|
|
x += fr.Width;
|
|
|
|
if ( fr.RunKind == FontRunKind.TrailingWhitespace )
|
|
trailingWhitespaceWidth += fr.Width;
|
|
}
|
|
else
|
|
{
|
|
System.Diagnostics.Debug.Assert( fr.RunKind == FontRunKind.Normal );
|
|
|
|
int j = i;
|
|
while ( j + 1 < line.Runs.Count && line.Runs[j + 1].Direction == TextDirection.RTL )
|
|
j++;
|
|
|
|
int continueFrom = j;
|
|
|
|
while ( j >= i )
|
|
{
|
|
fr = line.Runs[j];
|
|
fr.XCoord = x;
|
|
x += fr.Width;
|
|
j--;
|
|
}
|
|
|
|
i = continueFrom;
|
|
}
|
|
}
|
|
|
|
// Store content width
|
|
line.Width = x - trailingWhitespaceWidth;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Layout a line (RTL edition)
|
|
/// </summary>
|
|
/// <param name="line">The line to be laid out</param>
|
|
/// <returns>A final x-adjustment to be applied to the line's font runs</returns>
|
|
private float LayoutLineRTL( TextLine line )
|
|
{
|
|
float x = 0;
|
|
float trailingWhitespaceWidth = 0;
|
|
for ( int i = 0; i < line.Runs.Count; i++ )
|
|
{
|
|
var fr = line.Runs[i];
|
|
if ( fr.Direction == TextDirection.RTL )
|
|
{
|
|
x -= fr.Width;
|
|
fr.XCoord = x;
|
|
|
|
if ( fr.RunKind == FontRunKind.TrailingWhitespace )
|
|
trailingWhitespaceWidth += fr.Width;
|
|
}
|
|
else
|
|
{
|
|
System.Diagnostics.Debug.Assert( fr.RunKind == FontRunKind.Normal );
|
|
|
|
int j = i;
|
|
while ( j + 1 < line.Runs.Count && line.Runs[j + 1].Direction == TextDirection.LTR )
|
|
j++;
|
|
|
|
int continueFrom = j;
|
|
|
|
while ( j >= i )
|
|
{
|
|
fr = line.Runs[j];
|
|
x -= fr.Width;
|
|
fr.XCoord = x;
|
|
j--;
|
|
}
|
|
|
|
i = continueFrom;
|
|
}
|
|
}
|
|
|
|
// Work out final width
|
|
float totalWidth = -x - trailingWhitespaceWidth;
|
|
|
|
// Store content width
|
|
line.Width = totalWidth;
|
|
|
|
// Everything is currently laid out from 0 to the left, so we
|
|
// need to shift right to make it left aligned
|
|
return totalWidth;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Layout a line (MS Word RTL edition)
|
|
/// </summary>
|
|
/// <param name="line">The line to be laid out</param>
|
|
/// <returns>A final x-adjustment to be applied to the line's font runs</returns>
|
|
private float LayoutLineRTLWordStyle( TextLine line )
|
|
{
|
|
float xPos = 0;
|
|
float totalWidth = 0;
|
|
float trailingWhitespaceWidth = 0;
|
|
for ( int i = 0; i < line.Runs.Count; i++ )
|
|
{
|
|
// Get the direction of this group
|
|
var groupDirection = line.Runs[i].Direction;
|
|
|
|
// Count the number of runs with the same directionality
|
|
int j = i;
|
|
float groupWidth = 0;
|
|
while ( j < line.Runs.Count && line.Runs[j].Direction == groupDirection )
|
|
{
|
|
if ( line.Runs[i].RunKind == FontRunKind.TrailingWhitespace )
|
|
trailingWhitespaceWidth += line.Runs[i].Width;
|
|
|
|
groupWidth += line.Runs[j].Width;
|
|
j++;
|
|
}
|
|
|
|
totalWidth += groupWidth;
|
|
|
|
// How many runs in the group
|
|
var runsInGroup = j - i;
|
|
|
|
// Place the runs in the group
|
|
if ( groupDirection == TextDirection.LTR )
|
|
{
|
|
// Left to right group
|
|
for ( ; i < j; i++ )
|
|
{
|
|
line.Runs[i].XCoord = xPos;
|
|
xPos += line.Runs[i].Width;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Right to left group
|
|
xPos += groupWidth;
|
|
var x = xPos;
|
|
for ( ; i < j; i++ )
|
|
{
|
|
x -= line.Runs[i].Width;
|
|
line.Runs[i].XCoord = x;
|
|
}
|
|
}
|
|
|
|
i = j - 1;
|
|
}
|
|
|
|
line.Width = xPos - trailingWhitespaceWidth;
|
|
|
|
return _maxWidthResolved - (xPos - trailingWhitespaceWidth);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finalize lines, positioning for horizontal alignment and
|
|
/// moving all glyphs into position (relative to the text block top left)
|
|
/// </summary>
|
|
void FinalizeLines()
|
|
{
|
|
// Work out the measured width
|
|
foreach ( var l in _lines )
|
|
{
|
|
if ( l.Width > _measuredWidth )
|
|
_measuredWidth = l.Width;
|
|
}
|
|
|
|
var ta = ResolveTextAlignment();
|
|
foreach ( var line in _lines )
|
|
{
|
|
// Work out x-alignement adjust for this line
|
|
float xAdjust = 0;
|
|
switch ( ta )
|
|
{
|
|
case TextAlignment.Right:
|
|
xAdjust = (_maxWidth ?? _measuredWidth) - line.Width;
|
|
break;
|
|
|
|
case TextAlignment.Center:
|
|
xAdjust = ((_maxWidth ?? _measuredWidth) - line.Width) / 2;
|
|
break;
|
|
|
|
case TextAlignment.Justify:
|
|
// Justify works by widening the spaces in-place rather than shifting the whole
|
|
// line, so it leaves xAdjust at 0 and adjusts glyph positions itself.
|
|
JustifyLine( line );
|
|
break;
|
|
}
|
|
|
|
// Position each run
|
|
for ( int frIndex = 0; frIndex < line.Runs.Count; frIndex++ )
|
|
{
|
|
var fr = line.Runs[frIndex];
|
|
fr.Line = line;
|
|
fr.XCoord += xAdjust;
|
|
fr.MoveGlyphs( fr.XCoord, line.YCoord + line.BaseLine );
|
|
}
|
|
}
|
|
|
|
// Remove the font runs that weren't assigned to a line because the text
|
|
// was truncated
|
|
while ( _fontRuns.Count > 0 && _fontRuns[_fontRuns.Count - 1].Line == null )
|
|
{
|
|
_fontRuns.RemoveAt( _fontRuns.Count - 1 );
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Distributes the slack on a line across its inter-word spaces so the line fills the available
|
|
/// width (CSS <c>text-align: justify</c>). The last line of a paragraph is left unjustified and
|
|
/// only left-to-right content is handled. Mutates glyph positions and code-point x-coords together
|
|
/// so rendering and hit-testing stay consistent. Called from <see cref="FinalizeLines"/> while the
|
|
/// glyph positions are still relative to their run, before they're moved into final position.
|
|
/// </summary>
|
|
void JustifyLine( TextLine line )
|
|
{
|
|
var runs = line.Runs;
|
|
if ( runs.Count == 0 )
|
|
return;
|
|
|
|
// The last line of the block isn't justified.
|
|
if ( line == _lines[_lines.Count - 1] )
|
|
return;
|
|
|
|
// Don't justify a line that ends at a hard line break (the last line of a paragraph).
|
|
var lastCps = runs[runs.Count - 1].CodePoints;
|
|
if ( lastCps.Length > 0 )
|
|
{
|
|
int last = lastCps[lastCps.Length - 1];
|
|
if ( last == '\n' || last == '\r' || last == 0x2028 || last == 0x2029 )
|
|
return;
|
|
}
|
|
|
|
// Right-to-left justification isn't implemented; leave those lines as laid out.
|
|
for ( int i = 0; i < runs.Count; i++ )
|
|
{
|
|
if ( runs[i].Direction == TextDirection.RTL )
|
|
return;
|
|
}
|
|
|
|
// How much width to make up, and how many spaces to spread it across.
|
|
float target = _maxWidth ?? _measuredWidth;
|
|
float slack = target - line.Width;
|
|
if ( slack <= 0.01f )
|
|
return;
|
|
|
|
int gaps = 0;
|
|
for ( int i = 0; i < runs.Count; i++ )
|
|
{
|
|
var run = runs[i];
|
|
if ( run.RunKind == FontRunKind.TrailingWhitespace )
|
|
break;
|
|
|
|
var cps = run.CodePoints;
|
|
for ( int c = 0; c < cps.Length; c++ )
|
|
{
|
|
if ( cps[c] == ' ' )
|
|
gaps++;
|
|
}
|
|
}
|
|
|
|
if ( gaps == 0 )
|
|
return;
|
|
|
|
float extra = slack / gaps;
|
|
|
|
// Walk the runs left-to-right. Within a run, each code point is shifted right by the space
|
|
// opened up before it; 'lineShift' carries that running total forward between runs.
|
|
float lineShift = 0;
|
|
for ( int i = 0; i < runs.Count; i++ )
|
|
{
|
|
var run = runs[i];
|
|
run.XCoord += lineShift;
|
|
|
|
if ( run.RunKind == FontRunKind.TrailingWhitespace )
|
|
continue;
|
|
|
|
var cps = run.CodePoints;
|
|
|
|
// Per code-point shift: a space adds 'extra' to everything after it.
|
|
var shift = new float[cps.Length];
|
|
float acc = 0;
|
|
for ( int c = 0; c < cps.Length; c++ )
|
|
{
|
|
shift[c] = acc;
|
|
if ( cps[c] == ' ' )
|
|
acc += extra;
|
|
}
|
|
|
|
// Glyph positions (rendering): map each glyph to its code point via the cluster.
|
|
for ( int g = 0; g < run.GlyphPositions.Length; g++ )
|
|
{
|
|
int cp = run.Clusters[g] - run.Start;
|
|
if ( cp >= 0 && cp < shift.Length )
|
|
run.GlyphPositions[g].X += shift[cp];
|
|
}
|
|
|
|
// Code-point x-coords (hit-testing / caret) shifted the same way.
|
|
for ( int c = 0; c < run.RelativeCodePointXCoords.Length && c < shift.Length; c++ )
|
|
run.RelativeCodePointXCoords[c] += shift[c];
|
|
|
|
run.Width += acc;
|
|
lineShift += acc;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds the font run containing a specified code point index
|
|
/// </summary>
|
|
/// <param name="from">The font run index to start from</param>
|
|
/// <param name="codePointIndex">The code point index being searched for</param>
|
|
/// <returns>The index of the font run containing the specified code point
|
|
/// index, or font run count if past the end</returns>
|
|
int FindFontRunForCodePointIndex( int from, int codePointIndex )
|
|
{
|
|
int frIndex = from;
|
|
while ( true )
|
|
{
|
|
var fr = _fontRuns[frIndex];
|
|
if ( codePointIndex < fr.Start )
|
|
{
|
|
System.Diagnostics.Debug.Assert( frIndex > 0 );
|
|
frIndex--;
|
|
}
|
|
else if ( codePointIndex >= fr.End )
|
|
{
|
|
frIndex++;
|
|
if ( frIndex == _fontRuns.Count )
|
|
{
|
|
System.Diagnostics.Debug.Assert( codePointIndex == _codePoints.Length );
|
|
return _codePoints.Length;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return frIndex;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Re-usable buffer holding just the ellipsis character
|
|
/// </summary>
|
|
static Utf32Buffer ellipsis = new Utf32Buffer( "…" );
|
|
|
|
static ushort[] spaceCharacter = { ' ' };
|
|
|
|
/// <summary>
|
|
/// Create a special font run containing the ellipsis character
|
|
/// based on an existing run
|
|
/// </summary>
|
|
/// <param name="basedOn">The run to base the styling on</param>
|
|
/// <returns>A new font run containing the ellipsis character</returns>
|
|
FontRun CreateOverflowRun( FontRun basedOn )
|
|
{
|
|
// Get the type face
|
|
var typeface = TypefaceFromStyle( basedOn.Style, true );
|
|
|
|
// Split into font fallback runs (there should only ever be just one)
|
|
var fontRun = FontFallback.GetFontRuns( ellipsis.AsSlice(), typeface ).Single();
|
|
|
|
// Create the new run and mark is as a special run type for ellipsis
|
|
var fr = CreateFontRun( basedOn.StyleRun, ellipsis.SubSlice( fontRun.Start, fontRun.Length ), _resolvedBaseDirection, basedOn.Style, fontRun.Typeface, typeface );
|
|
fr.RunKind = FontRunKind.Ellipsis;
|
|
|
|
// Done
|
|
return fr;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update the current line with a new font run containing the trailing ellipsis
|
|
/// </summary>
|
|
/// <param name="line">The line to be updated</param>
|
|
/// <param name="postLayout">True if the ellipsis is being added post layout via a user call to AddEllipsis()</param>
|
|
void AdornLineWithOverflowContent( TextLine line, bool postLayout = false )
|
|
{
|
|
var lastRun = line.Runs[line.Runs.Count - 1];
|
|
|
|
// Don't adorn if the last run actually
|
|
// has all the text...
|
|
if ( !postLayout && lastRun.End == _codePoints.Length )
|
|
return;
|
|
|
|
// Don't adorn if overflow is set to clip
|
|
if ( _overflow == TextOverflow.Clip )
|
|
return;
|
|
|
|
// Remove all trailing whitespace from the line
|
|
for ( int i = line.Runs.Count - 1; i >= 0; i-- )
|
|
{
|
|
var r = line.Runs[i];
|
|
if ( r.RunKind == FontRunKind.TrailingWhitespace )
|
|
{
|
|
line.RunsInternal.RemoveAt( i );
|
|
if ( postLayout )
|
|
{
|
|
_fontRuns.Remove( r );
|
|
}
|
|
}
|
|
}
|
|
|
|
// Calculate the total width of the line
|
|
float totalWidth = 0;
|
|
for ( int i = 0; i < line.Runs.Count; i++ )
|
|
{
|
|
totalWidth += line.Runs[i].Width;
|
|
}
|
|
|
|
// Get the new last run (if any)
|
|
if ( line.Runs.Count > 0 )
|
|
lastRun = line.Runs[line.Runs.Count - 1];
|
|
|
|
// Create a new run for the ellipsis
|
|
var ellipsisRun = CreateOverflowRun( lastRun );
|
|
|
|
// Work out how much room we've got. If this is user request
|
|
// to append the ellipsis and MaxWidth isn't set then make sure
|
|
// we don't change our previously measured width be using that
|
|
// previous measurement as the limit
|
|
var maxWidth = _maxWidthResolved;
|
|
|
|
var removeWidth = totalWidth + ellipsisRun.Width - maxWidth;
|
|
if ( removeWidth > 0 )
|
|
{
|
|
for ( int i = line.Runs.Count - 1; i >= 0; i-- )
|
|
{
|
|
var fr = line.Runs[i];
|
|
|
|
// Does this run have enough to remove?
|
|
// No, remove it all
|
|
if ( fr.Width < removeWidth )
|
|
{
|
|
removeWidth -= fr.Width;
|
|
line.RunsInternal.RemoveAt( i );
|
|
if ( postLayout )
|
|
_fontRuns.Remove( fr );
|
|
continue;
|
|
}
|
|
|
|
// Work out where to split this run
|
|
int pos = fr.FindBreakPosition( fr.Width - removeWidth, false );
|
|
if ( pos == fr.Start )
|
|
{
|
|
// Nothing fits, remove it all
|
|
line.RunsInternal.RemoveAt( i );
|
|
if ( postLayout )
|
|
_fontRuns.Remove( fr );
|
|
}
|
|
else
|
|
{
|
|
// Split it
|
|
var remaining = fr.Split( pos );
|
|
|
|
// Keep the remaining part in case we need it later (not sure why,
|
|
// but seems wise).
|
|
if ( !postLayout )
|
|
{
|
|
_fontRuns.Insert( _fontRuns.IndexOf( fr ) + 1, remaining );
|
|
// _fontRuns.Remove(fr);
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Add it to the line
|
|
line.RunsInternal.Add( ellipsisRun );
|
|
|
|
if ( postLayout )
|
|
{
|
|
// No layout adjustmenst if this is post layout change
|
|
LayoutLine( line );
|
|
}
|
|
else
|
|
{
|
|
// Remember old line height
|
|
var oldHeight = line.Height;
|
|
_measuredHeight -= line.Height;
|
|
|
|
// Layout the line again
|
|
LayoutLine( line );
|
|
|
|
// Adjust height just in case it changed
|
|
_measuredHeight += line.Height;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if the current layout has exceeded any height restrictions
|
|
/// and if so, remove any offending lines and optionally create the
|
|
/// ellipsis at the end indicating the text has been truncated
|
|
/// </summary>
|
|
/// <returns>True if can continue adding lines; otherwise false</returns>
|
|
bool CheckHeightConstraints()
|
|
{
|
|
// Special case for the first line not fitting
|
|
if ( _lines.Count == 1 && _measuredHeight > _maxHeightResolved )
|
|
{
|
|
_truncated = true;
|
|
_measuredWidth = 0;
|
|
_lines.RemoveAt( 0 );
|
|
return false;
|
|
}
|
|
|
|
// Have we exceeded the height limit
|
|
if ( _measuredHeight > _maxHeightResolved || _maxHeightResolved <= 0 )
|
|
{
|
|
// Remove the last line (unless it's the only line)
|
|
if ( _lines.Count > 1 )
|
|
{
|
|
_measuredHeight -= _lines[_lines.Count - 1].Height;
|
|
_lines.RemoveAt( _lines.Count - 1 );
|
|
}
|
|
if ( _lines.Count > 0 )
|
|
{
|
|
AdornLineWithOverflowContent( _lines[_lines.Count - 1] );
|
|
}
|
|
_truncated = true;
|
|
return false;
|
|
}
|
|
|
|
// Have we hit the line count limit?
|
|
if ( _lines.Count >= _maxLinesResolved )
|
|
{
|
|
if ( _lines.Count > 0 )
|
|
{
|
|
AdornLineWithOverflowContent( _lines[_lines.Count - 1] );
|
|
}
|
|
_truncated = true;
|
|
return false;
|
|
}
|
|
|
|
// Are we nowrap and overflowed?
|
|
if ( _noWrap && _overflow != TextOverflow.None && _lines.Count > 0 )
|
|
{
|
|
AdornLineWithOverflowContent( _lines[_lines.Count - 1] );
|
|
_truncated = true;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets and internal object and memory pools.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// For performance reasons and to reduce pressure on the
|
|
/// garbage collector, RichTextKit maintains several internal
|
|
/// per-thread memory and object pools.
|
|
///
|
|
/// If you create a very large text block, these pools will be
|
|
/// enlarged to cope with content of larger text blocks.
|
|
///
|
|
/// This method can be used to reset those pools to reclaim the
|
|
/// extra memory they consumed.
|
|
///
|
|
/// In general you can ignore this method, unless you know you're
|
|
/// working with very large text blocks (which you shouldn't be
|
|
/// anyway, since a text block is only supposed to be a single
|
|
/// paragraph).
|
|
/// /// </remarks>
|
|
public static void ResetPooledMemory()
|
|
{
|
|
TextLine.Pool.Value = new ObjectPool<TextLine>();
|
|
FontRun.Pool.Value = new ObjectPool<FontRun>();
|
|
StyleRun.Pool.Value = new ObjectPool<StyleRun>();
|
|
Bidi.Instance.Value = new Bidi();
|
|
}
|
|
|
|
|
|
|
|
}
|
|
}
|