Browse Source

Merge branch 'master' into x11-focus-proxy

pull/12751/head
Vladyslav Lubenskyi 3 years ago
committed by GitHub
parent
commit
5a921677b9
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      build/ExternalConsumers.props
  2. 2
      readme.md
  3. 25
      samples/ControlCatalog/Pages/NotificationsPage.xaml
  4. 8
      samples/ControlCatalog/Pages/NotificationsPage.xaml.cs
  5. 10
      samples/ControlCatalog/ViewModels/NotificationViewModel.cs
  6. 51
      src/Avalonia.Base/Animation/Transition.cs
  7. 100
      src/Avalonia.Base/Animation/TransitionBase.cs
  8. 20
      src/Avalonia.Base/Media/DrawingContext.cs
  9. 14
      src/Avalonia.Base/Media/DrawingGroup.cs
  10. 24
      src/Avalonia.Base/Media/Geometry.cs
  11. 19
      src/Avalonia.Base/Media/ImmutableGeometry.cs
  12. 4
      src/Avalonia.Base/Media/PlatformDrawingContext.cs
  13. 11
      src/Avalonia.Base/Platform/IDrawingContextImpl.cs
  14. 8
      src/Avalonia.Base/Platform/IGeometryImpl.cs
  15. 15
      src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataNodes.cs
  16. 8
      src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataDrawingContext.cs
  17. 10
      src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs
  18. 149
      src/Avalonia.Base/Utilities/HashCode.cs
  19. 5
      src/Avalonia.Controls/Documents/InlineUIContainer.cs
  20. 3
      src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs
  21. 55
      src/Avalonia.Controls/Notifications/Notification.cs
  22. 101
      src/Avalonia.Controls/Notifications/NotificationCard.cs
  23. 118
      src/Avalonia.Controls/Notifications/WindowNotificationManager.cs
  24. 2
      src/Avalonia.Controls/Primitives/AdornerLayer.cs
  25. 2
      src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
  26. 5
      src/Avalonia.Controls/Primitives/ScrollBar.cs
  27. 16
      src/Avalonia.Controls/TextBlock.cs
  28. 13
      src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj
  29. 7
      src/Avalonia.FreeDesktop/DBusMenuExporter.cs
  30. 6
      src/Avalonia.FreeDesktop/DBusSystemDialog.cs
  31. 0
      src/Avalonia.FreeDesktop/DBusXml/com.canonical.dbusmenu.xml
  32. 0
      src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.DBus.xml
  33. 0
      src/Avalonia.FreeDesktop/DBusXml/org.kde.StatusNotifierItem.xml
  34. 0
      src/Avalonia.FreeDesktop/DBusXml/org.kde.StatusNotifierWatcher.xml
  35. 2
      src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs
  36. 12
      src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  37. 34
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs
  38. 38
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  39. 64
      src/Skia/Avalonia.Skia/GeometryImpl.cs
  40. 28
      src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs
  41. 38
      src/Skia/Avalonia.Skia/Helpers/PenHelper.cs
  42. 68
      src/Skia/Avalonia.Skia/Helpers/SKPathHelper.cs
  43. 14
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  44. 17
      src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs
  45. 42
      src/Windows/Avalonia.Win32/Win32Platform.cs
  46. 26
      src/Windows/Avalonia.Win32/Win32PlatformOptions.cs
  47. 2
      src/Windows/Avalonia.Win32/WindowImpl.cs
  48. 1
      tests/Avalonia.Controls.UnitTests/TextBlockTests.cs
  49. 18
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs
  50. 50
      tests/Avalonia.RenderTests/Shapes/PathTests.cs
  51. BIN
      tests/TestFiles/Direct2D1/Shapes/Path/GetWidenedPathGeometry_Line.expected.png
  52. BIN
      tests/TestFiles/Direct2D1/Shapes/Path/GetWidenedPathGeometry_Line_Dash.expected.png
  53. BIN
      tests/TestFiles/Skia/Shapes/Path/GetWidenedPathGeometry_Line.expected.png
  54. BIN
      tests/TestFiles/Skia/Shapes/Path/GetWidenedPathGeometry_Line_Dash.expected.png

1
build/ExternalConsumers.props

@ -29,5 +29,6 @@
<InternalsVisibleTo Include="PresentationFramework.Classic, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9" />
<InternalsVisibleTo Include="UIAutomationProvider, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9" />
<InternalsVisibleTo Include="PresentationCore, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9" />
<InternalsVisibleTo Include="Avalonia.ForTestingOnly, PublicKey=$(AvaloniaPublicKey)" />
</ItemGroup>
</Project>

2
readme.md

