Browse Source

Make IFormattedTextImpl immutable.

Previously `IFormattedTextImpl` had `SetForegroundBrush` which set
mutable state. Make `FormattedText` fully mutable (before it was kinda
mutable, kinda immutable) and create immutable `IFormattedTextImpl`s on
demand.
scenegraph-after-breakage
Steven Kirk 10 years ago
parent
commit
376535b198
  1. 21
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  2. 18
      src/Avalonia.Controls/Primitives/AccessText.cs
  3. 19
      src/Avalonia.Controls/TextBlock.cs
  4. 6
      src/Avalonia.HtmlRenderer/Adapters/GraphicsAdapter.cs
  5. 1
      src/Avalonia.Visuals/Avalonia.Visuals.csproj
  6. 157
      src/Avalonia.Visuals/Media/FormattedText.cs
  7. 21
      src/Avalonia.Visuals/Media/FormattedTextStyleSpan.cs
  8. 35
      src/Avalonia.Visuals/Media/Typeface.cs
  9. 15
      src/Avalonia.Visuals/Platform/IFormattedTextImpl.cs
  10. 14
      src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
  11. 23
      src/Avalonia.Visuals/Rendering/RendererBase.cs
  12. 17
      src/Gtk/Avalonia.Cairo/CairoPlatform.cs
  13. 111
      src/Gtk/Avalonia.Cairo/Media/FormattedTextImpl.cs
  14. 75
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs
  15. 11
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  16. 17
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  17. 113
      src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs
  18. 19
      tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs
  19. 47
      tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs
  20. 12
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
  21. 30
      tests/Avalonia.Visuals.UnitTests/Media/FormattedTextTests.cs
  22. 21
      tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs
  23. 8
      tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs

21
src/Avalonia.Controls/Presenters/TextPresenter.cs

@ -212,7 +212,10 @@ namespace Avalonia.Controls.Presenters
if (length > 0) if (length > 0)
{ {
result.SetForegroundBrush(Brushes.White, start, length); result.Spans = new[]
{
new FormattedTextStyleSpan(start, length, foregroundBrush: Brushes.White),
};
} }
return result; return result;
@ -228,15 +231,13 @@ namespace Avalonia.Controls.Presenters
} }
else else
{ {
// TODO: Pretty sure that measuring "X" isn't the right way to do this... return new FormattedText
return new FormattedText( {
"X", Text = "X",
FontFamily, Typeface = new Typeface(FontFamily, FontSize, FontStyle, FontWeight),
FontSize, TextAlignment = TextAlignment,
availableSize, Constraint = availableSize,
FontStyle, }.Measure();
TextAlignment,
FontWeight).Measure();
} }
} }

18
src/Avalonia.Controls/Primitives/AccessText.cs

@ -85,16 +85,14 @@ namespace Avalonia.Controls.Primitives
/// <returns>A <see cref="FormattedText"/> object.</returns> /// <returns>A <see cref="FormattedText"/> object.</returns>
protected override FormattedText CreateFormattedText(Size constraint) protected override FormattedText CreateFormattedText(Size constraint)
{ {
var result = new FormattedText( return new FormattedText
StripAccessKey(Text), {
FontFamily, Constraint = constraint,
FontSize, Typeface = new Typeface(FontFamily, FontSize, FontStyle, FontWeight),
constraint, Text = StripAccessKey(Text),
FontStyle, TextAlignment = TextAlignment,
TextAlignment, Wrapping = TextWrapping,
FontWeight); };
result.Constraint = constraint;
return result;
} }
/// <summary> /// <summary>

19
src/Avalonia.Controls/TextBlock.cs

@ -350,17 +350,14 @@ namespace Avalonia.Controls
/// <returns>A <see cref="FormattedText"/> object.</returns> /// <returns>A <see cref="FormattedText"/> object.</returns>
protected virtual FormattedText CreateFormattedText(Size constraint) protected virtual FormattedText CreateFormattedText(Size constraint)
{ {
var result = new FormattedText( return new FormattedText
Text ?? string.Empty, {
FontFamily, Constraint = constraint,
FontSize, Typeface = new Typeface(FontFamily, FontSize, FontStyle, FontWeight),
constraint, Text = Text ?? string.Empty,
FontStyle, TextAlignment = TextAlignment,
TextAlignment, Wrapping = TextWrapping,
FontWeight, };
TextWrapping);
result.Constraint = constraint;
return result;
} }
/// <summary> /// <summary>

6
src/Avalonia.HtmlRenderer/Adapters/GraphicsAdapter.cs

@ -117,7 +117,11 @@ namespace TheArtOfDev.HtmlRenderer.Avalonia.Adapters
FormattedText GetText(string str, RFont font) FormattedText GetText(string str, RFont font)
{ {
var f = ((FontAdapter)font); var f = ((FontAdapter)font);
return new FormattedText(str, f.Name, font.Size, Size.Infinity, f.FontStyle, TextAlignment.Left, f.Weight); return new FormattedText
{
Text = str,
Typeface = new Typeface(f.Name, font.Size, f.FontStyle, f.Weight),
};
} }
public override void MeasureString(string str, RFont font, double maxWidth, out int charFit, out double charFitWidth) public override void MeasureString(string str, RFont font, double maxWidth, out int charFit, out double charFitWidth)

