Browse Source

Merge branch 'master' into master

pull/954/head
Steven Kirk 10 years ago
committed by GitHub
parent
commit
f47519b41f
  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);
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();
}
}

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

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

21
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
/// <returns>A <see cref="FormattedText"/> object.</returns>
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,
};
}
/// <summary>
@ -370,7 +368,6 @@ namespace Avalonia.Controls
if (_formattedText != null)
{
_constraint = _formattedText.Constraint;
_formattedText.Dispose();
_formattedText = null;
}

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

@ -117,7 +117,11 @@ namespace TheArtOfDev.HtmlRenderer.Avalonia.Adapters
FormattedText GetText(string str, RFont font)
{
var f = ((FontAdapter)font);
return new FormattedText(str, f.Name, font.Size, 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)

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

@ -10,63 +10,32 @@ namespace Avalonia.Media
/// <summary>
/// Represents a piece of text with formatting.
/// </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>
/// Initializes a new instance of the <see cref="FormattedText"/> class.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="fontFamilyName">The font family.</param>
/// <param name="fontSize">The font size.</param>
/// <param name="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)
public FormattedText()
{
Contract.Requires<ArgumentNullException>(text != null);
Contract.Requires<ArgumentNullException>(fontFamilyName != null);
if (fontSize <= 0)
{
throw new ArgumentException("FontSize must be greater than 0");
}
if (fontWeight <= 0)
{
throw new ArgumentException("FontWeight must be greater than 0");
}
Text = text;
FontFamilyName = fontFamilyName;
FontSize = fontSize;
FontStyle = fontStyle;
FontWeight = fontWeight;
TextAlignment = textAlignment;
Wrapping = wrapping;
var platform = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
if (platform == null)
{
throw new Exception("Could not create FormattedText: IPlatformRenderInterface not registered.");
}
_platform = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
}
PlatformImpl = platform.CreateFormattedText(
text,
fontFamilyName,
fontSize,
fontStyle,
textAlignment,
fontWeight,
wrapping);
/// <summary>
/// Initializes a new instance of the <see cref="FormattedText"/> class.
/// </summary>
/// <param name="platform">The platform render interface.</param>
public FormattedText(IPlatformRenderInterface platform)
{
_platform = platform;
}
/// <summary>
@ -74,64 +43,76 @@ namespace Avalonia.Media
/// </summary>
public Size Constraint
{
get
{
CheckDisposed();
return PlatformImpl.Constraint;
}
set
{
CheckDisposed();
PlatformImpl.Constraint = value;
}
get => _constraint;
set => Set(ref _constraint, value);
}
/// <summary>
/// Gets the font family.
/// Gets or sets the base typeface.
/// </summary>
public string FontFamilyName { get; }
public Typeface Typeface
{
get => _typeface;
set => Set(ref _typeface, value);
}
/// <summary>
/// Gets the font size.
/// Gets or sets a collection of spans that describe the formatting of subsections of the
/// text.
/// </summary>
public double FontSize { get; }
public IReadOnlyList<FormattedTextStyleSpan> Spans
{
get => _spans;
set => Set(ref _spans, value);
}
/// <summary>
/// Gets the font style.
/// Gets or sets the text.
/// </summary>
public FontStyle FontStyle { get; }
public string Text
{
get => _text;
set => Set(ref _text, value);
}
/// <summary>
/// Gets the font weight.
/// Gets or sets the aligment of the text.
/// </summary>
public FontWeight FontWeight { get; }
public TextAlignment TextAlignment
{
get => _textAlignment;
set => Set(ref _textAlignment, value);
}
/// <summary>
/// Gets the text.
/// Gets or sets the text wrapping.
/// </summary>
public string Text { get; }
public TextWrapping Wrapping
{
get => _wrapping;
set => Set(ref _wrapping, value);
}
/// <summary>
/// Gets platform-specific platform implementation.
/// </summary>
public IFormattedTextImpl PlatformImpl { get; }
/// <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()
public IFormattedTextImpl PlatformImpl
{
PlatformImpl.Dispose();
get
{
if (_platformImpl == null)
{
_platformImpl = _platform.CreateFormattedText(
_text,
_typeface,
_textAlignment,
_wrapping,
_constraint,
_spans);
}
return _platformImpl;
}
}
/// <summary>
@ -142,7 +123,6 @@ namespace Avalonia.Media
/// </returns>
public IEnumerable<FormattedTextLine> GetLines()
{
CheckDisposed();
return PlatformImpl.GetLines();
}
@ -155,7 +135,6 @@ namespace Avalonia.Media
/// </returns>
public TextHitTestResult HitTestPoint(Point point)
{
CheckDisposed();
return PlatformImpl.HitTestPoint(point);
}
@ -166,7 +145,6 @@ namespace Avalonia.Media
/// <returns>The character bounds.</returns>
public Rect HitTestTextPosition(int index)
{
CheckDisposed();
return PlatformImpl.HitTestTextPosition(index);
}
@ -178,7 +156,6 @@ namespace Avalonia.Media
/// <returns>The character bounds.</returns>
public IEnumerable<Rect> HitTestTextRange(int index, int length)
{
CheckDisposed();
return PlatformImpl.HitTestTextRange(index, length);
}
@ -188,20 +165,13 @@ namespace Avalonia.Media
/// <returns>The bounds box of the text.</returns>
public Size Measure()
{
CheckDisposed();
return PlatformImpl.Measure();
return PlatformImpl.Size;
}
/// <summary>
/// Sets the foreground brush for the specified text range.
/// </summary>
/// <param name="brush">The brush.</param>
/// <param name="startIndex">The start of the text range.</param>
/// <param name="length">The length of the text range.</param>
public void SetForegroundBrush(IBrush brush, int startIndex, int length)
private void Set<T>(ref T field, T value)
{
CheckDisposed();
PlatformImpl.SetForegroundBrush(brush, startIndex, length);
field = value;
_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>
static Geometry()
{
TransformProperty.Changed.Subscribe(x =>
{
((Geometry)x.Sender).PlatformImpl.Transform = ((Transform)x.NewValue).Value;
});
TransformProperty.Changed.AddClassHandler<Geometry>(x => x.TransformChanged);
}
/// <summary>
@ -68,7 +65,7 @@ namespace Avalonia.Media
}
/// <summary>
/// Indicates whether the geometry contains the specified point.
/// Indicates whether the geometry's fill contains the specified point.
/// </summary>
/// <param name="point">The point.</param>
/// <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);
}
/// <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>
/// Defines the platform-specific interface for <see cref="FormattedText"/>.
/// </summary>
public interface IFormattedTextImpl : IDisposable
public interface IFormattedTextImpl
{
/// <summary>
/// Gets or sets the constraint of the text.
/// Gets the constraint of the text.
/// </summary>
Size Constraint { get; set; }
Size Constraint { get; }
/// <summary>
/// The measured size of the text.
/// </summary>
Size Size { get; }
/// <summary>
/// Gets the text.
@ -53,19 +58,5 @@ namespace Avalonia.Platform
/// <param name="length">The number of characters in the text range.</param>
/// <returns>The character bounds.</returns>
IEnumerable<Rect> HitTestTextRange(int index, int length);
/// <summary>
/// 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.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Media;
namespace Avalonia.Platform
{
/// <summary>
@ -14,9 +16,9 @@ namespace Avalonia.Platform
Rect Bounds { get; }
/// <summary>
/// Gets or sets a transform to apply to the geometry.
/// Gets the transform to applied to the geometry.
/// </summary>
Matrix Transform { get; set; }
Matrix Transform { get; }
/// <summary>
/// Gets the geometry's bounding rectangle with the specified stroke thickness.
@ -26,10 +28,32 @@ namespace Avalonia.Platform
Rect GetRenderBounds(double strokeThickness);
/// <summary>
/// Indicates whether the geometry contains the specified point.
/// Indicates whether the geometry's fill contains the specified point.
/// </summary>
/// <param name="point">The point.</param>
/// <returns><c>true</c> if the geometry contains the point; otherwise, <c>false</c>.</returns>
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.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="fontFamilyName">The font family.</param>
/// <param name="fontSize">The font size.</param>
/// <param name="fontStyle">The font style.</param>
/// <param name="typeface">The base typeface.</param>
/// <param name="textAlignment">The text alignment.</param>
/// <param name="fontWeight">The font weight.</param>
/// <param name="wrapping">The text wrapping mode.</param>
/// <param name="constraint">The text layout constraints.</param>
/// <param name="spans">The style spans.</param>
/// <returns>An <see cref="IFormattedTextImpl"/>.</returns>
IFormattedTextImpl CreateFormattedText(
string text,
string fontFamilyName,
double fontSize,
FontStyle fontStyle,
Typeface typeface,
TextAlignment textAlignment,
FontWeight fontWeight,
TextWrapping wrapping);
TextWrapping wrapping,
Size constraint,
IReadOnlyList<FormattedTextStyleSpan> spans);
/// <summary>
/// Creates a stream geometry implementation.

15
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);
}
}
}

