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

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

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

19
src/Avalonia.Controls/TextBlock.cs

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

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

@ -117,7 +117,11 @@ namespace TheArtOfDev.HtmlRenderer.Avalonia.Adapters
FormattedText GetText(string str, RFont 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)

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

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

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

@ -12,64 +12,30 @@ namespace Avalonia.Media
/// </summary>
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>
/// Initializes a new instance of the <see cref="FormattedText"/> class.
/// </summary>
/// <param name="text">The text.</param>
/// <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)
public FormattedText()
{
Contract.Requires<ArgumentNullException>(text != null);
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.");
}
_platform = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
}
PlatformImpl = platform.CreateFormattedText(
text,
fontFamilyName,
fontSize,
fontStyle,
textAlignment,
fontWeight,
wrapping,
constraint);
/// <summary>
/// Initializes a new instance of the <see cref="FormattedText"/> class.
/// </summary>
/// <param name="platform">The platform render interface.</param>
public FormattedText(IPlatformRenderInterface platform)
{
_platform = platform;
}
/// <summary>
@ -77,49 +43,77 @@ namespace Avalonia.Media
/// </summary>
public Size Constraint
{
get { return PlatformImpl.Constraint; }
set { PlatformImpl = PlatformImpl.WithConstraint(value); }
get => _constraint;
set => Set(ref _constraint, value);
}
/// <summary>
/// Gets the font family.
/// Gets or sets the base typeface.
/// </summary>
public string FontFamilyName { get; }
public Typeface Typeface
{
get => _typeface;
set => Set(ref _typeface, value);
}
/// <summary>
/// Gets the font size.
/// Gets or sets a collection of spans that describe the formatting of subsections of the
/// text.
/// </summary>
public double FontSize { get; }
public IReadOnlyList<FormattedTextStyleSpan> Spans
{
get => _spans;
set => Set(ref _spans, value);
}
/// <summary>
/// Gets the font style.
/// Gets or sets the text.
/// </summary>
public FontStyle FontStyle { get; }
public string Text
{
get => _text;
set => Set(ref _text, value);
}
/// <summary>
/// Gets the font weight.
/// Gets or sets the aligment of the text.
/// </summary>
public FontWeight FontWeight { get; }
public TextAlignment TextAlignment
{
get => _textAlignment;
set => Set(ref _textAlignment, value);
}
/// <summary>
/// Gets the text.
/// Gets or sets the text wrapping.
/// </summary>
public string Text { get; }
public TextWrapping Wrapping
{
get => _wrapping;
set => Set(ref _wrapping, value);
}
/// <summary>
/// Gets platform-specific platform implementation.
/// </summary>
public IFormattedTextImpl PlatformImpl { get; private set; }
/// <summary>
/// Gets the text alignment.
/// </summary>
public TextAlignment TextAlignment { get; }
/// <summary>
/// Gets the text wrapping.
/// </summary>
public TextWrapping Wrapping { get; }
public IFormattedTextImpl PlatformImpl
{
get
{
if (_platformImpl == null)
{
_platformImpl = _platform.CreateFormattedText(
_text,
_typeface,
_textAlignment,
_wrapping,
_constraint,
_spans);
}
return _platformImpl;
}
}
/// <summary>
/// Gets the lines in the text.
@ -174,15 +168,10 @@ namespace Avalonia.Media
return PlatformImpl.Size;
}
/// <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>
public void SetForegroundBrush(IBrush brush, int startIndex, int length)
private void Set<T>(ref T field, T value)
{
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>
/// <returns>The character bounds.</returns>
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.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="fontFamilyName">The font family.</param>
/// <param name="fontSize">The font size.</param>
/// <param name="fontStyle">The font style.</param>
/// <param name="typeface">The base typeface.</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="constraint">The text layout constraints.</param>
/// <param name="spans">The style spans.</param>
/// <returns>An <see cref="IFormattedTextImpl"/>.</returns>
IFormattedTextImpl CreateFormattedText(
string text,
string fontFamilyName,
double fontSize,
FontStyle fontStyle,
Typeface typeface,
TextAlignment textAlignment,
FontWeight fontWeight,
TextWrapping wrapping,
Size constraint);
Size constraint,
IReadOnlyList<FormattedTextStyleSpan> spans);
/// <summary>
/// Creates a stream geometry implementation.

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