1
src/Avalonia.Visuals/Avalonia.Visuals.csproj

@ -2,6 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard1.1</TargetFramework> <TargetFramework>netstandard1.1</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<RootNamespace>Avalonia</RootNamespace>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DebugSymbols>true</DebugSymbols> <DebugSymbols>true</DebugSymbols>

157
src/Avalonia.Visuals/Media/FormattedText.cs

@ -12,64 +12,30 @@ namespace Avalonia.Media
/// </summary> /// </summary>
public class FormattedText public class FormattedText
{ {
private readonly IPlatformRenderInterface _platform;
private Size _constraint = Size.Infinity;
private IFormattedTextImpl _platformImpl;
private IReadOnlyList<FormattedTextStyleSpan> _spans;
private Typeface _typeface;
private string _text;
private TextAlignment _textAlignment;
private TextWrapping _wrapping;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="FormattedText"/> class. /// Initializes a new instance of the <see cref="FormattedText"/> class.
/// </summary> /// </summary>
/// <param name="text">The text.</param> public FormattedText()
/// <param name="fontFamilyName">The font family.</param>
/// <param name="fontSize">The font size.</param>
/// <param name="constraint">The text layout constraints.</param>
/// <param name="fontStyle">The font style.</param>
/// <param name="textAlignment">The text alignment.</param>
/// <param name="fontWeight">The font weight.</param>
/// <param name="wrapping">The text wrapping mode.</param>
public FormattedText(
string text,
string fontFamilyName,
double fontSize,
Size constraint,
FontStyle fontStyle = FontStyle.Normal,
TextAlignment textAlignment = TextAlignment.Left,
FontWeight fontWeight = FontWeight.Normal,
TextWrapping wrapping = TextWrapping.Wrap)
{ {
Contract.Requires<ArgumentNullException>(text != null); _platform = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
Contract.Requires<ArgumentNullException>(fontFamilyName != null); }
if (fontSize <= 0)
{
throw new ArgumentException("FontSize must be greater than 0");
}
if (fontWeight <= 0)
{
throw new ArgumentException("FontWeight must be greater than 0");
}
Text = text;
FontFamilyName = fontFamilyName;
FontSize = fontSize;
FontStyle = fontStyle;
FontWeight = fontWeight;
TextAlignment = textAlignment;
Wrapping = wrapping;
var platform = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
if (platform == null)
{
throw new Exception("Could not create FormattedText: IPlatformRenderInterface not registered.");
}
PlatformImpl = platform.CreateFormattedText( /// <summary>
text, /// Initializes a new instance of the <see cref="FormattedText"/> class.
fontFamilyName, /// </summary>
fontSize, /// <param name="platform">The platform render interface.</param>
fontStyle, public FormattedText(IPlatformRenderInterface platform)
textAlignment, {
fontWeight, _platform = platform;
wrapping,
constraint);
} }
/// <summary> /// <summary>
@ -77,49 +43,77 @@ namespace Avalonia.Media
/// </summary> /// </summary>
public Size Constraint public Size Constraint
{ {
get { return PlatformImpl.Constraint; } get => _constraint;
set { PlatformImpl = PlatformImpl.WithConstraint(value); } set => Set(ref _constraint, value);
} }
/// <summary> /// <summary>
/// Gets the font family. /// Gets or sets the base typeface.
/// </summary> /// </summary>
public string FontFamilyName { get; } public Typeface Typeface
{
get => _typeface;
set => Set(ref _typeface, value);
}
/// <summary> /// <summary>
/// Gets the font size. /// Gets or sets a collection of spans that describe the formatting of subsections of the
/// text.
/// </summary> /// </summary>
public double FontSize { get; } public IReadOnlyList<FormattedTextStyleSpan> Spans
{
get => _spans;
set => Set(ref _spans, value);
}
/// <summary> /// <summary>
/// Gets the font style. /// Gets or sets the text.
/// </summary> /// </summary>
public FontStyle FontStyle { get; } public string Text
{
get => _text;
set => Set(ref _text, value);
}
/// <summary> /// <summary>
/// Gets the font weight. /// Gets or sets the aligment of the text.
/// </summary> /// </summary>
public FontWeight FontWeight { get; } public TextAlignment TextAlignment
{
get => _textAlignment;
set => Set(ref _textAlignment, value);
}
/// <summary> /// <summary>
/// Gets the text. /// Gets or sets the text wrapping.
/// </summary> /// </summary>
public string Text { get; } public TextWrapping Wrapping
{
get => _wrapping;
set => Set(ref _wrapping, value);
}
/// <summary> /// <summary>
/// Gets platform-specific platform implementation. /// Gets platform-specific platform implementation.
/// </summary> /// </summary>
public IFormattedTextImpl PlatformImpl { get; private set; } public IFormattedTextImpl PlatformImpl
{
/// <summary> get
/// Gets the text alignment. {
/// </summary> if (_platformImpl == null)
public TextAlignment TextAlignment { get; } {
_platformImpl = _platform.CreateFormattedText(
/// <summary> _text,
/// Gets the text wrapping. _typeface,
/// </summary> _textAlignment,
public TextWrapping Wrapping { get; } _wrapping,
_constraint,
_spans);
}
return _platformImpl;
}
}
/// <summary> /// <summary>
/// Gets the lines in the text. /// Gets the lines in the text.
@ -174,15 +168,10 @@ namespace Avalonia.Media
return PlatformImpl.Size; return PlatformImpl.Size;
} }
/// <summary> private void Set<T>(ref T field, T value)
/// Sets the foreground brush for the specified text range.
/// </summary>
/// <param name="brush">The brush.</param>
/// <param name="startIndex">The start of the text range.</param>
/// <param name="length">The length of the text range.</param>
public void SetForegroundBrush(IBrush brush, int startIndex, int length)
{ {
PlatformImpl.SetForegroundBrush(brush, startIndex, length); field = value;
_platformImpl = null;
} }
} }
} }