18
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<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)

85
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<FormattedTextStyleSpan> spans)
{
Contract.Requires<ArgumentNullException>(context != null);
Contract.Requires<ArgumentNullException>(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<FormattedTextLine> 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);
}
}
}
}

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)
{
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 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);
}
/// <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)
{
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);

87
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<FormattedTextStyleSpan> 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<FormattedTextLine> 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<FBrushRange, IBrush>(key, brush));
}
}
public override string ToString()
{
return Text;
@ -286,6 +270,7 @@ namespace Avalonia.Skia
private readonly List<FormattedTextLine> _lines = new List<FormattedTextLine>();
private readonly SKPaint _paint;
private readonly List<Rect> _rects = new List<Rect>();
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<FBrushRange, IBrush>(key, brush));
}
}
private struct AvaloniaFormattedTextLine
{
public float Height;

11
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<FormattedTextStyleSpan> spans)
{
return new FormattedTextImpl(text, fontFamilyName, fontSize, fontStyle, textAlignment, fontWeight, wrapping);
return new FormattedTextImpl(text, typeface, textAlignment, wrapping, constraint, spans);
}
public IStreamGeometryImpl CreateStreamGeometry()

63
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();
}

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

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

17
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<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)

2
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)

73
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<FormattedTextStyleSpan> spans)
{
Text = text;
var factory = AvaloniaLocator.Current.GetService<DWrite.Factory>();
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));
}
}
}

