diff --git a/.editorconfig b/.editorconfig
index 25e0135725..cb589a5ce1 100644
--- a/.editorconfig
+++ b/.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
diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm
index 5436ad22f3..01725ace03 100644
--- a/native/Avalonia.Native/src/OSX/AvnView.mm
+++ b/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;
+ }
}
}
diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
index 7f2bb128da..f133fa34f6 100644
--- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
+++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
@@ -298,14 +298,15 @@ HRESULT WindowBaseImpl::Resize(double x, double y, AvnPlatformResizeReason reaso
}
@try {
- lastSize = NSSize {x, y};
-
- if (!_shown) {
- BaseEvents->Resized(AvnSize{x, y}, reason);
- }
- else if(Window != nullptr) {
- [Window setContentSize:lastSize];
- [Window invalidateShadow];
+ if(x != lastSize.width || y != lastSize.height) {
+ lastSize = NSSize{x, y};
+
+ if (!_shown) {
+ BaseEvents->Resized(AvnSize{x, y}, reason);
+ } else if (Window != nullptr) {
+ [Window setContentSize:lastSize];
+ [Window invalidateShadow];
+ }
}
}
@finally {
diff --git a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj
index ec88852feb..e52430f50b 100644
--- a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj
+++ b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj
@@ -9,42 +9,37 @@
1.0
apk
true
+ android-arm64;android-x64
-
-
-
Resources\drawable\Icon.png
-
- False
- False
+
True
+
+
+
True
no-write-symbols,nodebug
Hybrid
True
-
- False
- False
-
-
-
- True
+
+ True
+ True
-
-
+
+
-
\ No newline at end of file
+
diff --git a/samples/ControlCatalog.Android/Properties/AndroidManifest.xml b/samples/ControlCatalog.Android/Properties/AndroidManifest.xml
index aa570ec504..6f551d2b01 100644
--- a/samples/ControlCatalog.Android/Properties/AndroidManifest.xml
+++ b/samples/ControlCatalog.Android/Properties/AndroidManifest.xml
@@ -1,4 +1,5 @@
-
+
+
diff --git a/samples/ControlCatalog.Android/environment.device.txt b/samples/ControlCatalog.Android/environment.device.txt
new file mode 100644
index 0000000000..107d68ca1b
--- /dev/null
+++ b/samples/ControlCatalog.Android/environment.device.txt
@@ -0,0 +1 @@
+DOTNET_DiagnosticPorts=127.0.0.1:9000,suspend
diff --git a/samples/ControlCatalog.Android/environment.emulator.txt b/samples/ControlCatalog.Android/environment.emulator.txt
new file mode 100644
index 0000000000..299a0ec30b
--- /dev/null
+++ b/samples/ControlCatalog.Android/environment.emulator.txt
@@ -0,0 +1 @@
+DOTNET_DiagnosticPorts=10.0.2.2:9001,suspend
diff --git a/samples/RenderDemo/Pages/FormattedTextPage.axaml.cs b/samples/RenderDemo/Pages/FormattedTextPage.axaml.cs
index 25e29c67a9..97a9320c95 100644
--- a/samples/RenderDemo/Pages/FormattedTextPage.axaml.cs
+++ b/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);
}
}
}
diff --git a/samples/RenderDemo/Pages/GlyphRunPage.xaml b/samples/RenderDemo/Pages/GlyphRunPage.xaml
index c2914e8847..7db58e5286 100644
--- a/samples/RenderDemo/Pages/GlyphRunPage.xaml
+++ b/samples/RenderDemo/Pages/GlyphRunPage.xaml
@@ -2,13 +2,13 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:local="clr-namespace:RenderDemo.Pages"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="RenderDemo.Pages.GlyphRunPage">
-
-
-
-
+
+
+
diff --git a/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
index 7f85606957..674ed8e61f 100644
--- a/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
+++ b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
@@ -9,14 +9,6 @@ namespace RenderDemo.Pages
{
public class GlyphRunPage : UserControl
{
- private Image _imageControl;
- private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface;
- private readonly Random _rand = new Random();
- private ushort[] _glyphIndices = new ushort[1];
- private char[] _characters = new char[1];
- private float _fontSize = 20;
- private int _direction = 10;
-
public GlyphRunPage()
{
this.InitializeComponent();
@@ -25,19 +17,43 @@ namespace RenderDemo.Pages
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
+ }
+ }
+
+ public class GlyphRunControl : Control
+ {
+ private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface;
+ private readonly Random _rand = new Random();
+ private ushort[] _glyphIndices = new ushort[1];
+ private char[] _characters = new char[1];
+ private float _fontSize = 20;
+ private int _direction = 10;
- _imageControl = this.FindControl("imageControl");
- _imageControl.Source = new DrawingImage();
+ private DispatcherTimer _timer;
- DispatcherTimer.Run(() =>
+ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ _timer = new DispatcherTimer
+ {
+ Interval = TimeSpan.FromSeconds(1)
+ };
+
+ _timer.Tick += (s,e) =>
{
- UpdateGlyphRun();
+ InvalidateVisual();
+ };
+
+ _timer.Start();
+ }
+
+ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ _timer.Stop();
- return true;
- }, TimeSpan.FromSeconds(1));
+ _timer = null;
}
- private void UpdateGlyphRun()
+ public override void Render(DrawingContext context)
{
var c = (char)_rand.Next(65, 90);
@@ -57,27 +73,70 @@ namespace RenderDemo.Pages
_characters[0] = c;
- var scale = (double)_fontSize / _glyphTypeface.DesignEmHeight;
+ var glyphRun = new GlyphRun(_glyphTypeface, _fontSize, _characters, _glyphIndices);
- var drawingGroup = new DrawingGroup();
+ context.DrawGlyphRun(Brushes.Black, glyphRun);
+ }
+ }
- var glyphRunDrawing = new GlyphRunDrawing
+ public class GlyphRunGeometryControl : Control
+ {
+ private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface;
+ private readonly Random _rand = new Random();
+ private ushort[] _glyphIndices = new ushort[1];
+ private char[] _characters = new char[1];
+ private float _fontSize = 20;
+ private int _direction = 10;
+
+ private DispatcherTimer _timer;
+
+ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ _timer = new DispatcherTimer
{
- Foreground = Brushes.Black,
- GlyphRun = new GlyphRun(_glyphTypeface, _fontSize, _characters, _glyphIndices)
+ Interval = TimeSpan.FromSeconds(1)
};
- drawingGroup.Children.Add(glyphRunDrawing);
-
- var geometryDrawing = new GeometryDrawing
+ _timer.Tick += (s, e) =>
{
- Pen = new Pen(Brushes.Black),
- Geometry = new RectangleGeometry { Rect = new Rect(glyphRunDrawing.GlyphRun.Size) }
+ InvalidateVisual();
};
- drawingGroup.Children.Add(geometryDrawing);
+ _timer.Start();
+ }
+
+ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ _timer.Stop();
+
+ _timer = null;
+ }
+
+ public override void Render(DrawingContext context)
+ {
+ var c = (char)_rand.Next(65, 90);
+
+ if (_fontSize + _direction > 200)
+ {
+ _direction = -10;
+ }
+
+ if (_fontSize + _direction < 20)
+ {
+ _direction = 10;
+ }
+
+ _fontSize += _direction;
+
+ _glyphIndices[0] = _glyphTypeface.GetGlyph(c);
+
+ _characters[0] = c;
+
+ var glyphRun = new GlyphRun(_glyphTypeface, _fontSize, _characters, _glyphIndices);
+
+ var geometry = glyphRun.BuildGeometry();
- (_imageControl.Source as DrawingImage).Drawing = drawingGroup;
+ context.DrawGeometry(Brushes.Green, null, geometry);
}
}
}
diff --git a/src/Avalonia.Base/GeometryCollection.cs b/src/Avalonia.Base/GeometryCollection.cs
deleted file mode 100644
index 0bd02d5438..0000000000
--- a/src/Avalonia.Base/GeometryCollection.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-using System.Collections;
-using System.Collections.Generic;
-using Avalonia.Animation;
-
-#nullable enable
-
-namespace Avalonia.Media
-{
- public class GeometryCollection : Animatable, IList, IReadOnlyList
- {
- private List _inner;
-
- public GeometryCollection() => _inner = new List();
- public GeometryCollection(IEnumerable collection) => _inner = new List(collection);
- public GeometryCollection(int capacity) => _inner = new List(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 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();
- }
-}
diff --git a/src/Avalonia.Base/Layout/LayoutManager.cs b/src/Avalonia.Base/Layout/LayoutManager.cs
index fc988a8d6c..446f135c83 100644
--- a/src/Avalonia.Base/Layout/LayoutManager.cs
+++ b/src/Avalonia.Base/Layout/LayoutManager.cs
@@ -28,7 +28,7 @@ namespace Avalonia.Layout
public LayoutManager(ILayoutRoot owner)
{
_owner = owner ?? throw new ArgumentNullException(nameof(owner));
- _executeLayoutPass = ExecuteLayoutPass;
+ _executeLayoutPass = ExecuteQueuedLayoutPass;
}
public virtual event EventHandler? LayoutUpdated;
@@ -94,6 +94,16 @@ namespace Avalonia.Layout
QueueLayoutPass();
}
+ private void ExecuteQueuedLayoutPass()
+ {
+ if (!_queued)
+ {
+ return;
+ }
+
+ ExecuteLayoutPass();
+ }
+
///
public virtual void ExecuteLayoutPass()
{
@@ -319,8 +329,8 @@ namespace Avalonia.Layout
{
if (!_queued && !_running)
{
- Dispatcher.UIThread.Post(_executeLayoutPass, DispatcherPriority.Layout);
_queued = true;
+ Dispatcher.UIThread.Post(_executeLayoutPass, DispatcherPriority.Layout);
}
}
diff --git a/src/Avalonia.Base/Media/DrawingCollection.cs b/src/Avalonia.Base/Media/DrawingCollection.cs
new file mode 100644
index 0000000000..a76f7743cc
--- /dev/null
+++ b/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
+ {
+ public DrawingCollection()
+ {
+ ResetBehavior = ResetBehavior.Remove;
+ }
+
+ public DrawingCollection(IEnumerable items) : base(items)
+ {
+ ResetBehavior = ResetBehavior.Remove;
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Media/DrawingGroup.cs b/src/Avalonia.Base/Media/DrawingGroup.cs
index eeb6318ebd..603bb1c1c1 100644
--- a/src/Avalonia.Base/Media/DrawingGroup.cs
+++ b/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 OpacityMaskProperty =
AvaloniaProperty.Register(nameof(OpacityMask));
+ public static readonly DirectProperty ChildrenProperty =
+ AvaloniaProperty.RegisterDirect(
+ 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);
}
+ ///
+ /// Gets or sets the collection that contains the child geometries.
+ ///
[Content]
- public AvaloniaList Children { get; } = new AvaloniaList();
+ 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();
+
+ 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? _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 source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void DrawBitmap(IRef 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;
+ }
+ }
+
+ ///
+ /// Pop
+ ///
+ 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();
+ }
+
+ ///
+ /// PushTransform -
+ /// Push a Transform which will apply to all drawing operations until the corresponding
+ /// Pop.
+ ///
+ /// The Transform to push.
+ 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;
+ }
+
+ ///
+ /// 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.
+ ///
+ 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(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;
+ }
+
+ ///
+ /// Contains the functionality common to GeometryDrawing operations of
+ /// instantiating the GeometryDrawing, setting it's Freezable state,
+ /// and Adding it to the Drawing Graph.
+ ///
+ 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);
+ }
+
+ ///
+ /// 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.
+ ///
+ 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(_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);
+ }
+ }
+ }
}
}
diff --git a/src/Avalonia.Base/Media/FormattedText.cs b/src/Avalonia.Base/Media/FormattedText.cs
index 7bdf59def0..5480336f84 100644
--- a/src/Avalonia.Base/Media/FormattedText.cs
+++ b/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
}
}
+ ///
+ /// Obtains geometry for the text, including underlines and strikethroughs.
+ ///
+ /// The left top origin of the resulting geometry.
+ /// 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.
+ 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;
+ }
+
///
/// Draws the text object
///
@@ -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();
diff --git a/src/Avalonia.Base/Media/GeometryCollection.cs b/src/Avalonia.Base/Media/GeometryCollection.cs
new file mode 100644
index 0000000000..2afa191dcf
--- /dev/null
+++ b/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
+ {
+ public GeometryCollection()
+ {
+ ResetBehavior = ResetBehavior.Remove;
+
+ this.ForEachItem(
+ x =>
+ {
+ Parent?.Invalidate();
+ },
+ x =>
+ {
+ Parent?.Invalidate();
+ },
+ () => throw new NotSupportedException());
+ }
+
+ public GeometryCollection(IEnumerable items) : base(items)
+ {
+ ResetBehavior = ResetBehavior.Remove;
+
+ this.ForEachItem(
+ x =>
+ {
+ Parent?.Invalidate();
+ },
+ x =>
+ {
+ Parent?.Invalidate();
+ },
+ () => throw new NotSupportedException());
+ }
+
+ public GeometryGroup? Parent { get; set; }
+ }
+}
diff --git a/src/Avalonia.Base/Media/GeometryDrawing.cs b/src/Avalonia.Base/Media/GeometryDrawing.cs
index 08e62df2cc..7df7d25954 100644
--- a/src/Avalonia.Base/Media/GeometryDrawing.cs
+++ b/src/Avalonia.Base/Media/GeometryDrawing.cs
@@ -21,14 +21,14 @@ namespace Avalonia.Media
///
/// Defines the property.
///
- public static readonly StyledProperty BrushProperty =
- AvaloniaProperty.Register(nameof(Brush), Brushes.Transparent);
+ public static readonly StyledProperty BrushProperty =
+ AvaloniaProperty.Register(nameof(Brush), Brushes.Transparent);
///
/// Defines the property.
///
- public static readonly StyledProperty PenProperty =
- AvaloniaProperty.Register(nameof(Pen));
+ public static readonly StyledProperty PenProperty =
+ AvaloniaProperty.Register(nameof(Pen));
///
/// Gets or sets the that describes the shape of this .
@@ -43,7 +43,7 @@ namespace Avalonia.Media
///
/// Gets or sets the used to fill the interior of the shape described by this .
///
- public IBrush Brush
+ public IBrush? Brush
{
get => GetValue(BrushProperty);
set => SetValue(BrushProperty, value);
@@ -52,7 +52,7 @@ namespace Avalonia.Media
///
/// Gets or sets the used to stroke this .
///
- public IPen Pen
+ public IPen? Pen
{
get => GetValue(PenProperty);
set => SetValue(PenProperty, value);
diff --git a/src/Avalonia.Base/GeometryGroup.cs b/src/Avalonia.Base/Media/GeometryGroup.cs
similarity index 64%
rename from src/Avalonia.Base/GeometryGroup.cs
rename to src/Avalonia.Base/Media/GeometryGroup.cs
index b90c9c6d8a..0326e606f4 100644
--- a/src/Avalonia.Base/GeometryGroup.cs
+++ b/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
///
public class GeometryGroup : Geometry
{
- public static readonly DirectProperty ChildrenProperty =
- AvaloniaProperty.RegisterDirect (
+ public static readonly DirectProperty ChildrenProperty =
+ AvaloniaProperty.RegisterDirect (
nameof(Children),
o => o.Children,
- (o, v) => o.Children = v);
+ (o, v)=> o.Children = v);
public static readonly StyledProperty FillRuleProperty =
AvaloniaProperty.Register(nameof(FillRule));
- private GeometryCollection? _children;
- private bool _childrenSet;
+ private GeometryCollection _children;
+
+ public GeometryGroup()
+ {
+ _children = new GeometryCollection
+ {
+ Parent = this
+ };
+ }
///
/// Gets or sets the collection that contains the child geometries.
///
[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();
+
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();
+ }
}
}
diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs
index 22be8d8865..6f1fa03990 100644
--- a/src/Avalonia.Base/Media/GlyphRun.cs
+++ b/src/Avalonia.Base/Media/GlyphRun.cs
@@ -194,6 +194,19 @@ namespace Avalonia.Media
}
}
+ ///
+ /// Obtains geometry for the glyph run.
+ ///
+ /// The geometry returned contains the combined geometry of all glyphs in the glyph run.
+ public Geometry BuildGeometry()
+ {
+ var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService();
+
+ var geometryImpl = platformRenderInterface.BuildGlyphRunGeometry(this);
+
+ return new PlatformGeometry(geometryImpl);
+ }
+
///
/// Retrieves the offset from the leading edge of the
/// to the leading or trailing edge of a caret stop containing the specified character hit.
diff --git a/src/Avalonia.Base/Media/PlatformGeometry.cs b/src/Avalonia.Base/Media/PlatformGeometry.cs
new file mode 100644
index 0000000000..f25a14540f
--- /dev/null
+++ b/src/Avalonia.Base/Media/PlatformGeometry.cs
@@ -0,0 +1,24 @@
+using Avalonia.Platform;
+
+namespace Avalonia.Media
+{
+ internal class PlatformGeometry : Geometry
+ {
+ private readonly IGeometryImpl _geometryImpl;
+
+ public PlatformGeometry(IGeometryImpl geometryImpl)
+ {
+ _geometryImpl = geometryImpl;
+ }
+
+ public override Geometry Clone()
+ {
+ return new PlatformGeometry(_geometryImpl);
+ }
+
+ protected override IGeometryImpl? CreateDefiningGeometry()
+ {
+ return _geometryImpl;
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
index 0eeefddf0b..e39a4e23df 100644
--- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
+++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
@@ -58,6 +58,13 @@ namespace Avalonia.Platform
/// A combined geometry.
IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2);
+ ///
+ /// Created a geometry implementation for the glyph run.
+ ///
+ /// The glyph run to build a geometry from.
+ /// The geometry returned contains the combined geometry of all glyphs in the glyph run.
+ IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun);
+
///
/// Creates a renderer.
///
diff --git a/src/Avalonia.Base/PropertyStore/PriorityValue.cs b/src/Avalonia.Base/PropertyStore/PriorityValue.cs
index 112cf6619f..182b2638c4 100644
--- a/src/Avalonia.Base/PropertyStore/PriorityValue.cs
+++ b/src/Avalonia.Base/PropertyStore/PriorityValue.cs
@@ -121,6 +121,7 @@ namespace Avalonia.PropertyStore
public void ClearLocalValue()
{
+ _localValue = default;
UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs(
_owner,
Property,
diff --git a/src/Avalonia.Base/Styling/PropertySetterInstance.cs b/src/Avalonia.Base/Styling/PropertySetterInstance.cs
index c4e8f47e67..9028224cc1 100644
--- a/src/Avalonia.Base/Styling/PropertySetterInstance.cs
+++ b/src/Avalonia.Base/Styling/PropertySetterInstance.cs
@@ -18,7 +18,7 @@ namespace Avalonia.Styling
private readonly DirectPropertyBase? _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(_value) : default);
+ PublishNext(IsActive ? new BindingValue(_value) : default);
+ }
+
+ private enum State
+ {
+ Inactive,
+ Active,
+ Disposed,
}
}
}
diff --git a/src/Avalonia.Base/Styling/Styles.cs b/src/Avalonia.Base/Styling/Styles.cs
index 7c0bc4ad7f..e4c3371007 100644
--- a/src/Avalonia.Base/Styling/Styles.cs
+++ b/src/Avalonia.Base/Styling/Styles.cs
@@ -17,7 +17,7 @@ namespace Avalonia.Styling
IStyle,
IResourceProvider
{
- private readonly AvaloniaList _styles = new AvaloniaList();
+ private readonly AvaloniaList _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 ToReadOnlyList(ICollection list)
{
- static IReadOnlyList ToReadOnlyList(IList list)
+ if (list is IReadOnlyList readOnlyList)
{
- if (list is IReadOnlyList)
- {
- return (IReadOnlyList)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(items));
+ (owner as IStyleHost)?.StylesAdded(ToReadOnlyList(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(items));
+ (owner as IStyleHost)?.StylesRemoved(ToReadOnlyList(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);
diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs
index cbf9b35a05..05be5ad00d 100644
--- a/src/Avalonia.Controls/ComboBox.cs
+++ b/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();
}
///
@@ -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)
diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs
index d6a5fa0727..16d4ef5c15 100644
--- a/src/Avalonia.Controls/Control.cs
+++ b/src/Avalonia.Controls/Control.cs
@@ -378,17 +378,12 @@ namespace Avalonia.Controls
bool bypassFlowDirectionPolicies = BypassFlowDirectionPolicies;
bool parentBypassFlowDirectionPolicies = false;
- var parent = this.FindAncestorOfType();
+ 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;
diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs
index 216e43e1f0..784d33ed58 100644
--- a/src/Avalonia.Controls/GridSplitter.cs
+++ b/src/Avalonia.Controls/GridSplitter.cs
@@ -221,7 +221,8 @@ namespace Avalonia.Controls
ShowsPreview = showsPreview,
ResizeDirection = resizeDirection,
SplitterLength = Math.Min(Bounds.Width, Bounds.Height),
- ResizeBehavior = GetEffectiveResizeBehavior(resizeDirection)
+ ResizeBehavior = GetEffectiveResizeBehavior(resizeDirection),
+ Scaling = (VisualRoot as ILayoutRoot)?.LayoutScaling ?? 1,
};
// Store the rows and columns to resize on drag events.
@@ -630,13 +631,17 @@ namespace Avalonia.Controls
{
double actualLength1 = GetActualLength(definition1);
double actualLength2 = GetActualLength(definition2);
+ double pixelLength = 1 / _resizeData.Scaling;
+ double epsilon = pixelLength + LayoutHelper.LayoutEpsilon;
// When splitting, Check to see if the total pixels spanned by the definitions
- // is the same as before starting resize. If not cancel the drag.
+ // is the same as before starting resize. If not cancel the drag. We need to account for
+ // layout rounding here, so ignore differences of less than a device pixel to avoid problems
+ // that WPF has, such as https://stackoverflow.com/questions/28464843.
if (_resizeData.SplitBehavior == SplitBehavior.Split &&
!MathUtilities.AreClose(
actualLength1 + actualLength2,
- _resizeData.OriginalDefinition1ActualLength + _resizeData.OriginalDefinition2ActualLength, LayoutHelper.LayoutEpsilon))
+ _resizeData.OriginalDefinition1ActualLength + _resizeData.OriginalDefinition2ActualLength, epsilon))
{
CancelResize();
@@ -798,6 +803,9 @@ namespace Avalonia.Controls
// The minimum of Width/Height of Splitter. Used to ensure splitter
// isn't hidden by resizing a row/column smaller than the splitter.
public double SplitterLength;
+
+ // The current layout scaling factor.
+ public double Scaling;
}
}
diff --git a/src/Avalonia.Controls/Templates/DataTemplates.cs b/src/Avalonia.Controls/Templates/DataTemplates.cs
index f203539536..d4eeda7908 100644
--- a/src/Avalonia.Controls/Templates/DataTemplates.cs
+++ b/src/Avalonia.Controls/Templates/DataTemplates.cs
@@ -1,3 +1,4 @@
+using System;
using Avalonia.Collections;
namespace Avalonia.Controls.Templates
@@ -13,6 +14,22 @@ namespace Avalonia.Controls.Templates
public DataTemplates()
{
ResetBehavior = ResetBehavior.Remove;
+
+ Validate += ValidateDataTemplate;
+ }
+
+ private static void ValidateDataTemplate(IDataTemplate template)
+ {
+ var valid = template switch
+ {
+ ITypedDataTemplate typed => typed.DataType is not null,
+ _ => true
+ };
+
+ if (!valid)
+ {
+ throw new InvalidOperationException("DataTemplate inside of DataTemplates must have a DataType set. Set DataType property or use ItemTemplate with single template instead.");
+ }
}
}
}
\ No newline at end of file
diff --git a/src/Avalonia.Controls/Templates/ITypedDataTemplate.cs b/src/Avalonia.Controls/Templates/ITypedDataTemplate.cs
new file mode 100644
index 0000000000..239dbd79f4
--- /dev/null
+++ b/src/Avalonia.Controls/Templates/ITypedDataTemplate.cs
@@ -0,0 +1,10 @@
+using System;
+using Avalonia.Metadata;
+
+namespace Avalonia.Controls.Templates;
+
+public interface ITypedDataTemplate : IDataTemplate
+{
+ [DataType]
+ Type? DataType { get; }
+}
diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs
index 7652b23162..9531f719b9 100644
--- a/src/Avalonia.Controls/TextBox.cs
+++ b/src/Avalonia.Controls/TextBox.cs
@@ -53,7 +53,7 @@ namespace Avalonia.Controls
public static readonly StyledProperty PasswordCharProperty =
AvaloniaProperty.Register(nameof(PasswordChar));
-
+
public static readonly StyledProperty SelectionBrushProperty =
AvaloniaProperty.Register(nameof(SelectionBrush));
@@ -196,7 +196,6 @@ namespace Avalonia.Controls
private TextBoxTextInputMethodClient _imClient = new TextBoxTextInputMethodClient();
private UndoRedoHelper _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
{
diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
index 984f76adb6..b3c56e1ea0 100644
--- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
+++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
@@ -115,6 +115,11 @@ namespace Avalonia.Headless
return new HeadlessGlyphRunStub();
}
+ public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun)
+ {
+ return new HeadlessGeometryStub(new Rect(glyphRun.Size));
+ }
+
class HeadlessGeometryStub : IGeometryImpl
{
public HeadlessGeometryStub(Rect bounds)
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
index 1ca7be67a7..04a61e5f10 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
+++ b/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(
new AvaloniaXamlIlResolveClassesPropertiesTransformer(),
@@ -57,6 +56,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
new AvaloniaXamlIlResolveByNameMarkupExtensionReplacer()
);
+ InsertAfter(
+ new XDataTypeTransformer());
+
// After everything else
InsertBefore(
new AddNameScopeRegistration(),
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/XDataTypeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/XDataTypeTransformer.cs
new file mode 100644
index 0000000000..845dc5f831
--- /dev/null
+++ b/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";
+
+ ///
+ /// Converts x:DataType directives to regular DataType assignments if property with Avalonia.Metadata.DataTypeAttribute exists.
+ ///
+ ///
+ 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()
+ .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;
+ }
+ }
+}
diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs
index d2b24979cc..4da6b1b791 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs
@@ -5,7 +5,7 @@ using Avalonia.Metadata;
namespace Avalonia.Markup.Xaml.Templates
{
- public class DataTemplate : IRecyclingDataTemplate
+ public class DataTemplate : IRecyclingDataTemplate, ITypedDataTemplate
{
[DataType]
public Type DataType { get; set; }
diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs
index 10061c3d48..04e8b0a9c0 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs
@@ -9,7 +9,7 @@ using Avalonia.Metadata;
namespace Avalonia.Markup.Xaml.Templates
{
- public class TreeDataTemplate : ITreeDataTemplate
+ public class TreeDataTemplate : ITreeDataTemplate, ITypedDataTemplate
{
[DataType]
public Type DataType { get; set; }
diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
index d3c3585cd0..91fe4fc085 100644
--- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
+++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
@@ -62,6 +62,49 @@ namespace Avalonia.Skia
return new CombinedGeometryImpl(combineMode, g1, g2);
}
+ public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun)
+ {
+ if (glyphRun.GlyphTypeface.PlatformImpl is not GlyphTypefaceImpl glyphTypeface)
+ {
+ throw new InvalidOperationException("PlatformImpl can't be null.");
+ }
+
+ var fontRenderingEmSize = (float)glyphRun.FontRenderingEmSize;
+ var skFont = new SKFont(glyphTypeface.Typeface, fontRenderingEmSize)
+ {
+ Size = fontRenderingEmSize,
+ Edging = SKFontEdging.Antialias,
+ Hinting = SKFontHinting.None,
+ LinearMetrics = true
+ };
+
+ SKPath path = new SKPath();
+
+ var (currentX, currentY) = glyphRun.BaselineOrigin;
+
+ for (var i = 0; i < glyphRun.GlyphIndices.Count; i++)
+ {
+ var glyph = glyphRun.GlyphIndices[i];
+ var glyphPath = skFont.GetGlyphPath(glyph);
+
+ if (!glyphPath.IsEmpty)
+ {
+ path.AddPath(glyphPath, (float)currentX, (float)currentY);
+ }
+
+ if (glyphRun.GlyphAdvances != null)
+ {
+ currentX += glyphRun.GlyphAdvances[i];
+ }
+ else
+ {
+ currentX += glyphPath.Bounds.Right;
+ }
+ }
+
+ return new StreamGeometryImpl(path);
+ }
+
///
public IBitmapImpl LoadBitmap(string fileName)
{
diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
index d9e992bb80..7f1af46e97 100644
--- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
+++ b/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,6 +160,47 @@ namespace Avalonia.Direct2D1
public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => new GeometryGroupImpl(fillRule, children);
public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2);
+ public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun)
+ {
+ if (glyphRun.GlyphTypeface.PlatformImpl is not GlyphTypefaceImpl glyphTypeface)
+ {
+ throw new InvalidOperationException("PlatformImpl can't be null.");
+ }
+
+ var pathGeometry = new SharpDX.Direct2D1.PathGeometry(Direct2D1Factory);
+
+ using (var sink = pathGeometry.Open())
+ {
+ var glyphs = new short[glyphRun.GlyphIndices.Count];
+
+ for (int i = 0; i < glyphRun.GlyphIndices.Count; i++)
+ {
+ glyphs[i] = (short)glyphRun.GlyphIndices[i];
+ }
+
+ glyphTypeface.FontFace.GetGlyphRunOutline((float)glyphRun.FontRenderingEmSize, glyphs, null, null, false, !glyphRun.IsLeftToRight, sink);
+
+ sink.Close();
+ }
+
+ var (baselineOriginX, baselineOriginY) = glyphRun.BaselineOrigin;
+
+ 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)
+ {
+
+ }
+ }
+
///
public IBitmapImpl LoadBitmap(string fileName)
{
diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs
index 954a609315..72162a4d8e 100644
--- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs
+++ b/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()
{
diff --git a/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs b/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs
index f0e8e1cd11..37e07c244e 100644
--- a/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs
@@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Layout;
+using Avalonia.Threading;
using Xunit;
namespace Avalonia.Base.UnitTests.Layout
@@ -421,5 +422,22 @@ namespace Avalonia.Base.UnitTests.Layout
Assert.Equal(new Size(200, 200), control.Bounds.Size);
Assert.Equal(new Size(200, 200), control.DesiredSize);
}
+
+ [Fact]
+ public void LayoutManager_Execute_Layout_Pass_Should_Clear_Queued_LayoutPasses()
+ {
+ var control = new LayoutTestControl();
+ var root = new LayoutTestRoot { Child = control };
+
+ int layoutCount = 0;
+ root.LayoutUpdated += (_, _) => layoutCount++;
+
+ root.LayoutManager.InvalidateArrange(control);
+ root.LayoutManager.ExecuteInitialLayoutPass();
+
+ Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout);
+
+ Assert.Equal(1, layoutCount);
+ }
}
}
diff --git a/tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs b/tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs
index 8f80238903..fb4c35a1a8 100644
--- a/tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs
+++ b/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);
}
}
}
diff --git a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs
index 01afe85b8b..5cc9f57c8e 100644
--- a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs
+++ b/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()
{
diff --git a/tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs b/tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs
index ed4c78aa3e..c684466200 100644
--- a/tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs
+++ b/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