Browse Source

Merge branch 'master' into feature/7120-control-themes

pull/8263/head
Max Katz 4 years ago
committed by GitHub
parent
commit
9feaa7543b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      Avalonia.sln
  2. 1
      samples/ControlCatalog/App.xaml
  3. 34
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  4. 13
      src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs
  5. 44
      src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs
  6. 6
      src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs
  7. 3
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
  8. 35
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
  9. 10
      src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs
  10. 2
      src/Avalonia.Base/Rendering/Composition/Visual.cs
  11. 40
      src/Avalonia.Controls/ItemsControl.cs
  12. 10
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  13. 8
      src/Avalonia.Controls/TrayIcon.cs
  14. 98
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json
  15. 3
      src/Avalonia.FreeDesktop/DBusHelper.cs
  16. 33
      src/Avalonia.FreeDesktop/DBusSystemDialog.cs
  17. 1
      src/Avalonia.Themes.Default/Controls/NativeMenuBar.xaml
  18. 1
      src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml
  19. 64
      src/Avalonia.X11/NativeDialogs/CompositeStorageProvider.cs
  20. 2
      src/Avalonia.X11/NativeDialogs/Gtk.cs
  21. 30
      src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs
  22. 8
      src/Avalonia.X11/X11Platform.cs
  23. 9
      src/Avalonia.X11/X11Window.cs
  24. 20
      src/Windows/Avalonia.Win32/IconImpl.cs
  25. 4
      src/Windows/Avalonia.Win32/TrayIconImpl.cs
  26. 29
      src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs
  27. 8
      src/Windows/Avalonia.Win32/Win32Platform.cs
  28. 417
      tests/Avalonia.Base.UnitTests/Rendering/CompositorHitTestingTests.cs
  29. 109
      tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationTests.cs
  30. 208
      tests/Avalonia.Base.UnitTests/Rendering/CompositorTestsBase.cs
  31. 63
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  32. 27
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

1
Avalonia.sln

@ -559,6 +559,7 @@ Global
{2B390431-288C-435C-BB6B-A374033BD8D1} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{EABE2161-989B-42BF-BD8D-1E34B20C21F1} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

1
samples/ControlCatalog/App.xaml

@ -43,6 +43,7 @@
<NativeMenuItemSeparator />
<NativeMenuItem Header="Option 3" ToggleType="CheckBox" IsChecked="True" Command="{Binding ToggleCommand}" />
<NativeMenuItem Icon="/Assets/test_icon.ico" Header="Restore Defaults" Command="{Binding ToggleCommand}" />
<NativeMenuItem Header="Disabled option" IsEnabled="False" />
</NativeMenu>
</NativeMenuItem>
<NativeMenuItem Header="Exit" Command="{Binding ExitCommand}" />

34
src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs

@ -24,11 +24,12 @@ public class CompositingRenderer : IRendererWithCompositor
DrawingContext _recordingContext;
private HashSet<Visual> _dirty = new();
private HashSet<Visual> _recalculateChildren = new();
private readonly CompositionTarget _target;
private bool _queuedUpdate;
private Action _update;
private Action _invalidateScene;
internal CompositionTarget CompositionTarget;
/// <summary>
/// Asks the renderer to only draw frames on the render thread. Makes Paint to wait until frame is rendered.
/// </summary>
@ -40,8 +41,8 @@ public class CompositingRenderer : IRendererWithCompositor
_root = root;
_compositor = compositor;
_recordingContext = new DrawingContext(_recorder);
_target = compositor.CreateCompositionTarget(root.CreateRenderTarget);
_target.Root = ((Visual)root!.VisualRoot!).AttachToCompositor(compositor);
CompositionTarget = compositor.CreateCompositionTarget(root.CreateRenderTarget);
CompositionTarget.Root = ((Visual)root!.VisualRoot!).AttachToCompositor(compositor);
_update = Update;
_invalidateScene = InvalidateScene;
}
@ -49,15 +50,15 @@ public class CompositingRenderer : IRendererWithCompositor
/// <inheritdoc/>
public bool DrawFps
{
get => _target.DrawFps;
set => _target.DrawFps = value;
get => CompositionTarget.DrawFps;
set => CompositionTarget.DrawFps = value;
}
/// <inheritdoc/>
public bool DrawDirtyRects
{
get => _target.DrawDirtyRects;
set => _target.DrawDirtyRects = value;
get => CompositionTarget.DrawDirtyRects;
set => CompositionTarget.DrawDirtyRects = value;
}
/// <inheritdoc/>
@ -81,12 +82,11 @@ public class CompositingRenderer : IRendererWithCompositor
/// <inheritdoc/>
public IEnumerable<IVisual> HitTest(Point p, IVisual root, Func<IVisual, bool>? filter)
{
var res = _target.TryHitTest(p, filter);
var res = CompositionTarget.TryHitTest(p, filter);
if(res == null)
yield break;
for (var index = res.Count - 1; index >= 0; index--)
foreach(var v in res)
{
var v = res[index];
if (v is CompositionDrawListVisual dv)
{
if (filter == null || filter(dv.Visual))
@ -234,8 +234,8 @@ public class CompositingRenderer : IRendererWithCompositor
SyncChildren(v);
_dirty.Clear();
_recalculateChildren.Clear();
_target.Size = _root.ClientSize;
_target.Scaling = _root.RenderScaling;
CompositionTarget.Size = _root.ClientSize;
CompositionTarget.Scaling = _root.RenderScaling;
Compositor.InvokeOnNextCommit(_invalidateScene);
}
@ -246,24 +246,24 @@ public class CompositingRenderer : IRendererWithCompositor
public void Paint(Rect rect)
{
Update();
_target.RequestRedraw();
CompositionTarget.RequestRedraw();
if(RenderOnlyOnRenderThread && Compositor.Loop.RunsInBackground)
Compositor.RequestCommitAsync().Wait();
else
_target.ImmediateUIThreadRender();
CompositionTarget.ImmediateUIThreadRender();
}
public void Start() => _target.IsEnabled = true;
public void Start() => CompositionTarget.IsEnabled = true;
public void Stop()
{
_target.IsEnabled = false;
CompositionTarget.IsEnabled = false;
}
public void Dispose()
{
Stop();
_target.Dispose();
CompositionTarget.Dispose();
// Wait for the composition batch to be applied and rendered to guarantee that
// render target is not used anymore and can be safely disposed

13
src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs

@ -54,13 +54,20 @@ internal class CompositionDrawListVisual : CompositionContainerVisual
internal override bool HitTest(Point pt, Func<IVisual, bool>? filter)
{
if (DrawList == null)
var custom = Visual as ICustomHitTest;
if (DrawList == null && custom == null)
return false;
if (filter != null && !filter(Visual))
return false;
if (Visual is ICustomHitTest custom)
if (custom != null)
{
// Simulate the old behavior
// TODO: Change behavior once legacy renderers are removed
pt += new Point(Offset.X, Offset.Y);
return custom.HitTest(pt);
foreach (var op in DrawList)
}
foreach (var op in DrawList!)
if (op.Item.HitTest(pt))
return true;
return false;

44
src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs

@ -62,7 +62,7 @@ namespace Avalonia.Rendering.Composition
bool TryGetInvertedTransform(CompositionVisual visual, out Matrix matrix)
{
var m = visual.TryGetServerTransform();
var m = visual.TryGetServerGlobalTransform();
if (m == null)
{
matrix = default;
@ -73,52 +73,44 @@ namespace Avalonia.Rendering.Composition
return m33.TryInvert(out matrix);
}
bool TryTransformTo(CompositionVisual visual, ref Point v)
bool TryTransformTo(CompositionVisual visual, Point globalPoint, out Point v)
{
v = default;
if (TryGetInvertedTransform(visual, out var m))
{
v = v * m;
v = globalPoint * m;
return true;
}
return false;
}
bool HitTestCore(CompositionVisual visual, Point point, PooledList<CompositionVisual> result,
void HitTestCore(CompositionVisual visual, Point globalPoint, PooledList<CompositionVisual> result,
Func<IVisual, bool>? filter)
{
//TODO: Check readback too
if (visual.Visible == false)
return false;
if (!TryTransformTo(visual, ref point))
return false;
return;
if (!TryTransformTo(visual, globalPoint, out var point))
return;
if (visual.ClipToBounds
&& (point.X < 0 || point.Y < 0 || point.X > visual.Size.X || point.Y > visual.Size.Y))
return false;
if (visual.Clip?.FillContains(point) == false)
return false;
return;
bool success = false;
// Hit-test the current node
if (visual.HitTest(point, filter))
{
result.Add(visual);
success = true;
}
// Inspect children too
if (visual.Clip?.FillContains(point) == false)
return;
// Inspect children
if (visual is CompositionContainerVisual cv)
for (var c = cv.Children.Count - 1; c >= 0; c--)
{
var ch = cv.Children[c];
var hit = HitTestCore(ch, point, result, filter);
if (hit)
return true;
HitTestCore(ch, globalPoint, result, filter);
}
return success;
// Hit-test the current node
if (visual.HitTest(point, filter))
result.Add(visual);
}
/// <summary>

6
src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs

@ -0,0 +1,6 @@
namespace Avalonia.Rendering.Composition;
internal interface ICompositionTargetDebugEvents
{
void RectInvalidated(Rect rc);
}

3
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs

@ -33,7 +33,7 @@ namespace Avalonia.Rendering.Composition.Server
private HashSet<ServerCompositionVisual> _attachedVisuals = new();
private Queue<ServerCompositionVisual> _adornerUpdateQueue = new();
public ICompositionTargetDebugEvents? DebugEvents { get; set; }
public ReadbackIndices Readback { get; } = new();
public int RenderedVisuals { get; set; }
@ -173,6 +173,7 @@ namespace Avalonia.Rendering.Composition.Server
if(rect.IsEmpty)
return;
var snapped = SnapToDevicePixels(rect, Scaling);
DebugEvents?.RectInvalidated(rect);
_dirtyRect = _dirtyRect.Union(snapped);
_redrawRequested = true;
}

35
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs

@ -119,14 +119,15 @@ namespace Avalonia.Rendering.Composition.Server
var oldTransformedContentBounds = TransformedOwnContentBounds;
var oldCombinedTransformedClipBounds = _combinedTransformedClipBounds;
var dirtyOldBounds = false;
if (_parent?.IsDirtyComposition == true)
{
IsDirtyComposition = true;
_isDirtyForUpdate = true;
dirtyOldBounds = true;
}
var invalidateOldBounds = _isDirtyForUpdate;
var invalidateNewBounds = _isDirtyForUpdate;
GlobalTransformMatrix = newTransform;
@ -157,30 +158,20 @@ namespace Avalonia.Rendering.Composition.Server
EffectiveOpacity = Opacity * (Parent?.EffectiveOpacity ?? 1);
IsVisibleInFrame = Visible && EffectiveOpacity > 0.04 && !_isBackface &&
IsVisibleInFrame = _parent?.IsVisibleInFrame != false && Visible && EffectiveOpacity > 0.04 && !_isBackface &&
!_combinedTransformedClipBounds.IsEmpty;
if (wasVisible != IsVisibleInFrame)
_isDirtyForUpdate = true;
// Invalidate previous rect and queue new rect based on visibility
if (positionChanged)
{
if (wasVisible)
dirtyOldBounds = true;
if (IsVisibleInFrame)
_isDirtyForUpdate = true;
if (wasVisible != IsVisibleInFrame || positionChanged)
{
invalidateOldBounds |= wasVisible;
invalidateNewBounds |= IsVisibleInFrame;
}
// Invalidate new bounds
if (IsVisibleInFrame && _isDirtyForUpdate)
{
dirtyOldBounds = true;
if (invalidateNewBounds)
AddDirtyRect(TransformedOwnContentBounds.Intersect(_combinedTransformedClipBounds));
}
if (dirtyOldBounds && wasVisible)
if (invalidateOldBounds)
AddDirtyRect(oldTransformedContentBounds.Intersect(oldCombinedTransformedClipBounds));
@ -190,7 +181,7 @@ namespace Avalonia.Rendering.Composition.Server
var i = Root!.Readback;
ref var readback = ref GetReadback(i.WriteIndex);
readback.Revision = root.Revision;
readback.Matrix = CombinedTransformMatrix;
readback.Matrix = GlobalTransformMatrix;
readback.TargetId = Root.Id;
readback.Visible = IsVisibleInFrame;
}

10
src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs

@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.ConstrainedExecution;
using System.Runtime.InteropServices;
using Avalonia.Platform;
using Avalonia.Threading;
namespace Avalonia.Rendering.Composition.Transport;
@ -17,6 +17,7 @@ internal abstract class BatchStreamPoolBase<T> : IDisposable
int _usage;
readonly int[] _usageStatistics = new int[10];
int _usageStatisticsSlot;
bool _reclaimImmediately;
public int CurrentUsage => _usage;
public int CurrentPool => _pool.Count;
@ -27,7 +28,10 @@ internal abstract class BatchStreamPoolBase<T> : IDisposable
GC.SuppressFinalize(needsFinalize);
var updateRef = new WeakReference<BatchStreamPoolBase<T>>(this);
StartUpdateTimer(startTimer, updateRef);
if (AvaloniaLocator.Current.GetService<IPlatformThreadingInterface>() == null)
_reclaimImmediately = true;
else
StartUpdateTimer(startTimer, updateRef);
}
static void StartUpdateTimer(Action<Func<bool>>? startTimer, WeakReference<BatchStreamPoolBase<T>> updateRef)
@ -90,7 +94,7 @@ internal abstract class BatchStreamPoolBase<T> : IDisposable
lock (_pool)
{
_usage--;
if (!_disposed)
if (!_disposed && !_reclaimImmediately)
{
_pool.Push(item);
return;

2
src/Avalonia.Base/Rendering/Composition/Visual.cs

@ -31,7 +31,7 @@ namespace Avalonia.Rendering.Composition
}
}
internal Matrix4x4? TryGetServerTransform()
internal Matrix4x4? TryGetServerGlobalTransform()
{
if (Root == null)
return null;

40
src/Avalonia.Controls/ItemsControl.cs

@ -523,25 +523,47 @@ namespace Avalonia.Controls
IInputElement? from,
bool wrap)
{
IInputElement? result;
var c = from;
var current = from;
do
for (;;)
{
result = container.GetControl(direction, c, wrap);
var result = container.GetControl(direction, current, wrap);
if (result != null &&
result.Focusable &&
if (result is null)
{
return null;
}
if (result.Focusable &&
result.IsEffectivelyEnabled &&
result.IsEffectivelyVisible)
{
return result;
}
c = result;
} while (c != null && c != from && direction != NavigationDirection.First && direction != NavigationDirection.Last);
current = result;
return null;
if (current == from)
{
return null;
}
switch (direction)
{
//We did not find an enabled first item. Move downwards until we find one.
case NavigationDirection.First:
direction = NavigationDirection.Down;
from = result;
break;
//We did not find an enabled last item. Move upwards until we find one.
case NavigationDirection.Last:
direction = NavigationDirection.Up;
from = result;
break;
}
}
}
private void PresenterChildIndexChanged(object? sender, ChildIndexChangedEventArgs e)

10
src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs

@ -53,7 +53,7 @@ namespace Avalonia.Controls.Platform
Menu.PointerPressed += PointerPressed;
Menu.PointerReleased += PointerReleased;
Menu.AddHandler(AccessKeyHandler.AccessKeyPressedEvent, AccessKeyPressed);
Menu.AddHandler(Avalonia.Controls.Menu.MenuOpenedEvent, MenuOpened);
Menu.AddHandler(MenuBase.MenuOpenedEvent, MenuOpened);
Menu.AddHandler(MenuItem.PointerEnteredItemEvent, PointerEntered);
Menu.AddHandler(MenuItem.PointerExitedItemEvent, PointerExited);
Menu.AddHandler(InputElement.PointerMovedEvent, PointerMoved);
@ -89,7 +89,7 @@ namespace Avalonia.Controls.Platform
Menu.PointerPressed -= PointerPressed;
Menu.PointerReleased -= PointerReleased;
Menu.RemoveHandler(AccessKeyHandler.AccessKeyPressedEvent, AccessKeyPressed);
Menu.RemoveHandler(Avalonia.Controls.Menu.MenuOpenedEvent, MenuOpened);
Menu.RemoveHandler(MenuBase.MenuOpenedEvent, MenuOpened);
Menu.RemoveHandler(MenuItem.PointerEnteredItemEvent, PointerEntered);
Menu.RemoveHandler(MenuItem.PointerExitedItemEvent, PointerExited);
Menu.RemoveHandler(InputElement.PointerMovedEvent, PointerMoved);
@ -175,7 +175,11 @@ namespace Avalonia.Controls.Platform
case Key.Left:
{
if (item?.Parent is IMenuItem parent && !parent.IsTopLevel && parent.IsSubMenuOpen)
if (item is { IsSubMenuOpen: true, SelectedItem: null })
{
item.Close();
}
else if (item?.Parent is IMenuItem { IsTopLevel: false, IsSubMenuOpen: true } parent)
{
parent.Close();
parent.Focus();

8
src/Avalonia.Controls/TrayIcon.cs

@ -61,6 +61,10 @@ namespace Avalonia.Controls
args.NewValue.Value.CollectionChanged += Icons_CollectionChanged;
}
}
else
{
throw new InvalidOperationException("TrayIcon.Icons must be set on the Application.");
}
});
var app = Application.Current ?? throw new InvalidOperationException("Application not yet initialized.");
@ -123,9 +127,9 @@ namespace Avalonia.Controls
public static readonly StyledProperty<bool> IsVisibleProperty =
Visual.IsVisibleProperty.AddOwner<TrayIcon>();
public static void SetIcons(AvaloniaObject o, TrayIcons trayIcons) => o.SetValue(IconsProperty, trayIcons);
public static void SetIcons(Application o, TrayIcons trayIcons) => o.SetValue(IconsProperty, trayIcons);
public static TrayIcons GetIcons(AvaloniaObject o) => o.GetValue(IconsProperty);
public static TrayIcons GetIcons(Application o) => o.GetValue(IconsProperty);
/// <summary>
/// Gets or sets the <see cref="Command"/> property of a TrayIcon.

98
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json

@ -10,6 +10,55 @@
"integrity": "sha512-ws57AidsDvREKrZKYffXddNkyaF14iHNHm8VQnZH6t99E8gczjNN0GpvcGny0imC80yQ0tHz1xVUKk/KFQSUyA==",
"dev": true
},
"@jridgewell/gen-mapping": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
"integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
"dev": true,
"requires": {
"@jridgewell/set-array": "^1.0.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.9"
}
},
"@jridgewell/resolve-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
"dev": true
},
"@jridgewell/set-array": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
"dev": true
},
"@jridgewell/source-map": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz",
"integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==",
"dev": true,
"requires": {
"@jridgewell/gen-mapping": "^0.3.0",
"@jridgewell/trace-mapping": "^0.3.9"
}
},
"@jridgewell/sourcemap-codec": {
"version": "1.4.14",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
"dev": true
},
"@jridgewell/trace-mapping": {
"version": "0.3.14",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz",
"integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==",
"dev": true,
"requires": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"@types/eslint": {
"version": "8.4.1",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.1.tgz",
@ -2136,6 +2185,12 @@
"integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==",
"dev": true
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
},
"source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
@ -2153,6 +2208,16 @@
"source-map-js": "^1.0.1"
}
},
"source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"dev": true,
"requires": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
}
},
"string-template": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz",
@ -2208,13 +2273,14 @@
"dev": true
},
"terser": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz",
"integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==",
"version": "5.14.2",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz",
"integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==",
"dev": true,
"requires": {
"@jridgewell/source-map": "^0.3.2",
"acorn": "^8.5.0",
"commander": "^2.20.0",
"source-map": "~0.7.2",
"source-map-support": "~0.5.20"
},
"dependencies": {
@ -2223,30 +2289,6 @@
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
},
"source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
"dev": true
},
"source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"dev": true,
"requires": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
}
}
},