@ -2,7 +2,7 @@
![Header](https://user-images.githubusercontent.com/552074/235865745-2a8e7274-4f66-4f77-8f05-feeb76e7d478.png)
[![Telegram](https://raw.githubusercontent.com/Patrolavia/telegram-badge/master/chat.svg)](https://t.me/Avalonia)
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) [![Discord](https://img.shields.io/badge/discord-join%20chat-46BC99)]( https://aka.ms/dotnet-discord) [![Build Status](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_apis/build/status/AvaloniaUI.Avalonia)](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/latest?definitionId=4) [![Backers on Open Collective](https://opencollective.com/Avalonia/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/Avalonia/sponsors/badge.svg)](#sponsors) ![License](https://img.shields.io/github/license/avaloniaui/avalonia.svg)
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) [![Discord](https://img.shields.io/badge/discord-join%20chat-46BC99)]( https://aka.ms/dotnet-discord) [![Build Status](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_apis/build/status/AvaloniaUI.Avalonia)](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/latest?definitionId=4) [![Backers on Open Collective](https://img.shields.io/opencollective/backers/Avalonia?logo=opencollective)](#backers) [![Sponsors on Open Collective](https://img.shields.io/opencollective/sponsors/Avalonia?logo=opencollective)](#sponsors) [![GitHub Sponsors](https://img.shields.io/github/sponsors/AvaloniaUI?logo=github)](https://github.com/sponsors/AvaloniaUI) ![License](https://img.shields.io/github/license/avaloniaui/avalonia.svg)
<br />
[![NuGet](https://img.shields.io/nuget/v/Avalonia.svg)](https://www.nuget.org/packages/Avalonia) [![downloads](https://img.shields.io/nuget/dt/avalonia)](https://www.nuget.org/packages/Avalonia) ![Size](https://img.shields.io/github/repo-size/avaloniaui/avalonia.svg)

25
samples/ControlCatalog/Pages/NotificationsPage.xaml

@ -1,11 +1,28 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:viewModels="using:ControlCatalog.ViewModels"
xmlns:system="clr-namespace:System;assembly=System.Runtime"
x:Class="ControlCatalog.Pages.NotificationsPage"
x:DataType="viewModels:NotificationViewModel">
<StackPanel Orientation="Vertical" Spacing="4" HorizontalAlignment="Left">
<Button Content="Show Standard Managed Notification" Command="{Binding ShowManagedNotificationCommand}" />
<Button Content="Show Custom Managed Notification" Command="{Binding ShowCustomManagedNotificationCommand}" />
<Button Content="Show Native Notification" Command="{Binding ShowNativeNotificationCommand}" />
<DockPanel>
<TextBlock DockPanel.Dock="Top"
Margin="2" Classes="h2" TextWrapping="Wrap">TopLevel bound notification manager.</TextBlock>
<StackPanel DockPanel.Dock="Top"
Orientation="Vertical" Spacing="4" HorizontalAlignment="Left">
<Button Content="Show Standard Managed Notification" Command="{Binding ShowManagedNotificationCommand}" />
<Button Content="Show Custom Managed Notification" Command="{Binding ShowCustomManagedNotificationCommand}" />
</StackPanel>
<TextBlock DockPanel.Dock="Top"
Margin="2" Classes="h2" TextWrapping="Wrap">XAML only notification manager.</TextBlock>
<Button DockPanel.Dock="Top"
Content="Show XAML only Notification" Command="{Binding #ControlNotifications.Show}">
<Button.CommandParameter>
<Notification Title="Title" Message="Message" OnClick="NotificationOnClick" />
</Button.CommandParameter>
</Button>
<Border Padding="10" BorderBrush="{DynamicResource SystemAccentColor}">
<WindowNotificationManager x:Name="ControlNotifications" />
</Border>
</DockPanel>
</UserControl>

8
samples/ControlCatalog/Pages/NotificationsPage.xaml.cs

@ -1,5 +1,6 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Avalonia.Markup.Xaml;
using ControlCatalog.ViewModels;
@ -27,7 +28,12 @@ namespace ControlCatalog.Pages
{
base.OnAttachedToVisualTree(e);
_viewModel.NotificationManager = new Avalonia.Controls.Notifications.WindowNotificationManager(TopLevel.GetTopLevel(this));
_viewModel.NotificationManager = new WindowNotificationManager(TopLevel.GetTopLevel(this)!);
}
public void NotificationOnClick()
{
this.Get<WindowNotificationManager>("ControlNotifications").Show("Notification clicked");
}
}
}

10
samples/ControlCatalog/ViewModels/NotificationViewModel.cs

@ -11,7 +11,7 @@ namespace ControlCatalog.ViewModels
{
ShowCustomManagedNotificationCommand = MiniCommand.Create(() =>
{
NotificationManager?.Show(new NotificationViewModel() { Title = "Hey There!", Message = "Did you know that Avalonia now supports Custom In-Window Notifications?" , NotificationManager = NotificationManager});
NotificationManager?.Show(new NotificationViewModel() { Title = "Hey There!", Message = "Did you know that Avalonia now supports Custom In-Window Notifications?" , NotificationManager = NotificationManager}, NotificationType.Warning);
});
ShowManagedNotificationCommand = MiniCommand.Create(() =>
@ -19,11 +19,6 @@ namespace ControlCatalog.ViewModels
NotificationManager?.Show(new Avalonia.Controls.Notifications.Notification("Welcome", "Avalonia now supports Notifications.", NotificationType.Information));
});
ShowNativeNotificationCommand = MiniCommand.Create(() =>
{
NotificationManager?.Show(new Avalonia.Controls.Notifications.Notification("Error", "Native Notifications are not quite ready. Coming soon.", NotificationType.Error));
});
YesCommand = MiniCommand.Create(() =>
{
NotificationManager?.Show(new Avalonia.Controls.Notifications.Notification("Avalonia Notifications", "Start adding notifications to your app today."));
@ -45,8 +40,5 @@ namespace ControlCatalog.ViewModels
public MiniCommand ShowCustomManagedNotificationCommand { get; }
public MiniCommand ShowManagedNotificationCommand { get; }
public MiniCommand ShowNativeNotificationCommand { get; }
}
}

51
src/Avalonia.Base/Animation/Transition.cs

@ -1,53 +1,22 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Animation.Easings;
namespace Avalonia.Animation
{
/// <summary>
/// Defines how a property should be animated using a transition.
/// </summary>
public abstract class Transition<T> : AvaloniaObject, ITransition
public abstract class Transition<T> : TransitionBase
{
private AvaloniaProperty? _prop;
/// <summary>
/// Gets or sets the duration of the transition.
/// </summary>
public TimeSpan Duration { get; set; }
/// <summary>
/// Gets or sets delay before starting the transition.
/// </summary>
public TimeSpan Delay { get; set; } = TimeSpan.Zero;
/// <summary>
/// Gets the easing class to be used.
/// </summary>
public Easing Easing { get; set; } = new LinearEasing();
/// <inheritdocs/>
[DisallowNull]
public AvaloniaProperty? Property
static Transition()
{
get
{
return _prop;
}
set
{
if (!(value.PropertyType.IsAssignableFrom(typeof(T))))
throw new InvalidCastException
($"Invalid property type \"{typeof(T).Name}\" for this transition: {GetType().Name}.");
_prop = value;
}
PropertyProperty.Changed.AddClassHandler<Transition<T>>((x, e) => x.OnPropertyPropertyChanged(e));
}
AvaloniaProperty ITransition.Property
private void OnPropertyPropertyChanged(AvaloniaPropertyChangedEventArgs e)
{
get => Property ?? throw new InvalidOperationException("Transition has no property specified.");
set => Property = value;
if ((e.NewValue is AvaloniaProperty newValue) && !newValue.PropertyType.IsAssignableFrom(typeof(T)))
throw new InvalidCastException
($"Invalid property type \"{typeof(T).Name}\" for this transition: {GetType().Name}.");
}
/// <summary>
@ -55,11 +24,7 @@ namespace Avalonia.Animation
/// </summary>
internal abstract IObservable<T> DoTransition(IObservable<double> progress, T oldValue, T newValue);
/// <inheritdocs/>
IDisposable ITransition.Apply(Animatable control, IClock clock, object? oldValue, object? newValue)
=> Apply(control, clock, oldValue, newValue);
internal virtual IDisposable Apply(Animatable control, IClock clock, object? oldValue, object? newValue)
internal override IDisposable Apply(Animatable control, IClock clock, object? oldValue, object? newValue)
{
if (Property is null)
throw new InvalidOperationException("Transition has no property specified.");

100
src/Avalonia.Base/Animation/TransitionBase.cs

@ -0,0 +1,100 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Animation.Easings;
namespace Avalonia.Animation
{
/// <summary>
/// Defines how a property should be animated using a transition.
/// </summary>
public abstract class TransitionBase : AvaloniaObject, ITransition
{
/// <summary>
/// Defines the <see cref="Duration"/> property.
/// </summary>
public static readonly DirectProperty<TransitionBase, TimeSpan> DurationProperty =
AvaloniaProperty.RegisterDirect<TransitionBase, TimeSpan>(
nameof(Duration),
o => o._duration,
(o, v) => o._duration = v);
/// <summary>
/// Defines the <see cref="Delay"/> property.
/// </summary>
public static readonly DirectProperty<TransitionBase, TimeSpan> DelayProperty =
AvaloniaProperty.RegisterDirect<TransitionBase, TimeSpan>(
nameof(Delay),
o => o._delay,
(o, v) => o._delay = v);
/// <summary>
/// Defines the <see cref="Easing"/> property.
/// </summary>
public static readonly DirectProperty<TransitionBase, Easing> EasingProperty =
AvaloniaProperty.RegisterDirect<TransitionBase, Easing>(
nameof(Easing),
o => o._easing,
(o, v) => o._easing = v);
/// <summary>
/// Defines the <see cref="Property"/> property.
/// </summary>
public static readonly DirectProperty<TransitionBase, AvaloniaProperty?> PropertyProperty =
AvaloniaProperty.RegisterDirect<TransitionBase, AvaloniaProperty?>(
nameof(Property),
o => o._prop,
(o, v) => o._prop = v);
private TimeSpan _duration;
private TimeSpan _delay = TimeSpan.Zero;
private Easing _easing = new LinearEasing();
private AvaloniaProperty? _prop;
/// <summary>
/// Gets or sets the duration of the transition.
/// </summary>
public TimeSpan Duration
{
get { return _duration; }
set { SetAndRaise(DurationProperty, ref _duration, value); }
}
/// <summary>
/// Gets or sets delay before starting the transition.
/// </summary>
public TimeSpan Delay
{
get { return _delay; }
set { SetAndRaise(DelayProperty, ref _delay, value); }
}
/// <summary>
/// Gets the easing class to be used.
/// </summary>
public Easing Easing
{
get { return _easing; }
set { SetAndRaise(EasingProperty, ref _easing, value); }
}
/// <inheritdocs/>
[DisallowNull]
public AvaloniaProperty? Property
{
get { return _prop; }
set { SetAndRaise(PropertyProperty, ref _prop, value); }
}
AvaloniaProperty ITransition.Property
{
get => Property ?? throw new InvalidOperationException("Transition has no property specified.");
set => Property = value;
}
/// <inheritdocs/>
IDisposable ITransition.Apply(Animatable control, IClock clock, object? oldValue, object? newValue)
=> Apply(control, clock, oldValue, newValue);
internal abstract IDisposable Apply(Animatable control, IClock clock, object? oldValue, object? newValue);
}
}

20
src/Avalonia.Base/Media/DrawingContext.cs

@ -283,7 +283,8 @@ namespace Avalonia.Media
Opacity,
Clip,
GeometryClip,
OpacityMask
OpacityMask,
RenderOptions
}
public RestoreState(DrawingContext context, PushedStateType type)
@ -308,6 +309,8 @@ namespace Avalonia.Media
_context.PopGeometryClipCore();
else if (_type == PushedStateType.OpacityMask)
_context.PopOpacityMaskCore();
else if (_type == PushedStateType.RenderOptions)
_context.PopRenderOptionsCore();
}
}
@ -400,6 +403,20 @@ namespace Avalonia.Media
return new PushedState(this);
}
/// <summary>
/// Pushes render options.
/// </summary>
/// <param name="renderOptions">The render options.</param>
/// <returns>A disposable to undo the render options.</returns>
public PushedState PushRenderOptions(RenderOptions renderOptions)
{
PushRenderOptionsCore(renderOptions);
_states ??= StateStackPool.Get();
_states.Push(new RestoreState(this, RestoreState.PushedStateType.RenderOptions));
return new PushedState(this);
}
protected abstract void PushRenderOptionsCore(RenderOptions renderOptions);
[Obsolete("Use PushTransform"), EditorBrowsable(EditorBrowsableState.Never)]
public PushedState PushPreTransform(Matrix matrix) => PushTransform(matrix);
[Obsolete("Use PushTransform"), EditorBrowsable(EditorBrowsableState.Never)]
@ -415,6 +432,7 @@ namespace Avalonia.Media
protected abstract void PopOpacityCore();
protected abstract void PopOpacityMaskCore();
protected abstract void PopTransformCore();
protected abstract void PopRenderOptionsCore();
private static bool PenIsVisible(IPen? pen)
{

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

@ -53,6 +53,8 @@ namespace Avalonia.Media
set => SetValue(OpacityMaskProperty, value);
}
internal RenderOptions? RenderOptions { get; set; }
/// <summary>
/// Gets or sets the collection that contains the child geometries.
/// </summary>
@ -75,6 +77,7 @@ namespace Avalonia.Media
using (context.PushOpacity(Opacity))
using (ClipGeometry != null ? context.PushGeometryClip(ClipGeometry) : default)
using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, bounds) : default)
using (RenderOptions != null ? context.PushRenderOptions(RenderOptions.Value) : default)
{
foreach (var drawing in Children)
{
@ -313,6 +316,15 @@ namespace Avalonia.Media
drawingGroup.Transform = new MatrixTransform(matrix);
}
protected override void PushRenderOptionsCore(RenderOptions renderOptions)
{
// Instantiate a new drawing group and set it as the _currentDrawingGroup
var drawingGroup = PushNewDrawingGroup();
// Set the render options on the new DrawingGroup
drawingGroup.RenderOptions = renderOptions;
}
protected override void PopClipCore() => Pop();
protected override void PopGeometryClipCore() => Pop();
@ -323,6 +335,8 @@ namespace Avalonia.Media
protected override void PopTransformCore() => Pop();
protected override void PopRenderOptionsCore() => Pop();
/// <summary>
/// Creates a new DrawingGroup for a Push* call by setting the
/// _currentDrawingGroup to a newly instantiated DrawingGroup,

24
src/Avalonia.Base/Media/Geometry.cs

@ -21,6 +21,7 @@ namespace Avalonia.Media
AvaloniaProperty.Register<Geometry, Transform?>(nameof(Transform));
private bool _isDirty = true;
private bool _canInvaldate = true;
private IGeometryImpl? _platformImpl;
static Geometry()
@ -30,9 +31,14 @@ namespace Avalonia.Media
internal Geometry()
{
}
private protected Geometry(IGeometryImpl? platformImpl)
{
_platformImpl = platformImpl;
_isDirty = _canInvaldate = false;
}
/// <summary>
/// Raised when the geometry changes.
/// </summary>
@ -118,6 +124,17 @@ namespace Avalonia.Media
return PlatformImpl?.StrokeContains(pen, point) == true;
}
/// <summary>
/// Gets a <see cref="Geometry"/> that is the shape defined by the stroke on the Geometry
/// produced by the specified Pen.
/// </summary>
/// <param name="pen">The pen to use.</param>
/// <returns>The outlined geometry.</returns>
public Geometry GetWidenedGeometry(IPen pen)
{
return new ImmutableGeometry(PlatformImpl?.GetWidenedGeometry(pen));
}
/// <summary>
/// Marks a property as affecting the geometry's <see cref="PlatformImpl"/>.
/// </summary>
@ -146,6 +163,9 @@ namespace Avalonia.Media
/// </summary>
protected void InvalidateGeometry()
{
if (!_canInvaldate)
return;
_isDirty = true;
_platformImpl = null;
Changed?.Invoke(this, EventArgs.Empty);

19
src/Avalonia.Base/Media/ImmutableGeometry.cs

@ -0,0 +1,19 @@
using System;
using Avalonia.Platform;
namespace Avalonia.Media;
internal class ImmutableGeometry : Geometry
{
public ImmutableGeometry(IGeometryImpl? platformImpl)
: base(platformImpl)
{
}
public override Geometry Clone() => new ImmutableGeometry(PlatformImpl);
private protected override IGeometryImpl? CreateDefiningGeometry()
{
return PlatformImpl;
}
}

4
src/Avalonia.Base/Media/PlatformDrawingContext.cs

@ -90,6 +90,8 @@ internal sealed class PlatformDrawingContext : DrawingContext
_impl.Transform = matrix * current;
}
protected override void PushRenderOptionsCore(RenderOptions renderOptions) => _impl.PushRenderOptions(renderOptions);
protected override void PopClipCore() => _impl.PopClip();
protected override void PopGeometryClipCore() => _impl.PopGeometryClip();
@ -102,6 +104,8 @@ internal sealed class PlatformDrawingContext : DrawingContext
_impl.Transform =
(_transforms ?? throw new ObjectDisposedException(nameof(PlatformDrawingContext))).Pop();
protected override void PopRenderOptionsCore() => _impl.PopRenderOptions();
protected override void DisposeCore()
{
if (_ownsImpl)

11
src/Avalonia.Base/Platform/IDrawingContextImpl.cs

@ -160,6 +160,17 @@ namespace Avalonia.Platform
/// </summary>
void PopGeometryClip();
/// <summary>
/// Pushes render options.
/// </summary>
/// <param name="renderOptions">The render options.</param>
void PushRenderOptions(RenderOptions renderOptions);
/// <summary>
/// Pops the latest render options.
/// </summary>
void PopRenderOptions();
/// <summary>
/// Attempts to get an optional feature from the drawing context implementation.
/// </summary>

8
src/Avalonia.Base/Platform/IGeometryImpl.cs

@ -28,6 +28,14 @@ namespace Avalonia.Platform
/// <returns>The bounding rectangle.</returns>
Rect GetRenderBounds(IPen? pen);
/// <summary>
/// Gets a geometry that is the shape defined by the stroke on the geometry
/// produced by the specified Pen.
/// </summary>
/// <param name="pen">The pen to use.</param>
/// <returns>The outlined geometry.</returns>
IGeometryImpl GetWidenedGeometry(IPen pen);
/// <summary>
/// Indicates whether the geometry's fill contains the specified point.
/// </summary>

15
src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataNodes.cs

@ -211,3 +211,18 @@ abstract class RenderDataBrushAndPenNode : IRenderDataItemWithServerResources
public abstract Rect? Bounds { get; }
public abstract bool HitTest(Point p);
}
class RenderDataRenderOptionsNode : RenderDataPushNode
{
public RenderOptions RenderOptions { get; set; }
public override void Push(ref RenderDataNodeRenderContext context)
{
context.Context.PushRenderOptions(RenderOptions);
}
public override void Pop(ref RenderDataNodeRenderContext context)
{
context.Context.PopRenderOptions();
}
}

8
src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataDrawingContext.cs

@ -259,6 +259,12 @@ internal class RenderDataDrawingContext : DrawingContext
});
}
protected override void PushRenderOptionsCore(RenderOptions renderOptions) => Push(new RenderDataRenderOptionsNode()
{
RenderOptions = renderOptions
});
protected override void PopClipCore() => Pop<RenderDataClipNode>();
protected override void PopGeometryClipCore() => Pop<RenderDataGeometryClipNode>();
@ -269,6 +275,8 @@ internal class RenderDataDrawingContext : DrawingContext
protected override void PopTransformCore() => Pop<RenderDataPushMatrixNode>();
protected override void PopRenderOptionsCore() => Pop<RenderDataRenderOptionsNode>();
internal override void DrawBitmap(IRef<IBitmapImpl>? source, double opacity, Rect sourceRect, Rect destRect)
{
if (source == null || sourceRect.IsEmpty() || destRect.IsEmpty())

10
src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs

@ -122,6 +122,11 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl,
_impl.PushOpacityMask(mask, bounds);
}
public void PushRenderOptions(RenderOptions renderOptions)
{
_impl.PushRenderOptions(renderOptions);
}
public void PopOpacityMask()
{
_impl.PopOpacityMask();
@ -137,6 +142,11 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl,
_impl.PopGeometryClip();
}
public void PopRenderOptions()
{
_impl.PopRenderOptions();
}
public object? GetFeature(Type t) => _impl.GetFeature(t);

