Browse Source

Merge branch 'master' into Stylus

Stylus
Max Katz 4 years ago
committed by GitHub
parent
commit
39f48d2ac9
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .editorconfig
  2. 7
      native/Avalonia.Native/src/OSX/AvnView.mm
  3. 27
      samples/ControlCatalog.Android/ControlCatalog.Android.csproj
  4. 3
      samples/ControlCatalog.Android/Properties/AndroidManifest.xml
  5. 1
      samples/ControlCatalog.Android/environment.device.txt
  6. 1
      samples/ControlCatalog.Android/environment.emulator.txt
  7. 4
      samples/RenderDemo/Pages/FormattedTextPage.axaml.cs
  8. 37
      src/Avalonia.Base/GeometryCollection.cs
  9. 18
      src/Avalonia.Base/Media/DrawingCollection.cs
  10. 420
      src/Avalonia.Base/Media/DrawingGroup.cs
  11. 129
      src/Avalonia.Base/Media/FormattedText.cs
  12. 45
      src/Avalonia.Base/Media/GeometryCollection.cs
  13. 12
      src/Avalonia.Base/Media/GeometryDrawing.cs
  14. 58
      src/Avalonia.Base/Media/GeometryGroup.cs
  15. 10
      src/Avalonia.Base/Media/GlyphRun.cs
  16. 3
      src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
  17. 1
      src/Avalonia.Base/PropertyStore/PriorityValue.cs
  18. 27
      src/Avalonia.Base/Styling/PropertySetterInstance.cs
  19. 95
      src/Avalonia.Base/Styling/Styles.cs
  20. 34
      src/Avalonia.Controls/ComboBox.cs
  21. 7
      src/Avalonia.Controls/Control.cs
  22. 74
      src/Avalonia.Controls/TextBox.cs
  23. 4
      src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  24. 4
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
  25. 73
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/XDataTypeTransformer.cs
  26. 26
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  27. 22
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  28. 15
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs
  29. 14
      tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs
  30. 33
      tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs
  31. 42
      tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs
  32. 2
      tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs
  33. 2
      tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
  34. 101
      tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
  35. 57
      tests/Avalonia.Controls.UnitTests/FlowDirectionTests.cs
  36. 28
      tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs
  37. 28
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  38. 3
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs
  39. 92
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs
  40. 17
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TreeDataTemplateTests.cs
  41. 35
      tests/Avalonia.RenderTests/Media/GlyphRunTests.cs
  42. 4
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
  43. BIN
      tests/TestFiles/Direct2D1/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png
  44. BIN
      tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png

1
.editorconfig

@ -21,6 +21,7 @@ csharp_new_line_before_finally = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_between_query_expression_clauses = true
trim_trailing_whitespace = true
# Indentation preferences
csharp_indent_block_contents = true

7
native/Avalonia.Native/src/OSX/AvnView.mm

@ -439,7 +439,12 @@
if(_parent != nullptr)
{
_lastKeyHandled = _parent->BaseEvents->RawKeyEvent(type, timestamp, modifiers, key);
auto handled = _parent->BaseEvents->RawKeyEvent(type, timestamp, modifiers, key);
if (key != LeftCtrl && key != RightCtrl) {
_lastKeyHandled = handled;
} else {
_lastKeyHandled = false;
}
}
}

27
samples/ControlCatalog.Android/ControlCatalog.Android.csproj

@ -9,42 +9,37 @@
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<AndroidPackageFormat>apk</AndroidPackageFormat>
<MSBuildEnableWorkloadResolver>true</MSBuildEnableWorkloadResolver>
<RuntimeIdentifiers>android-arm64;android-x64</RuntimeIdentifiers>
</PropertyGroup>
<ItemGroup>
<None Remove="Assets\AboutAssets.txt" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="..\..\build\Assets\Icon.png">
<Link>Resources\drawable\Icon.png</Link>
</AndroidResource>
</ItemGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release' and '$(TF_BUILD)' == ''">
<DebugSymbols>False</DebugSymbols>
<UseInterpreter>False</UseInterpreter>
<PropertyGroup Condition="'$(RunAOTCompilation)'=='' and '$(Configuration)'=='Release' and '$(TF_BUILD)'==''">
<RunAOTCompilation>True</RunAOTCompilation>
</PropertyGroup>
<PropertyGroup Condition="'$(RunAOTCompilation)'=='True'">
<EnableLLVM>True</EnableLLVM>
<AndroidAotAdditionalArguments>no-write-symbols,nodebug</AndroidAotAdditionalArguments>
<AndroidAotMode>Hybrid</AndroidAotMode>
<AndroidGenerateJniMarshalMethods>True</AndroidGenerateJniMarshalMethods>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<EmbedAssembliesIntoApk>False</EmbedAssembliesIntoApk>
<RunAOTCompilation>False</RunAOTCompilation>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<EmbedAssembliesIntoApk>True</EmbedAssembliesIntoApk>
<PropertyGroup Condition="'$(AndroidEnableProfiler)'=='True'">
<IsEmulator Condition="'$(IsEmulator)' == ''">True</IsEmulator>
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.3.1.3" />
<PackageReference Include="Xamarin.AndroidX.Lifecycle.ViewModel" Version="2.3.1.3" />
<AndroidEnvironment Condition="'$(IsEmulator)'=='True'" Include="environment.emulator.txt" />
<AndroidEnvironment Condition="'$(IsEmulator)'!='True'" Include="environment.device.txt" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Android\Avalonia.Android\Avalonia.Android.csproj" />
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />
</ItemGroup>
</Project>
</Project>

3
samples/ControlCatalog.Android/Properties/AndroidManifest.xml

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:installLocation="auto">
<application android:label="ControlCatalog.Android" android:icon="@drawable/Icon"></application>
<application android:label="ControlCatalog.Android" android:icon="@drawable/Icon"></application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>

1
samples/ControlCatalog.Android/environment.device.txt

@ -0,0 +1 @@
DOTNET_DiagnosticPorts=127.0.0.1:9000,suspend

1
samples/ControlCatalog.Android/environment.emulator.txt

@ -0,0 +1 @@
DOTNET_DiagnosticPorts=10.0.2.2:9001,suspend

4
samples/RenderDemo/Pages/FormattedTextPage.axaml.cs

@ -55,6 +55,10 @@ namespace RenderDemo.Pages
formattedText.SetFontStyle(FontStyle.Italic, 28, 28);
context.DrawText(formattedText, new Point(10, 0));
var geometry = formattedText.BuildGeometry(new Point(10 + formattedText.Width + 10, 0));
context.DrawGeometry(gradient, null, geometry);
}
}
}

37
src/Avalonia.Base/GeometryCollection.cs