3
src/Avalonia.FreeDesktop/DBusHelper.cs

@ -24,8 +24,7 @@ namespace Avalonia.FreeDesktop
if (_ctx is not null)
_ctx?.Post(d, state);
else
lock (_lock)
d(state);
d(state);
}
}

33
src/Avalonia.FreeDesktop/DBusSystemDialog.cs

@ -15,27 +15,26 @@ namespace Avalonia.FreeDesktop
{
internal class DBusSystemDialog : BclStorageProvider
{
private static readonly Lazy<IFileChooser?> s_fileChooser = new(() =>
private static readonly Lazy<IFileChooser?> s_fileChooser = new(() => DBusHelper.Connection?
.CreateProxy<IFileChooser>("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"));
internal static async Task<IStorageProvider?> TryCreate(IPlatformHandle handle)
{
var fileChooser = DBusHelper.Connection?.CreateProxy<IFileChooser>("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop");
if (fileChooser is null)
return null;
try
if (handle.HandleDescriptor == "XID" && s_fileChooser.Value is { } fileChooser)
{
_ = fileChooser.GetVersionAsync();
return fileChooser;
}
catch (Exception e)
{
Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform)?.Log(null, $"Unable to connect to org.freedesktop.portal.Desktop: {e.Message}");
return null;
try
{
await fileChooser.GetVersionAsync();
return new DBusSystemDialog(fileChooser, handle);
}
catch (Exception e)
{
Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform)?.Log(null, $"Unable to connect to org.freedesktop.portal.Desktop: {e.Message}");
return null;
}
}
});
internal static DBusSystemDialog? TryCreate(IPlatformHandle handle)
{
return handle.HandleDescriptor == "XID" && s_fileChooser.Value is { } fileChooser
? new DBusSystemDialog(fileChooser, handle) : null;
return null;
}
private readonly IFileChooser _fileChooser;