21
src/Avalonia.Visuals/Media/FormattedTextStyleSpan.cs

@ -0,0 +1,21 @@
using System;
namespace Avalonia.Media
{
public class FormattedTextStyleSpan
{
public FormattedTextStyleSpan(
int startIndex,
int length,
IBrush foregroundBrush = null)
{
StartIndex = startIndex;
Length = length;
ForegroundBrush = foregroundBrush;
}
public int StartIndex { get; }
public int Length { get; }
public IBrush ForegroundBrush { get; }
}
}

35
src/Avalonia.Visuals/Media/Typeface.cs

@ -0,0 +1,35 @@
using System;
using Avalonia.Media;
namespace Avalonia.Media
{
public class Typeface
{
public Typeface(
string fontFamilyName,
double fontSize,
FontStyle style = FontStyle.Normal,
FontWeight weight = FontWeight.Normal)
{
if (fontSize <= 0)
{
throw new ArgumentException("Font size must be > 0.");
}
if (weight <= 0)
{
throw new ArgumentException("Font weight must be > 0.");
}
FontFamilyName = fontFamilyName;
FontSize = fontSize;
Style = style;
Weight = weight;
}
public string FontFamilyName { get; }
public double FontSize { get; }
public FontStyle Style { get; }
public FontWeight Weight { get; }
}
}

15
src/Avalonia.Visuals/Platform/IFormattedTextImpl.cs

@ -58,20 +58,5 @@ namespace Avalonia.Platform
/// <param name="length">The number of characters in the text range.</param> /// <param name="length">The number of characters in the text range.</param>
/// <returns>The character bounds.</returns> /// <returns>The character bounds.</returns>
IEnumerable<Rect> HitTestTextRange(int index, int length); IEnumerable<Rect> HitTestTextRange(int index, int length);
/// <summary>
/// Sets the foreground brush for the specified text range.
/// </summary>
/// <param name="brush">The brush.</param>
/// <param name="startIndex">The start of the text range.</param>
/// <param name="length">The length of the text range.</param>
void SetForegroundBrush(IBrush brush, int startIndex, int length);
/// <summary>
/// Makes a clone of the formatted text with the specified constraint.
/// </summary>
/// <param name="constraint">The constraint.</param>
/// <returns>The cloned formatted text.</returns>
IFormattedTextImpl WithConstraint(Size constraint);
} }
} }

14
src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs

@ -17,23 +17,19 @@ namespace Avalonia.Platform
/// Creates a formatted text implementation. /// Creates a formatted text implementation.
/// </summary> /// </summary>
/// <param name="text">The text.</param> /// <param name="text">The text.</param>
/// <param name="fontFamilyName">The font family.</param> /// <param name="typeface">The base typeface.</param>
/// <param name="fontSize">The font size.</param>
/// <param name="fontStyle">The font style.</param>
/// <param name="textAlignment">The text alignment.</param> /// <param name="textAlignment">The text alignment.</param>
/// <param name="fontWeight">The font weight.</param>
/// <param name="wrapping">The text wrapping mode.</param> /// <param name="wrapping">The text wrapping mode.</param>
/// <param name="constraint">The text layout constraints.</param> /// <param name="constraint">The text layout constraints.</param>
/// <param name="spans">The style spans.</param>
/// <returns>An <see cref="IFormattedTextImpl"/>.</returns> /// <returns>An <see cref="IFormattedTextImpl"/>.</returns>
IFormattedTextImpl CreateFormattedText( IFormattedTextImpl CreateFormattedText(
string text, string text,
string fontFamilyName, Typeface typeface,
double fontSize,
FontStyle fontStyle,
TextAlignment textAlignment, TextAlignment textAlignment,
FontWeight fontWeight,
TextWrapping wrapping, TextWrapping wrapping,
Size constraint); Size constraint,
IReadOnlyList<FormattedTextStyleSpan> spans);
/// <summary> /// <summary>
/// Creates a stream geometry implementation. /// Creates a stream geometry implementation.

23
src/Avalonia.Visuals/Rendering/RendererBase.cs