@ -1,37 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using Avalonia.Animation;
#nullable enable
namespace Avalonia.Media
{
public class GeometryCollection : Animatable, IList<Geometry>, IReadOnlyList<Geometry>
{
private List<Geometry> _inner;
public GeometryCollection() => _inner = new List<Geometry>();
public GeometryCollection(IEnumerable<Geometry> collection) => _inner = new List<Geometry>(collection);
public GeometryCollection(int capacity) => _inner = new List<Geometry>(capacity);
public Geometry this[int index]
{
get => _inner[index];
set => _inner[index] = value;
}
public int Count => _inner.Count;
public bool IsReadOnly => false;
public void Add(Geometry item) => _inner.Add(item);
public void Clear() => _inner.Clear();
public bool Contains(Geometry item) => _inner.Contains(item);
public void CopyTo(Geometry[] array, int arrayIndex) => _inner.CopyTo(array, arrayIndex);
public IEnumerator<Geometry> GetEnumerator() => _inner.GetEnumerator();
public int IndexOf(Geometry item) => _inner.IndexOf(item);
public void Insert(int index, Geometry item) => _inner.Insert(index, item);
public bool Remove(Geometry item) => _inner.Remove(item);
public void RemoveAt(int index) => _inner.RemoveAt(index);
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

18
src/Avalonia.Base/Media/DrawingCollection.cs

@ -0,0 +1,18 @@
using System.Collections.Generic;
using Avalonia.Collections;
namespace Avalonia.Media
{
public sealed class DrawingCollection : AvaloniaList<Drawing>
{
public DrawingCollection()
{
ResetBehavior = ResetBehavior.Remove;
}
public DrawingCollection(IEnumerable<Drawing> items) : base(items)
{
ResetBehavior = ResetBehavior.Remove;
}
}
}

420
src/Avalonia.Base/Media/DrawingGroup.cs

@ -1,6 +1,10 @@
using Avalonia.Collections;
using System;
using System.Collections.Generic;
using Avalonia.Media.Imaging;
using Avalonia.Metadata;
using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Utilities;
namespace Avalonia.Media
{
@ -18,6 +22,14 @@ namespace Avalonia.Media
public static readonly StyledProperty<IBrush> OpacityMaskProperty =
AvaloniaProperty.Register<DrawingGroup, IBrush>(nameof(OpacityMask));
public static readonly DirectProperty<DrawingGroup, DrawingCollection> ChildrenProperty =
AvaloniaProperty.RegisterDirect<DrawingGroup, DrawingCollection>(
nameof(Children),
o => o.Children,
(o, v) => o.Children = v);
private DrawingCollection _children = new DrawingCollection();
public double Opacity
{
get => GetValue(OpacityProperty);
@ -42,8 +54,23 @@ namespace Avalonia.Media
set => SetValue(OpacityMaskProperty, value);
}
/// <summary>
/// Gets or sets the collection that contains the child geometries.
/// </summary>
[Content]
public AvaloniaList<Drawing> Children { get; } = new AvaloniaList<Drawing>();
public DrawingCollection Children
{
get => _children;
set
{
SetAndRaise(ChildrenProperty, ref _children, value);
}
}
public DrawingContext Open()
{
return new DrawingContext(new DrawingGroupDrawingContext(this));
}
public override void Draw(DrawingContext context)
{
@ -75,5 +102,394 @@ namespace Avalonia.Media
return rect;
}
private class DrawingGroupDrawingContext : IDrawingContextImpl
{
private readonly DrawingGroup _drawingGroup;
private readonly IPlatformRenderInterface _platformRenderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
private Matrix _transform;
private bool _disposed;
// Root drawing created by this DrawingContext.
//
// If there is only a single child of the root DrawingGroup, _rootDrawing
// will reference the single child, and the root _currentDrawingGroup
// value will be null. Otherwise, _rootDrawing will reference the
// root DrawingGroup, and be the same value as the root _currentDrawingGroup.
//
// Either way, _rootDrawing always references the root drawing.
protected Drawing? _rootDrawing;
// Current DrawingGroup that new children are added to
protected DrawingGroup? _currentDrawingGroup;
// Previous values of _currentDrawingGroup
private Stack<DrawingGroup?>? _previousDrawingGroupStack;
public DrawingGroupDrawingContext(DrawingGroup drawingGroup)
{
_drawingGroup = drawingGroup;
}
public Matrix Transform
{
get => _transform;
set
{
_transform = value;
PushTransform(new MatrixTransform(value));
}
}
public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect)
{
if ((brush == null) && (pen == null))
{
return;
}
// Instantiate the geometry
var geometry = _platformRenderInterface.CreateEllipseGeometry(rect);
// Add Drawing to the Drawing graph
AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry));
}
public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry)
{
if (((brush == null) && (pen == null)) || (geometry == null))
{
return;
}
AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry));
}
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun)
{
if (foreground == null || glyphRun == null)
{
return;
}
// Add a GlyphRunDrawing to the Drawing graph
GlyphRunDrawing glyphRunDrawing = new GlyphRunDrawing
{
Foreground = foreground,
GlyphRun = glyphRun,
};
// Add Drawing to the Drawing graph
AddDrawing(glyphRunDrawing);
}
public void DrawLine(IPen pen, Point p1, Point p2)
{
if (pen == null)
{
return;
}
// Instantiate the geometry
var geometry = _platformRenderInterface.CreateLineGeometry(p1, p2);
// Add Drawing to the Drawing graph
AddNewGeometryDrawing(null, pen, new PlatformGeometry(geometry));
}
public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows boxShadows = default)
{
if ((brush == null) && (pen == null))
{
return;
}
// Instantiate the geometry
var geometry = _platformRenderInterface.CreateRectangleGeometry(rect.Rect);
// Add Drawing to the Drawing graph
AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry));
}
public void Clear(Color color)
{
throw new NotImplementedException();
}
public IDrawingContextLayerImpl CreateLayer(Size size)
{
throw new NotImplementedException();
}
public void Custom(ICustomDrawOperation custom)
{
throw new NotImplementedException();
}
public void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default)
{
throw new NotImplementedException();
}
public void DrawBitmap(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect)
{
throw new NotImplementedException();
}
public void PopBitmapBlendMode()
{
throw new NotImplementedException();
}
public void PopClip()
{
throw new NotImplementedException();
}
public void PopGeometryClip()
{
throw new NotImplementedException();
}
public void PopOpacity()
{
throw new NotImplementedException();
}
public void PopOpacityMask()
{
throw new NotImplementedException();
}
public void PushBitmapBlendMode(BitmapBlendingMode blendingMode)
{
throw new NotImplementedException();
}
public void PushClip(Rect clip)
{
throw new NotImplementedException();
}
public void PushClip(RoundedRect clip)
{
throw new NotImplementedException();
}
public void PushGeometryClip(IGeometryImpl clip)
{
throw new NotImplementedException();
}
public void PushOpacity(double opacity)
{
throw new NotImplementedException();
}
public void PushOpacityMask(IBrush mask, Rect bounds)
{
throw new NotImplementedException();
}
public void Dispose()
{
// Dispose may be called multiple times without throwing
// an exception.
if (!_disposed)
{
// Match any outstanding Push calls with a Pop
if (_previousDrawingGroupStack != null)
{
int stackCount = _previousDrawingGroupStack.Count;
for (int i = 0; i < stackCount; i++)
{
Pop();
}
}
// Call CloseCore with the root DrawingGroup's children
DrawingCollection rootChildren;
if (_currentDrawingGroup != null)
{
// If we created a root DrawingGroup because multiple elements
// exist at the root level, provide it's Children collection
// directly.
rootChildren = _currentDrawingGroup.Children;
}
else
{
// Create a new DrawingCollection if we didn't create a
// root DrawingGroup because the root level only contained
// a single child.
//
// This collection is needed by DrawingGroup.Open because
// Open always replaces it's Children collection. It isn't
// strictly needed for Append, but always using a collection
// simplifies the TransactionalAppend implementation (i.e.,
// a seperate implemention isn't needed for a single element)
rootChildren = new DrawingCollection();
//
// We may need to opt-out of inheritance through the new Freezable.
// This is controlled by this.CanBeInheritanceContext.
//
if (_rootDrawing != null)
{
rootChildren.Add(_rootDrawing);
}
}
// Inform our derived classes that Close was called
_drawingGroup.Children = rootChildren;
_disposed = true;
}
}
/// <summary>
/// Pop
/// </summary>
private void Pop()
{
// Verify that Pop hasn't been called too many times
if ((_previousDrawingGroupStack == null) || (_previousDrawingGroupStack.Count == 0))
{
throw new InvalidOperationException("DrawingGroupStack count missmatch.");
}
// Restore the previous value of the current drawing group
_currentDrawingGroup = _previousDrawingGroupStack.Pop();
}
/// <summary>
/// PushTransform -
/// Push a Transform which will apply to all drawing operations until the corresponding
/// Pop.
/// </summary>
/// <param name="transform"> The Transform to push. </param>
private void PushTransform(Transform transform)
{
// Instantiate a new drawing group and set it as the _currentDrawingGroup
var drawingGroup = PushNewDrawingGroup();
// Set the transform on the new DrawingGroup
drawingGroup.Transform = transform;
}
/// <summary>
/// Creates a new DrawingGroup for a Push* call by setting the
/// _currentDrawingGroup to a newly instantiated DrawingGroup,
/// and saving the previous _currentDrawingGroup value on the
/// _previousDrawingGroupStack.
/// </summary>
private DrawingGroup PushNewDrawingGroup()
{
// Instantiate a new drawing group
DrawingGroup drawingGroup = new DrawingGroup();
// Add it to the drawing graph, like any other Drawing
AddDrawing(drawingGroup);
// Lazily allocate the stack when it is needed because many uses
// of DrawingDrawingContext will have a depth of one.
if (null == _previousDrawingGroupStack)
{
_previousDrawingGroupStack = new Stack<DrawingGroup?>(2);
}
// Save the previous _currentDrawingGroup value.
//
// If this is the first call, the value of _currentDrawingGroup
// will be null because AddDrawing doesn't create a _currentDrawingGroup
// for the first drawing. Having null on the stack is valid, and simply
// denotes that this new DrawingGroup is the first child in the root
// DrawingGroup. It is also possible for the first value on the stack
// to be non-null, which means that the root DrawingGroup has other
// children.
_previousDrawingGroupStack.Push(_currentDrawingGroup);
// Set this drawing group as the current one so that subsequent drawing's
// are added as it's children until Pop is called.
_currentDrawingGroup = drawingGroup;
return drawingGroup;
}
/// <summary>
/// Contains the functionality common to GeometryDrawing operations of
/// instantiating the GeometryDrawing, setting it's Freezable state,
/// and Adding it to the Drawing Graph.
/// </summary>
private void AddNewGeometryDrawing(IBrush? brush, IPen? pen, Geometry? geometry)
{
if (geometry == null)
{
throw new ArgumentNullException(nameof(geometry));
}
// Instantiate the GeometryDrawing
GeometryDrawing geometryDrawing = new GeometryDrawing
{
// We may need to opt-out of inheritance through the new Freezable.
// This is controlled by this.CanBeInheritanceContext.
Brush = brush,
Pen = pen,
Geometry = geometry
};
// Add it to the drawing graph
AddDrawing(geometryDrawing);
}
/// <summary>
/// Adds a new Drawing to the DrawingGraph.
///
/// This method avoids creating a DrawingGroup for the common case
/// where only a single child exists in the root DrawingGroup.
/// </summary>
private void AddDrawing(Drawing newDrawing)
{
if (newDrawing == null)
{
throw new ArgumentNullException(nameof(newDrawing));
}
if (_rootDrawing == null)
{
// When a DrawingGroup is set, it should be made the root if
// a root drawing didnt exist.
Contract.Requires<NotSupportedException>(_currentDrawingGroup == null);
// If this is the first Drawing being added, avoid creating a DrawingGroup
// and set this drawing as the root drawing. This optimizes the common
// case where only a single child exists in the root DrawingGroup.
_rootDrawing = newDrawing;
}
else if (_currentDrawingGroup == null)
{
// When the second drawing is added at the root level, set a
// DrawingGroup as the root and add both drawings to it.
// Instantiate the DrawingGroup
_currentDrawingGroup = new DrawingGroup();
// Add both Children
_currentDrawingGroup.Children.Add(_rootDrawing);
_currentDrawingGroup.Children.Add(newDrawing);
// Set the new DrawingGroup as the current
_rootDrawing = _currentDrawingGroup;
}
else
{
// If there already is a current drawing group, then simply add
// the new drawing too it.
_currentDrawingGroup.Children.Add(newDrawing);
}
}
}
}
}