149
src/Avalonia.Base/Utilities/HashCode.cs

@ -0,0 +1,149 @@
// Taken from:
// https://github.com/mono/SkiaSharp/blob/main/binding/Binding.Shared/HashCode.cs
// Partial code copied from:
// https://github.com/dotnet/runtime/blob/6072e4d3a7a2a1493f514cdf4be75a3d56580e84/src/libraries/System.Private.CoreLib/src/System/HashCode.cs
#if NETSTANDARD2_0
#nullable disable
using System.Runtime.CompilerServices;
namespace System;
internal unsafe struct HashCode
{
private static readonly uint s_seed = GenerateGlobalSeed();
private const uint Prime1 = 2654435761U;
private const uint Prime2 = 2246822519U;
private const uint Prime3 = 3266489917U;
private const uint Prime4 = 668265263U;
private const uint Prime5 = 374761393U;
private uint _v1, _v2, _v3, _v4;
private uint _queue1, _queue2, _queue3;
private uint _length;
private static unsafe uint GenerateGlobalSeed()
{
var rnd = new Random();
var result = rnd.Next();
return unchecked((uint)result);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void Initialize(out uint v1, out uint v2, out uint v3, out uint v4)
{
v1 = s_seed + Prime1 + Prime2;
v2 = s_seed + Prime2;
v3 = s_seed;
v4 = s_seed - Prime1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint Round(uint hash, uint input) =>
RotateLeft(hash + input * Prime2, 13) * Prime1;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint QueueRound(uint hash, uint queuedValue) =>
RotateLeft(hash + queuedValue * Prime3, 17) * Prime4;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint MixState(uint v1, uint v2, uint v3, uint v4) =>
RotateLeft(v1, 1) + RotateLeft(v2, 7) + RotateLeft(v3, 12) + RotateLeft(v4, 18);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint RotateLeft(uint value, int offset) =>
(value << offset) | (value >> (32 - offset));
private static uint MixEmptyState() =>
s_seed + Prime5;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint MixFinal(uint hash)
{
hash ^= hash >> 15;
hash *= Prime2;
hash ^= hash >> 13;
hash *= Prime3;
hash ^= hash >> 16;
return hash;
}
public void Add(void* value) =>
Add(value == null ? 0 : ((IntPtr)value).GetHashCode());
public void Add<T>(T value) =>
Add(value?.GetHashCode() ?? 0);
private void Add(int value)
{
uint val = (uint)value;
// Storing the value of _length locally shaves of quite a few bytes
// in the resulting machine code.
uint previousLength = _length++;
uint position = previousLength % 4;
// Switch can't be inlined.
if (position == 0)
_queue1 = val;
else if (position == 1)
_queue2 = val;
else if (position == 2)
_queue3 = val;
else // position == 3
{
if (previousLength == 3)
Initialize(out _v1, out _v2, out _v3, out _v4);
_v1 = Round(_v1, _queue1);
_v2 = Round(_v2, _queue2);
_v3 = Round(_v3, _queue3);
_v4 = Round(_v4, val);
}
}
public int ToHashCode()
{
// Storing the value of _length locally shaves of quite a few bytes
// in the resulting machine code.
uint length = _length;
// position refers to the *next* queue position in this method, so
// position == 1 means that _queue1 is populated; _queue2 would have
// been populated on the next call to Add.
uint position = length % 4;
// If the length is less than 4, _v1 to _v4 don't contain anything
// yet. xxHash32 treats this differently.
uint hash = length < 4 ? MixEmptyState() : MixState(_v1, _v2, _v3, _v4);
// _length is incremented once per Add(Int32) and is therefore 4
// times too small (xxHash length is in bytes, not ints).
hash += length * 4;
// Mix what remains in the queue
// Switch can't be inlined right now, so use as few branches as
// possible by manually excluding impossible scenarios (position > 1
// is always false if position is not > 0).
if (position > 0)
{
hash = QueueRound(hash, _queue1);
if (position > 1)
{
hash = QueueRound(hash, _queue2);
if (position > 2)
hash = QueueRound(hash, _queue3);
}
}
hash = MixFinal(hash);
return (int)hash;
}
}
#endif

5
src/Avalonia.Controls/Documents/InlineUIContainer.cs

@ -58,6 +58,11 @@ namespace Avalonia.Controls.Documents
internal override void BuildTextRun(IList<TextRun> textRuns)
{
if(!Child.IsMeasureValid)
{
Child.Measure(Size.Infinity);
}
textRuns.Add(new EmbeddedControlRun(Child, CreateTextRunProperties()));
}

3
src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs

@ -1,4 +1,5 @@
using Avalonia.Metadata;
using System;
using Avalonia.Metadata;
namespace Avalonia.Controls.Notifications
{

55
src/Avalonia.Controls/Notifications/Notification.cs

@ -1,4 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace Avalonia.Controls.Notifications
{
@ -9,8 +12,10 @@ namespace Avalonia.Controls.Notifications
/// This class represents a notification that can be displayed either in a window using
/// <see cref="WindowNotificationManager"/> or by the host operating system (to be implemented).
/// </remarks>
public class Notification : INotification
public class Notification : INotification, INotifyPropertyChanged
{
private string? _title, _message;
/// <summary>
/// Initializes a new instance of the <see cref="Notification"/> class.
/// </summary>
@ -35,23 +40,59 @@ namespace Avalonia.Controls.Notifications
OnClick = onClick;
OnClose = onClose;
}
/// <summary>
/// Initializes a new instance of the <see cref="Notification"/> class.
/// </summary>
public Notification() : this(null, null)
{
}
/// <inheritdoc/>
public string? Title { get; private set; }
public string? Title
{
get => _title;
set
{
if (_title != value)
{
_title = value;
OnPropertyChanged();
}
}
}
/// <inheritdoc/>
public string? Message { get; private set; }
public string? Message
{
get => _message;
set
{
if (_message != value)
{
_message = value;
OnPropertyChanged();
}
}
}
/// <inheritdoc/>
public NotificationType Type { get; private set; }
public NotificationType Type { get; set; }
/// <inheritdoc/>
public TimeSpan Expiration { get; private set; }
public TimeSpan Expiration { get; set; }
/// <inheritdoc/>
public Action? OnClick { get; private set; }
public Action? OnClick { get; set; }
/// <inheritdoc/>
public Action? OnClose { get; private set; }
public Action? OnClose { get; set; }
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

101
src/Avalonia.Controls/Notifications/NotificationCard.cs

@ -1,4 +1,4 @@
using System;
using System;
using System.Linq;
using Avalonia.Reactive;
using Avalonia.Controls.Metadata;
@ -25,42 +25,7 @@ namespace Avalonia.Controls.Notifications
/// </summary>
public NotificationCard()
{
this.GetObservable(IsClosedProperty)
.Subscribe(x =>
{
if (!IsClosing && !IsClosed)
{
return;
}
RaiseEvent(new RoutedEventArgs(NotificationClosedEvent));
});
this.GetObservable(ContentProperty)
.Subscribe(x =>
{
if (x is INotification notification)
{
switch (notification.Type)
{
case NotificationType.Error:
PseudoClasses.Add(":error");
break;
case NotificationType.Information:
PseudoClasses.Add(":information");
break;
case NotificationType.Success:
PseudoClasses.Add(":success");
break;
case NotificationType.Warning:
PseudoClasses.Add(":warning");
break;
}
}
});
UpdateNotificationType();
}
/// <summary>
@ -93,6 +58,21 @@ namespace Avalonia.Controls.Notifications
public static readonly StyledProperty<bool> IsClosedProperty =
AvaloniaProperty.Register<NotificationCard, bool>(nameof(IsClosed));
/// <summary>
/// Gets or sets the type of the notification
/// </summary>
public NotificationType NotificationType
{
get { return GetValue(NotificationTypeProperty); }
set { SetValue(NotificationTypeProperty, value); }
}
/// <summary>
/// Defines the <see cref="NotificationType" /> property
/// </summary>
public static readonly StyledProperty<NotificationType> NotificationTypeProperty =
AvaloniaProperty.Register<NotificationCard, NotificationType>(nameof(NotificationType));
/// <summary>
/// Defines the <see cref="NotificationClosed"/> event.
/// </summary>
@ -163,5 +143,52 @@ namespace Avalonia.Controls.Notifications
IsClosing = true;
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if (e.Property == ContentProperty && e.NewValue is INotification notification)
{
SetValue(NotificationTypeProperty, notification.Type);
}
if (e.Property == NotificationTypeProperty)
{
UpdateNotificationType();
}
if (e.Property == IsClosedProperty)
{
if (!IsClosing && !IsClosed)
{
return;
}
RaiseEvent(new RoutedEventArgs(NotificationClosedEvent));
}
}
private void UpdateNotificationType()
{
switch (NotificationType)
{
case NotificationType.Error:
PseudoClasses.Add(":error");
break;
case NotificationType.Information:
PseudoClasses.Add(":information");
break;
case NotificationType.Success:
PseudoClasses.Add(":success");
break;
case NotificationType.Warning:
PseudoClasses.Add(":warning");
break;
}
}
}
}

118
src/Avalonia.Controls/Notifications/WindowNotificationManager.cs

@ -1,11 +1,12 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Reactive;
using System.Threading.Tasks;
using Avalonia.Collections;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Rendering;
using Avalonia.Threading;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Notifications
@ -18,7 +19,6 @@ namespace Avalonia.Controls.Notifications
public class WindowNotificationManager : TemplatedControl, IManagedNotificationManager
{
private IList? _items;
/// <summary>
/// Defines the <see cref="Position"/> property.
/// </summary>
@ -49,18 +49,24 @@ namespace Avalonia.Controls.Notifications
get { return GetValue(MaxItemsProperty); }
set { SetValue(MaxItemsProperty, value); }
}
/// <summary>
/// Initializes a new instance of the <see cref="WindowNotificationManager"/> class.
/// </summary>
/// <param name="host">The window that will host the control.</param>
public WindowNotificationManager(TopLevel? host)
/// <param name="host">The TopLevel that will host the control.</param>
public WindowNotificationManager(TopLevel? host) : this()
{
if (host != null)
if (host is not null)
{
Install(host);
InstallFromTopLevel(host);
}
}
/// <summary>
/// Initializes a new instance of the <see cref="WindowNotificationManager"/> class.
/// </summary>
public WindowNotificationManager()
{
UpdatePseudoClasses(Position);
}
@ -73,6 +79,8 @@ namespace Avalonia.Controls.Notifications
/// <inheritdoc/>
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
var itemsControl = e.NameScope.Find<Panel>("PART_Items");
_items = itemsControl?.Children;
}
@ -80,49 +88,85 @@ namespace Avalonia.Controls.Notifications
/// <inheritdoc/>
public void Show(INotification content)
{
Show(content as object);
Show(content, content.Type, content.Expiration, content.OnClick, content.OnClose);
}
/// <inheritdoc/>
public async void Show(object content)
{
var notification = content as INotification;
if (content is INotification notification)
{
Show(notification, notification.Type, notification.Expiration, notification.OnClick, notification.OnClose);
}
else
{
Show(content, NotificationType.Information);
}
}
/// <summary>
/// Shows a Notification
/// </summary>
/// <param name="content">the content of the notification</param>
/// <param name="type">the type of the notification</param>
/// <param name="expiration">the expiration time of the notification after which it will automatically close. If the value is Zero then the notification will remain open until the user closes it</param>
/// <param name="onClick">an Action to be run when the notification is clicked</param>
/// <param name="onClose">an Action to be run when the notification is closed</param>
/// <param name="classes">style classes to apply</param>
public async void Show(object content,
NotificationType type,
TimeSpan? expiration = null,
Action? onClick = null,
Action? onClose = null,
string[]? classes = null)
{
Dispatcher.UIThread.VerifyAccess();
var notificationControl = new NotificationCard
{
Content = content
Content = content,
NotificationType = type
};
// Add style classes if any
if (classes != null)
{
foreach (var @class in classes)
{
notificationControl.Classes.Add(@class);
}
}
notificationControl.NotificationClosed += (sender, args) =>
{
notification?.OnClose?.Invoke();
onClose?.Invoke();
_items?.Remove(sender);
};
notificationControl.PointerPressed += (sender, args) =>
{
if (notification != null && notification.OnClick != null)
{
notification.OnClick.Invoke();
}
onClick?.Invoke();
(sender as NotificationCard)?.Close();
};
_items?.Add(notificationControl);
if (_items?.OfType<NotificationCard>().Count(i => !i.IsClosing) > MaxItems)
Dispatcher.UIThread.Post(() =>
{
_items.OfType<NotificationCard>().First(i => !i.IsClosing).Close();
}
_items?.Add(notificationControl);
if (notification != null && notification.Expiration == TimeSpan.Zero)
if (_items?.OfType<NotificationCard>().Count(i => !i.IsClosing) > MaxItems)
{
_items.OfType<NotificationCard>().First(i => !i.IsClosing).Close();
}
});
if (expiration == TimeSpan.Zero)
{
return;
}
await Task.Delay(notification?.Expiration ?? TimeSpan.FromSeconds(5));
await Task.Delay(expiration ?? TimeSpan.FromSeconds(5));
notificationControl.Close();
}
@ -139,18 +183,30 @@ namespace Avalonia.Controls.Notifications
/// <summary>
/// Installs the <see cref="WindowNotificationManager"/> within the <see cref="AdornerLayer"/>
/// of the host <see cref="Window"/>.
/// </summary>
/// <param name="host">The <see cref="Window"/> that will be the host.</param>
private void Install(TemplatedControl host)
private void InstallFromTopLevel(TopLevel topLevel)
{
var adornerLayer = host.FindDescendantOfType<VisualLayerManager>()?.AdornerLayer;
topLevel.TemplateApplied += TopLevelOnTemplateApplied;
var adorner = topLevel.FindDescendantOfType<VisualLayerManager>()?.AdornerLayer;
if (adorner is not null)
{
adorner.Children.Add(this);
AdornerLayer.SetAdornedElement(this, adorner);
}
}
if (adornerLayer is not null)
private void TopLevelOnTemplateApplied(object? sender, TemplateAppliedEventArgs e)
{
if (Parent is AdornerLayer adornerLayer)
{
adornerLayer.Children.Add(this);
AdornerLayer.SetAdornedElement(this, adornerLayer);
adornerLayer.Children.Remove(this);
AdornerLayer.SetAdornedElement(this, null);
}
// Reinstall notification manager on template reapplied.
var topLevel = (TopLevel)sender!;
topLevel.TemplateApplied -= TopLevelOnTemplateApplied;
InstallFromTopLevel(topLevel);
}
private void UpdatePseudoClasses(NotificationPosition position)

