From 376535b1984fcea9b2987a1b2668faa72b48fbaf Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 4 Apr 2017 00:08:42 +0200 Subject: [PATCH] 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. --- .../Presenters/TextPresenter.cs | 21 +-- .../Primitives/AccessText.cs | 18 +- src/Avalonia.Controls/TextBlock.cs | 19 +-- .../Adapters/GraphicsAdapter.cs | 6 +- src/Avalonia.Visuals/Avalonia.Visuals.csproj | 1 + src/Avalonia.Visuals/Media/FormattedText.cs | 157 ++++++++---------- .../Media/FormattedTextStyleSpan.cs | 21 +++ src/Avalonia.Visuals/Media/Typeface.cs | 35 ++++ .../Platform/IFormattedTextImpl.cs | 15 -- .../Platform/IPlatformRenderInterface.cs | 14 +- .../Rendering/RendererBase.cs | 23 +-- src/Gtk/Avalonia.Cairo/CairoPlatform.cs | 17 +- .../Avalonia.Cairo/Media/FormattedTextImpl.cs | 111 +++++-------- src/Skia/Avalonia.Skia/FormattedTextImpl.cs | 75 +++++---- .../Avalonia.Skia/PlatformRenderInterface.cs | 11 +- .../Avalonia.Direct2D1/Direct2D1Platform.cs | 17 +- .../Media/FormattedTextImpl.cs | 113 +++++-------- .../FullLayoutTests.cs | 19 +-- .../Media/FormattedTextImplTests.cs | 47 ++---- .../MockPlatformRenderInterface.cs | 12 +- .../Media/FormattedTextTests.cs | 30 ---- .../Media/TypefaceTests.cs | 21 +++ .../VisualTree/MockRenderInterface.cs | 8 +- 23 files changed, 374 insertions(+), 437 deletions(-) create mode 100644 src/Avalonia.Visuals/Media/FormattedTextStyleSpan.cs create mode 100644 src/Avalonia.Visuals/Media/Typeface.cs delete mode 100644 tests/Avalonia.Visuals.UnitTests/Media/FormattedTextTests.cs create mode 100644 tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 59f0d551b0..43088eb0ea 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/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(); } } diff --git a/src/Avalonia.Controls/Primitives/AccessText.cs b/src/Avalonia.Controls/Primitives/AccessText.cs index 29032ae430..4bb80e6d3f 100644 --- a/src/Avalonia.Controls/Primitives/AccessText.cs +++ b/src/Avalonia.Controls/Primitives/AccessText.cs @@ -85,16 +85,14 @@ namespace Avalonia.Controls.Primitives /// A object. 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, + }; } /// diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 1c8ed01e47..9d71dcfe73 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -350,17 +350,14 @@ namespace Avalonia.Controls /// A object. 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, + }; } /// diff --git a/src/Avalonia.HtmlRenderer/Adapters/GraphicsAdapter.cs b/src/Avalonia.HtmlRenderer/Adapters/GraphicsAdapter.cs index 72dd1d603c..93f8790741 100644 --- a/src/Avalonia.HtmlRenderer/Adapters/GraphicsAdapter.cs +++ b/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) diff --git a/src/Avalonia.Visuals/Avalonia.Visuals.csproj b/src/Avalonia.Visuals/Avalonia.Visuals.csproj index c820a83c2a..dd9a795937 100644 --- a/src/Avalonia.Visuals/Avalonia.Visuals.csproj +++ b/src/Avalonia.Visuals/Avalonia.Visuals.csproj @@ -2,6 +2,7 @@ netstandard1.1 false + Avalonia true diff --git a/src/Avalonia.Visuals/Media/FormattedText.cs b/src/Avalonia.Visuals/Media/FormattedText.cs index 1308091b2f..9a052f4eb9 100644 --- a/src/Avalonia.Visuals/Media/FormattedText.cs +++ b/src/Avalonia.Visuals/Media/FormattedText.cs @@ -12,64 +12,30 @@ namespace Avalonia.Media /// public class FormattedText { + private readonly IPlatformRenderInterface _platform; + private Size _constraint = Size.Infinity; + private IFormattedTextImpl _platformImpl; + private IReadOnlyList _spans; + private Typeface _typeface; + private string _text; + private TextAlignment _textAlignment; + private TextWrapping _wrapping; + /// /// Initializes a new instance of the class. /// - /// The text. - /// The font family. - /// The font size. - /// The text layout constraints. - /// The font style. - /// The text alignment. - /// The font weight. - /// The text wrapping mode. - 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(text != null); - Contract.Requires(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(); - - if (platform == null) - { - throw new Exception("Could not create FormattedText: IPlatformRenderInterface not registered."); - } + _platform = AvaloniaLocator.Current.GetService(); + } - PlatformImpl = platform.CreateFormattedText( - text, - fontFamilyName, - fontSize, - fontStyle, - textAlignment, - fontWeight, - wrapping, - constraint); + /// + /// Initializes a new instance of the class. + /// + /// The platform render interface. + public FormattedText(IPlatformRenderInterface platform) + { + _platform = platform; } /// @@ -77,49 +43,77 @@ namespace Avalonia.Media /// public Size Constraint { - get { return PlatformImpl.Constraint; } - set { PlatformImpl = PlatformImpl.WithConstraint(value); } + get => _constraint; + set => Set(ref _constraint, value); } /// - /// Gets the font family. + /// Gets or sets the base typeface. /// - public string FontFamilyName { get; } + public Typeface Typeface + { + get => _typeface; + set => Set(ref _typeface, value); + } /// - /// Gets the font size. + /// Gets or sets a collection of spans that describe the formatting of subsections of the + /// text. /// - public double FontSize { get; } + public IReadOnlyList Spans + { + get => _spans; + set => Set(ref _spans, value); + } /// - /// Gets the font style. + /// Gets or sets the text. /// - public FontStyle FontStyle { get; } + public string Text + { + get => _text; + set => Set(ref _text, value); + } /// - /// Gets the font weight. + /// Gets or sets the aligment of the text. /// - public FontWeight FontWeight { get; } + public TextAlignment TextAlignment + { + get => _textAlignment; + set => Set(ref _textAlignment, value); + } /// - /// Gets the text. + /// Gets or sets the text wrapping. /// - public string Text { get; } + public TextWrapping Wrapping + { + get => _wrapping; + set => Set(ref _wrapping, value); + } /// /// Gets platform-specific platform implementation. /// - public IFormattedTextImpl PlatformImpl { get; private set; } - - /// - /// Gets the text alignment. - /// - public TextAlignment TextAlignment { get; } - - /// - /// Gets the text wrapping. - /// - public TextWrapping Wrapping { get; } + public IFormattedTextImpl PlatformImpl + { + get + { + if (_platformImpl == null) + { + _platformImpl = _platform.CreateFormattedText( + _text, + _typeface, + _textAlignment, + _wrapping, + _constraint, + _spans); + } + + return _platformImpl; + } + } /// /// Gets the lines in the text. @@ -174,15 +168,10 @@ namespace Avalonia.Media return PlatformImpl.Size; } - /// - /// Sets the foreground brush for the specified text range. - /// - /// The brush. - /// The start of the text range. - /// The length of the text range. - public void SetForegroundBrush(IBrush brush, int startIndex, int length) + private void Set(ref T field, T value) { - PlatformImpl.SetForegroundBrush(brush, startIndex, length); + field = value; + _platformImpl = null; } } } diff --git a/src/Avalonia.Visuals/Media/FormattedTextStyleSpan.cs b/src/Avalonia.Visuals/Media/FormattedTextStyleSpan.cs new file mode 100644 index 0000000000..75bf76c996 --- /dev/null +++ b/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; } + } +} diff --git a/src/Avalonia.Visuals/Media/Typeface.cs b/src/Avalonia.Visuals/Media/Typeface.cs new file mode 100644 index 0000000000..b05d5d02e5 --- /dev/null +++ b/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; } + } +} diff --git a/src/Avalonia.Visuals/Platform/IFormattedTextImpl.cs b/src/Avalonia.Visuals/Platform/IFormattedTextImpl.cs index 9ffa3d6228..606da02c49 100644 --- a/src/Avalonia.Visuals/Platform/IFormattedTextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IFormattedTextImpl.cs @@ -58,20 +58,5 @@ namespace Avalonia.Platform /// The number of characters in the text range. /// The character bounds. IEnumerable HitTestTextRange(int index, int length); - - /// - /// Sets the foreground brush for the specified text range. - /// - /// The brush. - /// The start of the text range. - /// The length of the text range. - void SetForegroundBrush(IBrush brush, int startIndex, int length); - - /// - /// Makes a clone of the formatted text with the specified constraint. - /// - /// The constraint. - /// The cloned formatted text. - IFormattedTextImpl WithConstraint(Size constraint); } } diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs index 075d795a4c..aab8521f6d 100644 --- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs @@ -17,23 +17,19 @@ namespace Avalonia.Platform /// Creates a formatted text implementation. /// /// The text. - /// The font family. - /// The font size. - /// The font style. + /// The base typeface. /// The text alignment. - /// The font weight. /// The text wrapping mode. /// The text layout constraints. + /// The style spans. /// An . IFormattedTextImpl CreateFormattedText( string text, - string fontFamilyName, - double fontSize, - FontStyle fontStyle, + Typeface typeface, TextAlignment textAlignment, - FontWeight fontWeight, TextWrapping wrapping, - Size constraint); + Size constraint, + IReadOnlyList spans); /// /// Creates a stream geometry implementation. diff --git a/src/Avalonia.Visuals/Rendering/RendererBase.cs b/src/Avalonia.Visuals/Rendering/RendererBase.cs index ae15eff7fc..707b31998a 100644 --- a/src/Avalonia.Visuals/Rendering/RendererBase.cs +++ b/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); } } } diff --git a/src/Gtk/Avalonia.Cairo/CairoPlatform.cs b/src/Gtk/Avalonia.Cairo/CairoPlatform.cs index b28535f02c..934cf593b1 100644 --- a/src/Gtk/Avalonia.Cairo/CairoPlatform.cs +++ b/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 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 surfaces) diff --git a/src/Gtk/Avalonia.Cairo/Media/FormattedTextImpl.cs b/src/Gtk/Avalonia.Cairo/Media/FormattedTextImpl.cs index 1d1bde004f..b8e4d1efec 100644 --- a/src/Gtk/Avalonia.Cairo/Media/FormattedTextImpl.cs +++ b/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 spans) { Contract.Requires(context != null); Contract.Requires(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(context != null); - Contract.Requires(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; diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index 363dd01d1e..cca3ce7b63 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/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 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(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 _rects = new List(); 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(key, brush)); + } + } + private struct AvaloniaFormattedTextLine { public float Height; diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 33fd2adb51..aea1dea584 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/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 spans) { - return new FormattedTextImpl(text, fontFamilyName, fontSize, fontStyle, textAlignment, fontWeight, wrapping, constraint); + return new FormattedTextImpl(text, typeface, textAlignment, wrapping, constraint, spans); } public IStreamGeometryImpl CreateStreamGeometry() diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 68d69c9ffc..8e69a56faf 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/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 spans) { return new FormattedTextImpl( text, - fontFamily, - fontSize, - fontStyle, + typeface, textAlignment, - fontWeight, wrapping, - constraint); + constraint, + spans); } + public IRenderTarget CreateRenderTarget(IEnumerable surfaces) { var nativeWindow = surfaces?.OfType().FirstOrDefault(); diff --git a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs index 604022d1ec..5578abc32c 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs +++ b/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 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(); + + 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(); - 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(); - - 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)); + } } } diff --git a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs index 0ea6a840da..42bc43073b 100644 --- a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs +++ b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs @@ -157,23 +157,20 @@ namespace Avalonia.Layout.UnitTests public IEnumerable 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(); var renderInterface = new Mock(); - renderInterface.Setup(x => x.CreateFormattedText(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + renderInterface.Setup(x => + x.CreateFormattedText( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>())) .Returns(new FormattedTextMock("TEST")); var windowImpl = new Mock(); diff --git a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs b/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs index 8456d5a8f4..4f8aeb93ac 100644 --- a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs +++ b/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(); 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); diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index 83657dd401..8c6c949e07 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/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 spans) { - var result = new Mock(); - result.Setup(x => x.WithConstraint(It.IsAny())).Returns(() => result.Object); - return result.Object; + return Mock.Of(); } public IRenderTarget CreateRenderTarget(IEnumerable surfaces) diff --git a/tests/Avalonia.Visuals.UnitTests/Media/FormattedTextTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/FormattedTextTests.cs deleted file mode 100644 index f7b9c137ea..0000000000 --- a/tests/Avalonia.Visuals.UnitTests/Media/FormattedTextTests.cs +++ /dev/null @@ -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(() => new FormattedText( - "foo", - "Ariel", - 0, - Size.Infinity)); - } - - [Fact] - public void Exception_Should_Be_Thrown_If_FontWeight_0() - { - Assert.Throws(() => new FormattedText( - "foo", - "Ariel", - 12, - Size.Infinity, - fontWeight: 0)); - } - } -} diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs new file mode 100644 index 0000000000..6b8bf16fd9 --- /dev/null +++ b/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(() => new Typeface("foo", 0)); + } + + [Fact] + public void Exception_Should_Be_Thrown_If_FontWeight_0() + { + Assert.Throws(() => new Typeface("foo", 12, weight: 0)); + } + } +} diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs index e5cda62d27..5fcf1cf1f2 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs +++ b/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 spans) { throw new NotImplementedException(); }