@ -7,11 +7,21 @@ namespace Avalonia.Rendering
{ {
public class RendererBase public class RendererBase
{ {
private static readonly Typeface s_fpsTypeface = new Typeface("Arial", 18);
private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
private int _framesThisSecond; private int _framesThisSecond;
private int _fps; private int _fps;
private FormattedText _fpsText;
private TimeSpan _lastFpsUpdate; private TimeSpan _lastFpsUpdate;
public RendererBase()
{
_fpsText = new FormattedText
{
Typeface = new Typeface(null, 18),
};
}
protected void RenderFps(IDrawingContextImpl context, Rect clientRect, bool incrementFrameCount) protected void RenderFps(IDrawingContextImpl context, Rect clientRect, bool incrementFrameCount)
{ {
var now = _stopwatch.Elapsed; var now = _stopwatch.Elapsed;
@ -29,20 +39,13 @@ namespace Avalonia.Rendering
_lastFpsUpdate = now; _lastFpsUpdate = now;
} }
var txt = new FormattedText( _fpsText.Text = string.Format("FPS: {0:000}", _fps);
string.Format("FPS: {0:000}", _fps), var size = _fpsText.Measure();
"Arial", 18,
Size.Infinity,
FontStyle.Normal,
TextAlignment.Left,
FontWeight.Normal,
TextWrapping.NoWrap);
var size = txt.Measure();
var rect = new Rect(clientRect.Right - size.Width, 0, size.Width, size.Height); var rect = new Rect(clientRect.Right - size.Width, 0, size.Width, size.Height);
context.Transform = Matrix.Identity; context.Transform = Matrix.Identity;
context.FillRectangle(Brushes.Black, rect); context.FillRectangle(Brushes.Black, rect);
context.DrawText(Brushes.White, rect.TopLeft, txt.PlatformImpl); context.DrawText(Brushes.White, rect.TopLeft, _fpsText.PlatformImpl);
} }
} }
} }

17
src/Gtk/Avalonia.Cairo/CairoPlatform.cs

@ -47,15 +47,20 @@ namespace Avalonia.Cairo
public IFormattedTextImpl CreateFormattedText( public IFormattedTextImpl CreateFormattedText(
string text, string text,
string fontFamily, Typeface typeface,
double fontSize,
FontStyle fontStyle,
TextAlignment textAlignment, TextAlignment textAlignment,
Avalonia.Media.FontWeight fontWeight,
TextWrapping wrapping, TextWrapping wrapping,
Size constraint) Size constraint,
IReadOnlyList<FormattedTextStyleSpan> spans)
{ {
return new FormattedTextImpl(s_pangoContext, text, fontFamily, fontSize, fontStyle, textAlignment, fontWeight, constraint); return new FormattedTextImpl(
s_pangoContext,
text,
typeface,
textAlignment,
wrapping,
constraint,
spans);
} }
public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces) public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)

111
src/Gtk/Avalonia.Cairo/Media/FormattedTextImpl.cs