129
src/Avalonia.Base/Media/FormattedText.cs

@ -1223,7 +1223,7 @@ namespace Avalonia.Media
public double OverhangTrailing
{
get
{
{
return BlackBoxMetrics.OverhangTrailing;
}
}
@ -1252,6 +1252,46 @@ namespace Avalonia.Media
}
}
/// <summary>
/// Obtains geometry for the text, including underlines and strikethroughs.
/// </summary>
/// <param name="origin">The left top origin of the resulting geometry.</param>
/// <returns>The geometry returned contains the combined geometry
/// of all of the glyphs, underlines and strikeThroughs that represent the formatted text.
/// Overlapping contours are merged by performing a Boolean union operation.</returns>
public Geometry? BuildGeometry(Point origin)
{
GeometryGroup? accumulatedGeometry = null;
var lineOrigin = origin;
DrawingGroup drawing = new DrawingGroup();
using (var ctx = drawing.Open())
{
using (var enumerator = GetEnumerator())
{
while (enumerator.MoveNext())
{
var currentLine = enumerator.Current;
if (currentLine != null)
{
currentLine.Draw(ctx, lineOrigin);
AdvanceLineOrigin(ref lineOrigin, currentLine);
}
}
}
}
Transform? transform = new TranslateTransform(origin.X, origin.Y);
// recursively go down the DrawingGroup to build up the geometry
CombineGeometryRecursive(drawing, ref transform, ref accumulatedGeometry);
return accumulatedGeometry;
}
/// <summary>
/// Draws the text object
/// </summary>
@ -1284,6 +1324,93 @@ namespace Avalonia.Media
}
}
private void CombineGeometryRecursive(Drawing drawing, ref Transform? transform, ref GeometryGroup? accumulatedGeometry)
{
if (drawing is DrawingGroup group)
{
transform = group.Transform;
if (group.Children is DrawingCollection children)
{
// recursively go down for DrawingGroup
foreach (var child in children)
{
CombineGeometryRecursive(child, ref transform, ref accumulatedGeometry);
}
}
}
else
{
if (drawing is GlyphRunDrawing glyphRunDrawing)
{
// process glyph run
var glyphRun = glyphRunDrawing.GlyphRun;
if (glyphRun != null)
{
var glyphRunGeometry = glyphRun.BuildGeometry();
glyphRunGeometry.Transform = transform;
if (accumulatedGeometry == null)
{
accumulatedGeometry = new GeometryGroup
{
FillRule = FillRule.NonZero
};
}
accumulatedGeometry.Children.Add(glyphRunGeometry);
}
}
else
{
if (drawing is GeometryDrawing geometryDrawing)
{
// process geometry (i.e. TextDecoration on the line)
var geometry = geometryDrawing.Geometry;
if (geometry != null)
{
geometry.Transform = transform;
if (geometry is LineGeometry lineGeometry)
{
// For TextDecoration drawn by DrawLine(), the geometry is a LineGeometry which has no
// bounding area. So this line won't show up. Work aroud it by increase the Bounding rect
// to be Pen's thickness
var bounds = lineGeometry.Bounds;
if (bounds.Height == 0)
{
bounds = bounds.WithHeight(geometryDrawing.Pen?.Thickness ?? 0);
}
else if (bounds.Width == 0)
{
bounds = bounds.WithWidth(geometryDrawing.Pen?.Thickness ?? 0);
}
// convert the line geometry into a rectangle geometry
// we lost line cap info here
geometry = new RectangleGeometry(bounds);
}
if (accumulatedGeometry == null)
{
accumulatedGeometry = new GeometryGroup
{
FillRule = FillRule.NonZero
};
}
accumulatedGeometry.Children.Add(geometry);
}
}
}
}
}
private CachedMetrics DrawAndCalculateMetrics(DrawingContext? drawingContext, Point drawingOffset, bool getBlackBoxMetrics)
{
var metrics = new CachedMetrics();

45
src/Avalonia.Base/Media/GeometryCollection.cs

@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using Avalonia.Collections;
#nullable enable
namespace Avalonia.Media
{
public sealed class GeometryCollection : AvaloniaList<Geometry>
{
public GeometryCollection()
{
ResetBehavior = ResetBehavior.Remove;
this.ForEachItem(
x =>
{
Parent?.Invalidate();
},
x =>
{
Parent?.Invalidate();
},
() => throw new NotSupportedException());
}
public GeometryCollection(IEnumerable<Geometry> items) : base(items)
{
ResetBehavior = ResetBehavior.Remove;
this.ForEachItem(
x =>
{
Parent?.Invalidate();
},
x =>
{
Parent?.Invalidate();
},
() => throw new NotSupportedException());
}
public GeometryGroup? Parent { get; set; }
}
}

12
src/Avalonia.Base/Media/GeometryDrawing.cs

@ -21,14 +21,14 @@ namespace Avalonia.Media
/// <summary>
/// Defines the <see cref="Brush"/> property.
/// </summary>
public static readonly StyledProperty<IBrush> BrushProperty =
AvaloniaProperty.Register<GeometryDrawing, IBrush>(nameof(Brush), Brushes.Transparent);
public static readonly StyledProperty<IBrush?> BrushProperty =
AvaloniaProperty.Register<GeometryDrawing, IBrush?>(nameof(Brush), Brushes.Transparent);
/// <summary>
/// Defines the <see cref="Pen"/> property.
/// </summary>
public static readonly StyledProperty<Pen> PenProperty =
AvaloniaProperty.Register<GeometryDrawing, Pen>(nameof(Pen));
public static readonly StyledProperty<Pen?> PenProperty =
AvaloniaProperty.Register<GeometryDrawing, Pen?>(nameof(Pen));
/// <summary>
/// Gets or sets the <see cref="Avalonia.Media.Geometry"/> that describes the shape of this <see cref="GeometryDrawing"/>.
@ -43,7 +43,7 @@ namespace Avalonia.Media
/// <summary>
/// Gets or sets the <see cref="Avalonia.Media.IBrush"/> used to fill the interior of the shape described by this <see cref="GeometryDrawing"/>.
/// </summary>
public IBrush Brush
public IBrush? Brush
{
get => GetValue(BrushProperty);
set => SetValue(BrushProperty, value);
@ -52,7 +52,7 @@ namespace Avalonia.Media
/// <summary>
/// Gets or sets the <see cref="Avalonia.Media.IPen"/> used to stroke this <see cref="GeometryDrawing"/>.
/// </summary>
public IPen Pen
public IPen? Pen
{
get => GetValue(PenProperty);
set => SetValue(PenProperty, value);

58
src/Avalonia.Base/GeometryGroup.cs → src/Avalonia.Base/Media/GeometryGroup.cs

@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Metadata;
using Avalonia.Metadata;
using Avalonia.Platform;
#nullable enable
@ -13,29 +10,36 @@ namespace Avalonia.Media
/// </summary>
public class GeometryGroup : Geometry
{
public static readonly DirectProperty<GeometryGroup, GeometryCollection?> ChildrenProperty =
AvaloniaProperty.RegisterDirect<GeometryGroup, GeometryCollection?> (
public static readonly DirectProperty<GeometryGroup, GeometryCollection> ChildrenProperty =
AvaloniaProperty.RegisterDirect<GeometryGroup, GeometryCollection> (
nameof(Children),
o => o.Children,
(o, v) => o.Children = v);
(o, v)=> o.Children = v);
public static readonly StyledProperty<FillRule> FillRuleProperty =
AvaloniaProperty.Register<GeometryGroup, FillRule>(nameof(FillRule));
private GeometryCollection? _children;
private bool _childrenSet;
private GeometryCollection _children;
public GeometryGroup()
{
_children = new GeometryCollection
{
Parent = this
};
}
/// <summary>
/// Gets or sets the collection that contains the child geometries.
/// </summary>
[Content]
public GeometryCollection? Children
public GeometryCollection Children
{
get => _children ??= (!_childrenSet ? new GeometryCollection() : null);
get => _children;
set
{
SetAndRaise(ChildrenProperty, ref _children, value);
_childrenSet = true;
OnChildrenChanged(_children, value);
SetAndRaise(ChildrenProperty, ref _children, value);
}
}
@ -52,16 +56,28 @@ namespace Avalonia.Media
public override Geometry Clone()
{
var result = new GeometryGroup { FillRule = FillRule, Transform = Transform };
if (_children?.Count > 0)
if (_children.Count > 0)
{
result.Children = new GeometryCollection(_children);
}
return result;
}
protected void OnChildrenChanged(GeometryCollection oldChildren, GeometryCollection newChildren)
{
oldChildren.Parent = null;
newChildren.Parent = this;
}
protected override IGeometryImpl? CreateDefiningGeometry()
{
if (_children?.Count > 0)
if (_children.Count > 0)
{
var factory = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
return factory.CreateGeometryGroup(FillRule, _children);
}
@ -72,10 +88,18 @@ namespace Avalonia.Media
{
base.OnPropertyChanged(change);
if (change.Property == ChildrenProperty || change.Property == FillRuleProperty)
switch (change.Property.Name)
{
InvalidateGeometry();
case nameof(FillRule):
case nameof(Children):
InvalidateGeometry();
break;
}
}
internal void Invalidate()
{
InvalidateGeometry();
}
}
}

10
src/Avalonia.Base/Media/GlyphRun.cs

@ -202,15 +202,9 @@ namespace Avalonia.Media
{
var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
var geometryImpl = platformRenderInterface.BuildGlyphRunGeometry(this, out var scale);
var geometryImpl = platformRenderInterface.BuildGlyphRunGeometry(this);
var geometry = new PlatformGeometry(geometryImpl);
var transform = new MatrixTransform(Matrix.CreateTranslation(geometry.Bounds.Left, -geometry.Bounds.Top) * scale);
geometry.Transform = transform;
return geometry;
return new PlatformGeometry(geometryImpl);
}
/// <summary>

3
src/Avalonia.Base/Platform/IPlatformRenderInterface.cs

@ -62,9 +62,8 @@ namespace Avalonia.Platform
/// Created a geometry implementation for the glyph run.
/// </summary>
/// <param name="glyphRun">The glyph run to build a geometry from.</param>
/// <param name="scale">The scaling of the produces geometry.</param>
/// <returns>The geometry returned contains the combined geometry of all glyphs in the glyph run.</returns>
IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale);
IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun);
/// <summary>
/// Creates a renderer.

1
src/Avalonia.Base/PropertyStore/PriorityValue.cs

@ -121,6 +121,7 @@ namespace Avalonia.PropertyStore
public void ClearLocalValue()
{
_localValue = default;
UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs<T>(
_owner,
Property,

27
src/Avalonia.Base/Styling/PropertySetterInstance.cs

@ -18,7 +18,7 @@ namespace Avalonia.Styling
private readonly DirectPropertyBase<T>? _directProperty;
private readonly T _value;
private IDisposable? _subscription;
private bool _isActive;
private State _state;
public PropertySetterInstance(
IStyleable target,
@ -40,6 +40,8 @@ namespace Avalonia.Styling
_value = value;
}
private bool IsActive => _state == State.Active;
public void Start(bool hasActivator)
{
if (hasActivator)
@ -70,31 +72,35 @@ namespace Avalonia.Styling
public void Activate()
{
if (!_isActive)
if (!IsActive)
{
_isActive = true;
_state = State.Active;
PublishNext();
}
}
public void Deactivate()
{
if (_isActive)
if (IsActive)
{
_isActive = false;
_state = State.Inactive;
PublishNext();
}
}
public override void Dispose()
{
if (_state == State.Disposed)
return;
_state = State.Disposed;
if (_subscription is object)
{
var sub = _subscription;
_subscription = null;
sub.Dispose();
}
else if (_isActive)
else if (IsActive)
{
if (_styledProperty is object)
{
@ -114,7 +120,14 @@ namespace Avalonia.Styling
private void PublishNext()
{
PublishNext(_isActive ? new BindingValue<T>(_value) : default);
PublishNext(IsActive ? new BindingValue<T>(_value) : default);
}
private enum State
{
Inactive,
Active,
Disposed,
}
}
}

95
src/Avalonia.Base/Styling/Styles.cs

@ -17,7 +17,7 @@ namespace Avalonia.Styling
IStyle,
IResourceProvider
{
private readonly AvaloniaList<IStyle> _styles = new AvaloniaList<IStyle>();
private readonly AvaloniaList<IStyle> _styles = new();
private IResourceHost? _owner;
private IResourceDictionary? _resources;
private StyleCache? _cache;
@ -62,16 +62,18 @@ namespace Avalonia.Styling
{
value = value ?? throw new ArgumentNullException(nameof(Resources));
if (Owner is object)
var currentOwner = Owner;
if (currentOwner is not null)
{
_resources?.RemoveOwner(Owner);
_resources?.RemoveOwner(currentOwner);
}
_resources = value;
if (Owner is object)
if (currentOwner is not null)
{
_resources.AddOwner(Owner);
_resources.AddOwner(currentOwner);
}
}
}
@ -89,7 +91,7 @@ namespace Avalonia.Styling
foreach (var i in this)
{
if (i is IResourceProvider p && p.HasResources)
if (i is IResourceProvider { HasResources: true })
{
return true;
}
@ -190,7 +192,7 @@ namespace Avalonia.Styling
{
owner = owner ?? throw new ArgumentNullException(nameof(owner));
if (Owner != null)
if (Owner is not null)
{
throw new InvalidOperationException("The Styles already has a owner.");
}
@ -227,70 +229,81 @@ namespace Avalonia.Styling
}
}
private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
private static IReadOnlyList<T> ToReadOnlyList<T>(ICollection list)
{
static IReadOnlyList<T> ToReadOnlyList<T>(IList list)
if (list is IReadOnlyList<T> readOnlyList)
{
if (list is IReadOnlyList<T>)
{
return (IReadOnlyList<T>)list;
}
else
{
var result = new T[list.Count];
list.CopyTo(result, 0);
return result;
}
return readOnlyList;
}
void Add(IList items)
var result = new T[list.Count];
list.CopyTo(result, 0);
return result;
}
private static void InternalAdd(IList items, IResourceHost? owner, ref StyleCache? cache)
{
if (owner is not null)
{
for (var i = 0; i < items.Count; ++i)
{
var style = (IStyle)items[i]!;
if (Owner is object && style is IResourceProvider resourceProvider)
if (items[i] is IResourceProvider provider)
{
resourceProvider.AddOwner(Owner);
provider.AddOwner(owner);
}
_cache = null;
}
(Owner as IStyleHost)?.StylesAdded(ToReadOnlyList<IStyle>(items));
(owner as IStyleHost)?.StylesAdded(ToReadOnlyList<IStyle>(items));
}
if (items.Count > 0)
{
cache = null;
}
}
void Remove(IList items)
private static void InternalRemove(IList items, IResourceHost? owner, ref StyleCache? cache)
{
if (owner is not null)
{
for (var i = 0; i < items.Count; ++i)
{
var style = (IStyle)items[i]!;
if (Owner is object && style is IResourceProvider resourceProvider)
if (items[i] is IResourceProvider provider)
{
resourceProvider.RemoveOwner(Owner);
provider.RemoveOwner(owner);
}
_cache = null;
}
(Owner as IStyleHost)?.StylesRemoved(ToReadOnlyList<IStyle>(items));
(owner as IStyleHost)?.StylesRemoved(ToReadOnlyList<IStyle>(items));
}
if (items.Count > 0)
{
cache = null;
}
}
private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Reset)
{
throw new InvalidOperationException("Reset should not be called on Styles.");
}
var currentOwner = Owner;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
Add(e.NewItems!);
InternalAdd(e.NewItems!, currentOwner, ref _cache);
break;
case NotifyCollectionChangedAction.Remove:
Remove(e.OldItems!);
InternalRemove(e.OldItems!, currentOwner, ref _cache);
break;
case NotifyCollectionChangedAction.Replace:
Remove(e.OldItems!);
Add(e.NewItems!);
InternalRemove(e.OldItems!, currentOwner, ref _cache);
InternalAdd(e.NewItems!, currentOwner, ref _cache);
break;
case NotifyCollectionChangedAction.Reset:
throw new InvalidOperationException("Reset should not be called on Styles.");
}
CollectionChanged?.Invoke(this, e);

34
src/Avalonia.Controls/ComboBox.cs

@ -181,26 +181,13 @@ namespace Avalonia.Controls
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
this.UpdateSelectionBoxItem(SelectedItem);
UpdateSelectionBoxItem(SelectedItem);
}
// Because the SelectedItem isn't connected to the visual tree
public override void InvalidateMirrorTransform()
{
base.InvalidateMirrorTransform();
if (SelectedItem is Control selectedControl)
{
selectedControl.InvalidateMirrorTransform();
foreach (var visual in selectedControl.GetVisualDescendants())
{
if (visual is Control childControl)
{
childControl.InvalidateMirrorTransform();
}
}
}
UpdateFlowDirection();
}
/// <inheritdoc/>
@ -365,6 +352,8 @@ namespace Avalonia.Controls
{
parent.GetObservable(IsVisibleProperty).Subscribe(IsVisibleChanged).DisposeWith(_subscriptionsOnOpen);
}
UpdateFlowDirection();
}
private void IsVisibleChanged(bool isVisible)
@ -432,6 +421,8 @@ namespace Avalonia.Controls
}
};
}
UpdateFlowDirection();
}
else
{
@ -439,6 +430,19 @@ namespace Avalonia.Controls
}
}
private void UpdateFlowDirection()
{
if (SelectionBoxItem is Rectangle rectangle)
{
if ((rectangle.Fill as VisualBrush)?.Visual is Control content)
{
var flowDirection = (((IVisual)content!).VisualParent as Control)?.FlowDirection ??
FlowDirection.LeftToRight;
rectangle.FlowDirection = flowDirection;
}
}
}
private void SelectFocusedItem()
{
foreach (ItemContainerInfo dropdownItem in ItemContainerGenerator.Containers)

7
src/Avalonia.Controls/Control.cs

@ -378,17 +378,12 @@ namespace Avalonia.Controls
bool bypassFlowDirectionPolicies = BypassFlowDirectionPolicies;
bool parentBypassFlowDirectionPolicies = false;
var parent = this.FindAncestorOfType<Control>();
var parent = ((IVisual)this).VisualParent as Control;
if (parent != null)
{
parentFlowDirection = parent.FlowDirection;
parentBypassFlowDirectionPolicies = parent.BypassFlowDirectionPolicies;
}
else if (Parent is Control logicalParent)
{
parentFlowDirection = logicalParent.FlowDirection;
parentBypassFlowDirectionPolicies = logicalParent.BypassFlowDirectionPolicies;
}
bool thisShouldBeMirrored = flowDirection == FlowDirection.RightToLeft && !bypassFlowDirectionPolicies;
bool parentShouldBeMirrored = parentFlowDirection == FlowDirection.RightToLeft && !parentBypassFlowDirectionPolicies;

74
src/Avalonia.Controls/TextBox.cs

@ -53,7 +53,7 @@ namespace Avalonia.Controls
public static readonly StyledProperty<char> PasswordCharProperty =
AvaloniaProperty.Register<TextBox, char>(nameof(PasswordChar));
public static readonly StyledProperty<IBrush?> SelectionBrushProperty =
AvaloniaProperty.Register<TextBox, IBrush?>(nameof(SelectionBrush));
@ -196,7 +196,6 @@ namespace Avalonia.Controls
private TextBoxTextInputMethodClient _imClient = new TextBoxTextInputMethodClient();
private UndoRedoHelper<UndoRedoState> _undoRedoHelper;
private bool _isUndoingRedoing;
private bool _ignoreTextChanges;
private bool _canCut;
private bool _canCopy;
private bool _canPaste;
@ -276,7 +275,7 @@ namespace Avalonia.Controls
get => GetValue(IsReadOnlyProperty);
set => SetValue(IsReadOnlyProperty, value);
}
public char PasswordChar
{
get => GetValue(PasswordCharProperty);
@ -368,21 +367,17 @@ namespace Avalonia.Controls
get => _text;
set
{
if (!_ignoreTextChanges)
{
var caretIndex = CaretIndex;
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
var caretIndex = CaretIndex;
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
CaretIndex = CoerceCaretIndex(caretIndex, value);
SelectionStart = CoerceCaretIndex(selectionStart, value);
SelectionEnd = CoerceCaretIndex(selectionEnd, value);
if (SetAndRaise(TextProperty, ref _text, value) && IsUndoEnabled && !_isUndoingRedoing)
{
_undoRedoHelper.Clear();
SnapshotUndoRedo(); // so we always have an initial state
}
CaretIndex = CoerceCaretIndex(caretIndex, value);
SelectionStart = CoerceCaretIndex(selectionStart, value);
SelectionEnd = CoerceCaretIndex(selectionEnd, value);
if (SetAndRaise(TextProperty, ref _text, value) && IsUndoEnabled && !_isUndoingRedoing)
{
_undoRedoHelper.Clear();
SnapshotUndoRedo(); // so we always have an initial state
}
}
}
@ -736,32 +731,23 @@ namespace Avalonia.Controls
{
var oldText = _text;
_ignoreTextChanges = true;
try
{
DeleteSelection(false);
var caretIndex = CaretIndex;
text = Text ?? string.Empty;
SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex));
ClearSelection();
if (IsUndoEnabled)
{
_undoRedoHelper.DiscardRedo();
}
if (_text != oldText)
{
RaisePropertyChanged(TextProperty, oldText, _text);
}
DeleteSelection(false);
var caretIndex = CaretIndex;
text = Text ?? string.Empty;
SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex));
ClearSelection();
CaretIndex = caretIndex + input.Length;
if (IsUndoEnabled)
{
_undoRedoHelper.DiscardRedo();
}
finally
if (_text != oldText)
{
_ignoreTextChanges = false;
RaisePropertyChanged(TextProperty, oldText, _text);
}
CaretIndex = caretIndex + input.Length;
}
}
@ -1499,15 +1485,7 @@ namespace Avalonia.Controls
{
if (raiseTextChanged)
{
try
{
_ignoreTextChanges = true;
SetAndRaise(TextProperty, ref _text, value);
}
finally
{
_ignoreTextChanges = false;
}
SetAndRaise(TextProperty, ref _text, value);
}
else
{

4
src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@ -114,10 +114,8 @@ namespace Avalonia.Headless
return new HeadlessGlyphRunStub();
}
public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale)
public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun)
{
scale = Matrix.Identity;
return new HeadlessGeometryStub(new Rect(glyphRun.Size));
}