2
src/Avalonia.Controls/Primitives/AdornerLayer.cs

@ -61,7 +61,7 @@ namespace Avalonia.Controls.Primitives
return adorner.GetValue(AdornedElementProperty);
}
public static void SetAdornedElement(Visual adorner, Visual adorned)
public static void SetAdornedElement(Visual adorner, Visual? adorned)
{
adorner.SetValue(AdornedElementProperty, adorned);
}

2
src/Avalonia.Controls/Primitives/OverlayPopupHost.cs

@ -78,7 +78,7 @@ namespace Avalonia.Controls.Primitives
Rect? rect = null)
{
_positionerParameters.ConfigurePosition((TopLevel)_overlayLayer.GetVisualRoot()!, target, placement, offset, anchor,
gravity, constraintAdjustment | PopupPositionerConstraintAdjustment.All, rect, FlowDirection);
gravity, constraintAdjustment, rect, FlowDirection);
UpdatePosition();
}

5
src/Avalonia.Controls/Primitives/ScrollBar.cs

@ -195,14 +195,15 @@ namespace Avalonia.Controls.Primitives
}
/// <summary>
/// Locates the first <see cref="ScrollViewer"/> ancestor and binds to its properties. Properties which have been set through other means are not bound.
/// Try to attach to TemplatedParent if it is a <see cref="ScrollViewer"/> and binds to its properties.
/// Properties which have been set through other means are not bound.
/// </summary>
/// <remarks>
/// This method is automatically called when the control is attached to a visual tree.
/// </remarks>
internal void AttachToScrollViewer()
{
var owner = this.FindAncestorOfType<ScrollViewer>();
var owner = this.TemplatedParent as ScrollViewer;
if (owner == null)
{

16
src/Avalonia.Controls/TextBlock.cs

@ -679,14 +679,18 @@ namespace Avalonia.Controls
_textRuns = textRuns;
foreach (var textRun in _textRuns)
foreach (var textLine in TextLayout.TextLines)
{
if (textRun is EmbeddedControlRun controlRun &&
controlRun.Control is Control control)
foreach (var run in textLine.TextRuns)
{
VisualChildren.Add(control);
control.Measure(Size.Infinity);
if (run is DrawableTextRun drawable)
{
if (drawable is EmbeddedControlRun controlRun
&& controlRun.Control is Control control)
{
VisualChildren.Add(control);
}
}
}
}
}

13
src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj

@ -13,7 +13,7 @@
<ItemGroup>
<PackageReference Include="Tmds.DBus.Protocol" Version="0.15.0" />
<PackageReference Include="Tmds.DBus.SourceGenerator" Version="0.0.8" PrivateAssets="All" />
<PackageReference Include="Tmds.DBus.SourceGenerator" Version="0.0.10" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
@ -26,19 +26,22 @@
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="DBusXml/DBus.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/StatusNotifierWatcher.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/com.canonical.AppMenu.Registrar.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/com.canonical.dbusmenu.xml" DBusGeneratorMode="Handler" />
<AdditionalFiles Include="DBusXml/org.fcitx.Fcitx.InputContext.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.fcitx.Fcitx.InputMethod.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.fcitx.Fcitx.InputContext1.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.fcitx.Fcitx.InputMethod1.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.freedesktop.DBus.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.freedesktop.IBus.Portal.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.freedesktop.portal.FileChooser.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.freedesktop.portal.Request.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.freedesktop.portal.Settings.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/DBusMenu.xml" DBusGeneratorMode="Handler" />
<AdditionalFiles Include="DBusXml/StatusNotifierItem.xml" DBusGeneratorMode="Handler" />
<AdditionalFiles Include="DBusXml/org.kde.StatusNotifierWatcher.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.kde.StatusNotifierItem.xml" DBusGeneratorMode="Handler" />
</ItemGroup>
</Project>

7
src/Avalonia.FreeDesktop/DBusMenuExporter.cs

@ -58,6 +58,7 @@ namespace Avalonia.FreeDesktop
private DBusMenuExporterImpl()
{
BackingProperties.Version = 4;
BackingProperties.Status = string.Empty;
BackingProperties.TextDirection = string.Empty;
BackingProperties.IconThemePath = Array.Empty<string>();
@ -280,11 +281,9 @@ namespace Avalonia.FreeDesktop
if (loader is not null)
{
var icon = loader.LoadIcon(item.Icon.PlatformImpl.Item);
using var ms = new MemoryStream();
icon.Save(ms);
return new DBusVariantItem("ay",
new DBusArrayItem(DBusType.Byte, ms.ToArray().Select(static x => new DBusByteItem(x))));
return new DBusVariantItem("ay", new DBusByteArrayItem(ms.ToArray()));
}
}
}
@ -325,7 +324,7 @@ namespace Avalonia.FreeDesktop
children[c] = new DBusVariantItem("(ia{sv}av)", new DBusStructItem(new DBusItem[]
{
new DBusInt32Item(layout.Item1),
new DBusArrayItem(DBusType.DictEntry, layout.Item2.Select(static x => new DBusDictEntryItem(new DBusStringItem(x.Key), x.Value))),
new DBusArrayItem(DBusType.DictEntry, layout.Item2.Select(static x => new DBusDictEntryItem(new DBusStringItem(x.Key), x.Value)).ToArray()),
new DBusArrayItem(DBusType.Variant, layout.Item3)
}));
}

