Browse Source

Merge pull request #948 from AvaloniaUI/immutable-media-impls

Ported immutable media impls from scenegraph.
pull/641/merge
Steven Kirk 10 years ago
committed by GitHub
parent
commit
7425d6c793
  1. 27
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  2. 17
      src/Avalonia.Controls/Primitives/AccessText.cs
  3. 21
      src/Avalonia.Controls/TextBlock.cs
  4. 6
      src/Avalonia.HtmlRenderer/Adapters/GraphicsAdapter.cs
  5. 176
      src/Avalonia.Visuals/Media/FormattedText.cs
  6. 41
      src/Avalonia.Visuals/Media/FormattedTextStyleSpan.cs
  7. 24
      src/Avalonia.Visuals/Media/Geometry.cs
  8. 59
      src/Avalonia.Visuals/Media/Typeface.cs
  9. 25
      src/Avalonia.Visuals/Platform/IFormattedTextImpl.cs
  10. 30
      src/Avalonia.Visuals/Platform/IGeometryImpl.cs
  11. 16
      src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
  12. 15
      src/Avalonia.Visuals/Rendering/RendererMixin.cs
  13. 18
      src/Gtk/Avalonia.Cairo/CairoPlatform.cs
  14. 85
      src/Gtk/Avalonia.Cairo/Media/FormattedTextImpl.cs
  15. 9
      src/Gtk/Avalonia.Cairo/Media/StreamGeometryContextImpl.cs
  16. 32
      src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs
  17. 2
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  18. 87
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs
  19. 11
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  20. 63
      src/Skia/Avalonia.Skia/StreamGeometryImpl.cs
  21. 1
      src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj
  22. 17
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  23. 2
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  24. 73
      src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs
  25. 99
      src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs
  26. 36
      src/Windows/Avalonia.Direct2D1/Media/StreamGeometryImpl.cs
  27. 24
      src/Windows/Avalonia.Direct2D1/Media/TransformedGeometryImpl.cs
  28. 17
      tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs
  29. 18
      tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs
  30. 136
      tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs
  31. 10
      tests/Avalonia.UnitTests/TestServices.cs
  32. 28
      tests/Avalonia.Visuals.UnitTests/Media/FormattedTextTests.cs
  33. 21
      tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs
  34. 24
      tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs

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

@ -127,11 +127,11 @@ namespace Avalonia.Controls.Presenters
base.Render(context); base.Render(context);
if (selectionStart == selectionEnd) if (selectionStart == selectionEnd)
{ {
var backgroundColor = (((Control)TemplatedParent).GetValue(BackgroundProperty) as SolidColorBrush)?.Color; var backgroundColor = (((Control)TemplatedParent).GetValue(BackgroundProperty) as SolidColorBrush)?.Color;
var caretBrush = Brushes.Black; var caretBrush = Brushes.Black;
if(backgroundColor.HasValue) if (backgroundColor.HasValue)
{ {
byte red = (byte)~(backgroundColor.Value.R); byte red = (byte)~(backgroundColor.Value.R);
byte green = (byte)~(backgroundColor.Value.G); byte green = (byte)~(backgroundColor.Value.G);
@ -139,7 +139,7 @@ namespace Avalonia.Controls.Presenters
caretBrush = new SolidColorBrush(Color.FromRgb(red, green, blue)); caretBrush = new SolidColorBrush(Color.FromRgb(red, green, blue));
} }
if (_caretBlink) if (_caretBlink)
{ {
var charPos = FormattedText.HitTestTextPosition(CaretIndex); var charPos = FormattedText.HitTestTextPosition(CaretIndex);
@ -212,7 +212,10 @@ namespace Avalonia.Controls.Presenters
if (length > 0) if (length > 0)
{ {
result.SetForegroundBrush(Brushes.White, start, length); result.Spans = new[]
{
new FormattedTextStyleSpan(start, length, foregroundBrush: Brushes.White),
};
} }
return result; return result;
@ -228,17 +231,13 @@ namespace Avalonia.Controls.Presenters
} }
else else
{ {
// TODO: Pretty sure that measuring "X" isn't the right way to do this... return new FormattedText
using (var formattedText = new FormattedText(
"X",
FontFamily,
FontSize,
FontStyle,
TextAlignment,
FontWeight))
{ {
return formattedText.Measure(); Text = "X",
} Typeface = new Typeface(FontFamily, FontSize, FontStyle, FontWeight),
TextAlignment = TextAlignment,
Constraint = availableSize,
}.Measure();
} }
} }

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

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

21
src/Avalonia.Controls/TextBlock.cs