4
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs

@ -35,7 +35,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
Transformers.Insert(2, _designTransformer = new AvaloniaXamlIlDesignPropertiesTransformer());
Transformers.Insert(3, _bindingTransformer = new AvaloniaBindingExtensionTransformer());
// Targeted
InsertBefore<PropertyReferenceResolver>(
new AvaloniaXamlIlResolveClassesPropertiesTransformer(),
@ -57,6 +56,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
new AvaloniaXamlIlResolveByNameMarkupExtensionReplacer()
);
InsertAfter<TypeReferenceResolver>(
new XDataTypeTransformer());
// After everything else
InsertBefore<NewObjectTransformer>(
new AddNameScopeRegistration(),

73
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/XDataTypeTransformer.cs

@ -0,0 +1,73 @@
using System.Collections.Generic;
using System.Linq;
using XamlX;
using XamlX.Ast;
using XamlX.Transform;
using XamlX.Transform.Transformers;
using XamlX.TypeSystem;
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
{
internal class XDataTypeTransformer : IXamlAstTransformer
{
private const string DataTypePropertyName = "DataType";
/// <summary>
/// Converts x:DataType directives to regular DataType assignments if property with Avalonia.Metadata.DataTypeAttribute exists.
/// </summary>
/// <returns></returns>
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
{
if (node is XamlAstObjectNode on)
{
for (var c = 0; c < on.Children.Count; c++)
{
var ch = on.Children[c];
if (ch is XamlAstXmlDirective { Namespace: XamlNamespaces.Xaml2006, Name: DataTypePropertyName } d)
{
if (on.Children.OfType<XamlAstXamlPropertyValueNode>()
.Any(p => ((XamlAstNamePropertyReference)p.Property)?.Name == DataTypePropertyName))
{
// Break iteration if any DataType property was already set by user code.
break;
}
var templateDataTypeAttribute = context.GetAvaloniaTypes().DataTypeAttribute;
var clrType = (on.Type as XamlAstClrTypeReference)?.Type;
if (clrType is null)
{
break;
}
// Technically it's possible to map "x:DataType" to a property with [DataType] attribute regardless of its name,
// but we go explicitly strict here and check the name as well.
var (declaringType, dataTypeProperty) = GetAllProperties(clrType)
.FirstOrDefault(t => t.property.Name == DataTypePropertyName && t.property.CustomAttributes
.Any(a => a.Type == templateDataTypeAttribute));
if (dataTypeProperty is not null)
{
on.Children[c] = new XamlAstXamlPropertyValueNode(d,
new XamlAstNamePropertyReference(d,
new XamlAstClrTypeReference(ch, declaringType, false), dataTypeProperty.Name,
on.Type),
d.Values);
}
}
}
}
return node;
}
private static IEnumerable<(IXamlType declaringType, IXamlProperty property)> GetAllProperties(IXamlType t)
{
foreach (var p in t.Properties)
yield return (t, p);
if(t.BaseType!=null)
foreach (var tuple in GetAllProperties(t.BaseType))
yield return tuple;
}
}
}

26
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@ -62,7 +62,7 @@ namespace Avalonia.Skia
return new CombinedGeometryImpl(combineMode, g1, g2);
}
public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale)
public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun)
{
if (glyphRun.GlyphTypeface.PlatformImpl is not GlyphTypefaceImpl glyphTypeface)
{
@ -79,21 +79,29 @@ namespace Avalonia.Skia
};
SKPath path = new SKPath();
var matrix = SKMatrix.Identity;
var currentX = 0f;
var (currentX, currentY) = glyphRun.BaselineOrigin;
foreach (var glyph in glyphRun.GlyphIndices)
for (var i = 0; i < glyphRun.GlyphIndices.Count; i++)
{
var p = skFont.GetGlyphPath(glyph);
var glyph = glyphRun.GlyphIndices[i];
var glyphPath = skFont.GetGlyphPath(glyph);
path.AddPath(p, currentX, 0);
if (!glyphPath.IsEmpty)
{
path.AddPath(glyphPath, (float)currentX, (float)currentY);
}
currentX += p.Bounds.Right;
if (glyphRun.GlyphAdvances != null)
{
currentX += glyphRun.GlyphAdvances[i];
}
else
{
currentX += glyphPath.Bounds.Right;
}
}
scale = Matrix.CreateScale(matrix.ScaleX, matrix.ScaleY);
return new StreamGeometryImpl(path);
}

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