6
src/Avalonia.FreeDesktop/DBusSystemDialog.cs

@ -88,7 +88,7 @@ namespace Avalonia.FreeDesktop
if (options.SuggestedFileName is { } currentName)
chooserOptions.Add("current_name", new DBusVariantItem("s", new DBusStringItem(currentName)));
if (options.SuggestedStartLocation?.TryGetLocalPath() is { } folderPath)
chooserOptions.Add("current_folder", new DBusVariantItem("ay", new DBusArrayItem(DBusType.Byte, Encoding.UTF8.GetBytes(folderPath).Select(static x => new DBusByteItem(x)))));
chooserOptions.Add("current_folder", new DBusVariantItem("ay", new DBusByteArrayItem(Encoding.UTF8.GetBytes(folderPath))));
objectPath = await _fileChooser.SaveFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions);
var request = new OrgFreedesktopPortalRequest(_connection, "org.freedesktop.portal.Desktop", objectPath);
@ -149,7 +149,7 @@ namespace Avalonia.FreeDesktop
if (fileTypes is null)
return null;
var filters = new DBusArrayItem(DBusType.Struct, new List<DBusItem>());
var filters = new List<DBusItem>();
foreach (var fileType in fileTypes)
{
@ -173,7 +173,7 @@ namespace Avalonia.FreeDesktop
}));
}
return filters.Count > 0 ? new DBusVariantItem("a(sa(us))", filters) : null;
return filters.Count > 0 ? new DBusVariantItem("a(sa(us))", new DBusArrayItem(DBusType.Struct, filters)) : null;
}
}
}