@ -116,7 +116,7 @@ namespace Avalonia.Controls
this.GetObservable(TextAlignmentProperty).Select(_ => Unit.Default), this.GetObservable(TextAlignmentProperty).Select(_ => Unit.Default),
this.GetObservable(FontSizeProperty).Select(_ => Unit.Default), this.GetObservable(FontSizeProperty).Select(_ => Unit.Default),
this.GetObservable(FontStyleProperty).Select(_ => Unit.Default), this.GetObservable(FontStyleProperty).Select(_ => Unit.Default),
this.GetObservable(FontWeightProperty).Select(_=>Unit.Default)) this.GetObservable(FontWeightProperty).Select(_ => Unit.Default))
.Subscribe(_ => .Subscribe(_ =>
{ {
InvalidateFormattedText(); InvalidateFormattedText();
@ -350,16 +350,14 @@ namespace Avalonia.Controls
/// <returns>A <see cref="FormattedText"/> object.</returns> /// <returns>A <see cref="FormattedText"/> object.</returns>
protected virtual FormattedText CreateFormattedText(Size constraint) protected virtual FormattedText CreateFormattedText(Size constraint)
{ {
var result = new FormattedText( return new FormattedText
Text ?? string.Empty, {
FontFamily, Constraint = constraint,
FontSize, Typeface = new Typeface(FontFamily, FontSize, FontStyle, FontWeight),
FontStyle, Text = Text ?? string.Empty,
TextAlignment, TextAlignment = TextAlignment,
FontWeight, Wrapping = TextWrapping,
TextWrapping); };
result.Constraint = constraint;
return result;
} }
/// <summary> /// <summary>
@ -370,7 +368,6 @@ namespace Avalonia.Controls
if (_formattedText != null) if (_formattedText != null)
{ {
_constraint = _formattedText.Constraint; _constraint = _formattedText.Constraint;
_formattedText.Dispose();
_formattedText = null; _formattedText = null;
} }

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

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

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

@ -10,63 +10,32 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Represents a piece of text with formatting. /// Represents a piece of text with formatting.
/// </summary> /// </summary>
public class FormattedText : AvaloniaDisposable public class FormattedText
{ {
private readonly IPlatformRenderInterface _platform;
private Size _constraint = Size.Infinity;
private IFormattedTextImpl _platformImpl;
private IReadOnlyList<FormattedTextStyleSpan> _spans;
private Typeface _typeface;
private string _text;
private TextAlignment _textAlignment;
private TextWrapping _wrapping;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="FormattedText"/> class. /// Initializes a new instance of the <see cref="FormattedText"/> class.
/// </summary> /// </summary>
/// <param name="text">The text.</param> public FormattedText()
/// <param name="fontFamilyName">The font family.</param>
/// <param name="fontSize">The font size.</param>
/// <param name="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,
FontStyle fontStyle = FontStyle.Normal,
TextAlignment textAlignment = TextAlignment.Left,
FontWeight fontWeight = FontWeight.Normal,
TextWrapping wrapping = TextWrapping.Wrap)
{ {
Contract.Requires<ArgumentNullException>(text != null); _platform = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
Contract.Requires<ArgumentNullException>(fontFamilyName != null); }
if (fontSize <= 0)
{
throw new ArgumentException("FontSize must be greater than 0");
}
if (fontWeight <= 0)
{
throw new ArgumentException("FontWeight must be greater than 0");
}
Text = text;
FontFamilyName = fontFamilyName;
FontSize = fontSize;
FontStyle = fontStyle;
FontWeight = fontWeight;
TextAlignment = textAlignment;
Wrapping = wrapping;
var platform = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
if (platform == null)
{
throw new Exception("Could not create FormattedText: IPlatformRenderInterface not registered.");
}
PlatformImpl = platform.CreateFormattedText( /// <summary>
text, /// Initializes a new instance of the <see cref="FormattedText"/> class.
fontFamilyName, /// </summary>
fontSize, /// <param name="platform">The platform render interface.</param>
fontStyle, public FormattedText(IPlatformRenderInterface platform)
textAlignment, {
fontWeight, _platform = platform;
wrapping);
} }
/// <summary> /// <summary>
@ -74,64 +43,76 @@ namespace Avalonia.Media
/// </summary> /// </summary>
public Size Constraint public Size Constraint
{ {
get get => _constraint;
{ set => Set(ref _constraint, value);
CheckDisposed();
return PlatformImpl.Constraint;
}
set
{
CheckDisposed();
PlatformImpl.Constraint = value;
}
} }
/// <summary> /// <summary>
/// Gets the font family. /// Gets or sets the base typeface.
/// </summary> /// </summary>
public string FontFamilyName { get; } public Typeface Typeface
{
get => _typeface;
set => Set(ref _typeface, value);
}
/// <summary> /// <summary>
/// Gets the font size. /// Gets or sets a collection of spans that describe the formatting of subsections of the
/// text.
/// </summary> /// </summary>
public double FontSize { get; } public IReadOnlyList<FormattedTextStyleSpan> Spans
{
get => _spans;
set => Set(ref _spans, value);
}
/// <summary> /// <summary>
/// Gets the font style. /// Gets or sets the text.
/// </summary> /// </summary>
public FontStyle FontStyle { get; } public string Text
{
get => _text;
set => Set(ref _text, value);
}
/// <summary> /// <summary>
/// Gets the font weight. /// Gets or sets the aligment of the text.
/// </summary> /// </summary>
public FontWeight FontWeight { get; } public TextAlignment TextAlignment
{
get => _textAlignment;
set => Set(ref _textAlignment, value);
}
/// <summary> /// <summary>
/// Gets the text. /// Gets or sets the text wrapping.
/// </summary> /// </summary>
public string Text { get; } public TextWrapping Wrapping
{
get => _wrapping;
set => Set(ref _wrapping, value);
}
/// <summary> /// <summary>
/// Gets platform-specific platform implementation. /// Gets platform-specific platform implementation.
/// </summary> /// </summary>
public IFormattedTextImpl PlatformImpl { get; } public IFormattedTextImpl PlatformImpl
/// <summary>
/// Gets the text alignment.
/// </summary>
public TextAlignment TextAlignment { get; }
/// <summary>
/// Gets the text wrapping.
/// </summary>
public TextWrapping Wrapping { get; }
/// <summary>
/// Disposes of unmanaged resources associated with the formatted text.
/// </summary>
protected override void DoDispose()
{ {
PlatformImpl.Dispose(); get
{
if (_platformImpl == null)
{
_platformImpl = _platform.CreateFormattedText(
_text,
_typeface,
_textAlignment,
_wrapping,
_constraint,
_spans);
}
return _platformImpl;
}
} }
/// <summary> /// <summary>
@ -142,7 +123,6 @@ namespace Avalonia.Media
/// </returns> /// </returns>
public IEnumerable<FormattedTextLine> GetLines() public IEnumerable<FormattedTextLine> GetLines()
{ {
CheckDisposed();
return PlatformImpl.GetLines(); return PlatformImpl.GetLines();
} }
@ -155,7 +135,6 @@ namespace Avalonia.Media
/// </returns> /// </returns>
public TextHitTestResult HitTestPoint(Point point) public TextHitTestResult HitTestPoint(Point point)
{ {
CheckDisposed();
return PlatformImpl.HitTestPoint(point); return PlatformImpl.HitTestPoint(point);
} }
@ -166,7 +145,6 @@ namespace Avalonia.Media
/// <returns>The character bounds.</returns> /// <returns>The character bounds.</returns>
public Rect HitTestTextPosition(int index) public Rect HitTestTextPosition(int index)
{ {
CheckDisposed();
return PlatformImpl.HitTestTextPosition(index); return PlatformImpl.HitTestTextPosition(index);
} }
@ -178,7 +156,6 @@ namespace Avalonia.Media
/// <returns>The character bounds.</returns> /// <returns>The character bounds.</returns>
public IEnumerable<Rect> HitTestTextRange(int index, int length) public IEnumerable<Rect> HitTestTextRange(int index, int length)
{ {
CheckDisposed();
return PlatformImpl.HitTestTextRange(index, length); return PlatformImpl.HitTestTextRange(index, length);
} }
@ -188,20 +165,13 @@ namespace Avalonia.Media
/// <returns>The bounds box of the text.</returns> /// <returns>The bounds box of the text.</returns>
public Size Measure() public Size Measure()
{ {
CheckDisposed(); return PlatformImpl.Size;
return PlatformImpl.Measure();
} }
/// <summary> private void Set<T>(ref T field, T value)
/// Sets the foreground brush for the specified text range.
/// </summary>
/// <param name="brush">The brush.</param>
/// <param name="startIndex">The start of the text range.</param>
/// <param name="length">The length of the text range.</param>
public void SetForegroundBrush(IBrush brush, int startIndex, int length)
{ {
CheckDisposed(); field = value;
PlatformImpl.SetForegroundBrush(brush, startIndex, length); _platformImpl = null;
} }
} }
} }

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

@ -0,0 +1,41 @@
using System;
namespace Avalonia.Media
{
/// <summary>
/// Describes the formatting for a span of text in a <see cref="FormattedText"/> object.
/// </summary>
public class FormattedTextStyleSpan
{
/// <summary>
/// Initializes a new instance of the <see cref="FormattedTextStyleSpan"/> class.
/// </summary>
/// <param name="startIndex">The index of the first character in the span.</param>
/// <param name="length">The length of the span.</param>
/// <param name="foregroundBrush">The span's foreground brush.</param>
public FormattedTextStyleSpan(
int startIndex,
int length,
IBrush foregroundBrush = null)
{
StartIndex = startIndex;
Length = length;
ForegroundBrush = foregroundBrush;
}
/// <summary>
/// Gets the index of the first character in the span.
/// </summary>
public int StartIndex { get; }
/// <summary>
/// Gets the length of the span.
/// </summary>
public int Length { get; }
/// <summary>
/// Gets the span's foreground brush.
/// </summary>
public IBrush ForegroundBrush { get; }
}
}

24
src/Avalonia.Visuals/Media/Geometry.cs

@ -22,10 +22,7 @@ namespace Avalonia.Media
/// </summary> /// </summary>
static Geometry() static Geometry()
{ {
TransformProperty.Changed.Subscribe(x => TransformProperty.Changed.AddClassHandler<Geometry>(x => x.TransformChanged);
{
((Geometry)x.Sender).PlatformImpl.Transform = ((Transform)x.NewValue).Value;
});
} }
/// <summary> /// <summary>
@ -68,7 +65,7 @@ namespace Avalonia.Media
} }
/// <summary> /// <summary>
/// Indicates whether the geometry contains the specified point. /// Indicates whether the geometry's fill contains the specified point.
/// </summary> /// </summary>
/// <param name="point">The point.</param> /// <param name="point">The point.</param>
/// <returns><c>true</c> if the geometry contains the point; otherwise, <c>false</c>.</returns> /// <returns><c>true</c> if the geometry contains the point; otherwise, <c>false</c>.</returns>
@ -76,5 +73,22 @@ namespace Avalonia.Media
{ {
return PlatformImpl.FillContains(point); return PlatformImpl.FillContains(point);
} }
/// <summary>
/// Indicates whether the geometry's stroke contains the specified point.
/// </summary>
/// <param name="pen">The pen to use.</param>
/// <param name="point">The point.</param>
/// <returns><c>true</c> if the geometry contains the point; otherwise, <c>false</c>.</returns>
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);
}
} }
} }

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