@ -13,6 +13,7 @@ using Avalonia.Media.Imaging;
using SharpDX.DirectWrite;
using GlyphRun = Avalonia.Media.GlyphRun;
using TextAlignment = Avalonia.Media.TextAlignment;
using SharpDX.Mathematics.Interop;
namespace Avalonia
{
@ -159,7 +160,7 @@ namespace Avalonia.Direct2D1
public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<Geometry> children) => new GeometryGroupImpl(fillRule, children);
public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2);
public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale)
public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun)
{
if (glyphRun.GlyphTypeface.PlatformImpl is not GlyphTypefaceImpl glyphTypeface)
{
@ -182,10 +183,23 @@ namespace Avalonia.Direct2D1
sink.Close();
}
scale = Matrix.Identity;
var (baselineOriginX, baselineOriginY) = glyphRun.BaselineOrigin;
return new StreamGeometryImpl(pathGeometry);
}
var transformedGeometry = new SharpDX.Direct2D1.TransformedGeometry(
Direct2D1Factory,
pathGeometry,
new RawMatrix3x2(1.0f, 0.0f, 0.0f, 1.0f, (float)baselineOriginX, (float)baselineOriginY));
return new TransformedGeometryWrapper(transformedGeometry);
}
private class TransformedGeometryWrapper : GeometryImpl
{
public TransformedGeometryWrapper(SharpDX.Direct2D1.TransformedGeometry geometry) : base(geometry)
{
}
}
/// <inheritdoc />
public IBitmapImpl LoadBitmap(string fileName)

