diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 39759f78f1..a2b5f3f8b4 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -127,11 +127,11 @@ namespace Avalonia.Controls.Presenters base.Render(context); if (selectionStart == selectionEnd) - { + { var backgroundColor = (((Control)TemplatedParent).GetValue(BackgroundProperty) as SolidColorBrush)?.Color; var caretBrush = Brushes.Black; - if(backgroundColor.HasValue) + if (backgroundColor.HasValue) { byte red = (byte)~(backgroundColor.Value.R); byte green = (byte)~(backgroundColor.Value.G); @@ -139,7 +139,7 @@ namespace Avalonia.Controls.Presenters caretBrush = new SolidColorBrush(Color.FromRgb(red, green, blue)); } - + if (_caretBlink) { var charPos = FormattedText.HitTestTextPosition(CaretIndex); @@ -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,17 +231,13 @@ namespace Avalonia.Controls.Presenters } else { - // TODO: Pretty sure that measuring "X" isn't the right way to do this... - using (var formattedText = new FormattedText( - "X", - FontFamily, - FontSize, - FontStyle, - TextAlignment, - FontWeight)) + return new FormattedText { - return formattedText.Measure(); - } + 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 42a6f9123b..4bb80e6d3f 100644 --- a/src/Avalonia.Controls/Primitives/AccessText.cs +++ b/src/Avalonia.Controls/Primitives/AccessText.cs @@ -85,15 +85,14 @@ namespace Avalonia.Controls.Primitives /// A object. protected override FormattedText CreateFormattedText(Size constraint) { - var result = new FormattedText( - StripAccessKey(Text), - FontFamily, - FontSize, - 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 028df729a5..c7a77bdf0e 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -116,7 +116,7 @@ namespace Avalonia.Controls this.GetObservable(TextAlignmentProperty).Select(_ => Unit.Default), this.GetObservable(FontSizeProperty).Select(_ => Unit.Default), this.GetObservable(FontStyleProperty).Select(_ => Unit.Default), - this.GetObservable(FontWeightProperty).Select(_=>Unit.Default)) + this.GetObservable(FontWeightProperty).Select(_ => Unit.Default)) .Subscribe(_ => { InvalidateFormattedText(); @@ -350,16 +350,14 @@ namespace Avalonia.Controls /// A object. protected virtual FormattedText CreateFormattedText(Size constraint) { - var result = new FormattedText( - Text ?? string.Empty, - FontFamily, - FontSize, - 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, + }; } /// @@ -370,7 +368,6 @@ namespace Avalonia.Controls if (_formattedText != null) { _constraint = _formattedText.Constraint; - _formattedText.Dispose(); _formattedText = null; } diff --git a/src/Avalonia.HtmlRenderer/Adapters/GraphicsAdapter.cs b/src/Avalonia.HtmlRenderer/Adapters/GraphicsAdapter.cs index c9dd2a786c..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, 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/Media/FormattedText.cs b/src/Avalonia.Visuals/Media/FormattedText.cs index f6aeaf5f5f..9a052f4eb9 100644 --- a/src/Avalonia.Visuals/Media/FormattedText.cs +++ b/src/Avalonia.Visuals/Media/FormattedText.cs @@ -10,63 +10,32 @@ namespace Avalonia.Media /// /// Represents a piece of text with formatting. /// - public class FormattedText : AvaloniaDisposable + 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 font style. - /// The text alignment. - /// The font weight. - /// The text wrapping mode. - public FormattedText( - string text, - string fontFamilyName, - double fontSize, - 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); + /// + /// Initializes a new instance of the class. + /// + /// The platform render interface. + public FormattedText(IPlatformRenderInterface platform) + { + _platform = platform; } /// @@ -74,64 +43,76 @@ namespace Avalonia.Media /// public Size Constraint { - get - { - CheckDisposed(); - return PlatformImpl.Constraint; - } - set - { - CheckDisposed(); - PlatformImpl.Constraint = 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; } - - /// - /// Gets the text alignment. - /// - public TextAlignment TextAlignment { get; } - - /// - /// Gets the text wrapping. - /// - public TextWrapping Wrapping { get; } - - /// - /// Disposes of unmanaged resources associated with the formatted text. - /// - protected override void DoDispose() + public IFormattedTextImpl PlatformImpl { - PlatformImpl.Dispose(); + get + { + if (_platformImpl == null) + { + _platformImpl = _platform.CreateFormattedText( + _text, + _typeface, + _textAlignment, + _wrapping, + _constraint, + _spans); + } + + return _platformImpl; + } } /// @@ -142,7 +123,6 @@ namespace Avalonia.Media /// public IEnumerable GetLines() { - CheckDisposed(); return PlatformImpl.GetLines(); } @@ -155,7 +135,6 @@ namespace Avalonia.Media /// public TextHitTestResult HitTestPoint(Point point) { - CheckDisposed(); return PlatformImpl.HitTestPoint(point); } @@ -166,7 +145,6 @@ namespace Avalonia.Media /// The character bounds. public Rect HitTestTextPosition(int index) { - CheckDisposed(); return PlatformImpl.HitTestTextPosition(index); } @@ -178,7 +156,6 @@ namespace Avalonia.Media /// The character bounds. public IEnumerable HitTestTextRange(int index, int length) { - CheckDisposed(); return PlatformImpl.HitTestTextRange(index, length); } @@ -188,20 +165,13 @@ namespace Avalonia.Media /// The bounds box of the text. public Size Measure() { - CheckDisposed(); - return PlatformImpl.Measure(); + 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) { - CheckDisposed(); - 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..96a5c05c74 --- /dev/null +++ b/src/Avalonia.Visuals/Media/FormattedTextStyleSpan.cs @@ -0,0 +1,41 @@ +using System; + +namespace Avalonia.Media +{ + /// + /// Describes the formatting for a span of text in a object. + /// + public class FormattedTextStyleSpan + { + /// + /// Initializes a new instance of the class. + /// + /// The index of the first character in the span. + /// The length of the span. + /// The span's foreground brush. + public FormattedTextStyleSpan( + int startIndex, + int length, + IBrush foregroundBrush = null) + { + StartIndex = startIndex; + Length = length; + ForegroundBrush = foregroundBrush; + } + + /// + /// Gets the index of the first character in the span. + /// + public int StartIndex { get; } + + /// + /// Gets the length of the span. + /// + public int Length { get; } + + /// + /// Gets the span's foreground brush. + /// + public IBrush ForegroundBrush { get; } + } +} diff --git a/src/Avalonia.Visuals/Media/Geometry.cs b/src/Avalonia.Visuals/Media/Geometry.cs index 092aea0c61..591c7d0468 100644 --- a/src/Avalonia.Visuals/Media/Geometry.cs +++ b/src/Avalonia.Visuals/Media/Geometry.cs @@ -22,10 +22,7 @@ namespace Avalonia.Media /// static Geometry() { - TransformProperty.Changed.Subscribe(x => - { - ((Geometry)x.Sender).PlatformImpl.Transform = ((Transform)x.NewValue).Value; - }); + TransformProperty.Changed.AddClassHandler(x => x.TransformChanged); } /// @@ -68,7 +65,7 @@ namespace Avalonia.Media } /// - /// Indicates whether the geometry contains the specified point. + /// Indicates whether the geometry's fill contains the specified point. /// /// The point. /// true if the geometry contains the point; otherwise, false. @@ -76,5 +73,22 @@ namespace Avalonia.Media { return PlatformImpl.FillContains(point); } + + /// + /// Indicates whether the geometry's stroke contains the specified point. + /// + /// The pen to use. + /// The point. + /// true if the geometry contains the point; otherwise, false. + public bool StrokeContains(Pen pen, Point point) + { + return PlatformImpl.StrokeContains(pen, point); + } + + private void TransformChanged(AvaloniaPropertyChangedEventArgs e) + { + var transform = (Transform)e.NewValue; + PlatformImpl = PlatformImpl.WithTransform(transform.Value); + } } } diff --git a/src/Avalonia.Visuals/Media/Typeface.cs b/src/Avalonia.Visuals/Media/Typeface.cs new file mode 100644 index 0000000000..12540b67e7 --- /dev/null +++ b/src/Avalonia.Visuals/Media/Typeface.cs @@ -0,0 +1,59 @@ +using System; + +namespace Avalonia.Media +{ + /// + /// Represents a typeface. + /// + public class Typeface + { + /// + /// Initializes a new instance of the class. + /// + /// The name of the font family. + /// The font size, in DIPs. + /// The font style. + /// The font weight. + 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; + } + + /// + /// Gets the name of the font family. + /// + public string FontFamilyName { get; } + + /// + /// Gets the size of the font in DIPs. + /// + public double FontSize { get; } + + /// + /// Gets the font style. + /// + public FontStyle Style { get; } + + /// + /// Gets the font weight. + /// + public FontWeight Weight { get; } + } +} diff --git a/src/Avalonia.Visuals/Platform/IFormattedTextImpl.cs b/src/Avalonia.Visuals/Platform/IFormattedTextImpl.cs index e3a44c437e..606da02c49 100644 --- a/src/Avalonia.Visuals/Platform/IFormattedTextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IFormattedTextImpl.cs @@ -10,12 +10,17 @@ namespace Avalonia.Platform /// /// Defines the platform-specific interface for . /// - public interface IFormattedTextImpl : IDisposable + public interface IFormattedTextImpl { /// - /// Gets or sets the constraint of the text. + /// Gets the constraint of the text. /// - Size Constraint { get; set; } + Size Constraint { get; } + + /// + /// The measured size of the text. + /// + Size Size { get; } /// /// Gets the text. @@ -53,19 +58,5 @@ namespace Avalonia.Platform /// The number of characters in the text range. /// The character bounds. IEnumerable HitTestTextRange(int index, int length); - - /// - /// Gets the size of the text, taking into account. - /// - /// The bounds box of the text. - Size Measure(); - - /// - /// 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); } } diff --git a/src/Avalonia.Visuals/Platform/IGeometryImpl.cs b/src/Avalonia.Visuals/Platform/IGeometryImpl.cs index e18a76d739..132e00e56b 100644 --- a/src/Avalonia.Visuals/Platform/IGeometryImpl.cs +++ b/src/Avalonia.Visuals/Platform/IGeometryImpl.cs @@ -1,6 +1,8 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using Avalonia.Media; + namespace Avalonia.Platform { /// @@ -14,9 +16,9 @@ namespace Avalonia.Platform Rect Bounds { get; } /// - /// Gets or sets a transform to apply to the geometry. + /// Gets the transform to applied to the geometry. /// - Matrix Transform { get; set; } + Matrix Transform { get; } /// /// Gets the geometry's bounding rectangle with the specified stroke thickness. @@ -26,10 +28,32 @@ namespace Avalonia.Platform Rect GetRenderBounds(double strokeThickness); /// - /// Indicates whether the geometry contains the specified point. + /// Indicates whether the geometry's fill contains the specified point. /// /// The point. /// true if the geometry contains the point; otherwise, false. bool FillContains(Point point); + + /// + /// Intersects the geometry with another geometry. + /// + /// The other geometry. + /// A new representing the intersection. + IGeometryImpl Intersect(IGeometryImpl geometry); + + /// + /// Indicates whether the geometry's stroke contains the specified point. + /// + /// The stroke to use. + /// The point. + /// true if the geometry contains the point; otherwise, false. + bool StrokeContains(Pen pen, Point point); + + /// + /// Makes a clone of the geometry with the specified transform. + /// + /// The transform. + /// The cloned geometry. + IGeometryImpl WithTransform(Matrix transform); } } diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs index ef58d52b4f..ba7b758484 100644 --- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs @@ -17,21 +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); + TextWrapping wrapping, + Size constraint, + IReadOnlyList spans); /// /// Creates a stream geometry implementation. diff --git a/src/Avalonia.Visuals/Rendering/RendererMixin.cs b/src/Avalonia.Visuals/Rendering/RendererMixin.cs index 3d13501edd..a80f89930b 100644 --- a/src/Avalonia.Visuals/Rendering/RendererMixin.cs +++ b/src/Avalonia.Visuals/Rendering/RendererMixin.cs @@ -58,16 +58,13 @@ namespace Avalonia.Rendering s_lastMeasure = now; } var pt = new Point(40, 40); - using ( - var txt = new FormattedText("Frame #" + s_frameNum + " FPS: " + s_fps, "Arial", 18, - FontStyle.Normal, - TextAlignment.Left, - FontWeight.Normal, - TextWrapping.NoWrap)) + var txt = new FormattedText { - ctx.FillRectangle(Brushes.White, new Rect(pt, txt.Measure())); - ctx.DrawText(Brushes.Black, pt, txt); - } + Text = "Frame #" + s_frameNum + " FPS: " + s_fps, + Typeface = new Typeface("Arial", 18) + }; + ctx.FillRectangle(Brushes.White, new Rect(pt, txt.Measure())); + ctx.DrawText(Brushes.Black, pt, txt); } } } diff --git a/src/Gtk/Avalonia.Cairo/CairoPlatform.cs b/src/Gtk/Avalonia.Cairo/CairoPlatform.cs index 9cf16312e9..dde92d5870 100644 --- a/src/Gtk/Avalonia.Cairo/CairoPlatform.cs +++ b/src/Gtk/Avalonia.Cairo/CairoPlatform.cs @@ -42,14 +42,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) + TextWrapping wrapping, + Size constraint, + IReadOnlyList spans) { - return new FormattedTextImpl(s_pangoContext, text, fontFamily, fontSize, fontStyle, textAlignment, fontWeight); + 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 6c023c212e..b8e4d1efec 100644 --- a/src/Gtk/Avalonia.Cairo/Media/FormattedTextImpl.cs +++ b/src/Gtk/Avalonia.Cairo/Media/FormattedTextImpl.cs @@ -12,7 +12,7 @@ namespace Avalonia.Cairo.Media { public class FormattedTextImpl : IFormattedTextImpl { - private Size _size; + private Size _constraint; static double CorrectScale(double input) { @@ -22,54 +22,64 @@ namespace Avalonia.Cairo.Media public FormattedTextImpl( Pango.Context context, string text, - string fontFamily, - double fontSize, - FontStyle fontStyle, + Typeface typeface, TextAlignment textAlignment, - FontWeight fontWeight) + TextWrapping wrapping, + Size constraint, + IReadOnlyList spans) { Contract.Requires(context != null); Contract.Requires(text != null); + Layout = new Pango.Layout(context); Layout.SetText(text); + Layout.FontDescription = new Pango.FontDescription { - Family = fontFamily, - Size = Pango.Units.FromDouble(CorrectScale(fontSize)), - Style = (Pango.Style)fontStyle, - Weight = fontWeight.ToCairo() + 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); - public string Text => Layout.Text; - - public Size Constraint - { - get + if (spans != null) { - return _size; + 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); + } + } } - set - { - _size = value; - Layout.Width = double.IsPositiveInfinity(value.Width) ? - -1 : Pango.Units.FromDouble(value.Width); - } + Size = Measure(); } - public Pango.Layout Layout + public FormattedTextImpl(Pango.Layout layout) { - get; + Layout = layout; + Size = Measure(); } - public void Dispose() - { - Layout.Dispose(); - } + public string Text => Layout.Text; + + public Size Constraint => _constraint; + + public Size Size { get; } + + public Pango.Layout Layout { get; } public IEnumerable GetLines() { @@ -124,7 +134,7 @@ namespace Avalonia.Cairo.Media return ranges; } - public Size Measure() + private Size Measure() { int width; int height; @@ -132,22 +142,5 @@ namespace Avalonia.Cairo.Media return new Size(width, height); } - - 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); - } - } } } diff --git a/src/Gtk/Avalonia.Cairo/Media/StreamGeometryContextImpl.cs b/src/Gtk/Avalonia.Cairo/Media/StreamGeometryContextImpl.cs index 0aca4b0b33..ca16ab5630 100644 --- a/src/Gtk/Avalonia.Cairo/Media/StreamGeometryContextImpl.cs +++ b/src/Gtk/Avalonia.Cairo/Media/StreamGeometryContextImpl.cs @@ -71,6 +71,15 @@ namespace Avalonia.Cairo.Media } } + internal bool StrokeContains(Pen pen, Point point) + { + using (var context = new Cairo.Context(new Cairo.ImageSurface(Cairo.Format.Argb32, 0, 0))) + { + context.AppendPath(Path); + return context.InStroke(point.X, point.Y); + } + } + public void LineTo(Point point) { if (this.Path == null) diff --git a/src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs b/src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs index c325ebab14..b0a8cd4fb0 100644 --- a/src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs +++ b/src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs @@ -35,19 +35,11 @@ namespace Avalonia.Cairo.Media private readonly StreamGeometryContextImpl _impl; private Matrix _transform = Matrix.Identity; + public Matrix Transform { get { return _transform; } - set - { - if (value != Transform) - { - if (!value.IsIdentity) - { - _transform = value; - } - } - } + private set { _transform = value; } } public FillRule FillRule { get; set; } @@ -60,7 +52,7 @@ namespace Avalonia.Cairo.Media public Rect GetRenderBounds(double strokeThickness) { // TODO: Calculate properly. - return Bounds.Inflate(strokeThickness); + return Bounds.TransformToAABB(Transform).Inflate(strokeThickness); } public IStreamGeometryContextImpl Open() @@ -72,5 +64,23 @@ namespace Avalonia.Cairo.Media { return _impl.FillContains(point); } + + public IGeometryImpl Intersect(IGeometryImpl geometry) + { + throw new NotImplementedException(); + } + + public bool StrokeContains(Pen pen, Point point) + { + return _impl.StrokeContains(pen, point); + } + + /// + public IGeometryImpl WithTransform(Matrix transform) + { + var result = (StreamGeometryImpl)Clone(); + result.Transform = transform; + return result; + } } } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index eade31523d..6d29e9f70c 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -292,7 +292,7 @@ namespace Avalonia.Skia public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text) { - using (var paint = CreatePaint(foreground, text.Measure())) + using (var paint = CreatePaint(foreground, text.Size)) { var textImpl = (FormattedTextImpl)text; textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint); diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index e427dc0f58..133d9cd789 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -12,15 +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) + public FormattedTextImpl( + string text, + Typeface typeface, + TextAlignment textAlignment, + TextWrapping wrapping, + Size constraint, + IReadOnlyList spans) { Text = text ?? string.Empty; // 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(); @@ -31,34 +39,31 @@ 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; - Rebuild(); - } - - public Size Constraint - { - get { return _constraint; } - set + if (spans != null) { - if (_constraint == value) - return; - - _constraint = value; - - Rebuild(); + foreach (var span in spans) + { + if (span.ForegroundBrush != null) + { + SetForegroundBrush(span.ForegroundBrush, span.StartIndex, span.Length); + } + } } + + Rebuild(); } - public string Text { get; } + public Size Constraint => _constraint; - public void Dispose() - { - } + public Size Size => _size; public IEnumerable GetLines() { @@ -161,27 +166,6 @@ namespace Avalonia.Skia return result; } - public Size Measure() - { - return _size; - } - - 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; @@ -286,6 +270,7 @@ namespace Avalonia.Skia private readonly List _lines = new List(); private readonly SKPaint _paint; private readonly List _rects = new List(); + public string Text { get; } private readonly TextWrapping _wrapping; private Size _constraint = new Size(double.PositiveInfinity, double.PositiveInfinity); private float _lineHeight = 0; @@ -619,6 +604,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 72f8d08d44..6912c52515 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -17,10 +17,15 @@ namespace Avalonia.Skia return CreateRenderTargetBitmap(width, height); } - public IFormattedTextImpl CreateFormattedText(string text, string fontFamilyName, double fontSize, FontStyle fontStyle, - TextAlignment textAlignment, FontWeight fontWeight, TextWrapping wrapping) + 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); + return new FormattedTextImpl(text, typeface, textAlignment, wrapping, constraint, spans); } public IStreamGeometryImpl CreateStreamGeometry() diff --git a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs index 33d4ce10c7..9a0a1dc434 100644 --- a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs @@ -13,16 +13,15 @@ namespace Avalonia.Skia class StreamGeometryImpl : IStreamGeometryImpl { SKPath _path; - SKPath _transformedPath; private Matrix _transform = Matrix.Identity; - public SKPath EffectivePath => (_transformedPath ?? _path); + public SKPath EffectivePath => _path; public Rect GetRenderBounds(double strokeThickness) { // TODO: Calculate properly. - return Bounds.Inflate(strokeThickness); + return Bounds.TransformToAABB(Transform).Inflate(strokeThickness); } public Rect Bounds { get; private set; } @@ -30,32 +29,6 @@ namespace Avalonia.Skia public Matrix Transform { get { return _transform; } - set - { - if (_transform == value) - return; - - _transform = value; - ApplyTransform(); - } - } - - void ApplyTransform() - { - if (_path == null) - return; - - if (_transformedPath != null) - { - _transformedPath.Dispose(); - _transformedPath = null; - } - - if (!Transform.IsIdentity) - { - _transformedPath = new SKPath(_path); - _transformedPath.Transform(Transform.ToSKMatrix()); - } } public IStreamGeometryImpl Clone() @@ -63,7 +36,6 @@ namespace Avalonia.Skia return new StreamGeometryImpl { _path = _path?.Clone(), - _transformedPath = _transformedPath?.Clone(), _transform = Transform, Bounds = Bounds }; @@ -84,6 +56,36 @@ namespace Avalonia.Skia return GetRenderBounds(0).Contains(point); } + public bool StrokeContains(Pen pen, Point point) + { + // TODO: Not supported by SkiaSharp yet, so use expanded Rect + // return EffectivePath.Contains(point.X, point.Y); + return GetRenderBounds(0).Contains(point); + } + + public IGeometryImpl Intersect(IGeometryImpl geometry) + { + throw new NotImplementedException(); + } + + public IGeometryImpl WithTransform(Matrix transform) + { + var result = (StreamGeometryImpl)Clone(); + + if (result.Transform != Matrix.Identity) + { + result._path.Transform(result.Transform.Invert().ToSKMatrix()); + } + + if (transform != Matrix.Identity) + { + result._path.Transform(transform.ToSKMatrix()); + } + + result._transform = transform; + return result; + } + class StreamContext : IStreamGeometryContextImpl { private readonly StreamGeometryImpl _geometryImpl; @@ -100,7 +102,6 @@ namespace Avalonia.Skia { SKRect rc; _path.GetBounds(out rc); - _geometryImpl.ApplyTransform(); _geometryImpl.Bounds = rc.ToAvaloniaRect(); } diff --git a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj index b2f65ef45f..ffba7a11ff 100644 --- a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj +++ b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj @@ -68,6 +68,7 @@ + diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index d5b0f22090..2b6a4e9844 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -88,14 +88,19 @@ namespace Avalonia.Direct2D1 public IFormattedTextImpl CreateFormattedText( string text, - string fontFamily, - double fontSize, - FontStyle fontStyle, + Typeface typeface, TextAlignment textAlignment, - FontWeight fontWeight, - TextWrapping wrapping) + TextWrapping wrapping, + Size constraint, + IReadOnlyList spans) { - return new FormattedTextImpl(text, fontFamily, fontSize, fontStyle, textAlignment, fontWeight, wrapping); + return new FormattedTextImpl( + text, + typeface, + textAlignment, + wrapping, + constraint, + spans); } public IRenderer CreateRenderer(IRenderRoot root, IRenderLoop renderLoop) diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 2c5ad778c2..332670681a 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -207,7 +207,7 @@ namespace Avalonia.Direct2D1.Media { var impl = (FormattedTextImpl)text; - using (var brush = CreateBrush(foreground, impl.Measure())) + using (var brush = CreateBrush(foreground, impl.Size)) using (var renderer = new AvaloniaTextRenderer(this, _renderTarget, brush.PlatformBrush)) { if (brush.PlatformBrush != null) diff --git a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs index ae226a5087..5578abc32c 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs @@ -14,51 +14,52 @@ namespace Avalonia.Direct2D1.Media { public FormattedTextImpl( string text, - string fontFamily, - double fontSize, - FontStyle fontStyle, + Typeface typeface, TextAlignment textAlignment, - FontWeight fontWeight, - TextWrapping wrapping) + TextWrapping wrapping, + Size constraint, + IReadOnlyList spans) { + Text = text; var factory = AvaloniaLocator.Current.GetService(); using (var format = new DWrite.TextFormat( factory, - fontFamily, - (DWrite.FontWeight)fontWeight, - (DWrite.FontStyle)fontStyle, - (float)fontSize)) + typeface?.FontFamilyName ?? "Courier New", + (DWrite.FontWeight)(typeface?.Weight ?? FontWeight.Normal), + (DWrite.FontStyle)(typeface?.Style ?? FontStyle.Normal), + (float)(typeface?.FontSize ?? 12))) { - Text = text; format.WordWrapping = wrapping == TextWrapping.Wrap ? - DWrite.WordWrapping.Wrap : DWrite.WordWrapping.NoWrap; + DWrite.WordWrapping.Wrap : + DWrite.WordWrapping.NoWrap; TextLayout = new DWrite.TextLayout( factory, text ?? string.Empty, format, - float.MaxValue, - float.MaxValue); + (float)constraint.Width, + (float)constraint.Height) + { + TextAlignment = textAlignment.ToDirect2D() + }; } - TextLayout.TextAlignment = textAlignment.ToDirect2D(); - } - - public Size Constraint - { - get + if (spans != null) { - return new Size(TextLayout.MaxWidth, TextLayout.MaxHeight); + foreach (var span in spans) + { + ApplySpan(span); + } } - set - { - TextLayout.MaxWidth = (float)value.Width; - TextLayout.MaxHeight = (float)value.Height; - } + Size = Measure(); } + public Size Constraint => new Size(TextLayout.MaxWidth, TextLayout.MaxHeight); + + public Size Size { get; } + public string Text { get; } public DWrite.TextLayout TextLayout { get; } @@ -113,7 +114,20 @@ namespace Avalonia.Direct2D1.Media return result.Select(x => new Rect(x.Left, x.Top, x.Width, x.Height)); } - public Size Measure() + private void ApplySpan(FormattedTextStyleSpan span) + { + if (span.Length > 0) + { + if (span.ForegroundBrush != null) + { + TextLayout.SetDrawingEffect( + new BrushWrapper(span.ForegroundBrush), + new DWrite.TextRange(span.StartIndex, span.Length)); + } + } + } + + private Size Measure() { var metrics = TextLayout.Metrics; var width = metrics.WidthIncludingTrailingWhitespace; @@ -125,12 +139,5 @@ namespace Avalonia.Direct2D1.Media return new Size(width, TextLayout.Metrics.Height); } - - public void SetForegroundBrush(IBrush brush, int startIndex, int count) - { - TextLayout.SetDrawingEffect( - new BrushWrapper(brush), - new DWrite.TextRange(startIndex, count)); - } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs index b98c944c7d..8bb901a1e4 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs @@ -1,6 +1,7 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; using Avalonia.Platform; using SharpDX.Direct2D1; @@ -11,85 +12,61 @@ namespace Avalonia.Direct2D1.Media /// public abstract class GeometryImpl : IGeometryImpl { - private TransformedGeometry _transformed; - - /// - /// Gets the geometry's bounding rectangle. - /// - public abstract Rect Bounds + public GeometryImpl(Geometry geometry) { - get; + Geometry = geometry; } - /// - /// Gets the geomentry without any transforms applied. - /// - public abstract Geometry DefiningGeometry + /// + public Rect Bounds => Geometry.GetWidenedBounds(0).ToAvalonia(); + + /// + public Geometry Geometry { get; } + + /// + public virtual Matrix Transform => Matrix.Identity; + + /// + public Rect GetRenderBounds(double strokeThickness) { - get; + return Geometry.GetWidenedBounds((float)strokeThickness).ToAvalonia(); } - /// - /// Gets the Direct2D . - /// - public Geometry Geometry => _transformed ?? DefiningGeometry; + /// + public bool FillContains(Point point) + { + return Geometry.FillContainsPoint(point.ToSharpDX()); + } - /// - /// Gets or sets the transform for the geometry. - /// - public Matrix Transform + /// + public IGeometryImpl Intersect(IGeometryImpl geometry) { - get - { - return _transformed != null ? - _transformed.Transform.ToAvalonia() : - Matrix.Identity; - } + var result = new PathGeometry(Geometry.Factory); - set + using (var sink = result.Open()) { - if (value != Transform) - { - if (_transformed != null) - { - _transformed.Dispose(); - _transformed = null; - } - - if (!value.IsIdentity) - { - Factory factory = AvaloniaLocator.Current.GetService(); - _transformed = new TransformedGeometry( - factory, - DefiningGeometry, - value.ToDirect2D()); - } - } + Geometry.Combine(((GeometryImpl)geometry).Geometry, CombineMode.Intersect, sink); + return new StreamGeometryImpl(result); } } - /// - /// Gets the geometry's bounding rectangle with the specified stroke thickness. - /// - /// The stroke thickness. - /// The bounding rectangle. - public Rect GetRenderBounds(double strokeThickness) + /// + public bool StrokeContains(Avalonia.Media.Pen pen, Point point) { - if (_transformed != null) - { - return _transformed.GetWidenedBounds((float)strokeThickness).ToAvalonia(); - } - else - { - return DefiningGeometry.GetWidenedBounds((float)strokeThickness).ToAvalonia(); - } + return Geometry.StrokeContainsPoint(point.ToSharpDX(), (float)pen.Thickness); } - - public bool FillContains(Point point) + /// + public IGeometryImpl WithTransform(Matrix transform) { - return Geometry.FillContainsPoint(point.ToSharpDX()); + var factory = AvaloniaLocator.Current.GetService(); + return new TransformedGeometryImpl( + new TransformedGeometry( + factory, + GetSourceGeometry(), + transform.ToDirect2D())); } + protected virtual Geometry GetSourceGeometry() => Geometry; } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryImpl.cs index 213c4b5c03..4c1bc3d6f7 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryImpl.cs @@ -3,7 +3,6 @@ using Avalonia.Platform; using SharpDX.Direct2D1; -using D2DGeometry = SharpDX.Direct2D1.Geometry; namespace Avalonia.Direct2D1.Media { @@ -12,55 +11,44 @@ namespace Avalonia.Direct2D1.Media /// public class StreamGeometryImpl : GeometryImpl, IStreamGeometryImpl { - private readonly PathGeometry _path; - /// /// Initializes a new instance of the class. /// public StreamGeometryImpl() + : base(CreateGeometry()) { - Factory factory = AvaloniaLocator.Current.GetService(); - _path = new PathGeometry(factory); } /// /// Initializes a new instance of the class. /// /// An existing Direct2D . - protected StreamGeometryImpl(PathGeometry geometry) + public StreamGeometryImpl(PathGeometry geometry) + : base(geometry) { - _path = geometry; } /// - public override Rect Bounds => _path.GetWidenedBounds(0).ToAvalonia(); - - /// - public override D2DGeometry DefiningGeometry => _path; - - /// - /// Clones the geometry. - /// - /// A cloned geometry. public IStreamGeometryImpl Clone() { Factory factory = AvaloniaLocator.Current.GetService(); var result = new PathGeometry(factory); var sink = result.Open(); - _path.Stream(sink); + ((PathGeometry)Geometry).Stream(sink); sink.Close(); return new StreamGeometryImpl(result); } - /// - /// Opens the geometry to start defining it. - /// - /// - /// An which can be used to define the geometry. - /// + /// public IStreamGeometryContextImpl Open() { - return new StreamGeometryContextImpl(_path.Open()); + return new StreamGeometryContextImpl(((PathGeometry)Geometry).Open()); + } + + private static Geometry CreateGeometry() + { + Factory factory = AvaloniaLocator.Current.GetService(); + return new PathGeometry(factory); } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/TransformedGeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TransformedGeometryImpl.cs new file mode 100644 index 0000000000..4043e180dc --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/Media/TransformedGeometryImpl.cs @@ -0,0 +1,24 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using SharpDX.Direct2D1; + +namespace Avalonia.Direct2D1.Media +{ + public class TransformedGeometryImpl : GeometryImpl + { + /// + /// Initializes a new instance of the class. + /// + /// An existing Direct2D . + public TransformedGeometryImpl(TransformedGeometry geometry) + : base(geometry) + { + } + + /// + public override Matrix Transform => ((TransformedGeometry)Geometry).Transform.ToAvalonia(); + + protected override Geometry GetSourceGeometry() => ((TransformedGeometry)Geometry).SourceGeometry; + } +} diff --git a/tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs b/tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs index a70130990a..6d1f321b5f 100644 --- a/tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs +++ b/tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs @@ -335,7 +335,7 @@ namespace Avalonia.Input.UnitTests class MockRenderInterface : IPlatformRenderInterface { - public IFormattedTextImpl CreateFormattedText(string text, string fontFamilyName, double fontSize, FontStyle fontStyle, TextAlignment textAlignment, FontWeight fontWeight, TextWrapping wrapping) + public IFormattedTextImpl CreateFormattedText(string text, Typeface typeface, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, IReadOnlyList spans) { throw new NotImplementedException(); } @@ -414,11 +414,26 @@ namespace Avalonia.Input.UnitTests throw new NotImplementedException(); } + public IGeometryImpl Intersect(IGeometryImpl geometry) + { + throw new NotImplementedException(); + } + public IStreamGeometryContextImpl Open() { return _impl; } + public bool StrokeContains(Pen pen, Point point) + { + throw new NotImplementedException(); + } + + public IGeometryImpl WithTransform(Matrix transform) + { + throw new NotImplementedException(); + } + class MockStreamGeometryContext : IStreamGeometryContextImpl { private List points = new List(); diff --git a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs index d996f881d2..6b7c73da2a 100644 --- a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs +++ b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs @@ -142,6 +142,8 @@ namespace Avalonia.Layout.UnitTests public string Text { get; } + public Size Size => new Size(); + public void Dispose() { } @@ -155,20 +157,22 @@ 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) - { - } } 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())) + 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(); Size clientSize = default(Size); diff --git a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs b/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs index 813db56fed..4f8aeb93ac 100644 --- a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs +++ b/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs @@ -50,37 +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); + wrapping, + 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 @@ -101,17 +104,15 @@ namespace Avalonia.Direct2D1.RenderTests.Media [InlineData(stringmiddlenewlines, FontSize, 72.01, 4 * FontSizeHeight)] public void Should_Measure_String_Correctly(string input, double fontSize, double expWidth, double expHeight) { - using (var fmt = Create(input, fontSize)) - { - var size = fmt.Measure(); + var fmt = Create(input, fontSize); + var size = fmt.Size; - Assert.Equal(expWidth, size.Width, 2); - Assert.Equal(expHeight, size.Height, 2); + Assert.Equal(expWidth, size.Width, 2); + Assert.Equal(expHeight, size.Height, 2); - var linesHeight = fmt.GetLines().Sum(l => l.Height); + var linesHeight = fmt.GetLines().Sum(l => l.Height); - Assert.Equal(expHeight, linesHeight, 2); - } + Assert.Equal(expHeight, linesHeight, 2); } #if AVALONIA_CAIRO @@ -135,16 +136,11 @@ namespace Avalonia.Direct2D1.RenderTests.Media double widthConstraint, TextWrapping wrap) { - using (var fmt = Create(input, FontSize, wrap)) - { - if (widthConstraint != -1) - { - fmt.Constraint = new Size(widthConstraint, 10000); - } + var fmt = Create(input, FontSize, wrap, widthConstraint); + var constrained = fmt; - var lines = fmt.GetLines().ToArray(); - Assert.Equal(linesCount, lines.Count()); - } + var lines = constrained.GetLines().ToArray(); + Assert.Equal(linesCount, lines.Count()); } #if AVALONIA_CAIRO @@ -178,14 +174,12 @@ namespace Avalonia.Direct2D1.RenderTests.Media double x, double y, bool isInside, bool isTrailing, int pos) { - using (var fmt = Create(input, FontSize)) - { - var htRes = fmt.HitTestPoint(new Point(x, y)); + var fmt = Create(input, FontSize); + var htRes = fmt.HitTestPoint(new Point(x, y)); - Assert.Equal(pos, htRes.TextPosition); - Assert.Equal(isInside, htRes.IsInside); - Assert.Equal(isTrailing, htRes.IsTrailing); - } + Assert.Equal(pos, htRes.TextPosition); + Assert.Equal(isInside, htRes.IsInside); + Assert.Equal(isTrailing, htRes.IsTrailing); } #if AVALONIA_CAIRO @@ -205,15 +199,13 @@ namespace Avalonia.Direct2D1.RenderTests.Media public void Should_HitTestPosition_Correctly(string input, int index, double x, double y, double width, double height) { - using (var fmt = Create(input, FontSize)) - { - var r = fmt.HitTestTextPosition(index); + var fmt = Create(input, FontSize); + var r = fmt.HitTestTextPosition(index); - Assert.Equal(x, r.X, 2); - Assert.Equal(y, r.Y, 2); - Assert.Equal(width, r.Width, 2); - Assert.Equal(height, r.Height, 2); - } + Assert.Equal(x, r.X, 2); + Assert.Equal(y, r.Y, 2); + Assert.Equal(width, r.Width, 2); + Assert.Equal(height, r.Height, 2); } #if AVALONIA_CAIRO @@ -229,17 +221,14 @@ namespace Avalonia.Direct2D1.RenderTests.Media double x, double y, double width, double height) { //parse expected - using (var fmt = Create(input, FontSize, TextAlignment.Right)) - { - fmt.Constraint = new Size(widthConstraint, 100); - - var r = fmt.HitTestTextPosition(index); + var fmt = Create(input, FontSize, TextAlignment.Right, widthConstraint); + var constrained = fmt; + var r = constrained.HitTestTextPosition(index); - Assert.Equal(x, r.X, 2); - Assert.Equal(y, r.Y, 2); - Assert.Equal(width, r.Width, 2); - Assert.Equal(height, r.Height, 2); - } + Assert.Equal(x, r.X, 2); + Assert.Equal(y, r.Y, 2); + Assert.Equal(width, r.Width, 2); + Assert.Equal(height, r.Height, 2); } #if AVALONIA_CAIRO @@ -255,17 +244,14 @@ namespace Avalonia.Direct2D1.RenderTests.Media double x, double y, double width, double height) { //parse expected - using (var fmt = Create(input, FontSize, TextAlignment.Center)) - { - fmt.Constraint = new Size(widthConstraint, 100); + var fmt = Create(input, FontSize, TextAlignment.Center, widthConstraint); + var constrained = fmt; + var r = constrained.HitTestTextPosition(index); - var r = fmt.HitTestTextPosition(index); - - Assert.Equal(x, r.X, 2); - Assert.Equal(y, r.Y, 2); - Assert.Equal(width, r.Width, 2); - Assert.Equal(height, r.Height, 2); - } + Assert.Equal(x, r.X, 2); + Assert.Equal(y, r.Y, 2); + Assert.Equal(width, r.Width, 2); + Assert.Equal(height, r.Height, 2); } #if AVALONIA_CAIRO @@ -291,22 +277,20 @@ namespace Avalonia.Direct2D1.RenderTests.Media return new Rect(v[0], v[1], v[2], v[3]); }).ToArray(); - using (var fmt = Create(input, FontSize)) - { - var htRes = fmt.HitTestTextRange(index, length).ToArray(); + var fmt = Create(input, FontSize); + var htRes = fmt.HitTestTextRange(index, length).ToArray(); - Assert.Equal(rects.Length, htRes.Length); + Assert.Equal(rects.Length, htRes.Length); - for (int i = 0; i < rects.Length; i++) - { - var exr = rects[i]; - var r = htRes[i]; + for (int i = 0; i < rects.Length; i++) + { + var exr = rects[i]; + var r = htRes[i]; - Assert.Equal(exr.X, r.X, 2); - Assert.Equal(exr.Y, r.Y, 2); - Assert.Equal(exr.Width, r.Width, 2); - Assert.Equal(exr.Height, r.Height, 2); - } + Assert.Equal(exr.X, r.X, 2); + Assert.Equal(exr.Y, r.Y, 2); + Assert.Equal(exr.Width, r.Width, 2); + Assert.Equal(exr.Height, r.Height, 2); } } } diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs index 3efbd6c9be..8c887343ee 100644 --- a/tests/Avalonia.UnitTests/TestServices.cs +++ b/tests/Avalonia.UnitTests/TestServices.cs @@ -13,6 +13,7 @@ using Avalonia.Styling; using Avalonia.Themes.Default; using Avalonia.Rendering; using System.Reactive.Concurrency; +using System.Collections.Generic; namespace Avalonia.UnitTests { @@ -163,12 +164,11 @@ namespace Avalonia.UnitTests return Mock.Of(x => x.CreateFormattedText( It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), + It.IsAny(), It.IsAny(), - It.IsAny(), - It.IsAny()) == Mock.Of() && + It.IsAny(), + It.IsAny(), + It.IsAny>()) == Mock.Of() && x.CreateStreamGeometry() == Mock.Of( y => y.Open() == Mock.Of())); } diff --git a/tests/Avalonia.Visuals.UnitTests/Media/FormattedTextTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/FormattedTextTests.cs deleted file mode 100644 index 648595fa2b..0000000000 --- a/tests/Avalonia.Visuals.UnitTests/Media/FormattedTextTests.cs +++ /dev/null @@ -1,28 +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)); - } - - [Fact] - public void Exception_Should_Be_Thrown_If_FontWeight_0() - { - Assert.Throws(() => new FormattedText( - "foo", - "Ariel", - 12, - 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..2c8f8eb9b2 --- /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_LessThanEqualTo_0() + { + Assert.Throws(() => new Typeface("foo", 0)); + } + + [Fact] + public void Exception_Should_Be_Thrown_If_FontWeight_LessThanEqualTo_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 303736be9c..57399f3df0 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs @@ -10,12 +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) + TextWrapping wrapping, + Size constraint, + IReadOnlyList spans) { throw new NotImplementedException(); } @@ -94,11 +93,26 @@ namespace Avalonia.Visuals.UnitTests.VisualTree throw new NotImplementedException(); } + public IGeometryImpl Intersect(IGeometryImpl geometry) + { + throw new NotImplementedException(); + } + public IStreamGeometryContextImpl Open() { return _impl; } + public bool StrokeContains(Pen pen, Point point) + { + throw new NotImplementedException(); + } + + public IGeometryImpl WithTransform(Matrix transform) + { + throw new NotImplementedException(); + } + class MockStreamGeometryContext : IStreamGeometryContextImpl { private List points = new List();