@ -0,0 +1,59 @@
using System;
namespace Avalonia.Media
{
/// <summary>
/// Represents a typeface.
/// </summary>
public class Typeface
{
/// <summary>
/// Initializes a new instance of the <see cref="Typeface"/> class.
/// </summary>
/// <param name="fontFamilyName">The name of the font family.</param>
/// <param name="fontSize">The font size, in DIPs.</param>
/// <param name="style">The font style.</param>
/// <param name="weight">The font weight.</param>
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;
}
/// <summary>
/// Gets the name of the font family.
/// </summary>
public string FontFamilyName { get; }
/// <summary>
/// Gets the size of the font in DIPs.
/// </summary>
public double FontSize { get; }
/// <summary>
/// Gets the font style.
/// </summary>
public FontStyle Style { get; }
/// <summary>
/// Gets the font weight.
/// </summary>
public FontWeight Weight { get; }
}
}

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

@ -10,12 +10,17 @@ namespace Avalonia.Platform
/// <summary> /// <summary>
/// Defines the platform-specific interface for <see cref="FormattedText"/>. /// Defines the platform-specific interface for <see cref="FormattedText"/>.
/// </summary> /// </summary>
public interface IFormattedTextImpl : IDisposable public interface IFormattedTextImpl
{ {
/// <summary> /// <summary>
/// Gets or sets the constraint of the text. /// Gets the constraint of the text.
/// </summary> /// </summary>
Size Constraint { get; set; } Size Constraint { get; }
/// <summary>
/// The measured size of the text.
/// </summary>
Size Size { get; }
/// <summary> /// <summary>
/// Gets the text. /// Gets the text.
@ -53,19 +58,5 @@ namespace Avalonia.Platform
/// <param name="length">The number of characters in the text range.</param> /// <param name="length">The number of characters in the text range.</param>
/// <returns>The character bounds.</returns> /// <returns>The character bounds.</returns>
IEnumerable<Rect> HitTestTextRange(int index, int length); IEnumerable<Rect> HitTestTextRange(int index, int length);
/// <summary>
/// Gets the size of the text, taking <see cref="Constraint"/> into account.
/// </summary>
/// <returns>The bounds box of the text.</returns>
Size Measure();
/// <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);
} }
} }

30
src/Avalonia.Visuals/Platform/IGeometryImpl.cs

@ -1,6 +1,8 @@
// Copyright (c) The Avalonia Project. All rights reserved. // 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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Media;
namespace Avalonia.Platform namespace Avalonia.Platform
{ {
/// <summary> /// <summary>
@ -14,9 +16,9 @@ namespace Avalonia.Platform
Rect Bounds { get; } Rect Bounds { get; }
/// <summary> /// <summary>
/// Gets or sets a transform to apply to the geometry. /// Gets the transform to applied to the geometry.
/// </summary> /// </summary>
Matrix Transform { get; set; } Matrix Transform { get; }
/// <summary> /// <summary>
/// Gets the geometry's bounding rectangle with the specified stroke thickness. /// Gets the geometry's bounding rectangle with the specified stroke thickness.
@ -26,10 +28,32 @@ namespace Avalonia.Platform
Rect GetRenderBounds(double strokeThickness); Rect GetRenderBounds(double strokeThickness);
/// <summary> /// <summary>
/// Indicates whether the geometry contains the specified point. /// Indicates whether the geometry's fill contains the specified point.
/// </summary> /// </summary>
/// <param name="point">The point.</param> /// <param name="point">The point.</param>
/// <returns><c>true</c> if the geometry contains the point; otherwise, <c>false</c>.</returns> /// <returns><c>true</c> if the geometry contains the point; otherwise, <c>false</c>.</returns>
bool FillContains(Point point); bool FillContains(Point point);
/// <summary>
/// Intersects the geometry with another geometry.
/// </summary>
/// <param name="geometry">The other geometry.</param>
/// <returns>A new <see cref="IGeometryImpl"/> representing the intersection.</returns>
IGeometryImpl Intersect(IGeometryImpl geometry);
/// <summary>
/// Indicates whether the geometry's stroke contains the specified point.
/// </summary>
/// <param name="pen">The stroke to use.</param>
/// <param name="point">The point.</param>
/// <returns><c>true</c> if the geometry contains the point; otherwise, <c>false</c>.</returns>
bool StrokeContains(Pen pen, Point point);
/// <summary>
/// Makes a clone of the geometry with the specified transform.
/// </summary>
/// <param name="transform">The transform.</param>
/// <returns>The cloned geometry.</returns>
IGeometryImpl WithTransform(Matrix transform);
} }
} }

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

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

15
src/Avalonia.Visuals/Rendering/RendererMixin.cs

@ -58,16 +58,13 @@ namespace Avalonia.Rendering
s_lastMeasure = now; s_lastMeasure = now;
} }
var pt = new Point(40, 40); var pt = new Point(40, 40);
using ( var txt = new FormattedText
var txt = new FormattedText("Frame #" + s_frameNum + " FPS: " + s_fps, "Arial", 18,
FontStyle.Normal,
TextAlignment.Left,
FontWeight.Normal,
TextWrapping.NoWrap))
{ {
ctx.FillRectangle(Brushes.White, new Rect(pt, txt.Measure())); Text = "Frame #" + s_frameNum + " FPS: " + s_fps,
ctx.DrawText(Brushes.Black, pt, txt); Typeface = new Typeface("Arial", 18)
} };
ctx.FillRectangle(Brushes.White, new Rect(pt, txt.Measure()));
ctx.DrawText(Brushes.Black, pt, txt);
} }
} }
} }

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

@ -42,14 +42,20 @@ namespace Avalonia.Cairo
public IFormattedTextImpl CreateFormattedText( public IFormattedTextImpl CreateFormattedText(
string text, string text,
string fontFamily, Typeface typeface,
double fontSize,
FontStyle fontStyle,
TextAlignment textAlignment, TextAlignment textAlignment,
Avalonia.Media.FontWeight fontWeight, TextWrapping wrapping,
TextWrapping wrapping) Size constraint,
IReadOnlyList<FormattedTextStyleSpan> 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<object> surfaces) public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)

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