0
src/Avalonia.FreeDesktop/DBusXml/DBusMenu.xml → src/Avalonia.FreeDesktop/DBusXml/com.canonical.dbusmenu.xml

0
src/Avalonia.FreeDesktop/DBusXml/DBus.xml → src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.DBus.xml

0
src/Avalonia.FreeDesktop/DBusXml/StatusNotifierItem.xml → src/Avalonia.FreeDesktop/DBusXml/org.kde.StatusNotifierItem.xml

0
src/Avalonia.FreeDesktop/DBusXml/StatusNotifierWatcher.xml → src/Avalonia.FreeDesktop/DBusXml/org.kde.StatusNotifierWatcher.xml

2
src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs

@ -108,8 +108,6 @@ namespace Avalonia.OpenGL.Controls
_visual.Size = new Vector(Bounds.Width, Bounds.Height);
_visual.Surface = _resources.Surface;
ElementComposition.SetElementChildVisual(this, _visual);
using (_resources.Context.MakeCurrent())
OnOpenGlInit(_resources.Context.GlInterface);
return true;
}

12
src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@ -182,6 +182,8 @@ namespace Avalonia.Headless
return Bounds.Inflate(pen.Thickness / 2);
}
public IGeometryImpl GetWidenedGeometry(IPen pen) => this;
public bool StrokeContains(IPen? pen, Point point)
{
return false;
@ -542,6 +544,16 @@ namespace Avalonia.Headless
{
}
public void PushRenderOptions(RenderOptions renderOptions)
{
}
public void PopRenderOptions()
{
}
}
private class HeadlessRenderTarget : IRenderTarget

34
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs

@ -1,6 +1,7 @@
using System.Linq;
using XamlX.Ast;
using XamlX.Transform;
using XamlX.Transform.Transformers;
using XamlX.TypeSystem;
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
@ -22,26 +23,19 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
IXamlAstTypeReference targetType;
var templatableBaseType = context.Configuration.TypeSystem.GetType("Avalonia.Controls.Control");
if ((tt?.Values.FirstOrDefault() is XamlTypeExtensionNode tn))
{
targetType = tn.Value;
}
else
targetType = tt?.Values.FirstOrDefault() switch
{
var parentScope = context.ParentNodes().OfType<AvaloniaXamlIlTargetTypeMetadataNode>()
.FirstOrDefault();
if (parentScope?.ScopeType == AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style)
targetType = parentScope.TargetType;
else if (context.ParentNodes().Skip(1).FirstOrDefault() is XamlAstObjectNode directParentNode
&& templatableBaseType.IsAssignableFrom(directParentNode.Type.GetClrType()))
targetType = directParentNode.Type;
else
targetType = new XamlAstClrTypeReference(node,
templatableBaseType, false);
}
XamlTypeExtensionNode tn => tn.Value,
XamlAstTextNode textNode => TypeReferenceResolver.ResolveType(context, textNode.Text, false, textNode, true),
_ when context.ParentNodes()
.OfType<AvaloniaXamlIlTargetTypeMetadataNode>()
.FirstOrDefault() is { ScopeType: AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style } parentScope => parentScope.TargetType,
_ when context.ParentNodes().Skip(1).FirstOrDefault() is XamlAstObjectNode directParentNode
&& templatableBaseType.IsAssignableFrom(directParentNode.Type.GetClrType()) => directParentNode.Type,
_ => new XamlAstClrTypeReference(node,
templatableBaseType, false)
};
return new AvaloniaXamlIlTargetTypeMetadataNode(on, targetType,
AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.ControlTemplate);
@ -59,7 +53,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
ControlTemplate,
Transitions
}
public AvaloniaXamlIlTargetTypeMetadataNode(IXamlAstValueNode value, IXamlAstTypeReference targetType,
ScopeTypes type)
: base(value, value)

