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((x, e) => x.ThumbChanged(e)); IncreaseButtonProperty.Changed.AddClassHandler((x, e) => x.ButtonChanged(e)); DecreaseButtonProperty.Changed.AddClassHandler((x, e) => x.ButtonChanged(e)); - AffectsArrange(MinimumProperty, MaximumProperty, ValueProperty, OrientationProperty); + AffectsArrange(IsDirectionReversedProperty, MinimumProperty, MaximumProperty, ValueProperty, OrientationProperty); } public Track() diff --git a/src/Avalonia.Controls/Shapes/Polygon.cs b/src/Avalonia.Controls/Shapes/Polygon.cs index 78def84448..e51e117e33 100644 --- a/src/Avalonia.Controls/Shapes/Polygon.cs +++ b/src/Avalonia.Controls/Shapes/Polygon.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Avalonia.Media; +using Avalonia.Data; namespace Avalonia.Controls.Shapes { @@ -15,9 +16,9 @@ namespace Avalonia.Controls.Shapes public Polygon() { - Points = new Points(); + SetValue(PointsProperty, new Points(), BindingPriority.Template); } - + public IList Points { get => GetValue(PointsProperty); diff --git a/src/Avalonia.Controls/Shapes/Polyline.cs b/src/Avalonia.Controls/Shapes/Polyline.cs index 2533794f89..84fccb66f1 100644 --- a/src/Avalonia.Controls/Shapes/Polyline.cs +++ b/src/Avalonia.Controls/Shapes/Polyline.cs @@ -1,9 +1,10 @@ using System.Collections.Generic; using Avalonia.Media; +using Avalonia.Data; namespace Avalonia.Controls.Shapes { - public class Polyline: Shape + public class Polyline : Shape { public static readonly StyledProperty> PointsProperty = AvaloniaProperty.Register>("Points"); @@ -16,7 +17,7 @@ namespace Avalonia.Controls.Shapes public Polyline() { - Points = new Points(); + SetValue(PointsProperty, new Points(), BindingPriority.Template); } public IList Points diff --git a/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs b/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs index ff8bd5dc0b..c493445ba1 100644 --- a/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs @@ -2,6 +2,7 @@ using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; +using Avalonia.Data; using Avalonia.Interactivity; using Avalonia.Styling; @@ -36,8 +37,7 @@ namespace Avalonia.Controls /// Defines the property. /// 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()).ObjectsCount)); } } + + [Fact] + public void Attached_Control_From_ContextMenu_Is_Freed() + { + using (Start()) + { + var contextMenu = new ContextMenu(); + Func run = () => + { + var window = new Window + { + Content = new TextBlock + { + ContextMenu = contextMenu + } + }; + + window.Show(); + + // Do a layout and make sure that TextBlock gets added to visual tree. + window.LayoutManager.ExecuteInitialLayoutPass(); + Assert.IsType(window.Presenter.Child); + + // Clear the content and ensure the TextBlock is removed. + window.Content = null; + window.LayoutManager.ExecuteLayoutPass(); + Assert.Null(window.Presenter.Child); + + return window; + }; + + var result = run(); + + // Process all Loaded events to free control reference(s) + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + + dotMemory.Check(memory => + Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + } + } [Fact] public void Standalone_ContextMenu_Is_Freed() diff --git a/tests/Avalonia.Skia.UnitTests/RenderBoundsTests.cs b/tests/Avalonia.Skia.UnitTests/RenderBoundsTests.cs new file mode 100644 index 0000000000..a659c2b8b2 --- /dev/null +++ b/tests/Avalonia.Skia.UnitTests/RenderBoundsTests.cs @@ -0,0 +1,43 @@ +using System; +using Avalonia.Controls.Shapes; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Skia.UnitTests +{ + public class RenderBoundsTests + { + [Theory, + InlineData("M10 20 L 20 10 L 30 20", PenLineCap.Round, PenLineJoin.Miter, 2, 10, + 8.585786819458008, 8.585786819458008, 22.828428268432617, 12.828428268432617), + InlineData("M10 10 L 20 10", PenLineCap.Round, PenLineJoin.Miter,2, 10, + 9,9,12,2), + InlineData("M10 10 L 20 15 L 10 20", PenLineCap.Flat, PenLineJoin.Miter, 2, 20, + 9.552786827087402, 9.105572700500488, 12.683281898498535, 11.788853645324707) + + ] + public void RenderBoundsAreCorrectlyCalculated(string path, PenLineCap cap, PenLineJoin join, double thickness, double miterLimit, double x, double y, double width, double height) + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface + .With(renderInterface: new PlatformRenderInterface()))) + { + var geo = PathGeometry.Parse(path); + var pen = new Pen(Brushes.Black, thickness, null, cap, join, miterLimit); + var bounds = geo.GetRenderBounds(pen); + var tolerance = 0.001; + if ( + Math.Abs(bounds.X - x) > tolerance + || Math.Abs(bounds.Y - y) > tolerance + || Math.Abs(bounds.Width - width) > tolerance + || Math.Abs(bounds.Height - height) > tolerance) + Assert.Fail($"Expected {x}:{y}:{width}:{height}, got {bounds}"); + + Assert.Equal(new Rect(x, y, width, height), bounds); + } + } + } +} diff --git a/tests/Avalonia.UnitTests/CompositorTestServices.cs b/tests/Avalonia.UnitTests/CompositorTestServices.cs index 53fd610a17..00645259a5 100644 --- a/tests/Avalonia.UnitTests/CompositorTestServices.cs +++ b/tests/Avalonia.UnitTests/CompositorTestServices.cs @@ -89,6 +89,13 @@ public class CompositorTestServices : IDisposable Events.Rects.Clear(); } + public void AssertRenderedVisuals(int renderVisuals) + { + RunJobs(); + Assert.Equal(Events.RenderedVisuals, renderVisuals); + Events.Rects.Clear(); + } + public void AssertHitTest(double x, double y, Func filter, params object[] expected) => AssertHitTest(new Point(x, y), filter, expected); @@ -110,6 +117,13 @@ public class CompositorTestServices : IDisposable { public List Rects = new(); + public int RenderedVisuals { get; private set; } + + public void IncrementRenderedVisuals() + { + RenderedVisuals++; + } + public void RectInvalidated(Rect rc) { Rects.Add(rc); @@ -118,6 +132,7 @@ public class CompositorTestServices : IDisposable public void Reset() { Rects.Clear(); + RenderedVisuals = 0; } } @@ -218,4 +233,4 @@ public class DispatcherCompositorScheduler : ICompositorScheduler { Dispatcher.UIThread.Post(() => compositor.Commit(), DispatcherPriority.UiThreadRender); } -} \ No newline at end of file +}