@ -12,7 +12,7 @@ namespace Avalonia.Cairo.Media
{ {
public class FormattedTextImpl : IFormattedTextImpl public class FormattedTextImpl : IFormattedTextImpl
{ {
private Size _size; private Size _constraint;
static double CorrectScale(double input) static double CorrectScale(double input)
{ {
@ -22,54 +22,64 @@ namespace Avalonia.Cairo.Media
public FormattedTextImpl( public FormattedTextImpl(
Pango.Context context, Pango.Context context,
string text, string text,
string fontFamily, Typeface typeface,
double fontSize,
FontStyle fontStyle,
TextAlignment textAlignment, TextAlignment textAlignment,
FontWeight fontWeight) TextWrapping wrapping,
Size constraint,
IReadOnlyList<FormattedTextStyleSpan> spans)
{ {
Contract.Requires<ArgumentNullException>(context != null); Contract.Requires<ArgumentNullException>(context != null);
Contract.Requires<ArgumentNullException>(text != null); Contract.Requires<ArgumentNullException>(text != null);
Layout = new Pango.Layout(context); Layout = new Pango.Layout(context);
Layout.SetText(text); Layout.SetText(text);
Layout.FontDescription = new Pango.FontDescription Layout.FontDescription = new Pango.FontDescription
{ {
Family = fontFamily, Family = typeface?.FontFamilyName ?? "monospace",
Size = Pango.Units.FromDouble(CorrectScale(fontSize)), Size = Pango.Units.FromDouble(CorrectScale(typeface?.FontSize ?? 12)),
Style = (Pango.Style)fontStyle, Style = (Pango.Style)(typeface?.Style ?? FontStyle.Normal),
Weight = fontWeight.ToCairo() Weight = (typeface?.Weight ?? FontWeight.Normal).ToCairo(),
}; };
Layout.Alignment = textAlignment.ToCairo(); Layout.Alignment = textAlignment.ToCairo();
Layout.Attributes = new Pango.AttrList(); Layout.Attributes = new Pango.AttrList();
} Layout.Width = double.IsPositiveInfinity(constraint.Width) ? -1 : Pango.Units.FromDouble(constraint.Width);
public string Text => Layout.Text; if (spans != null)
public Size Constraint
{
get
{ {
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 = Measure();
{
_size = value;
Layout.Width = double.IsPositiveInfinity(value.Width) ?
-1 : Pango.Units.FromDouble(value.Width);
}
} }
public Pango.Layout Layout public FormattedTextImpl(Pango.Layout layout)
{ {
get; Layout = layout;
Size = Measure();
} }
public void Dispose() public string Text => Layout.Text;
{
Layout.Dispose(); public Size Constraint => _constraint;
}
public Size Size { get; }
public Pango.Layout Layout { get; }
public IEnumerable<FormattedTextLine> GetLines() public IEnumerable<FormattedTextLine> GetLines()
{ {
@ -124,7 +134,7 @@ namespace Avalonia.Cairo.Media
return ranges; return ranges;
} }
public Size Measure() private Size Measure()
{ {
int width; int width;
int height; int height;
@ -132,22 +142,5 @@ namespace Avalonia.Cairo.Media
return new Size(width, height); 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);
}
}
} }
} }

9
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) public void LineTo(Point point)
{ {
if (this.Path == null) if (this.Path == null)

32
src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs

@ -35,19 +35,11 @@ namespace Avalonia.Cairo.Media
private readonly StreamGeometryContextImpl _impl; private readonly StreamGeometryContextImpl _impl;
private Matrix _transform = Matrix.Identity; private Matrix _transform = Matrix.Identity;
public Matrix Transform public Matrix Transform
{ {
get { return _transform; } get { return _transform; }
set private set { _transform = value; }
{
if (value != Transform)
{
if (!value.IsIdentity)
{
_transform = value;
}
}
}
} }
public FillRule FillRule { get; set; } public FillRule FillRule { get; set; }
@ -60,7 +52,7 @@ namespace Avalonia.Cairo.Media
public Rect GetRenderBounds(double strokeThickness) public Rect GetRenderBounds(double strokeThickness)
{ {
// TODO: Calculate properly. // TODO: Calculate properly.
return Bounds.Inflate(strokeThickness); return Bounds.TransformToAABB(Transform).Inflate(strokeThickness);
} }
public IStreamGeometryContextImpl Open() public IStreamGeometryContextImpl Open()
@ -72,5 +64,23 @@ namespace Avalonia.Cairo.Media
{ {
return _impl.FillContains(point); return _impl.FillContains(point);
} }
public IGeometryImpl Intersect(IGeometryImpl geometry)
{
throw new NotImplementedException();
}
public bool StrokeContains(Pen pen, Point point)
{
return _impl.StrokeContains(pen, point);
}
/// <inheritdoc/>
public IGeometryImpl WithTransform(Matrix transform)
{
var result = (StreamGeometryImpl)Clone();
result.Transform = transform;
return result;
}
} }
} }

2
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -292,7 +292,7 @@ namespace Avalonia.Skia
public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text) 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; var textImpl = (FormattedTextImpl)text;
textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint); textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint);

87
src/Skia/Avalonia.Skia/FormattedTextImpl.cs