15
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs

@ -17,6 +17,21 @@ namespace Avalonia.Base.UnitTests
Assert.Equal("foodefault", target.GetValue(Class1.FooProperty));
}
[Fact]
public void ClearValue_Resets_Value_To_Style_value()
{
Class1 target = new Class1();
target.SetValue(Class1.FooProperty, "style", BindingPriority.Style);
target.SetValue(Class1.FooProperty, "local");
Assert.Equal("local", target.GetValue(Class1.FooProperty));
target.ClearValue(Class1.FooProperty);
Assert.Equal("style", target.GetValue(Class1.FooProperty));
}
[Fact]
public void ClearValue_Raises_PropertyChanged()
{

14
tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs

@ -14,13 +14,21 @@ namespace Avalonia.Visuals.UnitTests.Media
}
[Fact]
public void Children_Can_Be_Set_To_Null()
public void Children_Change_Should_Raise_Changed()
{
var target = new GeometryGroup();
target.Children = null;
var children = new GeometryCollection();
Assert.Null(target.Children);
target.Children = children;
var isCalled = false;
target.Changed += (s, e) => isCalled = true;
children.Add(new StreamGeometry());
Assert.True(isCalled);
}
}
}

33
tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs

@ -349,6 +349,39 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph
}
}
[Fact]
public void MirrorTransform_For_Control_With_RenderTransform_Should_Be_Correct()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
Border border;
var tree = new TestRoot
{
Width = 400,
Height = 200,
Child = border = new Border
{
HorizontalAlignment = HorizontalAlignment.Left,
Background = Brushes.Red,
Width = 100,
RenderTransform = new ScaleTransform(0.5, 1),
FlowDirection = FlowDirection.RightToLeft
}
};
tree.Measure(Size.Infinity);
tree.Arrange(new Rect(tree.DesiredSize));
var scene = new Scene(tree);
var sceneBuilder = new SceneBuilder();
sceneBuilder.UpdateAll(scene);
var expectedTransform = new Matrix(-1, 0, 0, 1, 100, 0) * Matrix.CreateScale(0.5, 1) * Matrix.CreateTranslation(25, 0);
var borderNode = scene.FindNode(border);
Assert.Equal(expectedTransform, borderNode.Transform);
}
}
[Fact]
public void Should_Update_Border_Background_Node()
{

42
tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs

@ -150,13 +150,43 @@ namespace Avalonia.Base.UnitTests.Styling
Assert.Equal(BindingPriority.StyleTrigger, control.GetDiagnostic(TextBlock.TagProperty).Priority);
}
private IBinding CreateMockBinding(AvaloniaProperty property)
[Fact]
public void Disposing_Setter_Should_Preserve_LocalValue()
{
var subject = new Subject<object>();
var descriptor = InstancedBinding.OneWay(subject);
var binding = Mock.Of<IBinding>(x =>
x.Initiate(It.IsAny<IAvaloniaObject>(), property, null, false) == descriptor);
return binding;
var control = new Canvas();
var setter = new Setter(TextBlock.TagProperty, "foo");
var instance = setter.Instance(control);
instance.Start(true);
instance.Activate();
control.Tag = "bar";
instance.Dispose();
Assert.Equal("bar", control.Tag);
}
[Fact]
public void Disposing_Binding_Setter_Should_Preserve_LocalValue()
{
var control = new Canvas();
var source = new { Foo = "foo" };
var setter = new Setter(TextBlock.TagProperty, new Binding
{
Source = source,
Path = nameof(source.Foo),
});
var instance = setter.Instance(control);
instance.Start(true);
instance.Activate();
control.Tag = "bar";
instance.Dispose();
Assert.Equal("bar", control.Tag);
}
private class TestConverter : IValueConverter