1
src/Avalonia.Themes.Default/Controls/NativeMenuBar.xaml

@ -14,6 +14,7 @@
<Menu.Styles>
<Style Selector="MenuItem">
<Setter Property="Header" Value="{Binding Header}"/>
<Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>
<Setter Property="InputGesture" Value="{Binding Gesture}"/>
<Setter Property="Items" Value="{Binding Menu.Items}"/>
<Setter Property="Command" Value="{Binding Command}"/>

1
src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml

@ -15,6 +15,7 @@
<Menu.Styles>
<Style x:CompileBindings="False" Selector="MenuItem">
<Setter Property="Header" Value="{Binding Header}"/>
<Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>
<Setter Property="InputGesture" Value="{Binding Gesture}"/>
<Setter Property="Items" Value="{Binding Menu.Items}"/>
<Setter Property="Command" Value="{Binding Command}"/>

64
src/Avalonia.X11/NativeDialogs/CompositeStorageProvider.cs

@ -0,0 +1,64 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Platform.Storage;
namespace Avalonia.X11.NativeDialogs;
internal class CompositeStorageProvider : IStorageProvider
{
private readonly IEnumerable<Func<Task<IStorageProvider?>>> _factories;
public CompositeStorageProvider(IEnumerable<Func<Task<IStorageProvider?>>> factories)
{
_factories = factories;
}
public bool CanOpen => true;
public bool CanSave => true;
public bool CanPickFolder => true;
private async Task<IStorageProvider> EnsureStorageProvider()
{
foreach (var factory in _factories)
{
var provider = await factory();
if (provider is not null)
{
return provider;
}
}
throw new InvalidOperationException("Neither DBus nor GTK are available on the system");
}
public async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
var provider = await EnsureStorageProvider().ConfigureAwait(false);
return await provider.OpenFilePickerAsync(options).ConfigureAwait(false);
}
public async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
var provider = await EnsureStorageProvider().ConfigureAwait(false);
return await provider.SaveFilePickerAsync(options).ConfigureAwait(false);
}
public async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
{
var provider = await EnsureStorageProvider().ConfigureAwait(false);
return await provider.OpenFolderPickerAsync(options).ConfigureAwait(false);
}
public async Task<IStorageBookmarkFile?> OpenFileBookmarkAsync(string bookmark)
{
var provider = await EnsureStorageProvider().ConfigureAwait(false);
return await provider.OpenFileBookmarkAsync(bookmark).ConfigureAwait(false);
}
public async Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark)
{
var provider = await EnsureStorageProvider().ConfigureAwait(false);
return await provider.OpenFolderBookmarkAsync(bookmark).ConfigureAwait(false);
}
}

2
src/Avalonia.X11/NativeDialogs/Gtk.cs

@ -264,8 +264,6 @@ namespace Avalonia.X11.NativeDialogs
public static Task<bool> StartGtk()
{
return StartGtkCore();
lock (s_startGtkLock)
return s_startGtkTask ??= StartGtkCore();
}
private static void GtkThread(TaskCompletionSource<bool> tcs)

30
src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs

@ -17,10 +17,10 @@ namespace Avalonia.X11.NativeDialogs
{
internal class GtkSystemDialog : BclStorageProvider
{
private Task<bool>? _initialized;
private static Task<bool>? _initialized;
private readonly X11Window _window;
public GtkSystemDialog(X11Window window)
private GtkSystemDialog(X11Window window)
{
_window = window;
}
@ -31,10 +31,15 @@ namespace Avalonia.X11.NativeDialogs
public override bool CanPickFolder => true;
public override async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
internal static async Task<IStorageProvider?> TryCreate(X11Window window)
{
await EnsureInitialized();
_initialized ??= StartGtk();
return await _initialized ? new GtkSystemDialog(window) : null;
}
public override async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
return await await RunOnGlibThread(async () =>
{
var res = await ShowDialog(options.Title, _window, GtkFileChooserAction.Open,
@ -46,8 +51,6 @@ namespace Avalonia.X11.NativeDialogs
public override async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
{
await EnsureInitialized();
return await await RunOnGlibThread(async () =>
{
var res = await ShowDialog(options.Title, _window, GtkFileChooserAction.SelectFolder,
@ -59,8 +62,6 @@ namespace Avalonia.X11.NativeDialogs
public override async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
await EnsureInitialized();
return await await RunOnGlibThread(async () =>
{
var res = await ShowDialog(options.Title, _window, GtkFileChooserAction.Save,
@ -225,19 +226,6 @@ namespace Avalonia.X11.NativeDialogs
return tcs.Task;
}
private async Task EnsureInitialized()
{
if (_initialized == null)
{
_initialized = StartGtk();
}
if (!(await _initialized))
{
throw new Exception("Unable to initialize GTK on separate thread");
}
}
private static void UpdateParent(IntPtr chooser, IWindowImpl parentWindow)
{
var xid = parentWindow.Handle.Handle;

8
src/Avalonia.X11/X11Platform.cs

@ -216,16 +216,16 @@ namespace Avalonia
public bool OverlayPopups { get; set; }
/// <summary>
/// Enables native file dialogs as well as global menu support on Linux desktop environments where it's supported (e. g. XFCE and MATE with plugin, KDE, etc).
/// Enables global menu support on Linux desktop environments where it's supported (e. g. XFCE and MATE with plugin, KDE, etc).
/// The default value is true.
/// </summary>
public bool UseDBusMenu { get; set; } = true;
/// <summary>
/// Enables GTK file picker instead of default FreeDesktop.
/// The default value is true. And FreeDesktop file picker is used instead if available.
/// Enables DBus file picker instead of GTK.
/// The default value is true.
/// </summary>
public bool UseGtkFilePicker { get; set; } = false;
public bool UseDBusFilePicker { get; set; } = true;
/// <summary>
/// Deferred renderer would be used when set to true. Immediate renderer when set to false. The default value is true.

9
src/Avalonia.X11/X11Window.cs

@ -22,6 +22,7 @@ using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Avalonia.Threading;
using Avalonia.X11.Glx;
using Avalonia.X11.NativeDialogs;
using static Avalonia.X11.XLib;
// ReSharper disable IdentifierTypo
// ReSharper disable StringLiteralTypo
@ -215,9 +216,11 @@ namespace Avalonia.X11
_x11.Atoms.XA_CARDINAL, 32, PropertyMode.Replace, ref _xSyncCounter, 1);
}
var canUseFreeDekstopPicker = !platform.Options.UseGtkFilePicker && platform.Options.UseDBusMenu;
StorageProvider = canUseFreeDekstopPicker && DBusSystemDialog.TryCreate(Handle) is {} dBusStorage
? dBusStorage : new NativeDialogs.GtkSystemDialog(this);
StorageProvider = new CompositeStorageProvider(new Func<Task<IStorageProvider>>[]
{
() => _platform.Options.UseDBusFilePicker ? DBusSystemDialog.TryCreate(Handle) : Task.FromResult<IStorageProvider>(null),
() => GtkSystemDialog.TryCreate(this),
});
}
class SurfaceInfo : EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo

20
src/Windows/Avalonia.Win32/IconImpl.cs

@ -1,6 +1,5 @@
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using Avalonia.Platform;
@ -8,31 +7,18 @@ namespace Avalonia.Win32
{
class IconImpl : IWindowIconImpl
{
private Bitmap bitmap;
private Icon icon;
public IconImpl(Bitmap bitmap)
{
this.bitmap = bitmap;
}
private readonly Icon icon;
public IconImpl(Icon icon)
{
this.icon = icon;
}
public IntPtr HIcon => icon?.Handle ?? bitmap.GetHicon();
public IntPtr HIcon => icon.Handle;
public void Save(Stream outputStream)
{
if (icon != null)
{
icon.Save(outputStream);
}
else
{
bitmap.Save(outputStream, ImageFormat.Png);
}
icon.Save(outputStream);
}
}
}

4
src/Windows/Avalonia.Win32/TrayIconImpl.cs

@ -18,6 +18,8 @@ namespace Avalonia.Win32
[Unstable]
public class TrayIconImpl : ITrayIconImpl
{
private static readonly IntPtr s_emptyIcon = new System.Drawing.Bitmap(32, 32).GetHicon();
private readonly int _uniqueId;
private static int s_nextUniqueId;
private bool _iconAdded;
@ -86,7 +88,7 @@ namespace Avalonia.Win32
uID = _uniqueId,
uFlags = NIF.TIP | NIF.MESSAGE,
uCallbackMessage = (int)CustomWindowsMessage.WM_TRAYMOUSE,
hIcon = _icon?.HIcon ?? new IconImpl(new System.Drawing.Bitmap(32, 32)).HIcon,
hIcon = _icon?.HIcon ?? s_emptyIcon,
szTip = _tooltipText ?? ""
};

29
src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs

@ -1,6 +1,10 @@
using System.Collections.Generic;
using System.Reactive.Linq;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Media.Imaging;
using Avalonia.Utilities;
#nullable enable
@ -15,17 +19,28 @@ namespace Avalonia.Win32
_nativeMenu = nativeMenu;
}
private IEnumerable<MenuItem> Populate(NativeMenu nativeMenu)
private AvaloniaList<MenuItem> Populate(NativeMenu nativeMenu)
{
var result = new AvaloniaList<MenuItem>();
foreach (var menuItem in nativeMenu.Items)
{
if (menuItem is NativeMenuItemSeparator)
{
yield return new MenuItem { Header = "-" };
result.Add(new MenuItem { Header = "-" });
}
else if (menuItem is NativeMenuItem item)
{
var newItem = new MenuItem { Header = item.Header, Icon = item.Icon, Command = item.Command, CommandParameter = item.CommandParameter };
var newItem = new MenuItem
{
[!MenuItem.HeaderProperty] = item.GetObservable(NativeMenuItem.HeaderProperty).ToBinding(),
[!MenuItem.IconProperty] = item.GetObservable(NativeMenuItem.IconProperty)
.Select(i => i is {} bitmap ? new Image { Source = bitmap } : null).ToBinding(),
[!MenuItem.IsEnabledProperty] = item.GetObservable(NativeMenuItem.IsEnabledProperty).ToBinding(),
[!MenuItem.CommandProperty] = item.GetObservable(NativeMenuItem.CommandProperty).ToBinding(),
[!MenuItem.CommandParameterProperty] = item.GetObservable(NativeMenuItem.CommandParameterProperty).ToBinding(),
[!MenuItem.InputGestureProperty] = item.GetObservable(NativeMenuItem.GestureProperty).ToBinding()
};
if (item.Menu != null)
{
@ -33,15 +48,17 @@ namespace Avalonia.Win32
}
else if (item.HasClickHandlers && item is INativeMenuItemExporterEventsImplBridge bridge)
{
newItem.Click += (_, __) => bridge.RaiseClicked();
newItem.Click += (_, _) => bridge.RaiseClicked();
}
yield return newItem;
result.Add(newItem);
}
}
return result;
}
public IEnumerable<MenuItem>? GetMenu()
public AvaloniaList<MenuItem>? GetMenu()
{
if (_nativeMenu != null)
{

8
src/Windows/Avalonia.Win32/Win32Platform.cs

@ -359,7 +359,7 @@ namespace Avalonia.Win32
using (var memoryStream = new MemoryStream())
{
bitmap.Save(memoryStream);
return new IconImpl(new System.Drawing.Bitmap(memoryStream));
return CreateIconImpl(memoryStream);
}
}
@ -367,11 +367,15 @@ namespace Avalonia.Win32
{
try
{
// new Icon() will work only if stream is an "ico" file.
return new IconImpl(new System.Drawing.Icon(stream));
}
catch (ArgumentException)
{
return new IconImpl(new System.Drawing.Bitmap(stream));
// Fallback to Bitmap creation and converting into a windows icon.
using var icon = new System.Drawing.Bitmap(stream);
var hIcon = icon.GetHicon();
return new IconImpl(System.Drawing.Icon.FromHandle(hIcon));
}
}

417
tests/Avalonia.Base.UnitTests/Rendering/CompositorHitTestingTests.cs

@ -0,0 +1,417 @@
using System;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Shapes;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Moq;
using Xunit;
namespace Avalonia.Base.UnitTests.Rendering;
public class CompositorHitTestingTests : CompositorTestsBase
{
[Fact]
public void HitTest_Should_Find_Controls_At_Point()
{
using (var s = new CompositorServices(new Size(200, 200)))
{
var border = new Border
{
Width = 100,
Height = 100,
Background = Brushes.Red,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
};
s.TopLevel.Content = border;
s.AssertHitTest(new Point(100, 100), null, border);
}
}
[Fact]
public void HitTest_Should_Not_Find_Empty_Controls_At_Point()
{
using (var s = new CompositorServices(new Size(200, 200)))
{
var border = new Border
{
Width = 100,
Height = 100,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
};
s.TopLevel.Content = border;
s.AssertHitTest(new Point(100, 100), null);
}
}
[Fact]
public void HitTest_Should_Not_Find_Invisible_Controls_At_Point()
{
using (var s = new CompositorServices(new Size(200, 200)))
{
Border visible, border;
s.TopLevel.Content = border = new Border
{
Width = 100,
Height = 100,
Background = Brushes.Red,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
IsVisible = false,
Child = visible = new Border
{
Background = Brushes.Red,
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
}
};
s.AssertHitTest(new Point(100, 100), null);
}
}
[Fact]
public void HitTest_Should_Not_Find_Control_Outside_Point()
{
using (var s = new CompositorServices(new Size(200, 200)))
{
var border = new Border
{
Width = 100,
Height = 100,
Background = Brushes.Red,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
};
s.TopLevel.Content = border;
s.AssertHitTest(new Point(10, 10), null);
}
}
[Fact]
public void HitTest_Should_Return_Top_Controls_First()
{
using (var s = new CompositorServices(new Size(200, 200)))
{
Panel container = new Panel
{
Width = 200,
Height = 200,
Children =
{
new Border
{
Width = 100,
Height = 100,
Background = Brushes.Red,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
},
new Border
{
Width = 50,
Height = 50,
Background = Brushes.Blue,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
}
}
};
s.TopLevel.Content = container;
s.AssertHitTest(new Point(100, 100), null, container.Children[1], container.Children[0]);
}
}
[Fact]
public void HitTest_Should_Return_Top_Controls_First_With_ZIndex()
{
using (var s = new CompositorServices(new Size(200, 200)))
{
Panel container = new Panel
{
Width = 200,
Height = 200,
Children =
{
new Border
{
Width = 100,
Height = 100,
ZIndex = 1,
Background = Brushes.Red,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
},
new Border
{
Width = 50,
Height = 50,
Background = Brushes.Red,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
},
new Border
{
Width = 75,
Height = 75,
ZIndex = 2,
Background = Brushes.Red,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
}
}
};
s.TopLevel.Content = container;
s.AssertHitTest(new Point(100, 100), null, new[] { container.Children[2], container.Children[0], container.Children[1] });
}
}
[Fact]
public void HitTest_Should_Find_Control_Translated_Outside_Parent_Bounds()
{
using (var s = new CompositorServices(new Size(200, 200)))
{
Border target;
Panel container = new Panel
{
Width = 200,
Height = 200,
Background = Brushes.Red,
ClipToBounds = false,
Children =
{
new Border
{
Width = 100,
Height = 100,
ZIndex = 1,
Background = Brushes.Red,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
Child = target = new Border
{
Width = 50,
Height = 50,
Background = Brushes.Red,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
RenderTransform = new TranslateTransform(110, 110),
}
},
}
};
s.TopLevel.Content = container;
s.AssertHitTest(new Point(120, 120), null, target, container);
}
}
[Fact]
public void HitTest_Should_Not_Find_Control_Outside_Parent_Bounds_When_Clipped()
{
using (var s = new CompositorServices(new Size(200, 200)))
{
Border target;
Panel container = new Panel
{
Width = 100,
Height = 200,
Background = Brushes.Red,
Children =
{
new Panel()
{
Width = 100,
Height = 100,
Background = Brushes.Red,
Margin = new Thickness(0, 100, 0, 0),
ClipToBounds = true,
Children =
{
(target = new Border()
{
Width = 100,
Height = 100,
Background = Brushes.Red,
Margin = new Thickness(0, -100, 0, 0)
})
}
}
}
};
s.TopLevel.Content = container;
s.AssertHitTest(new Point(50, 50), null, container);
}
}
[Fact]
public void HitTest_Should_Not_Find_Control_Outside_Scroll_Viewport()
{
using (var s = new CompositorServices(new Size(100, 200)))
{
Border target;
Border item1;
Border item2;
ScrollContentPresenter scroll;
Panel container = new Panel
{
Width = 100,
Height = 200,
Background = Brushes.Red,
Children =
{
(target = new Border()
{
Name = "b1",
Width = 100,
Height = 100,
Background = Brushes.Red,
}),
new Border()
{
Name = "b2",
Width = 100,
Height = 100,
Background = Brushes.Red,
Margin = new Thickness(0, 100, 0, 0),
Child = scroll = new ScrollContentPresenter()
{
CanHorizontallyScroll = true,
CanVerticallyScroll = true,
Content = new StackPanel()
{
Children =
{
(item1 = new Border()
{
Name = "b3",
Width = 100,
Height = 100,
Background = Brushes.Red,
}),
(item2 = new Border()
{
Name = "b4",
Width = 100,
Height = 100,
Background = Brushes.Red,
}),
}
}
}
}
}
};
s.TopLevel.Content = container;
scroll.UpdateChild();
s.AssertHitTestFirst(new Point(50, 150), null, item1);
s.AssertHitTestFirst(new Point(50,50), null, target);
scroll.Offset = new Vector(0, 100);
s.AssertHitTestFirst(new Point(50, 150), null, item2);
s.AssertHitTestFirst(new Point(50,50), null, target);
}
}
[Fact]
public void HitTest_Should_Not_Find_Path_When_Outside_Fill()
{
using (var s = new CompositorServices(new Size(200, 200)))
{
Path path = new Path
{
Width = 200,
Height = 200,
Fill = Brushes.Red,
Data = StreamGeometry.Parse("M100,0 L0,100 100,100")
};
s.TopLevel.Content = path;
s.AssertHitTest(new Point(100, 100), null, path);
s.AssertHitTest(new Point(10, 10), null);
}
}
[Fact]
public void HitTest_Should_Respect_Geometry_Clip()
{
using (var s = new CompositorServices(new Size(400, 400)))
{
Canvas canvas;
Border border = new Border
{
Background = Brushes.Red,
Clip = StreamGeometry.Parse("M100,0 L0,100 100,100"),
Width = 200,
Height = 200,
Child = canvas = new Canvas
{
Background = Brushes.Yellow,
Margin = new Thickness(10),
}
};
s.TopLevel.Content = border;
s.RunJobs();
Assert.Equal(new Rect(100, 100, 200, 200), border.Bounds);
s.AssertHitTest(new Point(200,200), null, canvas, border);
s.AssertHitTest(new Point(110, 110), null);
}
}
[Fact]
public void HitTest_Should_Accommodate_ICustomHitTest()
{
using (var s = new CompositorServices(new Size(300, 200)))
{
Border border = new CustomHitTestBorder
{
ClipToBounds = false,
Width = 100,
Height = 100,
Background = Brushes.Red,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
};
s.TopLevel.Content = border;
s.AssertHitTest(75, 100, null, border);
s.AssertHitTest(125, 100, null, border);
s.AssertHitTest(175, 100, null);
}
}
private IDisposable TestApplication()
{
return UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
}
}

109
tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationTests.cs

@ -0,0 +1,109 @@
using Avalonia.Controls;
using Avalonia.Media;
using Xunit;
namespace Avalonia.Base.UnitTests.Rendering;
public class CompositorInvalidationTests : CompositorTestsBase
{
[Fact]
public void Control_Should_Invalidate_Own_Rect_When_Added()
{
using (var s = new CompositorCanvas())
{
var control = new Border()
{
Background = Brushes.Red, Width = 20, Height = 10,
[Canvas.LeftProperty] = 30, [Canvas.TopProperty] = 50
};
s.Canvas.Children.Add(control);
s.AssertRects(new Rect(30, 50, 20, 10));
}
}
[Fact]
public void Control_Should_Invalidate_Own_Rect_When_Removed()
{
using (var s = new CompositorCanvas())
{
var control = new Border()
{
Background = Brushes.Red, Width = 20, Height = 10,
[Canvas.LeftProperty] = 30, [Canvas.TopProperty] = 50
};
s.Canvas.Children.Add(control);
s.RunJobs();
s.Events.Rects.Clear();
s.Canvas.Children.Remove(control);
s.AssertRects(new Rect(30, 50, 20, 10));
}
}
[Fact]
public void Control_Should_Invalidate_Both_Own_Rects_When_Moved()
{
using (var s = new CompositorCanvas())
{
var control = new Border()
{
Background = Brushes.Red, Width = 20, Height = 10,
[Canvas.LeftProperty] = 30, [Canvas.TopProperty] = 50
};
s.Canvas.Children.Add(control);
s.RunJobs();
s.Events.Rects.Clear();
control[Canvas.LeftProperty] = 55;
s.AssertRects(new Rect(30, 50, 20, 10),
new Rect(55, 50, 20, 10)
);
}
}
[Fact]
public void Control_Should_Invalidate_Child_Rects_When_Moved()
{
using (var s = new CompositorCanvas())
{
var control = new Decorator()
{
[Canvas.LeftProperty] = 30, [Canvas.TopProperty] = 50,
Padding = new Thickness(10),
Child = new Border()
{
Width = 20, Height = 10,
Background = Brushes.Red
}
};
s.Canvas.Children.Add(control);
s.RunJobs();
s.Events.Rects.Clear();
control[Canvas.LeftProperty] = 55;
s.AssertRects(new Rect(40, 60, 20, 10),
new Rect(65, 60, 20, 10)
);
}
}
[Fact]
public void Control_Should_Invalidate_Child_Rects_When_Becomes_Invisible()
{
using (var s = new CompositorCanvas())
{
var control = new Decorator()
{
[Canvas.LeftProperty] = 30, [Canvas.TopProperty] = 50,
Padding = new Thickness(10),
Child = new Border()
{
Width = 20, Height = 10,
Background = Brushes.Red
}
};
s.Canvas.Children.Add(control);
s.RunJobs();
s.Events.Rects.Clear();
control.IsVisible = false;
s.AssertRects(new Rect(40, 60, 20, 10));
}
}
}

208
tests/Avalonia.Base.UnitTests/Rendering/CompositorTestsBase.cs

@ -0,0 +1,208 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reactive.Disposables;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Embedding;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Avalonia.Threading;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using JetBrains.Annotations;
using Xunit;
namespace Avalonia.Base.UnitTests.Rendering;
public class CompositorTestsBase
{
public class DebugEvents : ICompositionTargetDebugEvents
{
public List<Rect> Rects = new();
public void RectInvalidated(Rect rc)
{
Rects.Add(rc);
}
public void Reset()
{
Rects.Clear();
}
}
public class ManualRenderTimer : IRenderTimer
{
public event Action<TimeSpan> Tick;
public bool RunsInBackground => false;
public void TriggerTick() => Tick?.Invoke(TimeSpan.Zero);
public Task TriggerBackgroundTick() => Task.Run(TriggerTick);
}
class TopLevelImpl : ITopLevelImpl
{
private readonly Compositor _compositor;
public CompositingRenderer Renderer { get; private set; }
public TopLevelImpl(Compositor compositor, Size clientSize)
{
ClientSize = clientSize;
_compositor = compositor;
}
public void Dispose()
{
}
public Size ClientSize { get; }
public Size? FrameSize { get; }
public double RenderScaling => 1;
public IEnumerable<object> Surfaces { get; } = Array.Empty<object>();
public Action<RawInputEventArgs> Input { get; set; }
public Action<Rect> Paint { get; set; }
public Action<Size, PlatformResizeReason> Resized { get; set; }
public Action<double> ScalingChanged { get; set; }
public Action<WindowTransparencyLevel> TransparencyLevelChanged { get; set; }
public IRenderer CreateRenderer(IRenderRoot root)
{
return Renderer = new CompositingRenderer(root, _compositor);
}
public void Invalidate(Rect rect)
{
}
public void SetInputRoot(IInputRoot inputRoot)
{
}
public Point PointToClient(PixelPoint point) => default;
public PixelPoint PointToScreen(Point point) => new();
public void SetCursor(ICursorImpl cursor)
{
}
public Action Closed { get; set; }
public Action LostFocus { get; set; }
public IMouseDevice MouseDevice { get; } = new MouseDevice();
public IPopupImpl CreatePopup() => throw new NotImplementedException();
public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel)
{
}
public WindowTransparencyLevel TransparencyLevel { get; }
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; }
}
protected class CompositorServices : IDisposable
{
private readonly IDisposable _app;
public Compositor Compositor { get; }
public ManualRenderTimer Timer { get; } = new();
public EmbeddableControlRoot TopLevel { get; }
public CompositingRenderer Renderer { get; } = null!;
public DebugEvents Events { get; } = new();
public void Dispose()
{
TopLevel.Renderer.Stop();
TopLevel.Dispose();
_app.Dispose();
}
public CompositorServices(Size? size = null)
{
_app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
try
{
AvaloniaLocator.CurrentMutable.Bind<IRenderTimer>().ToConstant(Timer);
AvaloniaLocator.CurrentMutable.Bind<IRenderLoop>()
.ToConstant(new RenderLoop(Timer, Dispatcher.UIThread));
Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService<IRenderLoop>(), null);
var impl = new TopLevelImpl(Compositor, size ?? new Size(1000, 1000));
TopLevel = new EmbeddableControlRoot(impl)
{
Template = new FuncControlTemplate((parent, scope) =>
{
var presenter = new ContentPresenter
{
[~ContentPresenter.ContentProperty] = new TemplateBinding(ContentControl.ContentProperty)
};
scope.Register("PART_ContentPresenter", presenter);
return presenter;
})
};
Renderer = impl.Renderer;
TopLevel.Prepare();
TopLevel.Renderer.Start();
RunJobs();
Renderer.CompositionTarget.Server.DebugEvents = Events;
}
catch
{
_app.Dispose();
throw;
}
}
public void RunJobs()
{
Dispatcher.UIThread.RunJobs();
Timer.TriggerTick();
Dispatcher.UIThread.RunJobs();
}
public void AssertRects(params Rect[] rects)
{
RunJobs();
var toAssert = rects.Select(x => x.ToString()).Distinct().OrderBy(x => x);
var invalidated = Events.Rects.Select(x => x.ToString()).Distinct().OrderBy(x => x);
Assert.Equal(toAssert, invalidated);
Events.Rects.Clear();
}
public void AssertHitTest(double x, double y, Func<IVisual, bool> filter, params object[] expected)
=> AssertHitTest(new Point(x, y), filter, expected);
public void AssertHitTest(Point pt, Func<IVisual, bool> filter, params object[] expected)
{
RunJobs();
var tested = Renderer.HitTest(pt, TopLevel, filter);
Assert.Equal(expected, tested);
}
public void AssertHitTestFirst(Point pt, Func<IVisual, bool> filter, object expected)
{
RunJobs();
var tested = Renderer.HitTest(pt, TopLevel, filter).First();
Assert.Equal(expected, tested);
}
}
protected class CompositorCanvas : CompositorServices
{
public Canvas Canvas { get; } = new();
public CompositorCanvas()
{
TopLevel.Content = Canvas;
RunJobs();
Events.Reset();
}
}
}

63
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

@ -1595,8 +1595,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(new[] { "Bar" }, selectedItems);
}
[Fact]
public void MoveSelection_Wrap_Does_Not_Hang_With_No_Focusable_Controls()
[Fact(Timeout = 2000)]
public async Task MoveSelection_Wrap_Does_Not_Hang_With_No_Focusable_Controls()
{
// Issue #3094.
var target = new TestSelector
@ -1612,11 +1612,34 @@ namespace Avalonia.Controls.UnitTests.Primitives
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
target.MoveSelection(NavigationDirection.Next, true);
// Timeout in xUnit doesn't work with synchronous methods so we need to apply hack below.
// https://github.com/xunit/xunit/issues/2222
await Task.Run(() => target.MoveSelection(NavigationDirection.Next, true));
}
[Fact(Timeout = 2000)]
public async Task MoveSelection_Does_Not_Hang_With_No_Focusable_Controls_And_Moving_Selection_To_The_First_Item()
[Fact]
public void MoveSelection_Skips_Non_Focusable_Controls_When_Moving_To_Last_Item()
{
var target = new TestSelector
{
Template = Template(),
Items = new[]
{
new ListBoxItem(),
new ListBoxItem { Focusable = false },
}
};
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
target.MoveSelection(NavigationDirection.Last, true);
Assert.Equal(0, target.SelectedIndex);
}
[Fact]
public void MoveSelection_Skips_Non_Focusable_Controls_When_Moving_To_First_Item()
{
var target = new TestSelector
{
@ -1630,22 +1653,43 @@ namespace Avalonia.Controls.UnitTests.Primitives
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
target.MoveSelection(NavigationDirection.Last, true);
Assert.Equal(1, target.SelectedIndex);
}
// Timeout in xUnit doesen't work with synchronous methods so we need to apply hack below.
[Fact(Timeout = 2000)]
public async Task MoveSelection_Does_Not_Hang_When_All_Items_Are_Non_Focusable_And_We_Move_To_First_Item()
{
var target = new TestSelector
{
Template = Template(),
Items = new[]
{
new ListBoxItem { Focusable = false },
new ListBoxItem { Focusable = false },
}
};
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
// Timeout in xUnit doesn't work with synchronous methods so we need to apply hack below.
// https://github.com/xunit/xunit/issues/2222
await Task.Run(() => target.MoveSelection(NavigationDirection.First, true));
Assert.Equal(-1, target.SelectedIndex);
}
[Fact(Timeout = 2000)]
public async Task MoveSelection_Does_Not_Hang_With_No_Focusable_Controls_And_Moving_Selection_To_The_Last_Item()
public async Task MoveSelection_Does_Not_Hang_When_All_Items_Are_Non_Focusable_And_We_Move_To_Last_Item()
{
var target = new TestSelector
{
Template = Template(),
Items = new[]
{
new ListBoxItem(),
new ListBoxItem { Focusable = false },
new ListBoxItem { Focusable = false },
}
};
@ -1653,9 +1697,10 @@ namespace Avalonia.Controls.UnitTests.Primitives
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
// Timeout in xUnit doesen't work with synchronous methods so we need to apply hack below.
// Timeout in xUnit doesn't work with synchronous methods so we need to apply hack below.
// https://github.com/xunit/xunit/issues/2222
await Task.Run(() => target.MoveSelection(NavigationDirection.Last, true));
Assert.Equal(-1, target.SelectedIndex);
}

27
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

@ -4,6 +4,7 @@ using System.IO;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Media.Imaging;
using Avalonia.Rendering;
using Moq;
namespace Avalonia.UnitTests
@ -25,9 +26,33 @@ namespace Avalonia.UnitTests
return Mock.Of<IGeometryImpl>(x => x.Bounds == rect);
}
class MockRenderTarget : IRenderTarget
{
public void Dispose()
{
}
public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
{
var m = new Mock<IDrawingContextImpl>();
m.Setup(c => c.CreateLayer(It.IsAny<Size>()))
.Returns(() =>
{
var r = new Mock<IDrawingContextLayerImpl>();
r.Setup(r => r.CreateDrawingContext(It.IsAny<IVisualBrushRenderer>()))
.Returns(CreateDrawingContext(null));
return r.Object;
}
);
return m.Object;
}
}
public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)
{
return Mock.Of<IRenderTarget>();
return new MockRenderTarget();
}
public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi)

Loading…
Cancel
Save