@ -12,15 +12,23 @@ namespace Avalonia.Skia
{ {
public class FormattedTextImpl : IFormattedTextImpl public class FormattedTextImpl : IFormattedTextImpl
{ {
public FormattedTextImpl(string text, string fontFamilyName, double fontSize, FontStyle fontStyle, public FormattedTextImpl(
TextAlignment textAlignment, FontWeight fontWeight, TextWrapping wrapping) string text,
Typeface typeface,
TextAlignment textAlignment,
TextWrapping wrapping,
Size constraint,
IReadOnlyList<FormattedTextStyleSpan> spans)
{ {
Text = text ?? string.Empty; Text = text ?? string.Empty;
// Replace 0 characters with zero-width spaces (200B) // Replace 0 characters with zero-width spaces (200B)
Text = Text.Replace((char)0, (char)0x200B); Text = Text.Replace((char)0, (char)0x200B);
var typeface = TypefaceCache.GetTypeface(fontFamilyName, fontStyle, fontWeight); var skiaTypeface = TypefaceCache.GetTypeface(
typeface?.FontFamilyName ?? "monospace",
typeface?.Style ?? FontStyle.Normal,
typeface?.Weight ?? FontWeight.Normal);
_paint = new SKPaint(); _paint = new SKPaint();
@ -31,34 +39,31 @@ namespace Avalonia.Skia
_paint.IsAntialias = true; _paint.IsAntialias = true;
_paint.LcdRenderText = true; _paint.LcdRenderText = true;
_paint.SubpixelText = true; _paint.SubpixelText = true;
_paint.Typeface = typeface; _paint.Typeface = skiaTypeface;
_paint.TextSize = (float)fontSize; _paint.TextSize = (float)(typeface?.FontSize ?? 12);
_paint.TextAlign = textAlignment.ToSKTextAlign(); _paint.TextAlign = textAlignment.ToSKTextAlign();
_paint.XferMode = SKXferMode.Src;
_wrapping = wrapping; _wrapping = wrapping;
_constraint = constraint;
Rebuild(); if (spans != null)
}
public Size Constraint
{
get { return _constraint; }
set
{ {
if (_constraint == value) foreach (var span in spans)
return; {
if (span.ForegroundBrush != null)
_constraint = value; {
SetForegroundBrush(span.ForegroundBrush, span.StartIndex, span.Length);
Rebuild(); }
}
} }
Rebuild();
} }
public string Text { get; } public Size Constraint => _constraint;
public void Dispose() public Size Size => _size;
{
}
public IEnumerable<FormattedTextLine> GetLines() public IEnumerable<FormattedTextLine> GetLines()
{ {
@ -161,27 +166,6 @@ namespace Avalonia.Skia
return result; 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<FBrushRange, IBrush>(key, brush));
}
}
public override string ToString() public override string ToString()
{ {
return Text; return Text;
@ -286,6 +270,7 @@ namespace Avalonia.Skia
private readonly List<FormattedTextLine> _lines = new List<FormattedTextLine>(); private readonly List<FormattedTextLine> _lines = new List<FormattedTextLine>();
private readonly SKPaint _paint; private readonly SKPaint _paint;
private readonly List<Rect> _rects = new List<Rect>(); private readonly List<Rect> _rects = new List<Rect>();
public string Text { get; }
private readonly TextWrapping _wrapping; private readonly TextWrapping _wrapping;
private Size _constraint = new Size(double.PositiveInfinity, double.PositiveInfinity); private Size _constraint = new Size(double.PositiveInfinity, double.PositiveInfinity);
private float _lineHeight = 0; private float _lineHeight = 0;
@ -619,6 +604,22 @@ namespace Avalonia.Skia
return x; return x;
} }
private void SetForegroundBrush(IBrush brush, int startIndex, int length)
{
var key = new FBrushRange(startIndex, length);
int index = _foregroundBrushes.FindIndex(v => v.Key.Equals(key));
if (index > -1)
{
_foregroundBrushes.RemoveAt(index);
}
if (brush != null)
{
_foregroundBrushes.Insert(0, new KeyValuePair<FBrushRange, IBrush>(key, brush));
}
}
private struct AvaloniaFormattedTextLine private struct AvaloniaFormattedTextLine
{ {
public float Height; public float Height;

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

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

63
src/Skia/Avalonia.Skia/StreamGeometryImpl.cs

@ -13,16 +13,15 @@ namespace Avalonia.Skia
class StreamGeometryImpl : IStreamGeometryImpl class StreamGeometryImpl : IStreamGeometryImpl
{ {
SKPath _path; SKPath _path;
SKPath _transformedPath;
private Matrix _transform = Matrix.Identity; private Matrix _transform = Matrix.Identity;
public SKPath EffectivePath => (_transformedPath ?? _path); public SKPath EffectivePath => _path;
public Rect GetRenderBounds(double strokeThickness) public Rect GetRenderBounds(double strokeThickness)
{ {
// TODO: Calculate properly. // TODO: Calculate properly.
return Bounds.Inflate(strokeThickness); return Bounds.TransformToAABB(Transform).Inflate(strokeThickness);
} }
public Rect Bounds { get; private set; } public Rect Bounds { get; private set; }
@ -30,32 +29,6 @@ namespace Avalonia.Skia
public Matrix Transform public Matrix Transform
{ {
get { return _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() public IStreamGeometryImpl Clone()
@ -63,7 +36,6 @@ namespace Avalonia.Skia
return new StreamGeometryImpl return new StreamGeometryImpl
{ {
_path = _path?.Clone(), _path = _path?.Clone(),
_transformedPath = _transformedPath?.Clone(),
_transform = Transform, _transform = Transform,
Bounds = Bounds Bounds = Bounds
}; };
@ -84,6 +56,36 @@ namespace Avalonia.Skia
return GetRenderBounds(0).Contains(point); 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 class StreamContext : IStreamGeometryContextImpl
{ {
private readonly StreamGeometryImpl _geometryImpl; private readonly StreamGeometryImpl _geometryImpl;
@ -100,7 +102,6 @@ namespace Avalonia.Skia
{ {
SKRect rc; SKRect rc;
_path.GetBounds(out rc); _path.GetBounds(out rc);
_geometryImpl.ApplyTransform();
_geometryImpl.Bounds = rc.ToAvaloniaRect(); _geometryImpl.Bounds = rc.ToAvaloniaRect();
} }

1
src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj

@ -68,6 +68,7 @@
<Compile Include="Media\GeometryImpl.cs" /> <Compile Include="Media\GeometryImpl.cs" />
<Compile Include="Media\StreamGeometryImpl.cs" /> <Compile Include="Media\StreamGeometryImpl.cs" />
<Compile Include="Media\FormattedTextImpl.cs" /> <Compile Include="Media\FormattedTextImpl.cs" />
<Compile Include="Media\TransformedGeometryImpl.cs" />
<Compile Include="PrimitiveExtensions.cs" /> <Compile Include="PrimitiveExtensions.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="RenderTarget.cs" /> <Compile Include="RenderTarget.cs" />

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

@ -88,14 +88,19 @@ namespace Avalonia.Direct2D1
public IFormattedTextImpl CreateFormattedText( public IFormattedTextImpl CreateFormattedText(
string text, string text,
string fontFamily, Typeface typeface,
double fontSize,
FontStyle fontStyle,
TextAlignment textAlignment, TextAlignment textAlignment,
FontWeight fontWeight, TextWrapping wrapping,
TextWrapping wrapping) Size constraint,
IReadOnlyList<FormattedTextStyleSpan> 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) public IRenderer CreateRenderer(IRenderRoot root, IRenderLoop renderLoop)

2
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@ -207,7 +207,7 @@ namespace Avalonia.Direct2D1.Media
{ {
var impl = (FormattedTextImpl)text; 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)) using (var renderer = new AvaloniaTextRenderer(this, _renderTarget, brush.PlatformBrush))
{ {
if (brush.PlatformBrush != null) if (brush.PlatformBrush != null)

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

@ -14,51 +14,52 @@ namespace Avalonia.Direct2D1.Media
{ {
public FormattedTextImpl( public FormattedTextImpl(
string text, string text,
string fontFamily, Typeface typeface,
double fontSize,
FontStyle fontStyle,
TextAlignment textAlignment, TextAlignment textAlignment,
FontWeight fontWeight, TextWrapping wrapping,
TextWrapping wrapping) Size constraint,
IReadOnlyList<FormattedTextStyleSpan> spans)
{ {
Text = text;
var factory = AvaloniaLocator.Current.GetService<DWrite.Factory>(); var factory = AvaloniaLocator.Current.GetService<DWrite.Factory>();
using (var format = new DWrite.TextFormat( using (var format = new DWrite.TextFormat(
factory, factory,
fontFamily, typeface?.FontFamilyName ?? "Courier New",
(DWrite.FontWeight)fontWeight, (DWrite.FontWeight)(typeface?.Weight ?? FontWeight.Normal),
(DWrite.FontStyle)fontStyle, (DWrite.FontStyle)(typeface?.Style ?? FontStyle.Normal),
(float)fontSize)) (float)(typeface?.FontSize ?? 12)))
{ {
Text = text;
format.WordWrapping = wrapping == TextWrapping.Wrap ? format.WordWrapping = wrapping == TextWrapping.Wrap ?
DWrite.WordWrapping.Wrap : DWrite.WordWrapping.NoWrap; DWrite.WordWrapping.Wrap :
DWrite.WordWrapping.NoWrap;
TextLayout = new DWrite.TextLayout( TextLayout = new DWrite.TextLayout(
factory, factory,
text ?? string.Empty, text ?? string.Empty,
format, format,
float.MaxValue, (float)constraint.Width,
float.MaxValue); (float)constraint.Height)
{
TextAlignment = textAlignment.ToDirect2D()
};
} }
TextLayout.TextAlignment = textAlignment.ToDirect2D(); if (spans != null)
}
public Size Constraint
{
get
{ {
return new Size(TextLayout.MaxWidth, TextLayout.MaxHeight); foreach (var span in spans)
{
ApplySpan(span);
}
} }
set Size = Measure();
{
TextLayout.MaxWidth = (float)value.Width;
TextLayout.MaxHeight = (float)value.Height;
}
} }
public Size Constraint => new Size(TextLayout.MaxWidth, TextLayout.MaxHeight);
public Size Size { get; }
public string Text { get; } public string Text { get; }
public DWrite.TextLayout TextLayout { 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)); 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 metrics = TextLayout.Metrics;
var width = metrics.WidthIncludingTrailingWhitespace; var width = metrics.WidthIncludingTrailingWhitespace;
@ -125,12 +139,5 @@ namespace Avalonia.Direct2D1.Media
return new Size(width, TextLayout.Metrics.Height); 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));
}
} }
} }

99
src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs

@ -1,6 +1,7 @@
// Copyright (c) The Avalonia Project. All rights reserved. // 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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Platform; using Avalonia.Platform;
using SharpDX.Direct2D1; using SharpDX.Direct2D1;
@ -11,85 +12,61 @@ namespace Avalonia.Direct2D1.Media
/// </summary> /// </summary>
public abstract class GeometryImpl : IGeometryImpl public abstract class GeometryImpl : IGeometryImpl
{ {
private TransformedGeometry _transformed; public GeometryImpl(Geometry geometry)
/// <summary>
/// Gets the geometry's bounding rectangle.
/// </summary>
public abstract Rect Bounds
{ {
get; Geometry = geometry;
} }
/// <summary> /// <inheritdoc/>
/// Gets the geomentry without any transforms applied. public Rect Bounds => Geometry.GetWidenedBounds(0).ToAvalonia();
/// </summary>
public abstract Geometry DefiningGeometry /// <inheritdoc/>
public Geometry Geometry { get; }
/// <inheritdoc/>
public virtual Matrix Transform => Matrix.Identity;
/// <inheritdoc/>
public Rect GetRenderBounds(double strokeThickness)
{ {
get; return Geometry.GetWidenedBounds((float)strokeThickness).ToAvalonia();
} }
/// <summary> /// <inheritdoc/>
/// Gets the Direct2D <see cref="Geometry"/>. public bool FillContains(Point point)
/// </summary> {
public Geometry Geometry => _transformed ?? DefiningGeometry; return Geometry.FillContainsPoint(point.ToSharpDX());
}
/// <summary> /// <inheritdoc/>
/// Gets or sets the transform for the geometry. public IGeometryImpl Intersect(IGeometryImpl geometry)
/// </summary>
public Matrix Transform
{ {
get var result = new PathGeometry(Geometry.Factory);
{
return _transformed != null ?
_transformed.Transform.ToAvalonia() :
Matrix.Identity;
}
set using (var sink = result.Open())
{ {
if (value != Transform) Geometry.Combine(((GeometryImpl)geometry).Geometry, CombineMode.Intersect, sink);
{ return new StreamGeometryImpl(result);
if (_transformed != null)
{
_transformed.Dispose();
_transformed = null;
}
if (!value.IsIdentity)
{
Factory factory = AvaloniaLocator.Current.GetService<Factory>();
_transformed = new TransformedGeometry(
factory,
DefiningGeometry,
value.ToDirect2D());
}
}
} }
} }
/// <summary> /// <inheritdoc/>
/// Gets the geometry's bounding rectangle with the specified stroke thickness. public bool StrokeContains(Avalonia.Media.Pen pen, Point point)
/// </summary>
/// <param name="strokeThickness">The stroke thickness.</param>
/// <returns>The bounding rectangle.</returns>
public Rect GetRenderBounds(double strokeThickness)
{ {
if (_transformed != null) return Geometry.StrokeContainsPoint(point.ToSharpDX(), (float)pen.Thickness);
{
return _transformed.GetWidenedBounds((float)strokeThickness).ToAvalonia();
}
else
{
return DefiningGeometry.GetWidenedBounds((float)strokeThickness).ToAvalonia();
}
} }
/// <inheritdoc/>
public bool FillContains(Point point) public IGeometryImpl WithTransform(Matrix transform)
{ {
return Geometry.FillContainsPoint(point.ToSharpDX()); var factory = AvaloniaLocator.Current.GetService<Factory>();
return new TransformedGeometryImpl(
new TransformedGeometry(
factory,
GetSourceGeometry(),
transform.ToDirect2D()));
} }
protected virtual Geometry GetSourceGeometry() => Geometry;
} }
} }

36
src/Windows/Avalonia.Direct2D1/Media/StreamGeometryImpl.cs

@ -3,7 +3,6 @@
using Avalonia.Platform; using Avalonia.Platform;
using SharpDX.Direct2D1; using SharpDX.Direct2D1;
using D2DGeometry = SharpDX.Direct2D1.Geometry;
namespace Avalonia.Direct2D1.Media namespace Avalonia.Direct2D1.Media
{ {
@ -12,55 +11,44 @@ namespace Avalonia.Direct2D1.Media
/// </summary> /// </summary>
public class StreamGeometryImpl : GeometryImpl, IStreamGeometryImpl public class StreamGeometryImpl : GeometryImpl, IStreamGeometryImpl
{ {
private readonly PathGeometry _path;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class. /// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
/// </summary> /// </summary>
public StreamGeometryImpl() public StreamGeometryImpl()
: base(CreateGeometry())
{ {
Factory factory = AvaloniaLocator.Current.GetService<Factory>();
_path = new PathGeometry(factory);
} }
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class. /// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
/// </summary> /// </summary>
/// <param name="geometry">An existing Direct2D <see cref="PathGeometry"/>.</param> /// <param name="geometry">An existing Direct2D <see cref="PathGeometry"/>.</param>
protected StreamGeometryImpl(PathGeometry geometry) public StreamGeometryImpl(PathGeometry geometry)
: base(geometry)
{ {
_path = geometry;
} }
/// <inheritdoc/> /// <inheritdoc/>
public override Rect Bounds => _path.GetWidenedBounds(0).ToAvalonia();
/// <inheritdoc/>
public override D2DGeometry DefiningGeometry => _path;
/// <summary>
/// Clones the geometry.
/// </summary>
/// <returns>A cloned geometry.</returns>
public IStreamGeometryImpl Clone() public IStreamGeometryImpl Clone()
{ {
Factory factory = AvaloniaLocator.Current.GetService<Factory>(); Factory factory = AvaloniaLocator.Current.GetService<Factory>();
var result = new PathGeometry(factory); var result = new PathGeometry(factory);
var sink = result.Open(); var sink = result.Open();
_path.Stream(sink); ((PathGeometry)Geometry).Stream(sink);
sink.Close(); sink.Close();
return new StreamGeometryImpl(result); return new StreamGeometryImpl(result);
} }
/// <summary> /// <inheritdoc/>
/// Opens the geometry to start defining it.
/// </summary>
/// <returns>
/// An <see cref="Avalonia.Platform.IStreamGeometryContextImpl"/> which can be used to define the geometry.
/// </returns>
public IStreamGeometryContextImpl Open() public IStreamGeometryContextImpl Open()
{ {
return new StreamGeometryContextImpl(_path.Open()); return new StreamGeometryContextImpl(((PathGeometry)Geometry).Open());
}
private static Geometry CreateGeometry()
{
Factory factory = AvaloniaLocator.Current.GetService<Factory>();
return new PathGeometry(factory);
} }
} }
} }

24
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
{
/// <summary>
/// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
/// </summary>
/// <param name="geometry">An existing Direct2D <see cref="TransformedGeometry"/>.</param>
public TransformedGeometryImpl(TransformedGeometry geometry)
: base(geometry)
{
}
/// <inheritdoc/>
public override Matrix Transform => ((TransformedGeometry)Geometry).Transform.ToAvalonia();
protected override Geometry GetSourceGeometry() => ((TransformedGeometry)Geometry).SourceGeometry;
}
}

17
tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs

@ -335,7 +335,7 @@ namespace Avalonia.Input.UnitTests
class MockRenderInterface : IPlatformRenderInterface 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<FormattedTextStyleSpan> spans)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
@ -414,11 +414,26 @@ namespace Avalonia.Input.UnitTests
throw new NotImplementedException(); throw new NotImplementedException();
} }
public IGeometryImpl Intersect(IGeometryImpl geometry)
{
throw new NotImplementedException();
}
public IStreamGeometryContextImpl Open() public IStreamGeometryContextImpl Open()
{ {
return _impl; return _impl;
} }
public bool StrokeContains(Pen pen, Point point)
{
throw new NotImplementedException();
}
public IGeometryImpl WithTransform(Matrix transform)
{
throw new NotImplementedException();
}
class MockStreamGeometryContext : IStreamGeometryContextImpl class MockStreamGeometryContext : IStreamGeometryContextImpl
{ {
private List<Point> points = new List<Point>(); private List<Point> points = new List<Point>();

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

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

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

@ -50,37 +50,40 @@ namespace Avalonia.Direct2D1.RenderTests.Media
FontStyle fontStyle, FontStyle fontStyle,
TextAlignment textAlignment, TextAlignment textAlignment,
FontWeight fontWeight, FontWeight fontWeight,
TextWrapping wrapping) TextWrapping wrapping,
double widthConstraint)
{ {
var r = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>(); var r = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
return r.CreateFormattedText(text, return r.CreateFormattedText(text,
fontFamily, new Typeface(fontFamily, fontSize, fontStyle, fontWeight),
fontSize,
fontStyle,
textAlignment, textAlignment,
fontWeight, wrapping,
wrapping); widthConstraint == -1 ? Size.Infinity : new Size(widthConstraint, double.PositiveInfinity),
null);
} }
private IFormattedTextImpl Create(string text, double fontSize) private IFormattedTextImpl Create(string text, double fontSize)
{ {
return Create(text, FontName, fontSize, return Create(text, FontName, fontSize,
FontStyle.Normal, TextAlignment.Left, FontStyle.Normal, TextAlignment.Left,
FontWeight.Normal, TextWrapping.NoWrap); FontWeight.Normal, TextWrapping.NoWrap,
-1);
} }
private IFormattedTextImpl Create(string text, double fontSize, TextAlignment alignment) private IFormattedTextImpl Create(string text, double fontSize, TextAlignment alignment, double widthConstraint)
{ {
return Create(text, FontName, fontSize, return Create(text, FontName, fontSize,
FontStyle.Normal, alignment, FontStyle.Normal, alignment,
FontWeight.Normal, TextWrapping.NoWrap); FontWeight.Normal, TextWrapping.NoWrap,
widthConstraint);
} }
private IFormattedTextImpl Create(string text, double fontSize, TextWrapping wrap) private IFormattedTextImpl Create(string text, double fontSize, TextWrapping wrap, double widthConstraint)
{ {
return Create(text, FontName, fontSize, return Create(text, FontName, fontSize,
FontStyle.Normal, TextAlignment.Left, FontStyle.Normal, TextAlignment.Left,
FontWeight.Normal, wrap); FontWeight.Normal, wrap,
widthConstraint);
} }
#if AVALONIA_CAIRO #if AVALONIA_CAIRO
@ -101,17 +104,15 @@ namespace Avalonia.Direct2D1.RenderTests.Media
[InlineData(stringmiddlenewlines, FontSize, 72.01, 4 * FontSizeHeight)] [InlineData(stringmiddlenewlines, FontSize, 72.01, 4 * FontSizeHeight)]
public void Should_Measure_String_Correctly(string input, double fontSize, double expWidth, double expHeight) public void Should_Measure_String_Correctly(string input, double fontSize, double expWidth, double expHeight)
{ {
using (var fmt = Create(input, fontSize)) var fmt = Create(input, fontSize);
{ var size = fmt.Size;
var size = fmt.Measure();
Assert.Equal(expWidth, size.Width, 2); Assert.Equal(expWidth, size.Width, 2);
Assert.Equal(expHeight, size.Height, 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 #if AVALONIA_CAIRO
@ -135,16 +136,11 @@ namespace Avalonia.Direct2D1.RenderTests.Media
double widthConstraint, double widthConstraint,
TextWrapping wrap) TextWrapping wrap)
{ {
using (var fmt = Create(input, FontSize, wrap)) var fmt = Create(input, FontSize, wrap, widthConstraint);
{ var constrained = fmt;
if (widthConstraint != -1)
{
fmt.Constraint = new Size(widthConstraint, 10000);
}
var lines = fmt.GetLines().ToArray(); var lines = constrained.GetLines().ToArray();
Assert.Equal(linesCount, lines.Count()); Assert.Equal(linesCount, lines.Count());
}
} }
#if AVALONIA_CAIRO #if AVALONIA_CAIRO
@ -178,14 +174,12 @@ namespace Avalonia.Direct2D1.RenderTests.Media
double x, double y, double x, double y,
bool isInside, bool isTrailing, int pos) bool isInside, bool isTrailing, int pos)
{ {
using (var fmt = Create(input, FontSize)) var fmt = Create(input, FontSize);
{ var htRes = fmt.HitTestPoint(new Point(x, y));
var htRes = fmt.HitTestPoint(new Point(x, y));
Assert.Equal(pos, htRes.TextPosition); Assert.Equal(pos, htRes.TextPosition);
Assert.Equal(isInside, htRes.IsInside); Assert.Equal(isInside, htRes.IsInside);
Assert.Equal(isTrailing, htRes.IsTrailing); Assert.Equal(isTrailing, htRes.IsTrailing);
}
} }
#if AVALONIA_CAIRO #if AVALONIA_CAIRO
@ -205,15 +199,13 @@ namespace Avalonia.Direct2D1.RenderTests.Media
public void Should_HitTestPosition_Correctly(string input, public void Should_HitTestPosition_Correctly(string input,
int index, double x, double y, double width, double height) int index, double x, double y, double width, double height)
{ {
using (var fmt = Create(input, FontSize)) var fmt = Create(input, FontSize);
{ var r = fmt.HitTestTextPosition(index);
var r = fmt.HitTestTextPosition(index);
Assert.Equal(x, r.X, 2); Assert.Equal(x, r.X, 2);
Assert.Equal(y, r.Y, 2); Assert.Equal(y, r.Y, 2);
Assert.Equal(width, r.Width, 2); Assert.Equal(width, r.Width, 2);
Assert.Equal(height, r.Height, 2); Assert.Equal(height, r.Height, 2);
}
} }
#if AVALONIA_CAIRO #if AVALONIA_CAIRO
@ -229,17 +221,14 @@ namespace Avalonia.Direct2D1.RenderTests.Media
double x, double y, double width, double height) double x, double y, double width, double height)
{ {
//parse expected //parse expected
using (var fmt = Create(input, FontSize, TextAlignment.Right)) var fmt = Create(input, FontSize, TextAlignment.Right, widthConstraint);
{ var constrained = fmt;
fmt.Constraint = new Size(widthConstraint, 100); var r = constrained.HitTestTextPosition(index);
var r = fmt.HitTestTextPosition(index);
Assert.Equal(x, r.X, 2); Assert.Equal(x, r.X, 2);
Assert.Equal(y, r.Y, 2); Assert.Equal(y, r.Y, 2);
Assert.Equal(width, r.Width, 2); Assert.Equal(width, r.Width, 2);
Assert.Equal(height, r.Height, 2); Assert.Equal(height, r.Height, 2);
}
} }
#if AVALONIA_CAIRO #if AVALONIA_CAIRO
@ -255,17 +244,14 @@ namespace Avalonia.Direct2D1.RenderTests.Media
double x, double y, double width, double height) double x, double y, double width, double height)
{ {
//parse expected //parse expected
using (var fmt = Create(input, FontSize, TextAlignment.Center)) var fmt = Create(input, FontSize, TextAlignment.Center, widthConstraint);
{ var constrained = fmt;
fmt.Constraint = new Size(widthConstraint, 100); var r = constrained.HitTestTextPosition(index);
var r = fmt.HitTestTextPosition(index); Assert.Equal(x, r.X, 2);
Assert.Equal(y, r.Y, 2);
Assert.Equal(x, r.X, 2); Assert.Equal(width, r.Width, 2);
Assert.Equal(y, r.Y, 2); Assert.Equal(height, r.Height, 2);
Assert.Equal(width, r.Width, 2);
Assert.Equal(height, r.Height, 2);
}
} }
#if AVALONIA_CAIRO #if AVALONIA_CAIRO
@ -291,22 +277,20 @@ namespace Avalonia.Direct2D1.RenderTests.Media
return new Rect(v[0], v[1], v[2], v[3]); return new Rect(v[0], v[1], v[2], v[3]);
}).ToArray(); }).ToArray();
using (var fmt = Create(input, FontSize)) var fmt = Create(input, FontSize);
{ var htRes = fmt.HitTestTextRange(index, length).ToArray();
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++) for (int i = 0; i < rects.Length; i++)
{ {
var exr = rects[i]; var exr = rects[i];
var r = htRes[i]; var r = htRes[i];
Assert.Equal(exr.X, r.X, 2); Assert.Equal(exr.X, r.X, 2);
Assert.Equal(exr.Y, r.Y, 2); Assert.Equal(exr.Y, r.Y, 2);
Assert.Equal(exr.Width, r.Width, 2); Assert.Equal(exr.Width, r.Width, 2);
Assert.Equal(exr.Height, r.Height, 2); Assert.Equal(exr.Height, r.Height, 2);
}
} }
} }
} }