2
tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs

@ -121,7 +121,7 @@ namespace Avalonia.Base.UnitTests.VisualTree
throw new NotImplementedException();
}
public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale)
public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun)
{
throw new NotImplementedException();
}

2
tests/Avalonia.Benchmarks/NullRenderingPlatform.cs

@ -117,7 +117,7 @@ namespace Avalonia.Benchmarks
return new NullGlyphRun();
}
public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale)
public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun)
{
throw new NotImplementedException();
}

101
tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs

@ -8,7 +8,7 @@ using Avalonia.Data;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Threading;
using Avalonia.VisualTree;
using Avalonia.UnitTests;
using Xunit;
@ -336,5 +336,104 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(1, count);
}
}
[Fact]
public void FlowDirection_Of_RectangleContent_Shuold_Be_LeftToRight()
{
var items = new[]
{
new ComboBoxItem()
{
Content = new Control()
}
};
var target = new ComboBox
{
FlowDirection = FlowDirection.RightToLeft,
Items = items,
Template = GetTemplate()
};
var root = new TestRoot(target);
target.ApplyTemplate();
target.SelectedIndex = 0;
var rectangle = target.GetValue(ComboBox.SelectionBoxItemProperty) as Rectangle;
Assert.Equal(FlowDirection.LeftToRight, rectangle.FlowDirection);
}
[Fact]
public void FlowDirection_Of_RectangleContent_Updated_After_InvalidateMirrorTransform()
{
var parentContent = new Decorator()
{
Child = new Control()
};
var items = new[]
{
new ComboBoxItem()
{
Content = parentContent.Child
}
};
var target = new ComboBox
{
Items = items,
Template = GetTemplate()
};
var root = new TestRoot(target);
target.ApplyTemplate();
target.SelectedIndex = 0;
var rectangle = target.GetValue(ComboBox.SelectionBoxItemProperty) as Rectangle;
Assert.Equal(FlowDirection.LeftToRight, rectangle.FlowDirection);
parentContent.FlowDirection = FlowDirection.RightToLeft;
target.FlowDirection = FlowDirection.RightToLeft;
Assert.Equal(FlowDirection.RightToLeft, rectangle.FlowDirection);
}
[Fact]
public void FlowDirection_Of_RectangleContent_Updated_After_OpenPopup()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var parentContent = new Decorator()
{
Child = new Control()
};
var items = new[]
{
new ComboBoxItem()
{
Content = parentContent.Child
}
};
var target = new ComboBox
{
FlowDirection = FlowDirection.RightToLeft,
Items = items,
Template = GetTemplate()
};
var root = new TestRoot(target);
target.ApplyTemplate();
target.SelectedIndex = 0;
var rectangle = target.GetValue(ComboBox.SelectionBoxItemProperty) as Rectangle;
Assert.Equal(FlowDirection.LeftToRight, rectangle.FlowDirection);
parentContent.FlowDirection = FlowDirection.RightToLeft;
var popup = target.GetVisualDescendants().OfType<Popup>().First();
popup.PlacementTarget = new Window();
popup.Open();
Assert.Equal(FlowDirection.RightToLeft, rectangle.FlowDirection);
}
}
}
}

57
tests/Avalonia.Controls.UnitTests/FlowDirectionTests.cs

@ -0,0 +1,57 @@
using Avalonia.Media;
using Xunit;
namespace Avalonia.Controls.UnitTests
{
public class FlowDirectionTests
{
[Fact]
public void HasMirrorTransform_Should_Be_True()
{
var target = new Control
{
FlowDirection = FlowDirection.RightToLeft,
};
Assert.True(target.HasMirrorTransform);
}
[Fact]
public void HasMirrorTransform_Of_LTR_Children_Should_Be_True_For_RTL_Parent()
{
Control child;
var target = new Decorator
{
FlowDirection = FlowDirection.RightToLeft,
Child = child = new Control()
};
child.FlowDirection = FlowDirection.LeftToRight;
Assert.True(target.HasMirrorTransform);
Assert.True(child.HasMirrorTransform);
}
[Fact]
public void HasMirrorTransform_Of_Children_Is_Updated_After_Parent_Changeed()
{
Control child;
var target = new Decorator
{
FlowDirection = FlowDirection.LeftToRight,
Child = child = new Control()
{
FlowDirection = FlowDirection.LeftToRight,
}
};
Assert.False(target.HasMirrorTransform);
Assert.False(child.HasMirrorTransform);
target.FlowDirection = FlowDirection.RightToLeft;
Assert.True(target.HasMirrorTransform);
Assert.True(child.HasMirrorTransform);
}
}
}

28
tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs

@ -179,34 +179,6 @@ namespace Avalonia.Controls.UnitTests
}
}
[Fact]
public void Typing_Beginning_With_0_Should_Not_Modify_Text_When_Bound_To_Int()
{
using (Start())
{
var source = new Class1();
var target = new MaskedTextBox
{
DataContext = source,
Template = CreateTemplate(),
};
target.ApplyTemplate();
target.Bind(TextBox.TextProperty, new Binding(nameof(Class1.Foo), BindingMode.TwoWay));
Assert.Equal("0", target.Text);
target.CaretIndex = 1;
target.RaiseEvent(new TextInputEventArgs
{
RoutedEvent = InputElement.TextInputEvent,
Text = "2",
});
Assert.Equal("02", target.Text);
}
}
[Fact]
public void Control_Backspace_Should_Remove_The_Word_Before_The_Caret_If_There_Is_No_Selection()
{

28
tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

@ -180,34 +180,6 @@ namespace Avalonia.Controls.UnitTests
}
}
[Fact]
public void Typing_Beginning_With_0_Should_Not_Modify_Text_When_Bound_To_Int()
{
using (UnitTestApplication.Start(Services))
{
var source = new Class1();
var target = new TextBox
{
DataContext = source,
Template = CreateTemplate(),
};
target.ApplyTemplate();
target.Bind(TextBox.TextProperty, new Binding(nameof(Class1.Foo), BindingMode.TwoWay));
Assert.Equal("0", target.Text);
target.CaretIndex = 1;
target.RaiseEvent(new TextInputEventArgs
{
RoutedEvent = InputElement.TextInputEvent,
Text = "2",
});
Assert.Equal("02", target.Text);
}
}
[Fact]
public void Control_Backspace_Should_Remove_The_Word_Before_The_Caret_If_There_Is_No_Selection()
{

3
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs

@ -17,6 +17,7 @@ using Avalonia.Markup.Xaml.Templates;
using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.UnitTests;
using JetBrains.Annotations;
using XamlX;
using Xunit;
@ -1527,7 +1528,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
[TemplateContent]
public object Content { get; set; }
public bool Match(object data) => FancyDataType.IsInstanceOfType(data);
public bool Match(object data) => FancyDataType?.IsInstanceOfType(data) ?? true;
public IControl Build(object data) => TemplateContent.Load(Content)?.Control;
}

92
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs

@ -1,6 +1,11 @@
using System;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Markup.Xaml.Templates;
using Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;
using Avalonia.Metadata;
using Avalonia.UnitTests;
using Xunit;
@ -90,6 +95,93 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
}
}
[Fact]
public void XDataType_Should_Be_Assigned_To_Clr_Property()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:sys='clr-namespace:System;assembly=netstandard'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.DataTemplates>
<DataTemplate x:DataType='sys:String'>
<Canvas Name='foo'/>
</DataTemplate>
</Window.DataTemplates>
<ContentControl Name='target' Content='Foo'/>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var target = window.FindControl<ContentControl>("target");
var template = (DataTemplate)window.DataTemplates.First();
window.ApplyTemplate();
target.ApplyTemplate();
((ContentPresenter)target.Presenter).UpdateChild();
Assert.Equal(typeof(string), template.DataType);
Assert.IsType<Canvas>(target.Presenter.Child);
}
}
[Fact]
public void XDataType_Should_Be_Ignored_If_DataType_Already_Set()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:sys='clr-namespace:System;assembly=netstandard'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.DataTemplates>
<DataTemplate DataType='sys:String' x:DataType='UserControl'>
<Canvas Name='foo'/>
</DataTemplate>
</Window.DataTemplates>
<ContentControl Name='target' Content='Foo'/>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var target = window.FindControl<ContentControl>("target");
window.ApplyTemplate();
target.ApplyTemplate();
((ContentPresenter)target.Presenter).UpdateChild();
Assert.IsType<Canvas>(target.Presenter.Child);
}
}
[Fact]
public void XDataType_Should_Be_Ignored_If_DataType_Has_Non_Standard_Name()
{
// We don't want DataType to be mapped to FancyDataType, avoid possible confusion.
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:sys='clr-namespace:System;assembly=netstandard'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'>
<ContentControl Name='target' Content='Foo'>
<ContentControl.ContentTemplate>
<local:CustomDataTemplate x:DataType='local:TestDataContext'>
<TextBlock Text='{CompiledBinding StringProperty}' Name='textBlock' />
</local:CustomDataTemplate>
</ContentControl.ContentTemplate>
</ContentControl>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var target = window.FindControl<ContentControl>("target");
window.ApplyTemplate();
target.ApplyTemplate();
((ContentPresenter)target.Presenter).UpdateChild();
var dataTemplate = (CustomDataTemplate)target.ContentTemplate;
Assert.Null(dataTemplate.FancyDataType);
}
}
[Fact]
public void Can_Set_DataContext_In_DataTemplate()
{

17
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TreeDataTemplateTests.cs

@ -21,5 +21,22 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.IsType<Binding>(template.ItemsSource);
}
}
[Fact]
public void XDataType_Should_Be_Assigned_To_Clr_Property()
{
using (UnitTestApplication.Start(TestServices.MockPlatformWrapper))
{
var xaml = @"
<DataTemplates xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<TreeDataTemplate x:DataType='x:String' />
</DataTemplates>";
var templates = (DataTemplates)AvaloniaRuntimeXamlLoader.Load(xaml);
var template = (TreeDataTemplate)(templates.First());
Assert.Equal(typeof(string), template.DataType);
}
}
}
}

35
tests/Avalonia.RenderTests/Media/GlyphRunTests.cs

@ -23,26 +23,28 @@ namespace Avalonia.Direct2D1.RenderTests.Media
[Fact]
public async Task Should_Render_GlyphRun_Geometry()
{
Decorator target = new Decorator
var control = new GlyphRunGeometryControl
{
Padding = new Thickness(8),
Width = 200,
Height = 100,
Child = new GlyphRunGeometryControl
[TextElement.ForegroundProperty] = new LinearGradientBrush
{
[TextElement.ForegroundProperty] = new LinearGradientBrush
{
StartPoint = new RelativePoint(0, 0.5, RelativeUnit.Relative),
EndPoint = new RelativePoint(1, 0.5, RelativeUnit.Relative),
GradientStops =
StartPoint = new RelativePoint(0, 0.5, RelativeUnit.Relative),
EndPoint = new RelativePoint(1, 0.5, RelativeUnit.Relative),
GradientStops =
{
new GradientStop { Color = Colors.Red, Offset = 0 },
new GradientStop { Color = Colors.Blue, Offset = 1 }
}
}
}
};
Decorator target = new Decorator
{
Padding = new Thickness(8),
Width = 190,
Height = 120,
Child = control
};
await RenderToFile(target);
CompareImages();
@ -50,8 +52,6 @@ namespace Avalonia.Direct2D1.RenderTests.Media
public class GlyphRunGeometryControl : Control
{
private readonly Geometry _geometry;
public GlyphRunGeometryControl()
{
var glyphTypeface = new Typeface(TestFontFamily).GlyphTypeface;
@ -62,19 +62,16 @@ namespace Avalonia.Direct2D1.RenderTests.Media
var glyphRun = new GlyphRun(glyphTypeface, 100, characters, glyphIndices);
_geometry = glyphRun.BuildGeometry();
Geometry = glyphRun.BuildGeometry();
}
protected override Size MeasureOverride(Size availableSize)
{
return _geometry.Bounds.Size;
}
public Geometry Geometry { get; }
public override void Render(DrawingContext context)
{
var foreground = TextElement.GetForeground(this);
context.DrawGeometry(foreground, null, _geometry);
context.DrawGeometry(foreground, null, Geometry);
}
}
}

4
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

@ -122,10 +122,8 @@ namespace Avalonia.UnitTests
return Mock.Of<IGlyphRunImpl>();
}
public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale)
public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun)
{
scale = Matrix.Identity;
return Mock.Of<IGeometryImpl>();
}

BIN
tests/TestFiles/Direct2D1/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Loading…
Cancel
Save