@ -22,25 +22,48 @@ namespace Avalonia.Cairo.Media
public FormattedTextImpl( public FormattedTextImpl(
Pango.Context context, Pango.Context context,
string text, string text,
string fontFamily, Typeface typeface,
double fontSize,
FontStyle fontStyle,
TextAlignment textAlignment, TextAlignment textAlignment,
FontWeight fontWeight, TextWrapping wrapping,
Size constraint) Size constraint,
IReadOnlyList<FormattedTextStyleSpan> spans)
{ {
Contract.Requires<ArgumentNullException>(context != null); Contract.Requires<ArgumentNullException>(context != null);
Contract.Requires<ArgumentNullException>(text != null); Contract.Requires<ArgumentNullException>(text != null);
Layout = Create( Layout = new Pango.Layout(context);
context, Layout.SetText(text);
text,
fontFamily, Layout.FontDescription = new Pango.FontDescription
fontSize, {
(Pango.Style)fontStyle, Family = typeface?.FontFamilyName ?? "monospace",
textAlignment.ToCairo(), Size = Pango.Units.FromDouble(CorrectScale(typeface?.FontSize ?? 12)),
fontWeight.ToCairo(), Style = (Pango.Style)(typeface?.Style ?? FontStyle.Normal),
constraint); Weight = (typeface?.Weight ?? FontWeight.Normal).ToCairo(),
};
Layout.Alignment = textAlignment.ToCairo();
Layout.Attributes = new Pango.AttrList();
Layout.Width = double.IsPositiveInfinity(constraint.Width) ? -1 : Pango.Units.FromDouble(constraint.Width);
if (spans != null)
{
foreach (var span in spans)
{
if (span.ForegroundBrush is SolidColorBrush scb)
{
var color = new Pango.Color();
color.Parse(string.Format("#{0}", scb.Color.ToString().Substring(3)));
var brushAttr = new Pango.AttrForeground(color);
brushAttr.StartIndex = (uint)TextIndexToPangoIndex(span.StartIndex);
brushAttr.EndIndex = (uint)TextIndexToPangoIndex(span.StartIndex + span.Length);
this.Layout.Attributes.Insert(brushAttr);
}
}
}
Size = Measure(); Size = Measure();
} }
@ -111,66 +134,6 @@ namespace Avalonia.Cairo.Media
return ranges; return ranges;
} }
public void SetForegroundBrush(IBrush brush, int startIndex, int count)
{
var scb = brush as SolidColorBrush;
if (scb != null)
{
var color = new Pango.Color();
color.Parse(string.Format("#{0}", scb.Color.ToString().Substring(3)));
var brushAttr = new Pango.AttrForeground(color);
brushAttr.StartIndex = (uint)TextIndexToPangoIndex(startIndex);
brushAttr.EndIndex = (uint)TextIndexToPangoIndex(startIndex + count);
Layout.Attributes.Insert(brushAttr);
}
}
public IFormattedTextImpl WithConstraint(Size constraint)
{
return new FormattedTextImpl(Create(
Layout.Context,
Layout.Text,
Layout.FontDescription.Family,
Layout.FontDescription.Size,
Layout.FontDescription.Style,
Layout.Alignment,
Layout.FontDescription.Weight,
constraint));
}
private Pango.Layout Create(
Pango.Context context,
string text,
string fontFamily,
double fontSize,
Pango.Style fontStyle,
Pango.Alignment textAlignment,
Pango.Weight fontWeight,
Size constraint)
{
Contract.Requires<ArgumentNullException>(context != null);
Contract.Requires<ArgumentNullException>(text != null);
var result = new Pango.Layout(context);
result.SetText(text);
result.FontDescription = new Pango.FontDescription
{
Family = fontFamily,
Size = Pango.Units.FromDouble(CorrectScale(fontSize)),
Style = (Pango.Style)fontStyle,
Weight = fontWeight
};
result.Alignment = textAlignment;
result.Attributes = new Pango.AttrList();
result.Width = double.IsPositiveInfinity(constraint.Width) ? -1 : Pango.Units.FromDouble(constraint.Width);
return result;
}
private Size Measure() private Size Measure()
{ {
int width; int width;

75
src/Skia/Avalonia.Skia/FormattedTextImpl.cs

@ -12,17 +12,23 @@ namespace Avalonia.Skia
{ {
public class FormattedTextImpl : IFormattedTextImpl public class FormattedTextImpl : IFormattedTextImpl
{ {
public FormattedTextImpl(string text, string fontFamilyName, double fontSize, FontStyle fontStyle, public FormattedTextImpl(
TextAlignment textAlignment, FontWeight fontWeight, TextWrapping wrapping, Size constraint) string text,
Typeface typeface,
TextAlignment textAlignment,
TextWrapping wrapping,
Size constraint,
IReadOnlyList<FormattedTextStyleSpan> spans)
{ {
Text = text ?? string.Empty; Text = text ?? string.Empty;
_fontStyle = fontStyle;
_fontWeight = fontWeight;
// Replace 0 characters with zero-width spaces (200B) // Replace 0 characters with zero-width spaces (200B)
Text = Text.Replace((char)0, (char)0x200B); Text = Text.Replace((char)0, (char)0x200B);
var typeface = TypefaceCache.GetTypeface(fontFamilyName, fontStyle, fontWeight); var skiaTypeface = TypefaceCache.GetTypeface(
typeface?.FontFamilyName ?? "monospace",
typeface?.Style ?? FontStyle.Normal,
typeface?.Weight ?? FontWeight.Normal);
_paint = new SKPaint(); _paint = new SKPaint();
@ -33,14 +39,22 @@ namespace Avalonia.Skia
_paint.IsAntialias = true; _paint.IsAntialias = true;
_paint.LcdRenderText = true; _paint.LcdRenderText = true;
_paint.SubpixelText = true; _paint.SubpixelText = true;
_paint.Typeface = typeface; _paint.Typeface = skiaTypeface;
_paint.TextSize = (float)fontSize; _paint.TextSize = (float)(typeface?.FontSize ?? 12);
_paint.TextAlign = textAlignment.ToSKTextAlign(); _paint.TextAlign = textAlignment.ToSKTextAlign();
_paint.XferMode = SKXferMode.Src; _paint.XferMode = SKXferMode.Src;
_wrapping = wrapping; _wrapping = wrapping;
_constraint = constraint; _constraint = constraint;
foreach (var span in spans)
{
if (span.ForegroundBrush != null)
{
SetForegroundBrush(span.ForegroundBrush, span.StartIndex, span.Length);
}
}
Rebuild(); Rebuild();
} }
@ -149,40 +163,11 @@ namespace Avalonia.Skia
return result; return result;
} }
public void SetForegroundBrush(IBrush brush, int startIndex, int length)
{
var key = new FBrushRange(startIndex, length);
int index = _foregroundBrushes.FindIndex(v => v.Key.Equals(key));
if (index > -1)
{
_foregroundBrushes.RemoveAt(index);
}
if (brush != null)
{
_foregroundBrushes.Insert(0, new KeyValuePair<FBrushRange, IBrush>(key, brush));
}
}
public override string ToString() public override string ToString()
{ {
return Text; return Text;
} }
public IFormattedTextImpl WithConstraint(Size constraint)
{
return new FormattedTextImpl(
Text,
_paint.Typeface.FamilyName,
_paint.TextSize,
_fontStyle,
_paint.TextAlign.ToAvalonia(),
_fontWeight,
_wrapping,
constraint);
}
internal void Draw(DrawingContextImpl context, internal void Draw(DrawingContextImpl context,
SKCanvas canvas, SKPoint origin, SKCanvas canvas, SKPoint origin,
DrawingContextImpl.PaintWrapper foreground) DrawingContextImpl.PaintWrapper foreground)
@ -284,8 +269,6 @@ namespace Avalonia.Skia
private readonly List<Rect> _rects = new List<Rect>(); private readonly List<Rect> _rects = new List<Rect>();
public string Text { get; } public string Text { get; }
private readonly TextWrapping _wrapping; private readonly TextWrapping _wrapping;
private readonly FontStyle _fontStyle;
private readonly FontWeight _fontWeight;
private Size _constraint = new Size(double.PositiveInfinity, double.PositiveInfinity); private Size _constraint = new Size(double.PositiveInfinity, double.PositiveInfinity);
private float _lineHeight = 0; private float _lineHeight = 0;
private float _lineOffset = 0; private float _lineOffset = 0;
@ -618,6 +601,22 @@ namespace Avalonia.Skia
return x; return x;
} }
private void SetForegroundBrush(IBrush brush, int startIndex, int length)
{
var key = new FBrushRange(startIndex, length);
int index = _foregroundBrushes.FindIndex(v => v.Key.Equals(key));
if (index > -1)
{
_foregroundBrushes.RemoveAt(index);
}
if (brush != null)
{
_foregroundBrushes.Insert(0, new KeyValuePair<FBrushRange, IBrush>(key, brush));
}
}
private struct AvaloniaFormattedTextLine private struct AvaloniaFormattedTextLine
{ {
public float Height; public float Height;

11
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@ -16,10 +16,15 @@ namespace Avalonia.Skia
return CreateRenderTargetBitmap(width, height, 96, 96); return CreateRenderTargetBitmap(width, height, 96, 96);
} }
public IFormattedTextImpl CreateFormattedText(string text, string fontFamilyName, double fontSize, FontStyle fontStyle, public IFormattedTextImpl CreateFormattedText(
TextAlignment textAlignment, FontWeight fontWeight, TextWrapping wrapping, Size constraint) string text,
Typeface typeface,
TextAlignment textAlignment,
TextWrapping wrapping,
Size constraint,
IReadOnlyList<FormattedTextStyleSpan> spans)
{ {
return new FormattedTextImpl(text, fontFamilyName, fontSize, fontStyle, textAlignment, fontWeight, wrapping, constraint); return new FormattedTextImpl(text, typeface, textAlignment, wrapping, constraint, spans);
} }
public IStreamGeometryImpl CreateStreamGeometry() public IStreamGeometryImpl CreateStreamGeometry()

17
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@ -90,25 +90,22 @@ namespace Avalonia.Direct2D1
public IFormattedTextImpl CreateFormattedText( public IFormattedTextImpl CreateFormattedText(
string text, string text,
string fontFamily, Typeface typeface,
double fontSize,
FontStyle fontStyle,
TextAlignment textAlignment, TextAlignment textAlignment,
FontWeight fontWeight,
TextWrapping wrapping, TextWrapping wrapping,
Size constraint) Size constraint,
IReadOnlyList<FormattedTextStyleSpan> spans)
{ {
return new FormattedTextImpl( return new FormattedTextImpl(
text, text,
fontFamily, typeface,
fontSize,
fontStyle,
textAlignment, textAlignment,
fontWeight,
wrapping, wrapping,
constraint); constraint,
spans);
} }
public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces) public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)
{ {
var nativeWindow = surfaces?.OfType<IPlatformHandle>().FirstOrDefault(); var nativeWindow = surfaces?.OfType<IPlatformHandle>().FirstOrDefault();

113
src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs

@ -14,32 +14,45 @@ namespace Avalonia.Direct2D1.Media
{ {
public FormattedTextImpl( public FormattedTextImpl(
string text, string text,
string fontFamily, Typeface typeface,
double fontSize,
FontStyle fontStyle,
TextAlignment textAlignment, TextAlignment textAlignment,
FontWeight fontWeight,
TextWrapping wrapping, TextWrapping wrapping,
Size constraint) Size constraint,
IReadOnlyList<FormattedTextStyleSpan> spans)
{ {
Text = text; Text = text;
TextLayout = Create( var factory = AvaloniaLocator.Current.GetService<DWrite.Factory>();
text,
fontFamily, using (var format = new DWrite.TextFormat(
fontSize, factory,
(DWrite.FontStyle)fontStyle, typeface?.FontFamilyName ?? "Courier New",
textAlignment.ToDirect2D(), (DWrite.FontWeight)(typeface?.Weight ?? FontWeight.Normal),
(DWrite.FontWeight)fontWeight, (DWrite.FontStyle)(typeface?.Style ?? FontStyle.Normal),
wrapping == TextWrapping.Wrap ? DWrite.WordWrapping.Wrap : DWrite.WordWrapping.NoWrap, (float)(typeface?.FontSize ?? 12)))
(float)constraint.Width, {
(float)constraint.Height); format.WordWrapping = wrapping == TextWrapping.Wrap ?
Size = Measure(); DWrite.WordWrapping.Wrap :
} DWrite.WordWrapping.NoWrap;
TextLayout = new DWrite.TextLayout(
factory,
text ?? string.Empty,
format,
(float)constraint.Width,
(float)constraint.Height)
{
TextAlignment = textAlignment.ToDirect2D()
};
}
if (spans != null)
{
foreach (var span in spans)
{
ApplySpan(span);
}
}
public FormattedTextImpl(string text, DWrite.TextLayout textLayout)
{
Text = text;
TextLayout = textLayout;
Size = Measure(); Size = Measure();
} }
@ -101,58 +114,16 @@ namespace Avalonia.Direct2D1.Media
return result.Select(x => new Rect(x.Left, x.Top, x.Width, x.Height)); return result.Select(x => new Rect(x.Left, x.Top, x.Width, x.Height));
} }
public void SetForegroundBrush(IBrush brush, int startIndex, int count) private void ApplySpan(FormattedTextStyleSpan span)
{ {
TextLayout.SetDrawingEffect( if (span.Length > 0)
new BrushWrapper(brush),
new DWrite.TextRange(startIndex, count));
}
public IFormattedTextImpl WithConstraint(Size constraint)
{
var factory = AvaloniaLocator.Current.GetService<DWrite.Factory>();
return new FormattedTextImpl(Text, Create(
Text,
TextLayout.FontFamilyName,
TextLayout.FontSize,
TextLayout.FontStyle,
TextLayout.TextAlignment,
TextLayout.FontWeight,
TextLayout.WordWrapping,
(float)constraint.Width,
(float)constraint.Height));
}
private static DWrite.TextLayout Create(
string text,
string fontFamily,
double fontSize,
DWrite.FontStyle fontStyle,
DWrite.TextAlignment textAlignment,
DWrite.FontWeight fontWeight,
DWrite.WordWrapping wrapping,
float constraintX,
float constraintY)
{
var factory = AvaloniaLocator.Current.GetService<DWrite.Factory>();
using (var format = new DWrite.TextFormat(
factory,
fontFamily,
fontWeight,
fontStyle,
(float)fontSize))
{ {
format.WordWrapping = wrapping; if (span.ForegroundBrush != null)
{
var result = new DWrite.TextLayout( TextLayout.SetDrawingEffect(
factory, new BrushWrapper(span.ForegroundBrush),
text ?? string.Empty, new DWrite.TextRange(span.StartIndex, span.Length));
format, }
constraintX,
constraintY);
result.TextAlignment = textAlignment;
return result;
} }
} }

19
tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs

@ -157,23 +157,20 @@ namespace Avalonia.Layout.UnitTests
public IEnumerable<Rect> HitTestTextRange(int index, int length) => new Rect[0]; public IEnumerable<Rect> HitTestTextRange(int index, int length) => new Rect[0];
public Size Measure() => Constraint; public Size Measure() => Constraint;
public void SetForegroundBrush(IBrush brush, int startIndex, int length)
{
}
public IFormattedTextImpl WithConstraint(Size constraint)
{
return this;
}
} }
private void RegisterServices() private void RegisterServices()
{ {
var globalStyles = new Mock<IGlobalStyles>(); var globalStyles = new Mock<IGlobalStyles>();
var renderInterface = new Mock<IPlatformRenderInterface>(); var renderInterface = new Mock<IPlatformRenderInterface>();
renderInterface.Setup(x => x.CreateFormattedText(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<double>(), It.IsAny<FontStyle>(), renderInterface.Setup(x =>
It.IsAny<TextAlignment>(), It.IsAny<FontWeight>(), It.IsAny<TextWrapping>(), It.IsAny<Size>())) x.CreateFormattedText(
It.IsAny<string>(),
It.IsAny<Typeface>(),
It.IsAny<TextAlignment>(),
It.IsAny<TextWrapping>(),
It.IsAny<Size>(),
It.IsAny<IReadOnlyList<FormattedTextStyleSpan>>()))
.Returns(new FormattedTextMock("TEST")); .Returns(new FormattedTextMock("TEST"));
var windowImpl = new Mock<IWindowImpl>(); var windowImpl = new Mock<IWindowImpl>();

47
tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs

@ -50,38 +50,40 @@ namespace Avalonia.Direct2D1.RenderTests.Media
FontStyle fontStyle, FontStyle fontStyle,
TextAlignment textAlignment, TextAlignment textAlignment,
FontWeight fontWeight, FontWeight fontWeight,
TextWrapping wrapping) TextWrapping wrapping,
double widthConstraint)
{ {
var r = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>(); var r = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
return r.CreateFormattedText(text, return r.CreateFormattedText(text,
fontFamily, new Typeface(fontFamily, fontSize, fontStyle, fontWeight),
fontSize,
fontStyle,
textAlignment, textAlignment,
fontWeight,
wrapping, wrapping,
Size.Infinity); widthConstraint == -1 ? Size.Infinity : new Size(widthConstraint, double.PositiveInfinity),
null);
} }
private IFormattedTextImpl Create(string text, double fontSize) private IFormattedTextImpl Create(string text, double fontSize)
{ {
return Create(text, FontName, fontSize, return Create(text, FontName, fontSize,
FontStyle.Normal, TextAlignment.Left, FontStyle.Normal, TextAlignment.Left,
FontWeight.Normal, TextWrapping.NoWrap); FontWeight.Normal, TextWrapping.NoWrap,
-1);
} }
private IFormattedTextImpl Create(string text, double fontSize, TextAlignment alignment) private IFormattedTextImpl Create(string text, double fontSize, TextAlignment alignment, double widthConstraint)
{ {
return Create(text, FontName, fontSize, return Create(text, FontName, fontSize,
FontStyle.Normal, alignment, FontStyle.Normal, alignment,
FontWeight.Normal, TextWrapping.NoWrap); FontWeight.Normal, TextWrapping.NoWrap,
widthConstraint);
} }
private IFormattedTextImpl Create(string text, double fontSize, TextWrapping wrap) private IFormattedTextImpl Create(string text, double fontSize, TextWrapping wrap, double widthConstraint)
{ {
return Create(text, FontName, fontSize, return Create(text, FontName, fontSize,
FontStyle.Normal, TextAlignment.Left, FontStyle.Normal, TextAlignment.Left,
FontWeight.Normal, wrap); FontWeight.Normal, wrap,
widthConstraint);
} }
#if AVALONIA_CAIRO #if AVALONIA_CAIRO
@ -134,14 +136,9 @@ namespace Avalonia.Direct2D1.RenderTests.Media
double widthConstraint, double widthConstraint,
TextWrapping wrap) TextWrapping wrap)
{ {
var fmt = Create(input, FontSize, wrap); var fmt = Create(input, FontSize, wrap, widthConstraint);
var constrained = fmt; var constrained = fmt;
if (widthConstraint != -1)
{
constrained = fmt.WithConstraint(new Size(widthConstraint, 10000));
}
var lines = constrained.GetLines().ToArray(); var lines = constrained.GetLines().ToArray();
Assert.Equal(linesCount, lines.Count()); Assert.Equal(linesCount, lines.Count());
} }
@ -224,14 +221,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media
double x, double y, double width, double height) double x, double y, double width, double height)
{ {
//parse expected //parse expected
var fmt = Create(input, FontSize, TextAlignment.Right); var fmt = Create(input, FontSize, TextAlignment.Right, widthConstraint);
var constrained = fmt; var constrained = fmt;
if (widthConstraint != -1)
{
constrained = fmt.WithConstraint(new Size(widthConstraint, 100));
}
var r = constrained.HitTestTextPosition(index); var r = constrained.HitTestTextPosition(index);
Assert.Equal(x, r.X, 2); Assert.Equal(x, r.X, 2);
@ -253,14 +244,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media
double x, double y, double width, double height) double x, double y, double width, double height)
{ {
//parse expected //parse expected
var fmt = Create(input, FontSize, TextAlignment.Center); var fmt = Create(input, FontSize, TextAlignment.Center, widthConstraint);
var constrained = fmt; var constrained = fmt;
if (widthConstraint != -1)
{
constrained = fmt.WithConstraint(new Size(widthConstraint, 100));
}
var r = constrained.HitTestTextPosition(index); var r = constrained.HitTestTextPosition(index);
Assert.Equal(x, r.X, 2); Assert.Equal(x, r.X, 2);

12
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

@ -11,17 +11,13 @@ namespace Avalonia.UnitTests
{ {
public IFormattedTextImpl CreateFormattedText( public IFormattedTextImpl CreateFormattedText(
string text, string text,
string fontFamilyName, Typeface typeface,
double fontSize,
FontStyle fontStyle,
TextAlignment textAlignment, TextAlignment textAlignment,
FontWeight fontWeight,
TextWrapping wrapping, TextWrapping wrapping,
Size constraint) Size constraint,
IReadOnlyList<FormattedTextStyleSpan> spans)
{ {
var result = new Mock<IFormattedTextImpl>(); return Mock.Of<IFormattedTextImpl>();
result.Setup(x => x.WithConstraint(It.IsAny<Size>())).Returns(() => result.Object);
return result.Object;
} }
public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces) public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)

30
tests/Avalonia.Visuals.UnitTests/Media/FormattedTextTests.cs

@ -1,30 +0,0 @@
using System;
using Avalonia.Media;
using Xunit;
namespace Avalonia.Visuals.UnitTests.Media
{
public class FormattedTextTests
{
[Fact]
public void Exception_Should_Be_Thrown_If_FontSize_0()
{
Assert.Throws<ArgumentException>(() => new FormattedText(
"foo",
"Ariel",
0,
Size.Infinity));
}
[Fact]
public void Exception_Should_Be_Thrown_If_FontWeight_0()
{
Assert.Throws<ArgumentException>(() => new FormattedText(
"foo",
"Ariel",
12,
Size.Infinity,
fontWeight: 0));
}
}
}

21
tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs

@ -0,0 +1,21 @@
using System;
using Avalonia.Media;
using Xunit;
namespace Avalonia.Visuals.UnitTests.Media
{
public class TypefaceTests
{
[Fact]
public void Exception_Should_Be_Thrown_If_FontSize_0()
{
Assert.Throws<ArgumentException>(() => new Typeface("foo", 0));
}
[Fact]
public void Exception_Should_Be_Thrown_If_FontWeight_0()
{
Assert.Throws<ArgumentException>(() => new Typeface("foo", 12, weight: 0));
}
}
}

8
tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs

@ -10,13 +10,11 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
{ {
public IFormattedTextImpl CreateFormattedText( public IFormattedTextImpl CreateFormattedText(
string text, string text,
string fontFamilyName, Typeface typeface,
double fontSize,
FontStyle fontStyle,
TextAlignment textAlignment, TextAlignment textAlignment,
FontWeight fontWeight,
TextWrapping wrapping, TextWrapping wrapping,
Size constraint) Size constraint,
IReadOnlyList<FormattedTextStyleSpan> spans)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }

Loading…
Cancel
Save