38
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -6,6 +6,7 @@ using System.Threading;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering.Utilities;
using Avalonia.Skia.Helpers;
using Avalonia.Utilities;
using SkiaSharp;
using ISceneBrush = Avalonia.Media.ISceneBrush;
@ -23,6 +24,7 @@ namespace Avalonia.Skia
private readonly Vector _dpi;
private readonly Stack<PaintWrapper> _maskStack = new();
private readonly Stack<double> _opacityStack = new();
private readonly Stack<RenderOptions> _renderOptionsStack = new();
private readonly Matrix? _postTransform;
private double _currentOpacity = 1.0f;
private readonly bool _disableSubpixelTextRendering;
@ -634,6 +636,21 @@ namespace Avalonia.Skia
_currentOpacity = _opacityStack.Pop();
}
/// <inheritdoc />
public void PushRenderOptions(RenderOptions renderOptions)
{
CheckLease();
_renderOptionsStack.Push(RenderOptions);
RenderOptions = RenderOptions.MergeWith(renderOptions);
}
public void PopRenderOptions()
{
RenderOptions = _renderOptionsStack.Pop();
}
/// <inheritdoc />
public virtual void Dispose()
{
@ -1252,25 +1269,10 @@ namespace Avalonia.Skia
paint.StrokeMiter = (float) pen.MiterLimit;
if (pen.DashStyle?.Dashes != null && pen.DashStyle.Dashes.Count > 0)
if (DrawingContextHelper.TryCreateDashEffect(pen, out var dashEffect))
{
var srcDashes = pen.DashStyle.Dashes;
var count = srcDashes.Count % 2 == 0 ? srcDashes.Count : srcDashes.Count * 2;
var dashesArray = new float[count];
for (var i = 0; i < count; ++i)
{
dashesArray[i] = (float) srcDashes[i % srcDashes.Count] * paint.StrokeWidth;
}
var offset = (float)(pen.DashStyle.Offset * pen.Thickness);
var pe = SKPathEffect.CreateDash(dashesArray, offset);
paint.PathEffect = pe;
rv.AddDisposable(pe);
paint.PathEffect = dashEffect;
rv.AddDisposable(dashEffect);
}
return rv;

64
src/Skia/Avalonia.Skia/GeometryImpl.cs

@ -2,6 +2,8 @@ using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Skia.Helpers;
using Avalonia.Utilities;
using SkiaSharp;
namespace Avalonia.Skia
@ -75,6 +77,19 @@ namespace Avalonia.Skia
return _pathCache.RenderBounds;
}
public IGeometryImpl GetWidenedGeometry(IPen pen)
{
if (StrokePath is not null && SKPathHelper.CreateStrokedPath(StrokePath, pen) is { } path)
{
// The path returned to us by skia here does not have closed figures.
// Fix that by calling CreateClosedPath.
var closed = SKPathHelper.CreateClosedPath(path);
return new StreamGeometryImpl(closed, closed);
}
return new StreamGeometryImpl(new SKPath(), null);
}
/// <inheritdoc />
public ITransformedGeometryImpl WithTransform(Matrix transform)
{
@ -143,58 +158,36 @@ namespace Avalonia.Skia
_pathCache = default;
}
private struct PathCache
private struct PathCache : IDisposable
{
private double _width, _miterLimit;
private PenLineCap _cap;
private PenLineJoin _join;
private int _penHash;
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)
{
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))
if (PenHelper.GetHashCode(pen, includeBrush: false) is { } penHash &&
penHash == _penHash &&
strokePath == _cachedFor)
{
// 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)
{
_path = null;
return;
}
_penHash = penHash;
_path?.Dispose();
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);
if (strokePath is not null && pen is not null)
_path = SKPathHelper.CreateStrokedPath(strokePath, pen);
else
_path = null;
SKPaintCache.Shared.ReturnReset(paint);
}
public void Dispose()
@ -202,7 +195,6 @@ namespace Avalonia.Skia
_path?.Dispose();
_path = null;
}
}
}
}

28
src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs

@ -1,5 +1,6 @@
using Avalonia.Platform;
using Avalonia.Rendering;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Media;
using Avalonia.Platform;
using SkiaSharp;
namespace Avalonia.Skia.Helpers
@ -26,5 +27,28 @@ namespace Avalonia.Skia.Helpers
return new DrawingContextImpl(createInfo);
}
public static bool TryCreateDashEffect(IPen? pen, [NotNullWhen(true)] out SKPathEffect? effect)
{
if (pen?.DashStyle?.Dashes != null && pen.DashStyle.Dashes.Count > 0)
{
var srcDashes = pen.DashStyle.Dashes;
var count = srcDashes.Count % 2 == 0 ? srcDashes.Count : srcDashes.Count * 2;
var dashesArray = new float[count];
for (var i = 0; i < count; ++i)
{
dashesArray[i] = (float)srcDashes[i % srcDashes.Count] * (float)pen.Thickness;
}
var offset = (float)(pen.DashStyle.Offset * pen.Thickness);
effect = SKPathEffect.CreateDash(dashesArray, offset);
return true;
}
effect = null;
return false;
}
}
}

38
src/Skia/Avalonia.Skia/Helpers/PenHelper.cs

@ -0,0 +1,38 @@
using System;
using Avalonia.Media;
namespace Avalonia.Skia.Helpers;
internal static class PenHelper
{
/// <summary>
/// Gets a hash code for a pen, optionally including the brush.
/// </summary>
/// <param name="pen">The pen.</param>
/// <param name="includeBrush">Whether to include the brush in the hash code.</param>
/// <returns>The hash code.</returns>
public static int GetHashCode(IPen? pen, bool includeBrush)
{
if (pen is null)
return 0;
var hash = new HashCode();
hash.Add(pen.LineCap);
hash.Add(pen.LineJoin);
hash.Add(pen.MiterLimit);
hash.Add(pen.Thickness);
if (pen.DashStyle is { } dashStyle)
{
hash.Add(dashStyle.Offset);
for (var i = 0; i < dashStyle.Dashes?.Count; i++)
hash.Add(dashStyle.Dashes[i]);
}
if (includeBrush)
hash.Add(pen.Brush);
return hash.ToHashCode();
}
}

68
src/Skia/Avalonia.Skia/Helpers/SKPathHelper.cs

@ -0,0 +1,68 @@
using System;
using Avalonia.Media;
using Avalonia.Utilities;
using SkiaSharp;
namespace Avalonia.Skia.Helpers;
internal static class SKPathHelper
{
/// <summary>
/// Creates a new path that is a closed version of the source path.
/// </summary>
/// <param name="path">The source path.</param>
/// <returns>A closed path.</returns>
public static SKPath CreateClosedPath(SKPath path)
{
using var iter = path.CreateIterator(true);
SKPathVerb verb;
var points = new SKPoint[4];
var rv = new SKPath();
while ((verb = iter.Next(points)) != SKPathVerb.Done)
{
if (verb == SKPathVerb.Move)
rv.MoveTo(points[0]);
else if (verb == SKPathVerb.Line)
rv.LineTo(points[1]);
else if (verb == SKPathVerb.Close)
rv.Close();
else if (verb == SKPathVerb.Quad)
rv.QuadTo(points[1], points[2]);
else if (verb == SKPathVerb.Cubic)
rv.CubicTo(points[1], points[2], points[3]);
else if (verb == SKPathVerb.Conic)
rv.ConicTo(points[1], points[2], iter.ConicWeight());
}
return rv;
}
/// <summary>
/// Creates a path that is the result of a pen being applied to the stroke of the given path.
/// </summary>
/// <param name="path">The path to stroke.</param>
/// <param name="pen">The pen to use to stroke the path.</param>
/// <returns>The resulting path, or null if the pen has 0 thickness.</returns>
public static SKPath? CreateStrokedPath(SKPath path, IPen pen)
{
if (MathUtilities.IsZero(pen.Thickness))
return null;
var paint = SKPaintCache.Shared.Get();
paint.IsStroke = true;
paint.StrokeWidth = (float)pen.Thickness;
paint.StrokeCap = pen.LineCap.ToSKStrokeCap();
paint.StrokeJoin = pen.LineJoin.ToSKStrokeJoin();
paint.StrokeMiter = (float)pen.MiterLimit;
if (DrawingContextHelper.TryCreateDashEffect(pen, out var dashEffect))
paint.PathEffect = dashEffect;
var result = new SKPath();
paint.GetFillPath(path, result);
paint.PathEffect?.Dispose();
SKPaintCache.Shared.ReturnReset(paint);
return result;
}
}