@ -7,11 +7,21 @@ namespace Avalonia.Rendering
{
public class RendererBase
{
private static readonly Typeface s_fpsTypeface = new Typeface("Arial", 18);
private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
private int _framesThisSecond;
private int _fps;
private FormattedText _fpsText;
private TimeSpan _lastFpsUpdate;
public RendererBase()
{
_fpsText = new FormattedText
{
Typeface = new Typeface(null, 18),
};
}
protected void RenderFps(IDrawingContextImpl context, Rect clientRect, bool incrementFrameCount)
{
var now = _stopwatch.Elapsed;
@ -29,20 +39,13 @@ namespace Avalonia.Rendering
_lastFpsUpdate = now;
}
var txt = new FormattedText(
string.Format("FPS: {0:000}", _fps),
"Arial", 18,
Size.Infinity,
FontStyle.Normal,
TextAlignment.Left,
FontWeight.Normal,
TextWrapping.NoWrap);
var size = txt.Measure();
_fpsText.Text = string.Format("FPS: {0:000}", _fps);
var size = _fpsText.Measure();
var rect = new Rect(clientRect.Right - size.Width, 0, size.Width, size.Height);
context.Transform = Matrix.Identity;
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(
string text,
string fontFamily,
double fontSize,
FontStyle fontStyle,
Typeface typeface,
TextAlignment textAlignment,
Avalonia.Media.FontWeight fontWeight,
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)

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

@ -22,25 +22,48 @@ namespace Avalonia.Cairo.Media
public FormattedTextImpl(
Pango.Context context,
string text,
string fontFamily,
double fontSize,
FontStyle fontStyle,
Typeface typeface,
TextAlignment textAlignment,
FontWeight fontWeight,
Size constraint)
TextWrapping wrapping,
Size constraint,
IReadOnlyList<FormattedTextStyleSpan> spans)
{
Contract.Requires<ArgumentNullException>(context != null);
Contract.Requires<ArgumentNullException>(text != null);
Layout = Create(
context,
text,
fontFamily,
fontSize,
(Pango.Style)fontStyle,
textAlignment.ToCairo(),
fontWeight.ToCairo(),
constraint);
Layout = new Pango.Layout(context);
Layout.SetText(text);
Layout.FontDescription = new Pango.FontDescription
{
Family = typeface?.FontFamilyName ?? "monospace",
Size = Pango.Units.FromDouble(CorrectScale(typeface?.FontSize ?? 12)),
Style = (Pango.Style)(typeface?.Style ?? FontStyle.Normal),
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();
}
@ -111,66 +134,6 @@ namespace Avalonia.Cairo.Media
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()
{
int width;

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

@ -12,17 +12,23 @@ namespace Avalonia.Skia
{
public class FormattedTextImpl : IFormattedTextImpl
{
public FormattedTextImpl(string text, string fontFamilyName, double fontSize, FontStyle fontStyle,
TextAlignment textAlignment, FontWeight fontWeight, TextWrapping wrapping, Size constraint)
public FormattedTextImpl(
string text,
Typeface typeface,
TextAlignment textAlignment,
TextWrapping wrapping,
Size constraint,
IReadOnlyList<FormattedTextStyleSpan> spans)
{
Text = text ?? string.Empty;
_fontStyle = fontStyle;
_fontWeight = fontWeight;
// Replace 0 characters with zero-width spaces (200B)
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();
@ -33,14 +39,22 @@ namespace Avalonia.Skia
_paint.IsAntialias = true;
_paint.LcdRenderText = true;
_paint.SubpixelText = true;
_paint.Typeface = typeface;
_paint.TextSize = (float)fontSize;
_paint.Typeface = skiaTypeface;
_paint.TextSize = (float)(typeface?.FontSize ?? 12);
_paint.TextAlign = textAlignment.ToSKTextAlign();
_paint.XferMode = SKXferMode.Src;
_wrapping = wrapping;
_constraint = constraint;
foreach (var span in spans)
{
if (span.ForegroundBrush != null)
{
SetForegroundBrush(span.ForegroundBrush, span.StartIndex, span.Length);
}
}
Rebuild();
}
@ -149,40 +163,11 @@ namespace Avalonia.Skia
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()
{
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,
SKCanvas canvas, SKPoint origin,
DrawingContextImpl.PaintWrapper foreground)
@ -284,8 +269,6 @@ namespace Avalonia.Skia
private readonly List<Rect> _rects = new List<Rect>();
public string Text { get; }
private readonly TextWrapping _wrapping;
private readonly FontStyle _fontStyle;
private readonly FontWeight _fontWeight;
private Size _constraint = new Size(double.PositiveInfinity, double.PositiveInfinity);
private float _lineHeight = 0;
private float _lineOffset = 0;
@ -618,6 +601,22 @@ namespace Avalonia.Skia
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
{
public float Height;

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

@ -16,10 +16,15 @@ namespace Avalonia.Skia
return CreateRenderTargetBitmap(width, height, 96, 96);
}
public IFormattedTextImpl CreateFormattedText(string text, string fontFamilyName, double fontSize, FontStyle fontStyle,
TextAlignment textAlignment, FontWeight fontWeight, TextWrapping wrapping, Size constraint)
public IFormattedTextImpl CreateFormattedText(
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()

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

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

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

@ -14,32 +14,45 @@ namespace Avalonia.Direct2D1.Media
{
public FormattedTextImpl(
string text,
string fontFamily,
double fontSize,
FontStyle fontStyle,
Typeface typeface,
TextAlignment textAlignment,
FontWeight fontWeight,
TextWrapping wrapping,
Size constraint)
Size constraint,
IReadOnlyList<FormattedTextStyleSpan> spans)
{
Text = text;
TextLayout = Create(
text,
fontFamily,
fontSize,
(DWrite.FontStyle)fontStyle,
textAlignment.ToDirect2D(),
(DWrite.FontWeight)fontWeight,
wrapping == TextWrapping.Wrap ? DWrite.WordWrapping.Wrap : DWrite.WordWrapping.NoWrap,
(float)constraint.Width,
(float)constraint.Height);
Size = Measure();
}
var factory = AvaloniaLocator.Current.GetService<DWrite.Factory>();
using (var format = new DWrite.TextFormat(
factory,
typeface?.FontFamilyName ?? "Courier New",
(DWrite.FontWeight)(typeface?.Weight ?? FontWeight.Normal),
(DWrite.FontStyle)(typeface?.Style ?? FontStyle.Normal),
(float)(typeface?.FontSize ?? 12)))
{
format.WordWrapping = wrapping == TextWrapping.Wrap ?
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();
}
@ -101,58 +114,16 @@ namespace Avalonia.Direct2D1.Media
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(
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))
if (span.Length > 0)
{
format.WordWrapping = wrapping;
var result = new DWrite.TextLayout(
factory,
text ?? string.Empty,
format,
constraintX,
constraintY);
result.TextAlignment = textAlignment;
return result;
if (span.ForegroundBrush != null)
{
TextLayout.SetDrawingEffect(
new BrushWrapper(span.ForegroundBrush),
new DWrite.TextRange(span.StartIndex, span.Length));
}
}
}

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 Size Measure() => Constraint;
public void SetForegroundBrush(IBrush brush, int startIndex, int length)
{
}
public IFormattedTextImpl WithConstraint(Size constraint)
{
return this;
}
}
private void RegisterServices()
{
var globalStyles = new Mock<IGlobalStyles>();
var renderInterface = new Mock<IPlatformRenderInterface>();
renderInterface.Setup(x => x.CreateFormattedText(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<double>(), It.IsAny<FontStyle>(),
It.IsAny<TextAlignment>(), It.IsAny<FontWeight>(), It.IsAny<TextWrapping>(), It.IsAny<Size>()))
renderInterface.Setup(x =>
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"));
var windowImpl = new Mock<IWindowImpl>();

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

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

12
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

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

Loading…
Cancel
Save