10
tests/Avalonia.UnitTests/TestServices.cs

@ -13,6 +13,7 @@ using Avalonia.Styling;
using Avalonia.Themes.Default; using Avalonia.Themes.Default;
using Avalonia.Rendering; using Avalonia.Rendering;
using System.Reactive.Concurrency; using System.Reactive.Concurrency;
using System.Collections.Generic;
namespace Avalonia.UnitTests namespace Avalonia.UnitTests
{ {
@ -163,12 +164,11 @@ namespace Avalonia.UnitTests
return Mock.Of<IPlatformRenderInterface>(x => return Mock.Of<IPlatformRenderInterface>(x =>
x.CreateFormattedText( x.CreateFormattedText(
It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<string>(), It.IsAny<Typeface>(),
It.IsAny<double>(),
It.IsAny<FontStyle>(),
It.IsAny<TextAlignment>(), It.IsAny<TextAlignment>(),
It.IsAny<FontWeight>(), It.IsAny<TextWrapping>(),
It.IsAny<TextWrapping>()) == Mock.Of<IFormattedTextImpl>() && It.IsAny<Size>(),
It.IsAny<IReadOnlyList<FormattedTextStyleSpan>>()) == Mock.Of<IFormattedTextImpl>() &&
x.CreateStreamGeometry() == Mock.Of<IStreamGeometryImpl>( x.CreateStreamGeometry() == Mock.Of<IStreamGeometryImpl>(
y => y.Open() == Mock.Of<IStreamGeometryContextImpl>())); y => y.Open() == Mock.Of<IStreamGeometryContextImpl>()));
} }

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