99
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
/// </summary>
public abstract class GeometryImpl : IGeometryImpl
{
private TransformedGeometry _transformed;
/// <summary>
/// Gets the geometry's bounding rectangle.
/// </summary>
public abstract Rect Bounds
public GeometryImpl(Geometry geometry)
{
get;
Geometry = geometry;
}
/// <summary>
/// Gets the geomentry without any transforms applied.
/// </summary>
public abstract Geometry DefiningGeometry
/// <inheritdoc/>
public Rect Bounds => Geometry.GetWidenedBounds(0).ToAvalonia();
/// <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>
/// Gets the Direct2D <see cref="Geometry"/>.
/// </summary>
public Geometry Geometry => _transformed ?? DefiningGeometry;
/// <inheritdoc/>
public bool FillContains(Point point)
{
return Geometry.FillContainsPoint(point.ToSharpDX());
}
/// <summary>
/// Gets or sets the transform for the geometry.
/// </summary>
public Matrix Transform
/// <inheritdoc/>
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<Factory>();
_transformed = new TransformedGeometry(
factory,
DefiningGeometry,
value.ToDirect2D());
}
}
Geometry.Combine(((GeometryImpl)geometry).Geometry, CombineMode.Intersect, sink);
return new StreamGeometryImpl(result);
}
}
/// <summary>
/// Gets the geometry's bounding rectangle with the specified stroke thickness.
/// </summary>
/// <param name="strokeThickness">The stroke thickness.</param>
/// <returns>The bounding rectangle.</returns>
public Rect GetRenderBounds(double strokeThickness)
/// <inheritdoc/>
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)
/// <inheritdoc/>
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 SharpDX.Direct2D1;
using D2DGeometry = SharpDX.Direct2D1.Geometry;
namespace Avalonia.Direct2D1.Media
{
@ -12,55 +11,44 @@ namespace Avalonia.Direct2D1.Media
/// </summary>
public class StreamGeometryImpl : GeometryImpl, IStreamGeometryImpl
{
private readonly PathGeometry _path;
/// <summary>
/// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
/// </summary>
public StreamGeometryImpl()
: base(CreateGeometry())
{
Factory factory = AvaloniaLocator.Current.GetService<Factory>();
_path = new PathGeometry(factory);
}
/// <summary>
/// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
/// </summary>
/// <param name="geometry">An existing Direct2D <see cref="PathGeometry"/>.</param>
protected StreamGeometryImpl(PathGeometry geometry)
public StreamGeometryImpl(PathGeometry geometry)
: base(geometry)
{
_path = geometry;
}
/// <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()
{
Factory factory = AvaloniaLocator.Current.GetService<Factory>();
var result = new PathGeometry(factory);
var sink = result.Open();
_path.Stream(sink);
((PathGeometry)Geometry).Stream(sink);
sink.Close();
return new StreamGeometryImpl(result);
}
/// <summary>
/// Opens the geometry to start defining it.
/// </summary>
/// <returns>
/// An <see cref="Avalonia.Platform.IStreamGeometryContextImpl"/> which can be used to define the geometry.
/// </returns>
/// <inheritdoc/>
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
{
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();
}
@ -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<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 Size Size => new Size();
public void Dispose()
{
}
@ -155,20 +157,22 @@ namespace Avalonia.Layout.UnitTests
public IEnumerable<Rect> HitTestTextRange(int index, int length) => new Rect[0];
public Size Measure() => Constraint;
public void SetForegroundBrush(IBrush brush, int startIndex, int length)
{
}
}
private void RegisterServices()
{
var globalStyles = new Mock<IGlobalStyles>();
var renderInterface = new Mock<IPlatformRenderInterface>();
renderInterface.Setup(x => x.CreateFormattedText(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<double>(), It.IsAny<FontStyle>(),
It.IsAny<TextAlignment>(), It.IsAny<FontWeight>(), It.IsAny<TextWrapping>()))
renderInterface.Setup(x =>
x.CreateFormattedText(
It.IsAny<string>(),
It.IsAny<Typeface>(),
It.IsAny<TextAlignment>(),
It.IsAny<TextWrapping>(),
It.IsAny<Size>(),
It.IsAny<IReadOnlyList<FormattedTextStyleSpan>>()))
.Returns(new FormattedTextMock("TEST"));
var windowImpl = new Mock<IWindowImpl>();
Size clientSize = default(Size);

136
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<IPlatformRenderInterface>();
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);
}
}
}

10
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<IPlatformRenderInterface>(x =>
x.CreateFormattedText(
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<double>(),
It.IsAny<FontStyle>(),
It.IsAny<Typeface>(),
It.IsAny<TextAlignment>(),
It.IsAny<FontWeight>(),
It.IsAny<TextWrapping>()) == Mock.Of<IFormattedTextImpl>() &&
It.IsAny<TextWrapping>(),
It.IsAny<Size>(),
It.IsAny<IReadOnlyList<FormattedTextStyleSpan>>()) == Mock.Of<IFormattedTextImpl>() &&
x.CreateStreamGeometry() == Mock.Of<IStreamGeometryImpl>(
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(
string text,
string fontFamilyName,
double fontSize,
FontStyle fontStyle,
Typeface typeface,
TextAlignment textAlignment,
FontWeight fontWeight,
TextWrapping wrapping)
TextWrapping wrapping,
Size constraint,
IReadOnlyList<FormattedTextStyleSpan> 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<Point> points = new List<Point>();

Loading…
Cancel
Save