14
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@ -24,6 +24,8 @@ namespace Avalonia.Direct2D1.Media
private readonly SharpDX.DXGI.SwapChain1 _swapChain;
private readonly Action _finishedCallback;
private readonly Stack<RenderOptions> _renderOptionsStack = new();
/// <summary>
/// Initializes a new instance of the <see cref="DrawingContextImpl"/> class.
/// </summary>
@ -488,6 +490,18 @@ namespace Avalonia.Direct2D1.Media
PopLayer();
}
public void PushRenderOptions(RenderOptions renderOptions)
{
_renderOptionsStack.Push(RenderOptions);
RenderOptions = RenderOptions.MergeWith(renderOptions);
}
public void PopRenderOptions()
{
RenderOptions = _renderOptionsStack.Pop();
}
private void PopLayer()
{
var layer = _layers.Pop();

17
src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs

@ -47,6 +47,23 @@ namespace Avalonia.Direct2D1.Media
}
}
public IGeometryImpl GetWidenedGeometry(IPen pen)
{
var result = new PathGeometry(Direct2D1Platform.Direct2D1Factory);
using (var sink = result.Open())
{
Geometry.Widen(
(float)pen.Thickness,
pen.ToDirect2DStrokeStyle(Direct2D1Platform.Direct2D1Factory),
0.25f,
sink);
sink.Close();
}
return new StreamGeometryImpl(result);
}
/// <inheritdoc/>
public bool FillContains(Point point)
{

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

@ -49,7 +49,6 @@ namespace Avalonia.Win32
public Win32Platform()
{
SetDpiAwareness();
CreateMessageWindow();
_dispatcher = new Win32DispatcherImpl(_hwnd);
}
@ -80,6 +79,9 @@ namespace Avalonia.Win32
public static void Initialize(Win32PlatformOptions options)
{
s_options = options;
SetDpiAwareness();
var renderTimer = options.ShouldRenderOnUIThread ? new UiThreadRenderTimer(60) : new DefaultRenderTimer(60);
AvaloniaLocator.CurrentMutable
@ -264,12 +266,31 @@ namespace Avalonia.Win32
var user32 = LoadLibrary("user32.dll");
var method = GetProcAddress(user32, nameof(SetProcessDpiAwarenessContext));
var dpiAwareness = Options.DpiAwareness;
if (method != IntPtr.Zero)
{
if (SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) ||
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE))
if (dpiAwareness == Win32DpiAwareness.Unaware)
{
return;
if (SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_UNAWARE))
{
return;
}
}
else if (dpiAwareness == Win32DpiAwareness.SystemDpiAware)
{
if (SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_SYSTEM_AWARE))
{
return;
}
}
else if (dpiAwareness == Win32DpiAwareness.PerMonitorDpiAware)
{
if (SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) ||
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE))
{
return;
}
}
}
@ -278,11 +299,20 @@ namespace Avalonia.Win32
if (method != IntPtr.Zero)
{
SetProcessDpiAwareness(PROCESS_DPI_AWARENESS.PROCESS_PER_MONITOR_DPI_AWARE);
var awareness = (dpiAwareness) switch
{
Win32DpiAwareness.Unaware => PROCESS_DPI_AWARENESS.PROCESS_DPI_UNAWARE,
Win32DpiAwareness.SystemDpiAware => PROCESS_DPI_AWARENESS.PROCESS_SYSTEM_DPI_AWARE,
Win32DpiAwareness.PerMonitorDpiAware => PROCESS_DPI_AWARENESS.PROCESS_PER_MONITOR_DPI_AWARE,
_ => PROCESS_DPI_AWARENESS.PROCESS_PER_MONITOR_DPI_AWARE,
};
SetProcessDpiAwareness(awareness);
return;
}
SetProcessDPIAware();
if (dpiAwareness != Win32DpiAwareness.Unaware)
SetProcessDPIAware();
}
}
}

26
src/Windows/Avalonia.Win32/Win32PlatformOptions.cs

@ -25,6 +25,27 @@ public enum Win32RenderingMode
Wgl = 3
}
/// <summary>
/// Represents the DPI Awareness for the application.
/// </summary>
public enum Win32DpiAwareness
{
/// <summary>
/// The application is DPI unaware.
/// </summary>
Unaware,
/// <summary>
/// The application is system DPI aware. It will query DPI once and will not adjust to new DPI changes
/// </summary>
SystemDpiAware,
/// <summary>
/// The application is per-monitor DPI aware. It adjust its scale factor whenever DPI changes.
/// </summary>
PerMonitorDpiAware
}
/// <summary>
/// Represents the Win32 window composition mode.
/// </summary>
@ -137,4 +158,9 @@ public class Win32PlatformOptions
/// and <see cref="CompositionMode"/> only accepts null or <see cref="Win32CompositionMode.RedirectionSurface"/>.
/// </summary>
public IPlatformGraphics? CustomPlatformGraphics { get; set; }
/// <summary>
/// Gets or sets the application's DPI awareness.
/// </summary>
public Win32DpiAwareness DpiAwareness { get; set; } = Win32DpiAwareness.PerMonitorDpiAware;
}

2
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -552,7 +552,7 @@ namespace Avalonia.Win32
get
{
// Windows 10 and 11 add a 7 pixel invisible border on the left/right/bottom of windows for resizing
if (Win32Platform.WindowsVersion.Major < 10 || !HasFullDecorations)
if (Win32Platform.WindowsVersion.Major < 10 || !HasFullDecorations || GetStyle().HasFlag(WindowStyles.WS_POPUP))
{
return PixelSize.Empty;
}

1
tests/Avalonia.Controls.UnitTests/TextBlockTests.cs

@ -170,6 +170,7 @@ namespace Avalonia.Controls.UnitTests
target.Inlines.Add("123456");
target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize));
Assert.True(button.IsMeasureValid);
Assert.Equal(80, button.DesiredSize.Width);

18
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs

@ -286,6 +286,24 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.IsType(typeof(ContentPresenter), template.Build(new ContentControl()).Result);
}
[Fact]
public void ControlTemplate_With_String_TargetType()
{
var xaml = @"
<ControlTemplate xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
TargetType='ContentControl'>
<ContentPresenter Content='{TemplateBinding Content}' />
</ControlTemplate>
";
var template = AvaloniaRuntimeXamlLoader.Parse<ControlTemplate>(xaml);
Assert.Equal(typeof(ContentControl), template.TargetType);
Assert.IsType(typeof(ContentPresenter), template.Build(new ContentControl()).Result);
}
[Fact]
public void ControlTemplate_With_Panel_Children_Are_Added()
{

50
tests/Avalonia.RenderTests/Shapes/PathTests.cs

@ -434,5 +434,55 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes
await RenderToFile(target);
CompareImages();
}
[Fact]
public async Task GetWidenedPathGeometry_Line()
{
var pen = new Pen(Brushes.Black, 10);
var geometry = StreamGeometry.Parse("M 0,0 L 180,180").GetWidenedGeometry(pen);
Decorator target = new Decorator
{
Width = 200,
Height = 200,
Child = new Path
{
Stroke = Brushes.Red,
StrokeThickness = 1,
Fill = Brushes.Green,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Data = geometry,
}
};
await RenderToFile(target);
CompareImages();
}
[Fact]
public async Task GetWidenedPathGeometry_Line_Dash()
{
var pen = new Pen(Brushes.Black, 10, DashStyle.Dash);
var geometry = StreamGeometry.Parse("M 0,0 L 180,180").GetWidenedGeometry(pen);
Decorator target = new Decorator
{
Width = 200,
Height = 200,
Child = new Path
{
Stroke = Brushes.Red,
StrokeThickness = 1,
Fill = Brushes.Green,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Data = geometry,
}
};
await RenderToFile(target);
CompareImages();
}
}
}

BIN
tests/TestFiles/Direct2D1/Shapes/Path/GetWidenedPathGeometry_Line.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
tests/TestFiles/Direct2D1/Shapes/Path/GetWidenedPathGeometry_Line_Dash.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
tests/TestFiles/Skia/Shapes/Path/GetWidenedPathGeometry_Line.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
tests/TestFiles/Skia/Shapes/Path/GetWidenedPathGeometry_Line_Dash.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Loading…
Cancel
Save