diff --git a/Avalonia.sln b/Avalonia.sln
index d5419365ac..98ad0cadae 100644
--- a/Avalonia.sln
+++ b/Avalonia.sln
@@ -91,7 +91,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.NetCore", "s
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1-27F5-4255-9AFC-04ABFD11683A}"
ProjectSection(SolutionItems) = preProject
- build\ApiDiff.props = build\ApiDiff.props
build\AvaloniaPublicKey.props = build\AvaloniaPublicKey.props
build\Base.props = build\Base.props
build\Binding.props = build\Binding.props
diff --git a/NuGet.Config b/NuGet.Config
index 7d2bd8abd2..2042fea360 100644
--- a/NuGet.Config
+++ b/NuGet.Config
@@ -3,8 +3,7 @@
-
-
+
diff --git a/build/ApiDiff.props b/build/ApiDiff.props
deleted file mode 100644
index b0b1942f60..0000000000
--- a/build/ApiDiff.props
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
- 0.10.0
- $(PackageId)
- Avalonia
- false
-
-
-
-
-
-
-
diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
index f11b027173..a07532412d 100644
--- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
+++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
@@ -59,6 +59,7 @@
BC11A5BE2608D58F0017BAD0 /* automation.h in Headers */ = {isa = PBXBuildFile; fileRef = BC11A5BC2608D58F0017BAD0 /* automation.h */; };
BC11A5BF2608D58F0017BAD0 /* automation.mm in Sources */ = {isa = PBXBuildFile; fileRef = BC11A5BD2608D58F0017BAD0 /* automation.mm */; };
ED3791C42862E1F40080BD62 /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED3791C32862E1F40080BD62 /* UniformTypeIdentifiers.framework */; };
+ ED754D262A97306B0078B4DF /* PlatformRenderTimer.mm in Sources */ = {isa = PBXBuildFile; fileRef = ED754D252A97306B0078B4DF /* PlatformRenderTimer.mm */; };
EDF8CDCD2964CB01001EE34F /* PlatformSettings.mm in Sources */ = {isa = PBXBuildFile; fileRef = EDF8CDCC2964CB01001EE34F /* PlatformSettings.mm */; };
/* End PBXBuildFile section */
@@ -122,6 +123,7 @@
BC11A5BC2608D58F0017BAD0 /* automation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = automation.h; sourceTree = ""; };
BC11A5BD2608D58F0017BAD0 /* automation.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = automation.mm; sourceTree = ""; };
ED3791C32862E1F40080BD62 /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; };
+ ED754D252A97306B0078B4DF /* PlatformRenderTimer.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = PlatformRenderTimer.mm; sourceTree = ""; };
EDF8CDCC2964CB01001EE34F /* PlatformSettings.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = PlatformSettings.mm; sourceTree = ""; };
/* End PBXFileReference section */
@@ -162,6 +164,7 @@
AB7A61E62147C814003C5833 = {
isa = PBXGroup;
children = (
+ ED754D252A97306B0078B4DF /* PlatformRenderTimer.mm */,
855EDC9E28C6546F00807998 /* PlatformBehaviorInhibition.mm */,
8D2F3511292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h */,
8D300D68292E1E5D00320C49 /* AvnTextInputMethod.mm */,
@@ -333,6 +336,7 @@
18391068E48EF96E3DB5FDAB /* ResizeScope.mm in Sources */,
18391D4EB311BC7EF8B8C0A6 /* AvnView.mm in Sources */,
18391AA7E0BBA74D184C5734 /* AutoFitContentView.mm in Sources */,
+ ED754D262A97306B0078B4DF /* PlatformRenderTimer.mm in Sources */,
1839151F32D1BB1AB51A7BB6 /* AvnPanelWindow.mm in Sources */,
18391AC16726CBC45856233B /* AvnWindow.mm in Sources */,
18391D8CD1756DC858DC1A09 /* PopupImpl.mm in Sources */,
diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm
index a4999b8df3..ea6ba93fdb 100644
--- a/native/Avalonia.Native/src/OSX/AvnView.mm
+++ b/native/Avalonia.Native/src/OSX/AvnView.mm
@@ -538,10 +538,10 @@
{
_lastKeyHandled = false;
- [[self inputContext] handleEvent:event];
+ [self keyboardEvent:event withType:KeyDown];
if(!_lastKeyHandled){
- [self keyboardEvent:event withType:KeyDown];
+ [[self inputContext] handleEvent:event];
}
}
diff --git a/native/Avalonia.Native/src/OSX/PlatformRenderTimer.mm b/native/Avalonia.Native/src/OSX/PlatformRenderTimer.mm
new file mode 100644
index 0000000000..f372cc9047
--- /dev/null
+++ b/native/Avalonia.Native/src/OSX/PlatformRenderTimer.mm
@@ -0,0 +1,84 @@
+#include "common.h"
+
+class PlatformRenderTimer : public ComSingleObject
+{
+private:
+ ComPtr _callback;
+ CVDisplayLinkRef _displayLink;
+
+public:
+ FORWARD_IUNKNOWN()
+ virtual HRESULT RegisterTick (
+ IAvnActionCallback* callback) override
+ {
+ START_COM_CALL;
+
+ @autoreleasepool
+ {
+ if (_displayLink != nil)
+ {
+ return E_UNEXPECTED;
+ }
+
+ _callback = callback;
+ auto result = CVDisplayLinkCreateWithActiveCGDisplays(&_displayLink);
+ if (result != 0)
+ {
+ return E_FAIL;
+ }
+
+ result = CVDisplayLinkSetOutputCallback(_displayLink, OnTick, this);
+ if (result != 0)
+ {
+ return E_FAIL;
+ }
+ }
+ return S_OK;
+ }
+
+ virtual void Start () override
+ {
+ START_COM_CALL;
+
+ @autoreleasepool
+ {
+ if (CVDisplayLinkIsRunning(_displayLink) == false) {
+ CVDisplayLinkStart(_displayLink);
+ }
+ }
+ }
+
+ virtual void Stop () override
+ {
+ START_COM_CALL;
+
+ @autoreleasepool
+ {
+ if (CVDisplayLinkIsRunning(_displayLink) == true) {
+ CVDisplayLinkStop(_displayLink);
+ }
+ }
+ }
+
+ virtual bool RunsInBackground () override
+ {
+ START_COM_CALL;
+
+ @autoreleasepool
+ {
+ return true;
+ }
+ }
+
+ static CVReturn OnTick(CVDisplayLinkRef displayLink, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext)
+ {
+ PlatformRenderTimer *object = (PlatformRenderTimer *)displayLinkContext;
+ object->_callback->Run();
+ return kCVReturnSuccess;
+ }
+};
+
+extern IAvnPlatformRenderTimer* CreatePlatformRenderTimer()
+{
+ return new PlatformRenderTimer();
+}
diff --git a/native/Avalonia.Native/src/OSX/automation.mm b/native/Avalonia.Native/src/OSX/automation.mm
index d0c8d7a9db..9fe0ff3c60 100644
--- a/native/Avalonia.Native/src/OSX/automation.mm
+++ b/native/Avalonia.Native/src/OSX/automation.mm
@@ -73,6 +73,13 @@ private:
if (peer->IsRootProvider())
{
auto window = peer->RootProvider_GetWindow();
+
+ if (window == nullptr)
+ {
+ NSLog(@"IRootProvider.PlatformImpl returned null or a non-WindowBaseImpl.");
+ return nil;
+ }
+
auto holder = dynamic_cast(window);
auto view = holder->GetNSView();
return [[AvnRootAccessibilityElement alloc] initWithPeer:peer owner:view];
@@ -284,8 +291,8 @@ private:
- (id)accessibilityWindow
{
- id topLevel = [self accessibilityTopLevelUIElement];
- return [topLevel isKindOfClass:[NSWindow class]] ? topLevel : nil;
+ auto rootPeer = _peer->GetVisualRoot();
+ return [AvnAccessibilityElement acquire:rootPeer];
}
- (BOOL)isAccessibilityExpanded
diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h
index 672525c64a..44441ee15d 100644
--- a/native/Avalonia.Native/src/OSX/common.h
+++ b/native/Avalonia.Native/src/OSX/common.h
@@ -32,6 +32,7 @@ extern IAvnApplicationCommands* CreateApplicationCommands();
extern IAvnPlatformBehaviorInhibition* CreatePlatformBehaviorInhibition();
extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent);
extern IAvnPlatformSettings* CreatePlatformSettings();
+extern IAvnPlatformRenderTimer* CreatePlatformRenderTimer();
extern void SetAppMenu(IAvnMenu *menu);
extern void SetServicesMenu (IAvnMenu* menu);
extern IAvnMenu* GetAppMenu ();
diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm
index 3fddb72529..41d6fd37ab 100644
--- a/native/Avalonia.Native/src/OSX/main.mm
+++ b/native/Avalonia.Native/src/OSX/main.mm
@@ -446,6 +446,17 @@ public:
return S_OK;
}
}
+
+ virtual HRESULT CreatePlatformRenderTimer(IAvnPlatformRenderTimer** ppv) override
+ {
+ START_COM_CALL;
+
+ @autoreleasepool
+ {
+ *ppv = ::CreatePlatformRenderTimer();
+ return S_OK;
+ }
+ }
};
extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative()
diff --git a/native/Avalonia.Native/src/OSX/platformthreading.mm b/native/Avalonia.Native/src/OSX/platformthreading.mm
index d80df68fea..56b7ce97e0 100644
--- a/native/Avalonia.Native/src/OSX/platformthreading.mm
+++ b/native/Avalonia.Native/src/OSX/platformthreading.mm
@@ -17,7 +17,6 @@ public:
Cancelled = true;
if(Running)
{
- Running = false;
if(![NSThread isMainThread])
{
AddRef();
@@ -28,22 +27,22 @@ public:
});
return;
};
+
+ Running = false;
if(IsApp)
[NSApp stop:nil];
- else
- {
- // Wakeup the event loop
- NSEvent* event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
- location:NSMakePoint(0, 0)
- modifierFlags:0
- timestamp:0
- windowNumber:0
- context:nil
- subtype:0
- data1:0
- data2:0];
- [NSApp postEvent:event atStart:YES];
- }
+
+ // Wakeup the event loop
+ NSEvent* event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
+ location:NSMakePoint(0, 0)
+ modifierFlags:0
+ timestamp:0
+ windowNumber:0
+ context:nil
+ subtype:0
+ data1:0
+ data2:0];
+ [NSApp postEvent:event atStart:YES];
}
};
};
diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj
index 4258572df7..ebfa325067 100644
--- a/packages/Avalonia/Avalonia.csproj
+++ b/packages/Avalonia/Avalonia.csproj
@@ -5,7 +5,7 @@
-
+
all
diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj
index 16eb09de65..9aeb71f649 100644
--- a/src/Avalonia.Base/Avalonia.Base.csproj
+++ b/src/Avalonia.Base/Avalonia.Base.csproj
@@ -15,7 +15,6 @@
-
diff --git a/src/Avalonia.Base/Media/RenderOptions.cs b/src/Avalonia.Base/Media/RenderOptions.cs
index 639498543b..1ac2520919 100644
--- a/src/Avalonia.Base/Media/RenderOptions.cs
+++ b/src/Avalonia.Base/Media/RenderOptions.cs
@@ -8,12 +8,13 @@ namespace Avalonia.Media
public EdgeMode EdgeMode { get; init; }
public TextRenderingMode TextRenderingMode { get; init; }
public BitmapBlendingMode BitmapBlendingMode { get; init; }
+ public bool? RequiresFullOpacityHandling { get; init; }
///
/// Gets the value of the BitmapInterpolationMode attached property for a visual.
///
/// The control.
- /// The control's left coordinate.
+ /// The value.
public static BitmapInterpolationMode GetBitmapInterpolationMode(Visual visual)
{
return visual.RenderOptions.BitmapInterpolationMode;
@@ -23,7 +24,7 @@ namespace Avalonia.Media
/// Sets the value of the BitmapInterpolationMode attached property for a visual.
///
/// The control.
- /// The left value.
+ /// The value.
public static void SetBitmapInterpolationMode(Visual visual, BitmapInterpolationMode value)
{
visual.RenderOptions = visual.RenderOptions with { BitmapInterpolationMode = value };
@@ -33,7 +34,7 @@ namespace Avalonia.Media
/// Gets the value of the BitmapBlendingMode attached property for a visual.
///
/// The control.
- /// The control's left coordinate.
+ /// The value.
public static BitmapBlendingMode GetBitmapBlendingMode(Visual visual)
{
return visual.RenderOptions.BitmapBlendingMode;
@@ -53,7 +54,7 @@ namespace Avalonia.Media
/// Gets the value of the EdgeMode attached property for a visual.
///
/// The control.
- /// The control's left coordinate.
+ /// The value.
public static EdgeMode GetEdgeMode(Visual visual)
{
return visual.RenderOptions.EdgeMode;
@@ -63,7 +64,7 @@ namespace Avalonia.Media
/// Sets the value of the EdgeMode attached property for a visual.
///
/// The control.
- /// The left value.
+ /// The value.
public static void SetEdgeMode(Visual visual, EdgeMode value)
{
visual.RenderOptions = visual.RenderOptions with { EdgeMode = value };
@@ -73,7 +74,7 @@ namespace Avalonia.Media
/// Gets the value of the TextRenderingMode attached property for a visual.
///
/// The control.
- /// The control's left coordinate.
+ /// The value.
public static TextRenderingMode GetTextRenderingMode(Visual visual)
{
return visual.RenderOptions.TextRenderingMode;
@@ -83,12 +84,32 @@ namespace Avalonia.Media
/// Sets the value of the TextRenderingMode attached property for a visual.
///
/// The control.
- /// The left value.
+ /// The value.
public static void SetTextRenderingMode(Visual visual, TextRenderingMode value)
{
visual.RenderOptions = visual.RenderOptions with { TextRenderingMode = value };
}
+ ///
+ /// Gets the value of the RequiresFullOpacityHandling attached property for a visual.
+ ///
+ /// The control.
+ /// The value.
+ public static bool? GetRequiresFullOpacityHandling(Visual visual)
+ {
+ return visual.RenderOptions.RequiresFullOpacityHandling;
+ }
+
+ ///
+ /// Sets the value of the RequiresFullOpacityHandling attached property for a visual.
+ ///
+ /// The control.
+ /// The value.
+ public static void SetRequiresFullOpacityHandling(Visual visual, bool? value)
+ {
+ visual.RenderOptions = visual.RenderOptions with { RequiresFullOpacityHandling = value };
+ }
+
public RenderOptions MergeWith(RenderOptions other)
{
var bitmapInterpolationMode = BitmapInterpolationMode;
@@ -119,12 +140,20 @@ namespace Avalonia.Media
bitmapBlendingMode = other.BitmapBlendingMode;
}
+ var requiresFullOpacityHandling = RequiresFullOpacityHandling;
+
+ if (requiresFullOpacityHandling == null)
+ {
+ requiresFullOpacityHandling = other.RequiresFullOpacityHandling;
+ }
+
return new RenderOptions
{
BitmapInterpolationMode = bitmapInterpolationMode,
EdgeMode = edgeMode,
TextRenderingMode = textRenderingMode,
- BitmapBlendingMode = bitmapBlendingMode
+ BitmapBlendingMode = bitmapBlendingMode,
+ RequiresFullOpacityHandling = requiresFullOpacityHandling
};
}
}
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
index a2936f1857..085e60da5f 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
@@ -1278,14 +1278,11 @@ namespace Avalonia.Media.TextFormatting
var start = GetParagraphOffsetX(width, widthIncludingWhitespace);
var overhangLeading = Math.Max(0, bounds.Left - start);
var overhangTrailing = Math.Max(0, bounds.Width - widthIncludingWhitespace);
- var hasOverflowed = overhangLeading + widthIncludingWhitespace + overhangTrailing > _paragraphWidth;
+ var hasOverflowed = width > _paragraphWidth;
if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight))
{
- if (lineHeight > height)
- {
- height = lineHeight;
- }
+ height = lineHeight;
}
return new TextLineMetrics
diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGeometryNode.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGeometryNode.cs
index 67231c9012..5a40a6b279 100644
--- a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGeometryNode.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGeometryNode.cs
@@ -25,5 +25,5 @@ class RenderDataGeometryNode : RenderDataBrushAndPenNode
context.Context.DrawGeometry(ServerBrush, ServerPen, Geometry!);
}
- public override Rect? Bounds => Geometry?.GetRenderBounds(ServerPen).CalculateBoundsWithLineCaps(ServerPen) ?? default;
+ public override Rect? Bounds => Geometry?.GetRenderBounds(ServerPen) ?? default;
}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs b/src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs
index c830ca2c49..27aca436b8 100644
--- a/src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs
+++ b/src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs
@@ -2,5 +2,7 @@ namespace Avalonia.Rendering.Composition;
internal interface ICompositionTargetDebugEvents
{
+ int RenderedVisuals { get; }
+ void IncrementRenderedVisuals();
void RectInvalidated(Rect rc);
}
diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs
index c1037d5c67..51414c2250 100644
--- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs
@@ -38,6 +38,7 @@ partial class ServerCompositionVisual
CompositionVisualChangedFields.Size
| CompositionVisualChangedFields.SizeAnimated
| CompositionVisualChangedFields.ClipToBounds
+ | CompositionVisualChangedFields.Clip
| CompositionVisualChangedFields.ClipToBoundsAnimated;
partial void OnFieldsDeserialized(CompositionVisualChangedFields changed)
diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
index 45515a37e2..aeb228282e 100644
--- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
@@ -38,6 +38,7 @@ namespace Avalonia.Rendering.Composition.Server
return;
Root!.RenderedVisuals++;
+ Root!.DebugEvents?.IncrementRenderedVisuals();
var boundsRect = new Rect(new Size(Size.X, Size.Y));
@@ -182,11 +183,24 @@ namespace Avalonia.Rendering.Composition.Server
if (_clipSizeDirty || positionChanged)
{
- _transformedClipBounds = ClipToBounds
- ? new Rect(new Size(Size.X, Size.Y))
- .TransformToAABB(GlobalTransformMatrix)
- : null;
+ Rect? transformedVisualBounds = null;
+ Rect? transformedClipBounds = null;
+ if (ClipToBounds)
+ transformedVisualBounds = new Rect(new Size(Size.X, Size.Y)).TransformToAABB(GlobalTransformMatrix);
+
+ if (Clip != null)
+ transformedClipBounds = Clip.Bounds.TransformToAABB(GlobalTransformMatrix);
+
+ if (transformedVisualBounds != null && transformedClipBounds != null)
+ _transformedClipBounds = transformedVisualBounds.Value.Intersect(transformedClipBounds.Value);
+ else if (transformedVisualBounds != null)
+ _transformedClipBounds = transformedVisualBounds;
+ else if (transformedClipBounds != null)
+ _transformedClipBounds = transformedClipBounds;
+ else
+ _transformedClipBounds = null;
+
_clipSizeDirty = false;
}
diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GeometryBoundsHelper.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryBoundsHelper.cs
deleted file mode 100644
index b1129e81c4..0000000000
--- a/src/Avalonia.Base/Rendering/SceneGraph/GeometryBoundsHelper.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using System;
-using Avalonia.Media;
-using Avalonia.Utilities;
-
-namespace Avalonia.Rendering.SceneGraph;
-
-internal static class GeometryBoundsHelper
-{
- ///
- /// Calculates the bounds of a given geometry with respect to the pens
- ///
- /// The calculated bounds without s
- /// The pen with information about the s
- ///
- public static Rect CalculateBoundsWithLineCaps(this Rect originalBounds, IPen? pen)
- {
- if (pen is null || MathUtilities.IsZero(pen.Thickness)) return originalBounds;
-
- switch (pen.LineCap)
- {
- case PenLineCap.Flat:
- return originalBounds;
- case PenLineCap.Round:
- return originalBounds.Inflate(pen.Thickness / 2);
- case PenLineCap.Square:
- return originalBounds.Inflate(pen.Thickness);
- default:
- throw new ArgumentOutOfRangeException();
- }
- }
-}
diff --git a/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj b/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj
index 5a31053bdc..8a36e6900a 100644
--- a/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj
+++ b/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj
@@ -17,7 +17,6 @@
-
diff --git a/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj b/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj
index 6556ce721e..6c1e019603 100644
--- a/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj
+++ b/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj
@@ -14,7 +14,6 @@
-
diff --git a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
index 0cc620dae9..2d40721bbf 100644
--- a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
+++ b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
@@ -170,26 +170,28 @@
CornerRadius="{TemplateBinding CornerRadius}">
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
diff --git a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs
index 3d3fe35d29..fb7cdd87ed 100644
--- a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs
+++ b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using Avalonia.Automation.Provider;
namespace Avalonia.Automation.Peers
{
@@ -115,9 +116,14 @@ namespace Avalonia.Automation.Peers
///
/// Gets the that is the parent of this .
///
- ///
public AutomationPeer? GetParent() => GetParentCore();
+ ///
+ /// Gets the that is the root of this 's
+ /// visual tree.
+ ///
+ public AutomationPeer? GetVisualRoot() => GetVisualRootCore();
+
///
/// Gets a value that indicates whether the element that is associated with this automation
/// peer currently has keyboard focus.
@@ -247,6 +253,21 @@ namespace Avalonia.Automation.Peers
return GetAutomationControlTypeCore();
}
+ protected virtual AutomationPeer? GetVisualRootCore()
+ {
+ var peer = this;
+ var parent = peer.GetParent();
+
+ while (peer.GetProvider() is null && parent is not null)
+ {
+ peer = parent;
+ parent = peer.GetParent();
+ }
+
+ return peer;
+ }
+
+
protected virtual bool IsContentElementOverrideCore()
{
return IsControlElement() && IsContentElementCore();
diff --git a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
index c19d887230..69f267a605 100644
--- a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
+++ b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
@@ -120,6 +120,13 @@ namespace Avalonia.Automation.Peers
return _parent;
}
+ protected override AutomationPeer? GetVisualRootCore()
+ {
+ if (Owner.GetVisualRoot() is Control c)
+ return CreatePeerForElement(c);
+ return null;
+ }
+
///
/// Invalidates the peer's children and causes a re-read from .
///
diff --git a/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs
index db16bf0a53..64727c43c5 100644
--- a/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs
+++ b/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs
@@ -28,7 +28,7 @@ namespace Avalonia.Automation.Peers
if (!_searchedForScrollable)
{
if (Owner.GetValue(ListBox.ScrollProperty) is Control scrollable)
- _scroller = GetOrCreate(scrollable) as IScrollProvider;
+ _scroller = GetOrCreate(scrollable).GetProvider();
_searchedForScrollable = true;
}
diff --git a/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs
index aea91b5e26..dab8c45567 100644
--- a/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs
+++ b/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs
@@ -22,7 +22,7 @@ namespace Avalonia.Automation.Peers
if (Owner.Parent is Control parent)
{
var parentPeer = GetOrCreate(parent);
- return parentPeer as ISelectionProvider;
+ return parentPeer.GetProvider();
}
return null;
diff --git a/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs
index 9ec65592fa..ceb695422d 100644
--- a/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs
+++ b/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs
@@ -1,5 +1,6 @@
using System;
using System.ComponentModel;
+using System.Globalization;
using Avalonia.Automation.Provider;
using Avalonia.Controls;
using Avalonia.Input;
@@ -32,7 +33,21 @@ namespace Avalonia.Automation.Peers
public AutomationPeer? GetPeerFromPoint(Point p)
{
var hit = Owner.GetVisualAt(p)?.FindAncestorOfType(includeSelf: true);
- return hit is object ? GetOrCreate(hit) : null;
+
+ if (hit is null)
+ return null;
+
+ var peer = GetOrCreate(hit);
+
+ while (peer != this && peer.GetProvider() is { } embedded)
+ {
+ var embeddedHit = embedded.GetPeerFromPoint(p);
+ if (embeddedHit is null)
+ break;
+ peer = embeddedHit;
+ }
+
+ return peer;
}
protected void StartTrackingFocus()
diff --git a/src/Avalonia.Controls/Automation/Provider/IEmbeddedRootProvider.cs b/src/Avalonia.Controls/Automation/Provider/IEmbeddedRootProvider.cs
new file mode 100644
index 0000000000..1b1caef182
--- /dev/null
+++ b/src/Avalonia.Controls/Automation/Provider/IEmbeddedRootProvider.cs
@@ -0,0 +1,33 @@
+using System;
+using Avalonia.Automation.Peers;
+
+namespace Avalonia.Automation.Provider
+{
+ ///
+ /// Exposure methods and properties to support UI Automation client access to the root of an
+ /// automation tree hosted by another UI framework.
+ ///
+ ///
+ /// This interface is implemented by the class, and can be used
+ /// to embed an automation tree from a 3rd party UI framework that wishes to use Avalonia's
+ /// automation support.
+ ///
+ public interface IEmbeddedRootProvider
+ {
+ ///
+ /// Gets the currently focused element.
+ ///
+ AutomationPeer? GetFocus();
+
+ ///
+ /// Gets the element at the specified point, expressed in top-level coordinates.
+ ///
+ /// The point.
+ AutomationPeer? GetPeerFromPoint(Point p);
+
+ ///
+ /// Raised by the automation peer when the focus changes.
+ ///
+ event EventHandler? FocusChanged;
+ }
+}
diff --git a/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs b/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs
index ce38059559..6a266da5c5 100644
--- a/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs
+++ b/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs
@@ -4,11 +4,36 @@ using Avalonia.Platform;
namespace Avalonia.Automation.Provider
{
+ ///
+ /// Exposes methods and properties to support UI Automation client access to the root of an
+ /// automation tree.
+ ///
+ ///
+ /// This interface is implemented by the class, and should only
+ /// be implemented on true root elements, such as Windows. To embed an automation tree, use
+ /// instead.
+ ///
public interface IRootProvider
{
+ ///
+ /// Gets the platform implementation of the TopLevel for the element.
+ ///
ITopLevelImpl? PlatformImpl { get; }
+
+ ///
+ /// Gets the currently focused element.
+ ///
AutomationPeer? GetFocus();
+
+ ///
+ /// Gets the element at the specified point, expressed in top-level coordinates.
+ ///
+ /// The point.
AutomationPeer? GetPeerFromPoint(Point p);
+
+ ///
+ /// Raised by the automation peer when the focus changes.
+ ///
event EventHandler? FocusChanged;
}
}
diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj
index 304454777f..0dda861448 100644
--- a/src/Avalonia.Controls/Avalonia.Controls.csproj
+++ b/src/Avalonia.Controls/Avalonia.Controls.csproj
@@ -6,7 +6,6 @@
-
diff --git a/src/Avalonia.Controls/ContentControl.cs b/src/Avalonia.Controls/ContentControl.cs
index 867dca2be1..3564ce1faa 100644
--- a/src/Avalonia.Controls/ContentControl.cs
+++ b/src/Avalonia.Controls/ContentControl.cs
@@ -40,11 +40,6 @@ namespace Avalonia.Controls
public static readonly StyledProperty VerticalContentAlignmentProperty =
AvaloniaProperty.Register(nameof(VerticalContentAlignment));
- static ContentControl()
- {
- ContentProperty.Changed.AddClassHandler((x, e) => x.ContentChanged(e));
- }
-
///
/// Gets or sets the content to display.
///
@@ -100,6 +95,16 @@ namespace Avalonia.Controls
{
return RegisterContentPresenter(presenter);
}
+
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ base.OnPropertyChanged(change);
+
+ if (change.Property == ContentProperty)
+ {
+ ContentChanged(change);
+ }
+ }
///
/// Called when an is registered with the control.
diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs
index e3a419d7b3..78f6c5f77d 100644
--- a/src/Avalonia.Controls/ContextMenu.cs
+++ b/src/Avalonia.Controls/ContextMenu.cs
@@ -114,7 +114,6 @@ namespace Avalonia.Controls
///
static ContextMenu()
{
- ItemsPanelProperty.OverrideDefaultValue(DefaultPanel);
PlacementProperty.OverrideDefaultValue(PlacementMode.Pointer);
ContextMenuProperty.Changed.Subscribe(ContextMenuChanged);
AutomationProperties.AccessibilityViewProperty.OverrideDefaultValue(AccessibilityView.Control);
@@ -216,18 +215,23 @@ namespace Avalonia.Controls
if (e.OldValue is ContextMenu oldMenu)
{
control.ContextRequested -= ControlContextRequested;
+ control.AttachedToVisualTree -= ControlOnAttachedToVisualTree;
control.DetachedFromVisualTree -= ControlDetachedFromVisualTree;
oldMenu._attachedControls?.Remove(control);
((ISetLogicalParent?)oldMenu._popup)?.SetParent(null);
}
- if (e.NewValue is ContextMenu newMenu)
+ if (e.NewValue is ContextMenu)
{
- newMenu._attachedControls ??= new List();
- newMenu._attachedControls.Add(control);
control.ContextRequested += ControlContextRequested;
+ control.AttachedToVisualTree += ControlOnAttachedToVisualTree;
control.DetachedFromVisualTree += ControlDetachedFromVisualTree;
}
+
+ if (control.IsAttachedToVisualTree)
+ {
+ AttachControlToContextMenu(control);
+ }
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
@@ -428,11 +432,25 @@ namespace Avalonia.Controls
e.Handled = true;
}
}
+
+
+ private static void ControlOnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
+ {
+ AttachControlToContextMenu(sender);
+ }
+
+ private static void AttachControlToContextMenu(object? sender)
+ {
+ if (sender is Control { ContextMenu: { } contextMenu } control)
+ {
+ contextMenu._attachedControls ??= new List();
+ contextMenu._attachedControls.Add(control);
+ }
+ }
private static void ControlDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
- if (sender is Control control
- && control.ContextMenu is ContextMenu contextMenu)
+ if (sender is Control { ContextMenu: { } contextMenu } control)
{
if (contextMenu._popup?.Parent == control)
{
@@ -440,6 +458,7 @@ namespace Avalonia.Controls
}
contextMenu.Close();
+ contextMenu._attachedControls?.Remove(control);
}
}
diff --git a/src/Avalonia.Controls/Documents/Inline.cs b/src/Avalonia.Controls/Documents/Inline.cs
index 23b806583e..7de3ce9d98 100644
--- a/src/Avalonia.Controls/Documents/Inline.cs
+++ b/src/Avalonia.Controls/Documents/Inline.cs
@@ -10,12 +10,14 @@ namespace Avalonia.Controls.Documents
///
public abstract class Inline : TextElement
{
+ // TODO12: change the field type to an AttachedProperty for consistency (breaking change)
///
/// AvaloniaProperty for property.
///
public static readonly StyledProperty TextDecorationsProperty =
- AvaloniaProperty.Register(
- nameof(TextDecorations));
+ AvaloniaProperty.RegisterAttached(
+ nameof(TextDecorations),
+ inherits: true);
///
/// AvaloniaProperty for property.
@@ -43,7 +45,27 @@ namespace Avalonia.Controls.Documents
get { return GetValue(BaselineAlignmentProperty); }
set { SetValue(BaselineAlignmentProperty, value); }
}
+
+ ///
+ /// Gets the value of the attached on a control.
+ ///
+ /// The control.
+ /// The font style.
+ public static TextDecorationCollection? GetTextDecorations(Control control)
+ {
+ return control.GetValue(TextDecorationsProperty);
+ }
+ ///
+ /// Sets the value of the attached on a control.
+ ///
+ /// The control.
+ /// The property value to set.
+ public static void SetTextDecorations(Control control, TextDecorationCollection? value)
+ {
+ control.SetValue(TextDecorationsProperty, value);
+ }
+
internal abstract void BuildTextRun(IList textRuns);
internal abstract void AppendText(StringBuilder stringBuilder);
diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs
index 83aa88a7b6..41e9e7c111 100644
--- a/src/Avalonia.Controls/ItemsControl.cs
+++ b/src/Avalonia.Controls/ItemsControl.cs
@@ -67,6 +67,9 @@ namespace Avalonia.Controls
public static readonly StyledProperty DisplayMemberBindingProperty =
AvaloniaProperty.Register(nameof(DisplayMemberBinding));
+ private static readonly AttachedProperty AppliedItemContainerTheme =
+ AvaloniaProperty.RegisterAttached("AppliedItemContainerTheme");
+
///
/// Gets or sets the to use for binding to the display member of each item.
///
@@ -663,13 +666,26 @@ namespace Avalonia.Controls
internal void PrepareItemContainer(Control container, object? item, int index)
{
- var itemContainerTheme = ItemContainerTheme;
-
- if (itemContainerTheme is not null &&
- !container.IsSet(ThemeProperty) &&
- StyledElement.GetStyleKey(container) == itemContainerTheme.TargetType)
+ // If the container has no theme set, or we've already applied our ItemContainerTheme
+ // (and it hasn't changed since) then we're in control of the container's Theme and may
+ // need to update it.
+ if (!container.IsSet(ThemeProperty) || container.GetValue(AppliedItemContainerTheme) == container.Theme)
{
- container.Theme = itemContainerTheme;
+ var itemContainerTheme = ItemContainerTheme;
+
+ if (itemContainerTheme?.TargetType?.IsAssignableFrom(GetStyleKey(container)) == true)
+ {
+ // We have an ItemContainerTheme and it matches the container. Set the Theme
+ // property, and mark the container as having had ItemContainerTheme applied.
+ container.SetCurrentValue(ThemeProperty, itemContainerTheme);
+ container.SetValue(AppliedItemContainerTheme, itemContainerTheme);
+ }
+ else
+ {
+ // Otherwise clear the theme and the AppliedItemContainerTheme property.
+ container.ClearValue(ThemeProperty);
+ container.ClearValue(AppliedItemContainerTheme);
+ }
}
if (item is not Control)
diff --git a/src/Avalonia.Controls/Primitives/AccessText.cs b/src/Avalonia.Controls/Primitives/AccessText.cs
index 2e570e25c7..7f939f2d3a 100644
--- a/src/Avalonia.Controls/Primitives/AccessText.cs
+++ b/src/Avalonia.Controls/Primitives/AccessText.cs
@@ -3,6 +3,7 @@ using Avalonia.Input;
using Avalonia.Reactive;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
+using System;
namespace Avalonia.Controls.Primitives
{
@@ -68,11 +69,15 @@ namespace Avalonia.Controls.Primitives
if (underscore != -1 && ShowAccessKey)
{
var rect = TextLayout!.HitTestTextPosition(underscore);
- var offset = new Vector(0, -1.5);
+
+ var x1 = Math.Round(rect.Left, MidpointRounding.AwayFromZero);
+ var x2 = Math.Round(rect.Right, MidpointRounding.AwayFromZero);
+ var y = Math.Round(rect.Bottom, MidpointRounding.AwayFromZero) - 1.5;
+
context.DrawLine(
new Pen(Foreground, 1),
- rect.BottomLeft + offset,
- rect.BottomRight + offset);
+ new Point(x1, y),
+ new Point(x2, y));
}
}
diff --git a/src/Avalonia.Controls/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs
index f9aa5fc7a0..d823455e13 100644
--- a/src/Avalonia.Controls/Primitives/Track.cs
+++ b/src/Avalonia.Controls/Primitives/Track.cs
@@ -52,7 +52,7 @@ namespace Avalonia.Controls.Primitives
ThumbProperty.Changed.AddClassHandler
public static readonly StyledProperty IsCheckedProperty =
- AvaloniaProperty.Register(
- nameof(IsChecked));
+ AvaloniaProperty.Register(nameof(IsChecked), false, defaultBindingMode: BindingMode.TwoWay);
///
/// Initializes a new instance of the class.
diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs
index ea420c7c45..cc92bdd752 100644
--- a/src/Avalonia.Controls/TextBlock.cs
+++ b/src/Avalonia.Controls/TextBlock.cs
@@ -135,7 +135,7 @@ namespace Avalonia.Controls
/// Defines the property.
///
public static readonly StyledProperty TextDecorationsProperty =
- AvaloniaProperty.Register(nameof(TextDecorations));
+ Inline.TextDecorationsProperty.AddOwner();
///
/// Defines the property.
diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs
index 98b3b13c17..8a5ac4e495 100644
--- a/src/Avalonia.Controls/TextBox.cs
+++ b/src/Avalonia.Controls/TextBox.cs
@@ -1879,6 +1879,37 @@ namespace Avalonia.Controls
return text.Substring(start, end - start);
}
+ ///
+ /// Returns the sum of any vertical whitespace added between the and in the control template.
+ ///
+ /// The total vertical whitespace.
+ private double GetVerticalSpaceBetweenScrollViewerAndPresenter()
+ {
+ var verticalSpace = 0.0;
+ if (_presenter != null)
+ {
+ Visual? visual = _presenter;
+ while ((visual != null) && (visual != this))
+ {
+ if (visual == _scrollViewer)
+ {
+ // ScrollViewer is a stopping point and should only include the Padding
+ verticalSpace += _scrollViewer.Padding.Top + _scrollViewer.Padding.Bottom;
+ break;
+ }
+
+ var margin = visual.GetValue(Layoutable.MarginProperty);
+ var padding = visual.GetValue(Decorator.PaddingProperty);
+
+ verticalSpace += margin.Top + padding.Top + padding.Bottom + margin.Bottom;
+
+ visual = visual.VisualParent;
+ }
+ }
+
+ return verticalSpace;
+ }
+
///
/// Raises both the and events.
///
@@ -2032,8 +2063,9 @@ namespace Avalonia.Controls
var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
var paragraphProperties = TextLayout.CreateTextParagraphProperties(typeface, fontSize, null, default, default, null, default, LineHeight, default);
var textLayout = new TextLayout(new MaxLinesTextSource(MaxLines), paragraphProperties);
+ var verticalSpace = GetVerticalSpaceBetweenScrollViewerAndPresenter();
- maxHeight = Math.Ceiling(textLayout.Height);
+ maxHeight = Math.Ceiling(textLayout.Height + verticalSpace);
}
_scrollViewer.SetCurrentValue(MaxHeightProperty, maxHeight);
diff --git a/src/Avalonia.Controls/TransitioningContentControl.cs b/src/Avalonia.Controls/TransitioningContentControl.cs
index bf540698f1..0b04866d64 100644
--- a/src/Avalonia.Controls/TransitioningContentControl.cs
+++ b/src/Avalonia.Controls/TransitioningContentControl.cs
@@ -79,8 +79,12 @@ public class TransitioningContentControl : ContentControl
protected override bool RegisterContentPresenter(ContentPresenter presenter)
{
- if (!base.RegisterContentPresenter(presenter) &&
- presenter is ContentPresenter p &&
+ if (base.RegisterContentPresenter(presenter))
+ {
+ return true;
+ }
+
+ if (presenter is ContentPresenter p &&
p.Name == "PART_ContentPresenter2")
{
_presenter2 = p;
@@ -94,12 +98,13 @@ public class TransitioningContentControl : ContentControl
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
- base.OnPropertyChanged(change);
-
if (change.Property == ContentProperty)
{
UpdateContent(true);
+ return;
}
+
+ base.OnPropertyChanged(change);
}
private void UpdateContent(bool withTransition)
diff --git a/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj b/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj
index c5255b22cd..0c1b3e8a45 100644
--- a/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj
+++ b/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj
@@ -19,5 +19,4 @@
-
diff --git a/src/Avalonia.Desktop/Avalonia.Desktop.csproj b/src/Avalonia.Desktop/Avalonia.Desktop.csproj
index d180e6e34d..12cc977c84 100644
--- a/src/Avalonia.Desktop/Avalonia.Desktop.csproj
+++ b/src/Avalonia.Desktop/Avalonia.Desktop.csproj
@@ -12,7 +12,6 @@
-
diff --git a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj
index 65d1bea298..135bc1b685 100644
--- a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj
+++ b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj
@@ -24,7 +24,6 @@
-
diff --git a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
index be4bae26f1..3a2c18ac24 100644
--- a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
+++ b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
@@ -19,7 +19,6 @@
-
diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs
index b7c58cded9..f24c6fa96f 100644
--- a/src/Avalonia.Native/AvaloniaNativePlatform.cs
+++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs
@@ -108,7 +108,7 @@ namespace Avalonia.Native
.Bind().ToConstant(new NativePlatformSettings(_factory.CreatePlatformSettings()))
.Bind().ToConstant(this)
.Bind().ToConstant(new ClipboardImpl(_factory.CreateClipboard()))
- .Bind().ToConstant(new DefaultRenderTimer(60))
+ .Bind().ToConstant(new AvaloniaNativeRenderTimer(_factory.CreatePlatformRenderTimer()))
.Bind().ToConstant(new MacOSMountedVolumeInfoProvider())
.Bind().ToConstant(new AvaloniaNativeDragSource(_factory))
.Bind().ToConstant(applicationPlatform)
diff --git a/src/Avalonia.Native/AvaloniaNativeRenderTimer.cs b/src/Avalonia.Native/AvaloniaNativeRenderTimer.cs
new file mode 100644
index 0000000000..9021fdf345
--- /dev/null
+++ b/src/Avalonia.Native/AvaloniaNativeRenderTimer.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Diagnostics;
+using Avalonia.Native.Interop;
+using Avalonia.Rendering;
+#nullable enable
+
+namespace Avalonia.Native;
+
+internal sealed class AvaloniaNativeRenderTimer : NativeCallbackBase, IRenderTimer, IAvnActionCallback
+{
+ private readonly IAvnPlatformRenderTimer _platformRenderTimer;
+ private readonly Stopwatch _stopwatch;
+ private Action? _tick;
+ private int _subscriberCount;
+ private bool registered;
+
+ public AvaloniaNativeRenderTimer(IAvnPlatformRenderTimer platformRenderTimer)
+ {
+ _platformRenderTimer = platformRenderTimer;
+ _stopwatch = Stopwatch.StartNew();
+ }
+
+ public event Action Tick
+ {
+ add
+ {
+ _tick += value;
+
+ if (!registered)
+ {
+ registered = true;
+ _platformRenderTimer.RegisterTick(this);
+ }
+
+ if (_subscriberCount++ == 0)
+ {
+ _platformRenderTimer.Start();
+ }
+ }
+
+ remove
+ {
+ if (--_subscriberCount == 0)
+ {
+ _platformRenderTimer.Stop();
+ }
+
+ _tick -= value;
+ }
+ }
+
+ public bool RunsInBackground => _platformRenderTimer.RunsInBackground().FromComBool();
+
+ public void Run()
+ {
+ _tick?.Invoke(_stopwatch.Elapsed);
+ }
+}
diff --git a/src/Avalonia.Native/AvnAutomationPeer.cs b/src/Avalonia.Native/AvnAutomationPeer.cs
index 6c4e96b31b..af4958b02f 100644
--- a/src/Avalonia.Native/AvnAutomationPeer.cs
+++ b/src/Avalonia.Native/AvnAutomationPeer.cs
@@ -22,8 +22,8 @@ namespace Avalonia.Native
{
_inner = inner;
_inner.ChildrenChanged += (_, _) => Node?.ChildrenChanged();
- if (inner is WindowBaseAutomationPeer window)
- window.FocusChanged += (_, _) => Node?.FocusChanged();
+ if (inner is IRootProvider root)
+ root.FocusChanged += (_, _) => Node?.FocusChanged();
}
~AvnAutomationPeer() => Node?.Dispose();
@@ -39,6 +39,7 @@ namespace Avalonia.Native
public IAvnAutomationPeer? LabeledBy => Wrap(_inner.GetLabeledBy());
public IAvnString Name => _inner.GetName().ToAvnString();
public IAvnAutomationPeer? Parent => Wrap(_inner.GetParent());
+ public IAvnAutomationPeer? VisualRoot => Wrap(_inner.GetVisualRoot());
public int HasKeyboardFocus() => _inner.HasKeyboardFocus().AsComBool();
public int IsContentElement() => _inner.IsContentElement().AsComBool();
@@ -48,6 +49,13 @@ namespace Avalonia.Native
public void SetFocus() => _inner.SetFocus();
public int ShowContextMenu() => _inner.ShowContextMenu().AsComBool();
+ public void SetNode(IAvnAutomationNode node)
+ {
+ if (Node is not null)
+ throw new InvalidOperationException("The AvnAutomationPeer already has a node.");
+ Node = node;
+ }
+
public IAvnAutomationPeer? RootPeer
{
get
@@ -55,7 +63,7 @@ namespace Avalonia.Native
var peer = _inner;
var parent = peer.GetParent();
- while (peer is not IRootProvider && parent is not null)
+ while (peer.GetProvider() is null && parent is not null)
{
peer = parent;
parent = peer.GetParent();
@@ -65,26 +73,23 @@ namespace Avalonia.Native
}
}
- public void SetNode(IAvnAutomationNode node)
- {
- if (Node is not null)
- throw new InvalidOperationException("The AvnAutomationPeer already has a node.");
- Node = node;
- }
-
- public int IsRootProvider() => (_inner is IRootProvider).AsComBool();
+ private IEmbeddedRootProvider EmbeddedRootProvider => GetProvider();
+ private IExpandCollapseProvider ExpandCollapseProvider => GetProvider();
+ private IInvokeProvider InvokeProvider => GetProvider();
+ private IRangeValueProvider RangeValueProvider => GetProvider();
+ private IRootProvider RootProvider => GetProvider();
+ private ISelectionItemProvider SelectionItemProvider => GetProvider();
+ private IToggleProvider ToggleProvider => GetProvider();
+ private IValueProvider ValueProvider => GetProvider();
- public IAvnWindowBase RootProvider_GetWindow()
- {
- var window = (WindowBase)((ControlAutomationPeer)_inner).Owner;
- return ((WindowBaseImpl)window.PlatformImpl!).Native;
- }
-
- public IAvnAutomationPeer? RootProvider_GetFocus() => Wrap(((IRootProvider)_inner).GetFocus());
+ public int IsRootProvider() => IsProvider();
+
+ public IAvnWindowBase? RootProvider_GetWindow() => (RootProvider.PlatformImpl as WindowBaseImpl)?.Native;
+ public IAvnAutomationPeer? RootProvider_GetFocus() => Wrap(RootProvider.GetFocus());
public IAvnAutomationPeer? RootProvider_GetPeerFromPoint(AvnPoint point)
{
- var result = ((IRootProvider)_inner).GetPeerFromPoint(point.ToAvaloniaPoint());
+ var result = RootProvider.GetPeerFromPoint(point.ToAvaloniaPoint());
if (result is null)
return null;
@@ -103,46 +108,80 @@ namespace Avalonia.Native
return Wrap(result);
}
- public int IsExpandCollapseProvider() => (_inner is IExpandCollapseProvider).AsComBool();
- public int ExpandCollapseProvider_GetIsExpanded() => ((IExpandCollapseProvider)_inner).ExpandCollapseState switch
+ public int IsEmbeddedRootProvider() => IsProvider();
+
+ public IAvnAutomationPeer? EmbeddedRootProvider_GetFocus() => Wrap(EmbeddedRootProvider.GetFocus());
+
+ public IAvnAutomationPeer? EmbeddedRootProvider_GetPeerFromPoint(AvnPoint point)
+ {
+ var result = EmbeddedRootProvider.GetPeerFromPoint(point.ToAvaloniaPoint());
+
+ if (result is null)
+ return null;
+
+ // The OSX accessibility APIs expect non-ignored elements when hit-testing.
+ while (!result.IsControlElement())
+ {
+ var parent = result.GetParent();
+
+ if (parent is not null)
+ result = parent;
+ else
+ break;
+ }
+
+ return Wrap(result);
+ }
+
+ public int IsExpandCollapseProvider() => IsProvider();
+
+ public int ExpandCollapseProvider_GetIsExpanded() => ExpandCollapseProvider.ExpandCollapseState switch
{
ExpandCollapseState.Expanded => 1,
ExpandCollapseState.PartiallyExpanded => 1,
_ => 0,
};
- public int ExpandCollapseProvider_GetShowsMenu() => ((IExpandCollapseProvider)_inner).ShowsMenu.AsComBool();
- public void ExpandCollapseProvider_Expand() => ((IExpandCollapseProvider)_inner).Expand();
- public void ExpandCollapseProvider_Collapse() => ((IExpandCollapseProvider)_inner).Collapse();
+ public int ExpandCollapseProvider_GetShowsMenu() => ExpandCollapseProvider.ShowsMenu.AsComBool();
+ public void ExpandCollapseProvider_Expand() => ExpandCollapseProvider.Expand();
+ public void ExpandCollapseProvider_Collapse() => ExpandCollapseProvider.Collapse();
- public int IsInvokeProvider() => (_inner is IInvokeProvider).AsComBool();
- public void InvokeProvider_Invoke() => ((IInvokeProvider)_inner).Invoke();
+ public int IsInvokeProvider() => IsProvider();
+ public void InvokeProvider_Invoke() => InvokeProvider.Invoke();
- public int IsRangeValueProvider() => (_inner is IRangeValueProvider).AsComBool();
- public double RangeValueProvider_GetValue() => ((IRangeValueProvider)_inner).Value;
- public double RangeValueProvider_GetMinimum() => ((IRangeValueProvider)_inner).Minimum;
- public double RangeValueProvider_GetMaximum() => ((IRangeValueProvider)_inner).Maximum;
- public double RangeValueProvider_GetSmallChange() => ((IRangeValueProvider)_inner).SmallChange;
- public double RangeValueProvider_GetLargeChange() => ((IRangeValueProvider)_inner).LargeChange;
- public void RangeValueProvider_SetValue(double value) => ((IRangeValueProvider)_inner).SetValue(value);
+ public int IsRangeValueProvider() => IsProvider();
+ public double RangeValueProvider_GetValue() => RangeValueProvider.Value;
+ public double RangeValueProvider_GetMinimum() => RangeValueProvider.Minimum;
+ public double RangeValueProvider_GetMaximum() => RangeValueProvider.Maximum;
+ public double RangeValueProvider_GetSmallChange() => RangeValueProvider.SmallChange;
+ public double RangeValueProvider_GetLargeChange() => RangeValueProvider.LargeChange;
+ public void RangeValueProvider_SetValue(double value) => RangeValueProvider.SetValue(value);
- public int IsSelectionItemProvider() => (_inner is ISelectionItemProvider).AsComBool();
- public int SelectionItemProvider_IsSelected() => ((ISelectionItemProvider)_inner).IsSelected.AsComBool();
+ public int IsSelectionItemProvider() => IsProvider();
+ public int SelectionItemProvider_IsSelected() => SelectionItemProvider.IsSelected.AsComBool();
- public int IsToggleProvider() => (_inner is IToggleProvider).AsComBool();
- public int ToggleProvider_GetToggleState() => (int)((IToggleProvider)_inner).ToggleState;
- public void ToggleProvider_Toggle() => ((IToggleProvider)_inner).Toggle();
+ public int IsToggleProvider() => IsProvider();
+ public int ToggleProvider_GetToggleState() => (int)ToggleProvider.ToggleState;
+ public void ToggleProvider_Toggle() => ToggleProvider.Toggle();
- public int IsValueProvider() => (_inner is IValueProvider).AsComBool();
- public IAvnString ValueProvider_GetValue() => ((IValueProvider)_inner).Value.ToAvnString();
- public void ValueProvider_SetValue(string value) => ((IValueProvider)_inner).SetValue(value);
+ public int IsValueProvider() => IsProvider();
+ public IAvnString ValueProvider_GetValue() => ValueProvider.Value.ToAvnString();
+ public void ValueProvider_SetValue(string value) => ValueProvider.SetValue(value);
[return: NotNullIfNotNull("peer")]
public static AvnAutomationPeer? Wrap(AutomationPeer? peer)
{
return peer is null ? null : s_wrappers.GetValue(peer, x => new(peer));
}
+
+ private T GetProvider()
+ {
+ return _inner.GetProvider() ?? throw new InvalidOperationException(
+ $"The peer {_inner} does not implement {typeof(T)}.");
+ }
+
+ private int IsProvider() => (_inner.GetProvider() is not null).AsComBool();
}
internal class AvnAutomationPeerArray : NativeCallbackBase, IAvnAutomationPeerArray
diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl
index bc372bbcb5..7eac1d33a8 100644
--- a/src/Avalonia.Native/avn.idl
+++ b/src/Avalonia.Native/avn.idl
@@ -507,6 +507,7 @@ interface IAvaloniaNativeFactory : IUnknown
HRESULT CreateApplicationCommands(IAvnApplicationCommands** ppv);
HRESULT CreatePlatformSettings(IAvnPlatformSettings** ppv);
HRESULT CreatePlatformBehaviorInhibition(IAvnPlatformBehaviorInhibition** ppv);
+ HRESULT CreatePlatformRenderTimer(IAvnPlatformRenderTimer** ppv);
}
[uuid(233e094f-9b9f-44a3-9a6e-6948bbdd9fb1)]
@@ -921,6 +922,7 @@ interface IAvnAutomationPeer : IUnknown
IAvnAutomationPeer* GetLabeledBy();
IAvnString* GetName();
IAvnAutomationPeer* GetParent();
+ IAvnAutomationPeer* GetVisualRoot();
bool HasKeyboardFocus();
bool IsContentElement();
bool IsControlElement();
@@ -935,7 +937,11 @@ interface IAvnAutomationPeer : IUnknown
IAvnWindowBase* RootProvider_GetWindow();
IAvnAutomationPeer* RootProvider_GetFocus();
IAvnAutomationPeer* RootProvider_GetPeerFromPoint(AvnPoint point);
-
+
+ bool IsEmbeddedRootProvider();
+ IAvnAutomationPeer* EmbeddedRootProvider_GetFocus();
+ IAvnAutomationPeer* EmbeddedRootProvider_GetPeerFromPoint(AvnPoint point);
+
bool IsExpandCollapseProvider();
bool ExpandCollapseProvider_GetIsExpanded();
bool ExpandCollapseProvider_GetShowsMenu();
@@ -994,3 +1000,12 @@ interface IAvnPlatformBehaviorInhibition : IUnknown
{
void SetInhibitAppSleep(bool inhibitAppSleep, char* reason);
}
+
+[uuid(22edf20d-5803-2d3f-9247-b4842e5e9322)]
+interface IAvnPlatformRenderTimer : IUnknown
+{
+ HRESULT RegisterTick(IAvnActionCallback* callback);
+ void Start();
+ void Stop();
+ bool RunsInBackground();
+}
diff --git a/src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj b/src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj
index 4cae8e82df..30b5d80fbc 100644
--- a/src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj
+++ b/src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj
@@ -9,7 +9,6 @@
-
diff --git a/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj b/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj
index baeb8820f8..13d1ff9651 100644
--- a/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj
+++ b/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj
@@ -10,6 +10,5 @@
-
-
\ No newline at end of file
+
diff --git a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj
index 660661fc94..e62abff18b 100644
--- a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj
+++ b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj
@@ -11,7 +11,6 @@
-
diff --git a/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj b/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj
index 39da9a747f..9864cf24e0 100644
--- a/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj
+++ b/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj
@@ -11,7 +11,6 @@
-
diff --git a/src/Headless/Avalonia.Headless.NUnit/Avalonia.Headless.NUnit.csproj b/src/Headless/Avalonia.Headless.NUnit/Avalonia.Headless.NUnit.csproj
index 49f1de31f2..d176d74b5f 100644
--- a/src/Headless/Avalonia.Headless.NUnit/Avalonia.Headless.NUnit.csproj
+++ b/src/Headless/Avalonia.Headless.NUnit/Avalonia.Headless.NUnit.csproj
@@ -12,8 +12,7 @@
-
-
+
diff --git a/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj b/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj
index 1f06f28687..2020dca482 100644
--- a/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj
+++ b/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj
@@ -10,7 +10,6 @@
-
diff --git a/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj b/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj
index 4ab70eb07d..85879faa4c 100644
--- a/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj
+++ b/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj
@@ -13,7 +13,6 @@
-
diff --git a/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj
index 893cb0074c..fe071b594f 100644
--- a/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj
+++ b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj
@@ -7,7 +7,6 @@
-
diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
index 618379757b..d827530207 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
+++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
@@ -65,7 +65,6 @@
-
diff --git a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj
index d041e7d2e6..29583b6cb8 100644
--- a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj
+++ b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj
@@ -13,7 +13,6 @@
-
diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
index fbff4ab4e7..76d236e18a 100644
--- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
+++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
@@ -189,7 +189,8 @@ namespace Avalonia.Skia
var d = destRect.ToSKRect();
var paint = SKPaintCache.Shared.Get();
- paint.Color = new SKColor(255, 255, 255, (byte)(255 * opacity * (_useOpacitySaveLayer ? 1 : _currentOpacity)));
+
+ paint.Color = new SKColor(255, 255, 255, (byte)(255 * opacity * _currentOpacity));
paint.FilterQuality = RenderOptions.BitmapInterpolationMode.ToSKFilterQuality();
paint.BlendMode = RenderOptions.BitmapBlendingMode.ToSKBlendMode();
@@ -375,7 +376,7 @@ namespace Avalonia.Skia
{
if (boxShadow != default && !boxShadow.IsInset)
{
- using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _useOpacitySaveLayer ? 1 : _currentOpacity))
+ using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity))
{
var spread = (float)boxShadow.Spread;
if (boxShadow.IsInset)
@@ -432,7 +433,7 @@ namespace Avalonia.Skia
{
if (boxShadow != default && boxShadow.IsInset)
{
- using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _useOpacitySaveLayer ? 1 : _currentOpacity))
+ using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity))
{
var spread = (float)boxShadow.Spread;
var offsetX = (float)boxShadow.OffsetX;
@@ -592,8 +593,16 @@ namespace Avalonia.Skia
{
CheckLease();
- if(_useOpacitySaveLayer)
+ _opacityStack.Push(_currentOpacity);
+
+ var useOpacitySaveLayer = _useOpacitySaveLayer || RenderOptions.RequiresFullOpacityHandling == true;
+
+ if (useOpacitySaveLayer)
{
+ opacity = _currentOpacity * opacity; //Take current multiplied opacity
+
+ _currentOpacity = 1; //Opacity is applied via layering
+
if (bounds.HasValue)
{
var rect = bounds.Value.ToSKRect();
@@ -606,7 +615,6 @@ namespace Avalonia.Skia
}
else
{
- _opacityStack.Push(_currentOpacity);
_currentOpacity *= opacity;
}
}
@@ -616,14 +624,14 @@ namespace Avalonia.Skia
{
CheckLease();
- if(_useOpacitySaveLayer)
+ var useOpacitySaveLayer = _useOpacitySaveLayer || RenderOptions.RequiresFullOpacityHandling == true;
+
+ if (useOpacitySaveLayer)
{
Canvas.Restore();
}
- else
- {
- _currentOpacity = _opacityStack.Pop();
- }
+
+ _currentOpacity = _opacityStack.Pop();
}
///
@@ -1239,31 +1247,8 @@ namespace Avalonia.Skia
// https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/paths/dots
// TODO: Still something is off, dashes are now present, but don't look the same as D2D ones.
- switch (pen.LineCap)
- {
- case PenLineCap.Round:
- paint.StrokeCap = SKStrokeCap.Round;
- break;
- case PenLineCap.Square:
- paint.StrokeCap = SKStrokeCap.Square;
- break;
- default:
- paint.StrokeCap = SKStrokeCap.Butt;
- break;
- }
-
- switch (pen.LineJoin)
- {
- case PenLineJoin.Miter:
- paint.StrokeJoin = SKStrokeJoin.Miter;
- break;
- case PenLineJoin.Round:
- paint.StrokeJoin = SKStrokeJoin.Round;
- break;
- default:
- paint.StrokeJoin = SKStrokeJoin.Bevel;
- break;
- }
+ paint.StrokeCap = pen.LineCap.ToSKStrokeCap();
+ paint.StrokeJoin = pen.LineJoin.ToSKStrokeJoin();
paint.StrokeMiter = (float) pen.MiterLimit;
diff --git a/src/Skia/Avalonia.Skia/GeometryImpl.cs b/src/Skia/Avalonia.Skia/GeometryImpl.cs
index aee84d1346..c1ce4a661f 100644
--- a/src/Skia/Avalonia.Skia/GeometryImpl.cs
+++ b/src/Skia/Avalonia.Skia/GeometryImpl.cs
@@ -43,46 +43,11 @@ namespace Avalonia.Skia
///
public bool StrokeContains(IPen? pen, Point point)
{
- // Skia requires to compute stroke path to check for point containment.
- // Due to that we are caching using stroke width.
- // Usually this function is being called with same stroke width per path, so this saves a lot of Skia traffic.
+ _pathCache.UpdateIfNeeded(StrokePath, pen);
- var strokeWidth = (float)(pen?.Thickness ?? 0);
-
- if (!_pathCache.HasCacheFor(strokeWidth))
- {
- UpdatePathCache(strokeWidth);
- }
-
- return PathContainsCore(_pathCache.CachedStrokePath, point);
- }
-
- ///
- /// Update path cache for given stroke width.
- ///
- /// Stroke width.
- private void UpdatePathCache(float strokeWidth)
- {
- var strokePath = new SKPath();
-
- // For stroke widths close to 0 simply use empty path. Render bounds are cached from fill path.
- if (Math.Abs(strokeWidth) < float.Epsilon)
- {
- _pathCache.Cache(strokePath, strokeWidth, Bounds);
- }
- else
- {
- var paint = SKPaintCache.Shared.Get();
- paint.IsStroke = true;
- paint.StrokeWidth = strokeWidth;
- paint.GetFillPath(StrokePath, strokePath);
-
- SKPaintCache.Shared.ReturnReset(paint);
-
- _pathCache.Cache(strokePath, strokeWidth, strokePath.TightBounds.ToAvaloniaRect());
- }
+ return PathContainsCore(_pathCache.ExpandedPath, point);
}
-
+
///
/// Check Skia path if it contains a point.
///
@@ -106,14 +71,8 @@ namespace Avalonia.Skia
///
public Rect GetRenderBounds(IPen? pen)
{
- var strokeWidth = (float)(pen?.Thickness ?? 0);
-
- if (!_pathCache.HasCacheFor(strokeWidth))
- {
- UpdatePathCache(strokeWidth);
- }
-
- return _pathCache.CachedGeometryRenderBounds;
+ _pathCache.UpdateIfNeeded(StrokePath, pen);
+ return _pathCache.RenderBounds;
}
///
@@ -180,66 +139,70 @@ namespace Avalonia.Skia
///
protected void InvalidateCaches()
{
- _pathCache.Invalidate();
+ _pathCache.Dispose();
+ _pathCache = default;
}
private struct PathCache
{
- private float _cachedStrokeWidth;
-
- ///
- /// Tolerance for two stroke widths to be deemed equal
- ///
- public const float Tolerance = float.Epsilon;
-
- ///
- /// Cached contour path.
- ///
- public SKPath? CachedStrokePath { get; private set; }
-
- ///
- /// Cached geometry render bounds.
- ///
- public Rect CachedGeometryRenderBounds { get; private set; }
-
- ///
- /// Is cached valid for given stroke width.
- ///
- /// Stroke width to check.
- /// True, if CachedStrokePath can be used for given stroke width.
- public bool HasCacheFor(float strokeWidth)
+ private double _width, _miterLimit;
+ private PenLineCap _cap;
+ private PenLineJoin _join;
+ private SKPath? _path, _cachedFor;
+ private Rect? _renderBounds;
+ private static readonly SKPath s_emptyPath = new();
+
+
+ public Rect RenderBounds => _renderBounds ??= (_path ?? _cachedFor ?? s_emptyPath).Bounds.ToAvaloniaRect();
+ public SKPath ExpandedPath => _path ?? s_emptyPath;
+
+ public void UpdateIfNeeded(SKPath? strokePath, IPen? pen)
{
- return CachedStrokePath != null && Math.Abs(_cachedStrokeWidth - strokeWidth) < Tolerance;
- }
-
- ///
- /// Cache path for given stroke width. Takes ownership of a passed path.
- ///
- /// Path to cache.
- /// Stroke width to cache.
- /// Render bounds to use.
- public void Cache(SKPath path, float strokeWidth, Rect geometryRenderBounds)
- {
- if (CachedStrokePath != path)
+ var strokeWidth = pen?.Thickness ?? 0;
+ var miterLimit = pen?.MiterLimit ?? 0;
+ var cap = pen?.LineCap ?? default;
+ var join = pen?.LineJoin ?? default;
+
+ if (_cachedFor == strokePath
+ && _path != null
+ && cap == _cap
+ && join == _join
+ && Math.Abs(_width - strokeWidth) < float.Epsilon
+ && (join != PenLineJoin.Miter || Math.Abs(_miterLimit - miterLimit) > float.Epsilon))
+ // We are up to date
+ return;
+
+ _renderBounds = null;
+ _cachedFor = strokePath;
+ _width = strokeWidth;
+ _cap = cap;
+ _join = join;
+ _miterLimit = miterLimit;
+
+ if (strokePath == null || Math.Abs(strokeWidth) < float.Epsilon)
{
- CachedStrokePath?.Dispose();
+ _path = null;
+ return;
}
- CachedStrokePath = path;
- CachedGeometryRenderBounds = geometryRenderBounds;
- _cachedStrokeWidth = strokeWidth;
+ var paint = SKPaintCache.Shared.Get();
+ paint.IsStroke = true;
+ paint.StrokeWidth = (float)_width;
+ paint.StrokeCap = cap.ToSKStrokeCap();
+ paint.StrokeJoin = join.ToSKStrokeJoin();
+ paint.StrokeMiter = (float)miterLimit;
+ _path = new SKPath();
+ paint.GetFillPath(strokePath, _path);
+
+ SKPaintCache.Shared.ReturnReset(paint);
}
- ///
- /// Invalidate cache state.
- ///
- public void Invalidate()
+ public void Dispose()
{
- CachedStrokePath?.Dispose();
- CachedStrokePath = null;
- CachedGeometryRenderBounds = default;
- _cachedStrokeWidth = default;
+ _path?.Dispose();
+ _path = null;
}
+
}
}
}
diff --git a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
index 455e415ce1..44fe7aed89 100644
--- a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
+++ b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
@@ -196,6 +196,26 @@ namespace Avalonia.Skia
}
}
+ public static SKStrokeCap ToSKStrokeCap(this PenLineCap cap)
+ {
+ return cap switch
+ {
+ PenLineCap.Round => SKStrokeCap.Round,
+ PenLineCap.Square => SKStrokeCap.Square,
+ _ => SKStrokeCap.Butt
+ };
+ }
+
+ public static SKStrokeJoin ToSKStrokeJoin(this PenLineJoin join)
+ {
+ return join switch
+ {
+ PenLineJoin.Bevel => SKStrokeJoin.Bevel,
+ PenLineJoin.Round => SKStrokeJoin.Round,
+ _ => SKStrokeJoin.Miter
+ };
+ }
+
public static TextAlignment ToAvalonia(this SKTextAlign a)
{
switch (a)
diff --git a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs
index 9a93d1afd3..e1c08e0814 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs
@@ -1,6 +1,10 @@
+using System;
using Avalonia.Logging;
+using Avalonia.Media;
using Avalonia.Platform;
using SharpDX.Direct2D1;
+using Geometry = SharpDX.Direct2D1.Geometry;
+using PathGeometry = SharpDX.Direct2D1.PathGeometry;
namespace Avalonia.Direct2D1.Media
{
@@ -27,7 +31,20 @@ namespace Avalonia.Direct2D1.Media
///
public Rect GetRenderBounds(Avalonia.Media.IPen pen)
{
- return Geometry.GetWidenedBounds((float)(pen?.Thickness ?? 0)).ToAvalonia();
+ if (pen == null || Math.Abs(pen.Thickness) < float.Epsilon)
+ return Geometry.GetBounds().ToAvalonia();
+ var originalBounds = Geometry.GetWidenedBounds((float)pen.Thickness).ToAvalonia();
+ switch (pen.LineCap)
+ {
+ case PenLineCap.Flat:
+ return originalBounds;
+ case PenLineCap.Round:
+ return originalBounds.Inflate(pen.Thickness / 2);
+ case PenLineCap.Square:
+ return originalBounds.Inflate(pen.Thickness);
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
}
///
diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs
index 3eeedc4b5d..569f7da738 100644
--- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs
+++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs
@@ -54,25 +54,11 @@ namespace Avalonia.Win32.Automation
_runtimeId = new int[] { 3, GetHashCode() };
Peer = peer;
s_nodes.Add(peer, this);
- peer.ChildrenChanged += Peer_ChildrenChanged;
- peer.PropertyChanged += Peer_PropertyChanged;
- }
-
- private void Peer_ChildrenChanged(object? sender, EventArgs e)
- {
- ChildrenChanged();
- }
+ peer.ChildrenChanged += OnPeerChildrenChanged;
+ peer.PropertyChanged += OnPeerPropertyChanged;
- private void Peer_PropertyChanged(object? sender, AutomationPropertyChangedEventArgs e)
- {
- if (s_propertyMap.TryGetValue(e.Property, out var id))
- {
- UiaCoreProviderApi.UiaRaiseAutomationPropertyChangedEvent(
- this,
- (int)id,
- e.OldValue as IConvertible,
- e.NewValue as IConvertible);
- }
+ if (Peer.GetProvider() is { } embeddedRoot)
+ embeddedRoot.FocusChanged += OnEmbeddedRootFocusChanged;
}
public AutomationPeer Peer { get; protected set; }
@@ -89,21 +75,12 @@ namespace Avalonia.Win32.Automation
public virtual IRawElementProviderFragmentRoot? FragmentRoot
{
- get => InvokeSync(() => GetRoot()) as IRawElementProviderFragmentRoot;
+ get => InvokeSync(() => GetRoot());
}
public virtual IRawElementProviderSimple? HostRawElementProvider => null;
public ProviderOptions ProviderOptions => ProviderOptions.ServerSideProvider;
- public void ChildrenChanged()
- {
- UiaCoreProviderApi.UiaRaiseStructureChangedEvent(
- this,
- StructureChangeType.ChildrenInvalidated,
- null,
- 0);
- }
-
[return: MarshalAs(UnmanagedType.IUnknown)]
public virtual object? GetPatternProvider(int patternId)
{
@@ -250,21 +227,49 @@ namespace Avalonia.Win32.Automation
throw new NotSupportedException();
}
+ protected void RaiseChildrenChanged()
+ {
+ UiaCoreProviderApi.UiaRaiseStructureChangedEvent(
+ this,
+ StructureChangeType.ChildrenInvalidated,
+ null,
+ 0);
+ }
- private AutomationNode? GetRoot()
+ protected void RaiseFocusChanged(AutomationNode? focused)
+ {
+ UiaCoreProviderApi.UiaRaiseAutomationEvent(
+ focused,
+ (int)UiaEventId.AutomationFocusChanged);
+ }
+
+ private RootAutomationNode? GetRoot()
{
Dispatcher.UIThread.VerifyAccess();
+ return GetOrCreate(Peer.GetVisualRoot()) as RootAutomationNode;
+ }
- var peer = Peer;
- var parent = peer.GetParent();
+ private void OnPeerChildrenChanged(object? sender, EventArgs e)
+ {
+ RaiseChildrenChanged();
+ }
- while (peer.GetProvider() is null && parent is object)
+ private void OnPeerPropertyChanged(object? sender, AutomationPropertyChangedEventArgs e)
+ {
+ if (s_propertyMap.TryGetValue(e.Property, out var id))
{
- peer = parent;
- parent = peer.GetParent();
+ UiaCoreProviderApi.UiaRaiseAutomationPropertyChangedEvent(
+ this,
+ (int)id,
+ e.OldValue as IConvertible,
+ e.NewValue as IConvertible);
}
+ }
- return peer is object ? GetOrCreate(peer) : null;
+ private void OnEmbeddedRootFocusChanged(object? sender, EventArgs e)
+ {
+ if (Peer.GetProvider() is { } embeddedRoot)
+ RaiseFocusChanged(GetOrCreate(embeddedRoot.GetFocus()));
}
private static AutomationNode Create(AutomationPeer peer)
diff --git a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs
index 739f0ac251..7334186c80 100644
--- a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs
+++ b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs
@@ -9,18 +9,14 @@ using Avalonia.Win32.Interop.Automation;
namespace Avalonia.Win32.Automation
{
[RequiresUnreferencedCode("Requires .NET COM interop")]
- internal class RootAutomationNode : AutomationNode,
- IRawElementProviderFragmentRoot,
- IRawElementProviderAdviseEvents
+ internal class RootAutomationNode : AutomationNode, IRawElementProviderFragmentRoot
{
- private int _raiseFocusChanged;
-
public RootAutomationNode(AutomationPeer peer)
: base(peer)
{
Peer = base.Peer.GetProvider() ?? throw new AvaloniaInternalException(
"Attempt to create RootAutomationNode from peer which does not implement IRootProvider.");
- Peer.FocusChanged += FocusChanged;
+ Peer.FocusChanged += OnRootFocusChanged;
}
public override IRawElementProviderFragmentRoot? FragmentRoot => this;
@@ -44,41 +40,6 @@ namespace Avalonia.Win32.Automation
return GetOrCreate(focus);
}
- void IRawElementProviderAdviseEvents.AdviseEventAdded(int eventId, int[] properties)
- {
- switch ((UiaEventId)eventId)
- {
- case UiaEventId.AutomationFocusChanged:
- ++_raiseFocusChanged;
- break;
- }
- }
-
- void IRawElementProviderAdviseEvents.AdviseEventRemoved(int eventId, int[] properties)
- {
- switch ((UiaEventId)eventId)
- {
- case UiaEventId.AutomationFocusChanged:
- --_raiseFocusChanged;
- break;
- }
- }
-
- protected void RaiseFocusChanged(AutomationNode? focused)
- {
- if (_raiseFocusChanged > 0)
- {
- UiaCoreProviderApi.UiaRaiseAutomationEvent(
- focused,
- (int)UiaEventId.AutomationFocusChanged);
- }
- }
-
- public void FocusChanged(object? sender, EventArgs e)
- {
- RaiseFocusChanged(GetOrCreate(Peer.GetFocus()));
- }
-
public Rect ToScreen(Rect rect)
{
if (WindowImpl is null)
@@ -101,5 +62,10 @@ namespace Avalonia.Win32.Automation
return result;
}
}
+
+ private void OnRootFocusChanged(object? sender, EventArgs e)
+ {
+ RaiseFocusChanged(GetOrCreate(Peer.GetFocus()));
+ }
}
}
diff --git a/src/Windows/Avalonia.Win32/DataObject.cs b/src/Windows/Avalonia.Win32/DataObject.cs
index a215a0a322..df19cdc329 100644
--- a/src/Windows/Avalonia.Win32/DataObject.cs
+++ b/src/Windows/Avalonia.Win32/DataObject.cs
@@ -103,6 +103,7 @@ namespace Avalonia.Win32
private IDataObject _wrapped;
+ public IDataObject Wrapped => _wrapped;
public DataObject(IDataObject wrapped)
{
diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
index 68f3e3c670..226ed2a406 100644
--- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
+++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
@@ -1509,7 +1509,7 @@ namespace Avalonia.Win32.Interop
[DllImport("user32.dll")]
public static extern IntPtr SetClipboardData(ClipboardFormat uFormat, IntPtr hMem);
- [DllImport("ole32.dll", PreserveSig = false)]
+ [DllImport("ole32.dll", PreserveSig = true)]
public static extern int OleGetClipboard(out IntPtr dataObject);
[DllImport("ole32.dll", PreserveSig = true)]
diff --git a/src/Windows/Avalonia.Win32/OleDropTarget.cs b/src/Windows/Avalonia.Win32/OleDropTarget.cs
index a81652ffc2..94d744301a 100644
--- a/src/Windows/Avalonia.Win32/OleDropTarget.cs
+++ b/src/Windows/Avalonia.Win32/OleDropTarget.cs
@@ -217,7 +217,7 @@ namespace Avalonia.Win32
if (MicroComRuntime.TryUnwrapManagedObject(pDataObj) is DataObject dataObject)
{
- return dataObject;
+ return dataObject.Wrapped;
}
return new OleDataObject(pDataObj);
}
diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs
index 154b20ef84..e9156e553d 100644
--- a/src/Windows/Avalonia.Win32/WindowImpl.cs
+++ b/src/Windows/Avalonia.Win32/WindowImpl.cs
@@ -683,9 +683,10 @@ namespace Avalonia.Win32
if (parentHwnd == IntPtr.Zero && !_windowProperties.ShowInTaskbar)
{
parentHwnd = OffscreenParentWindow.Handle;
- _hiddenWindowIsParent = true;
}
+ _hiddenWindowIsParent = parentHwnd == OffscreenParentWindow.Handle;
+
SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, parentHwnd);
}
diff --git a/tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationClippingTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationClippingTests.cs
new file mode 100644
index 0000000000..c158ff4e75
--- /dev/null
+++ b/tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationClippingTests.cs
@@ -0,0 +1,68 @@
+using Avalonia.Controls;
+using Avalonia.Media;
+using Xunit;
+
+namespace Avalonia.Base.UnitTests.Rendering;
+///
+/// Test class that verifies how clipping influences rendering in the compositor
+///
+public class CompositorInvalidationClippingTests : CompositorTestsBase
+{
+ [Fact]
+ // Test case: When the ClipToBounds is false, all visuals should be rendered
+ public void Siblings_Should_Be_Rendered_On_Invalidate_Without_ClipToBounds()
+ {
+ AssertRenderedVisuals(clipToBounds: false, clipGeometry: false, expectedRenderedVisualsCount: 4);
+ }
+
+ [Fact]
+ // Test case: When the ClipToBounds is true, only visuals within the clipped boundary should be rendered
+ public void Siblings_Should_Not_Be_Rendered_On_Invalidate_With_ClipToBounds()
+ {
+ AssertRenderedVisuals(clipToBounds: true, clipGeometry: false, expectedRenderedVisualsCount: 3);
+ }
+
+ [Fact]
+ // Test case: When the Clip is used, only visuals within the clip geometry should be rendered
+ public void Siblings_Should_Not_Be_Rendered_On_Invalidate_With_Clip()
+ {
+ AssertRenderedVisuals(clipToBounds: false, clipGeometry: true, expectedRenderedVisualsCount: 3);
+ }
+
+ private void AssertRenderedVisuals(bool clipToBounds, bool clipGeometry, int expectedRenderedVisualsCount)
+ {
+ using (var s = new CompositorCanvas())
+ {
+ //#1 visual is top level
+ //#2 visual is s.Canvas
+
+ //#3 visual is border1
+ s.Canvas.Children.Add(new Border()
+ {
+ [Canvas.LeftProperty] = 0, [Canvas.TopProperty] = 0,
+ Width = 20, Height = 10,
+ Background = Brushes.Red,
+ ClipToBounds = clipToBounds,
+ Clip = clipGeometry ? new RectangleGeometry(new Rect(new Size(20, 10))) : null
+ });
+
+ //#4 visual is border2
+ s.Canvas.Children.Add(new Border()
+ {
+ [Canvas.LeftProperty] = 30, [Canvas.TopProperty] = 50,
+ Width = 20, Height = 10,
+ Background = Brushes.Red,
+ ClipToBounds = clipToBounds,
+ Clip = clipGeometry ? new RectangleGeometry(new Rect(new Size(20, 10))) : null
+ });
+ s.RunJobs();
+ s.Events.Reset();
+
+ //invalidate border1
+ s.Canvas.Children[0].IsVisible = false;
+ s.RunJobs();
+
+ s.AssertRenderedVisuals(expectedRenderedVisualsCount);
+ }
+ }
+}
diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
index 58e4ec1b75..7d3c1fe4f4 100644
--- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
@@ -14,6 +14,7 @@ using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml.Templates;
+using Avalonia.Media;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
@@ -128,6 +129,155 @@ namespace Avalonia.Controls.UnitTests
Assert.Same(container.Theme, theme);
}
+ [Fact]
+ public void Container_Should_Have_Theme_Set_To_ItemContainerTheme_With_Base_TargetType()
+ {
+ using var app = Start();
+ var theme = new ControlTheme { TargetType = typeof(Control) };
+ var target = CreateTarget(
+ itemsSource: new[] { "Foo" },
+ itemContainerTheme: theme);
+
+ var container = GetContainer(target);
+
+ Assert.Same(container.Theme, theme);
+ }
+
+ [Fact]
+ public void ItemContainerTheme_Can_Be_Changed()
+ {
+ using var app = Start();
+
+ var theme1 = new ControlTheme
+ {
+ TargetType = typeof(ContentPresenter),
+ Setters = { new Setter(ContentPresenter.BackgroundProperty, Brushes.Red) }
+ };
+
+ var theme2 = new ControlTheme
+ {
+ TargetType = typeof(ContentPresenter),
+ Setters = { new Setter(ContentPresenter.BackgroundProperty, Brushes.Green) }
+ };
+
+ var target = CreateTarget(
+ itemsSource: new[] { "Foo" },
+ itemContainerTheme: theme1);
+
+ var container = GetContainer(target);
+
+ Assert.Same(container.Theme, theme1);
+ Assert.Equal(container.Background, Brushes.Red);
+
+ target.ItemContainerTheme = theme2;
+
+ container = GetContainer(target);
+ Assert.Same(container.Theme, theme2);
+ Assert.Equal(container.Background, Brushes.Green);
+ }
+
+ [Fact]
+ public void ItemContainerTheme_Can_Be_Changed_Virtualizing()
+ {
+ using var app = Start();
+
+ var theme1 = new ControlTheme
+ {
+ TargetType = typeof(ContentPresenter),
+ Setters = { new Setter(ContentPresenter.BackgroundProperty, Brushes.Red) }
+ };
+
+ var theme2 = new ControlTheme
+ {
+ TargetType = typeof(ContentPresenter),
+ Setters = { new Setter(ContentPresenter.BackgroundProperty, Brushes.Green) }
+ };
+
+ var itemsPanel = new FuncTemplate(() => new VirtualizingStackPanel());
+ var target = CreateTarget(
+ itemsSource: new[] { "Foo" },
+ itemContainerTheme: theme1,
+ itemsPanel: itemsPanel);
+
+ var container = GetContainer(target);
+
+ Assert.Same(container.Theme, theme1);
+ Assert.Equal(container.Background, Brushes.Red);
+
+ target.ItemContainerTheme = theme2;
+ Layout(target);
+
+ container = GetContainer(target);
+ Assert.Same(container.Theme, theme2);
+ Assert.Equal(container.Background, Brushes.Green);
+ }
+
+ [Fact]
+ public void ItemContainerTheme_Can_Be_Cleared()
+ {
+ using var app = Start();
+
+ var theme = new ControlTheme
+ {
+ TargetType = typeof(ContentPresenter),
+ Setters = { new Setter(ContentPresenter.BackgroundProperty, Brushes.Red) }
+ };
+
+ var target = CreateTarget(
+ itemsSource: new[] { "Foo" },
+ itemContainerTheme: theme);
+
+ var container = GetContainer(target);
+
+ Assert.Same(container.Theme, theme);
+ Assert.Equal(container.Background, Brushes.Red);
+
+ target.ItemContainerTheme = null;
+
+ container = GetContainer(target);
+ Assert.Null(container.Theme);
+ Assert.Null(container.Background);
+ }
+
+ [Fact]
+ public void ItemContainerTheme_Should_Not_Override_LocalValue_Theme()
+ {
+ using var app = Start();
+
+ var theme1 = new ControlTheme
+ {
+ TargetType = typeof(ContentPresenter),
+ Setters = { new Setter(ContentPresenter.BackgroundProperty, Brushes.Red) }
+ };
+
+ var theme2 = new ControlTheme
+ {
+ TargetType = typeof(Control),
+ Setters = { new Setter(ContentPresenter.BackgroundProperty, Brushes.Green) }
+ };
+
+ var items = new object[]
+ {
+ new ContentPresenter(),
+ new ContentPresenter
+ {
+ Theme = theme2
+ },
+ };
+
+ var target = CreateTarget(
+ itemsSource: items,
+ itemContainerTheme: theme1);
+
+ Assert.Same(theme1, GetContainer(target, 0).Theme);
+ Assert.Same(theme2, GetContainer(target, 1).Theme);
+
+ target.ItemContainerTheme = null;
+
+ Assert.Null(GetContainer(target, 0).Theme);
+ Assert.Same(theme2, GetContainer(target, 1).Theme);
+ }
+
[Fact]
public void Container_Should_Have_LogicalParent_Set_To_ItemsControl()
{
@@ -851,6 +1001,7 @@ namespace Avalonia.Controls.UnitTests
IList? itemsSource = null,
ControlTheme? itemContainerTheme = null,
IDataTemplate? itemTemplate = null,
+ ITemplate? itemsPanel = null,
IEnumerable? dataTemplates = null,
bool performLayout = true)
{
@@ -861,6 +1012,7 @@ namespace Avalonia.Controls.UnitTests
itemsSource: itemsSource,
itemContainerTheme: itemContainerTheme,
itemTemplate: itemTemplate,
+ itemsPanel: itemsPanel,
dataTemplates: dataTemplates,
performLayout: performLayout);
}
diff --git a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs
index eb1d6f5ea4..fd3d80729a 100644
--- a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs
@@ -202,5 +202,26 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(0, target.Inlines.Count);
}
}
+
+ [Fact]
+ public void Setting_TextDecorations_Should_Update_Inlines()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var target = new TextBlock();
+
+ target.Inlines.Add(new Run("Hello World"));
+
+ Assert.Equal(1, target.Inlines.Count);
+
+ Assert.Null(target.Inlines[0].TextDecorations);
+
+ var underline = TextDecorations.Underline;
+
+ target.TextDecorations = underline;
+
+ Assert.Equal(underline, target.Inlines[0].TextDecorations);
+ }
+ }
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
index d4558c9e04..3c7fb6f9c0 100644
--- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
@@ -15,6 +15,7 @@ using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Avalonia.UnitTests;
+using Avalonia.VisualTree;
using Moq;
using Xunit;
@@ -916,6 +917,82 @@ namespace Avalonia.Controls.UnitTests
}
}
+ [Theory]
+ [InlineData(1)]
+ [InlineData(2)]
+ [InlineData(3)]
+ public void MaxLines_Sets_ScrollViewer_MaxHeight(int maxLines)
+ {
+ using (UnitTestApplication.Start(Services))
+ {
+ var target = new TextBox
+ {
+ Template = CreateTemplate(),
+ MaxLines = maxLines,
+
+ // Define explicit whole number line height for predictable calculations
+ LineHeight = 20
+ };
+
+ var impl = CreateMockTopLevelImpl();
+ var topLevel = new TestTopLevel(impl.Object)
+ {
+ Template = CreateTopLevelTemplate(),
+ Content = target
+ };
+ topLevel.ApplyTemplate();
+ topLevel.LayoutManager.ExecuteInitialLayoutPass();
+
+ var textPresenter = target.FindDescendantOfType();
+ Assert.Equal("PART_TextPresenter", textPresenter.Name);
+ Assert.Equal(new Thickness(0), textPresenter.Margin); // Test assumes no margin on TextPresenter
+
+ var scrollViewer = target.FindDescendantOfType();
+ Assert.Equal("PART_ScrollViewer", scrollViewer.Name);
+ Assert.Equal(maxLines * target.LineHeight, scrollViewer.MaxHeight);
+ }
+ }
+
+ [Theory]
+ [InlineData(1)]
+ [InlineData(2)]
+ [InlineData(3)]
+ public void MaxLines_Sets_ScrollViewer_MaxHeight_With_TextPresenter_Margin(int maxLines)
+ {
+ using (UnitTestApplication.Start(Services))
+ {
+ var target = new TextBox
+ {
+ Template = CreateTemplate(),
+ MaxLines = maxLines,
+
+ // Define explicit whole number line height for predictable calculations
+ LineHeight = 20
+ };
+
+ var impl = CreateMockTopLevelImpl();
+ var topLevel = new TestTopLevel(impl.Object)
+ {
+ Template = CreateTopLevelTemplate(),
+ Content = target
+ };
+ topLevel.ApplyTemplate();
+ topLevel.LayoutManager.ExecuteInitialLayoutPass();
+
+ var textPresenter = target.FindDescendantOfType();
+ Assert.Equal("PART_TextPresenter", textPresenter.Name);
+ var textPresenterMargin = new Thickness(horizontal: 0, vertical: 3);
+ textPresenter.Margin = textPresenterMargin;
+
+ target.InvalidateMeasure();
+ target.Measure(Size.Infinity);
+
+ var scrollViewer = target.FindDescendantOfType();
+ Assert.Equal("PART_ScrollViewer", scrollViewer.Name);
+ Assert.Equal((maxLines * target.LineHeight) + textPresenterMargin.Top + textPresenterMargin.Bottom, scrollViewer.MaxHeight);
+ }
+ }
+
[Fact]
public void CanUndo_CanRedo_Is_False_When_Initialized()
{
@@ -1125,7 +1202,7 @@ namespace Avalonia.Controls.UnitTests
return new FuncControlTemplate((control, scope) =>
new ScrollViewer
{
- Name = "Part_ScrollViewer",
+ Name = "PART_ScrollViewer",
Template = new FuncControlTemplate(ScrollViewerTests.CreateTemplate),
Content = new TextPresenter
{
diff --git a/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs b/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs
index 02dd4e6c03..47e53676d8 100644
--- a/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs
@@ -183,6 +183,34 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal("bar", presenter2.Content);
}
+ [Fact]
+ public void Logical_Children_Should_Not_Be_Duplicated()
+ {
+ using var app = Start();
+ var (target, transition) = CreateTarget("");
+ target.PageTransition = null;
+
+ var childControl = new Control();
+ target.Content = childControl;
+
+ Assert.Equal(1, target.LogicalChildren.Count);
+ Assert.Equal(target.LogicalChildren[0], childControl);
+ }
+
+ [Fact]
+ public void First_Presenter_Should_Register_TCC_As_His_Host()
+ {
+ using var app = Start();
+ var (target, transition) = CreateTarget("");
+ target.PageTransition = null;
+
+ var childControl = new Control();
+ target.Presenter!.Content = childControl;
+
+ Assert.Equal(1, target.LogicalChildren.Count);
+ Assert.Equal(target.LogicalChildren[0], childControl);
+ }
+
private static IDisposable Start()
{
return UnitTestApplication.Start(
diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs
index ab8e4377b6..c6f9c5eae0 100644
--- a/tests/Avalonia.LeakTests/ControlTests.cs
+++ b/tests/Avalonia.LeakTests/ControlTests.cs
@@ -583,6 +583,46 @@ namespace Avalonia.LeakTests
Assert.Equal(initialMenuItemCount, memory.GetObjects(where => where.Type.Is