@ -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<ArgumentException>(() => new FormattedText(
"foo",
"Ariel",
0));
}
[Fact]
public void Exception_Should_Be_Thrown_If_FontWeight_0()
{
Assert.Throws<ArgumentException>(() => new FormattedText(
"foo",
"Ariel",
12,
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_LessThanEqualTo_0()
{
Assert.Throws<ArgumentException>(() => new Typeface("foo", 0));
}
[Fact]
public void Exception_Should_Be_Thrown_If_FontWeight_LessThanEqualTo_0()
{
Assert.Throws<ArgumentException>(() => new Typeface("foo", 12, weight: 0));
}
}
}

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

@ -10,12 +10,11 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
{ {
public IFormattedTextImpl CreateFormattedText( public IFormattedTextImpl CreateFormattedText(
string text, string text,
string fontFamilyName, Typeface typeface,
double fontSize,
FontStyle fontStyle,
TextAlignment textAlignment, TextAlignment textAlignment,
FontWeight fontWeight, TextWrapping wrapping,
TextWrapping wrapping) Size constraint,
IReadOnlyList<FormattedTextStyleSpan> spans)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
@ -94,11 +93,26 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
throw new NotImplementedException(); throw new NotImplementedException();
} }
public IGeometryImpl Intersect(IGeometryImpl geometry)
{
throw new NotImplementedException();
}
public IStreamGeometryContextImpl Open() public IStreamGeometryContextImpl Open()
{ {
return _impl; return _impl;
} }
public bool StrokeContains(Pen pen, Point point)
{
throw new NotImplementedException();
}
public IGeometryImpl WithTransform(Matrix transform)
{
throw new NotImplementedException();
}
class MockStreamGeometryContext : IStreamGeometryContextImpl class MockStreamGeometryContext : IStreamGeometryContextImpl
{ {
private List<Point> points = new List<Point>(); private List<Point> points = new List<Point>();

Loading…
Cancel
Save