Browse Source

Merge branch 'master' into fix-mac-build-instructions

pull/5066/head
Hank G 6 years ago
committed by GitHub
parent
commit
60d15c690d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      native/Avalonia.Native/src/OSX/platformthreading.mm
  2. 10
      native/Avalonia.Native/src/OSX/rendertarget.mm
  3. 30
      samples/ControlCatalog/DecoratedWindow.xaml
  4. 66
      samples/ControlCatalog/MainWindow.xaml
  5. 24
      src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
  6. 11
      src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
  7. 4
      src/Avalonia.Build.Tasks/SpanCompat.cs
  8. 10
      src/Avalonia.Controls/GridLength.cs
  9. 3
      src/Avalonia.Controls/NativeMenuBar.cs
  10. 18
      src/Avalonia.Controls/NativeMenuItem.cs
  11. 2
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  12. 11
      src/Avalonia.Controls/Slider.cs
  13. 2
      src/Avalonia.Input/NavigationDirection.cs
  14. 2
      src/Avalonia.Native/AvaloniaNativeMenuExporter.cs
  15. 8
      src/Avalonia.Native/Extensions.cs
  16. 2
      src/Avalonia.Native/IAvnMenuItem.cs
  17. 6
      src/Avalonia.Native/PlatformThreadingInterface.cs
  18. 4
      src/Avalonia.Native/PopupImpl.cs
  19. 4
      src/Avalonia.Native/PredicateCallback.cs
  20. 2
      src/Avalonia.Native/ScreenImpl.cs
  21. 2
      src/Avalonia.Native/SystemDialogs.cs
  22. 12
      src/Avalonia.Native/WindowImpl.cs
  23. 12
      src/Avalonia.Native/WindowImplBase.cs
  24. 1
      src/Avalonia.Native/avn.idl
  25. 3
      src/Avalonia.Styling/ApiCompatBaseline.txt
  26. 13
      src/Avalonia.Styling/Controls/ResourceNodeExtensions.cs
  27. 15
      src/Avalonia.Themes.Default/PathIcon.xaml
  28. 2
      src/Avalonia.Themes.Default/ToggleSwitch.xaml
  29. 15
      src/Avalonia.Themes.Fluent/PathIcon.xaml
  30. 2
      src/Avalonia.Themes.Fluent/ToggleSwitch.xaml
  31. 42
      src/Avalonia.Visuals/ApiCompatBaseline.txt
  32. 14
      src/Avalonia.Visuals/Media/Color.cs
  33. 75
      src/Avalonia.Visuals/Media/DashStyle.cs
  34. 56
      src/Avalonia.Visuals/Media/EllipseGeometry.cs
  35. 9
      src/Avalonia.Visuals/Media/KnownColors.cs
  36. 72
      src/Avalonia.Visuals/Media/PathFigure.cs
  37. 20
      src/Avalonia.Visuals/Media/PathGeometry.cs
  38. 2
      src/Avalonia.X11/XI2Manager.cs
  39. 34
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AstNodes/AvaloniaXamlIlGridLengthAstNode.cs
  40. 147
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs
  41. 15
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  42. 18
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs
  43. 2
      src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs
  44. 3
      tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs
  45. 14
      tests/Avalonia.Benchmarks/NullCursorFactory.cs
  46. 17
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs
  47. 24
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ShapeTests.cs
  48. 34
      tests/Avalonia.Visuals.UnitTests/Media/PathSegmentTests.cs
  49. 18
      tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs

2
native/Avalonia.Native/src/OSX/platformthreading.mm

@ -101,7 +101,7 @@ public:
virtual bool GetCurrentThreadIsLoopThread() override
{
return [[NSThread currentThread] isMainThread];
return [NSThread isMainThread];
}
virtual void SetSignaledCallback(IAvnSignaledCallback* cb) override
{

10
native/Avalonia.Native/src/OSX/rendertarget.mm

@ -2,6 +2,7 @@
#include "rendertarget.h"
#import <IOSurface/IOSurface.h>
#import <IOSurface/IOSurfaceObjC.h>
#import <QuartzCore/QuartzCore.h>
#include <OpenGL/CGLIOSurface.h>
#include <OpenGL/OpenGL.h>
@ -143,13 +144,17 @@ static IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(IOSurfaceRenderTarget* ta
return _layer;
}
- (void)resize:(AvnPixelSize)size withScale: (float) scale;{
- (void)resize:(AvnPixelSize)size withScale: (float) scale{
@synchronized (lock) {
if(surface == nil
|| surface->size.Width != size.Width
|| surface->size.Height != size.Height
|| surface->scale != scale)
{
surface = [[IOSurfaceHolder alloc] initWithSize:size withScale:scale withOpenGlContext:_glContext.getRaw()];
[self updateLayer];
}
}
}
@ -159,12 +164,15 @@ static IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(IOSurfaceRenderTarget* ta
@synchronized (lock) {
if(_layer == nil)
return;
[CATransaction begin];
[_layer setContents: nil];
if(surface != nil)
{
[_layer setContentsScale: surface->scale];
[_layer setContents: (__bridge IOSurface*) surface->surface];
}
[CATransaction commit];
[CATransaction flush];
}
}
else

30
samples/ControlCatalog/DecoratedWindow.xaml

@ -6,25 +6,21 @@
<NativeMenu.Menu>
<NativeMenu>
<NativeMenuItem Header="Decorated">
<NativeMenuItem.Menu>
<NativeMenu>
<NativeMenuItem Header="Open"/>
<NativeMenuItem Header="Recent">
<NativeMenuItem.Menu>
<NativeMenu/>
</NativeMenuItem.Menu>
</NativeMenuItem>
<NativeMenuItem Header="Quit Avalonia" Gesture="CMD+Q"/>
</NativeMenu>
</NativeMenuItem.Menu>
<NativeMenu>
<NativeMenuItem Header="Open"/>
<NativeMenuItem Header="Recent">
<NativeMenuItem.Menu>
<NativeMenu/>
</NativeMenuItem.Menu>
</NativeMenuItem>
<NativeMenuItem Header="Quit Avalonia" Gesture="CMD+Q"/>
</NativeMenu>
</NativeMenuItem>
<NativeMenuItem Header="Edit">
<NativeMenuItem.Menu>
<NativeMenu>
<NativeMenuItem Header="Copy"/>
<NativeMenuItem Header="Paste"/>
</NativeMenu>
</NativeMenuItem.Menu>
<NativeMenu>
<NativeMenuItem Header="Copy"/>
<NativeMenuItem Header="Paste"/>
</NativeMenu>
</NativeMenuItem>
</NativeMenu>
</NativeMenu.Menu>

66
samples/ControlCatalog/MainWindow.xaml

@ -16,47 +16,39 @@
<NativeMenu.Menu>
<NativeMenu>
<NativeMenuItem Header="File">
<NativeMenuItem.Menu>
<NativeMenu>
<NativeMenuItem Icon="/Assets/test_icon.ico" Header="Open" Clicked="OnOpenClicked" Gesture="Ctrl+O"/>
<NativeMenuItemSeperator/>
<NativeMenuItem Icon="/Assets/github_icon.png" Header="Recent">
<NativeMenuItem.Menu>
<NativeMenu/>
</NativeMenuItem.Menu>
</NativeMenuItem>
<NativeMenuItemSeperator/>
<NativeMenuItem Header="{x:Static local:MainWindow.MenuQuitHeader}"
Gesture="{x:Static local:MainWindow.MenuQuitGesture}"
Clicked="OnCloseClicked" />
</NativeMenu>
</NativeMenuItem.Menu>
<NativeMenu>
<NativeMenuItem Icon="/Assets/test_icon.ico" Header="Open" Clicked="OnOpenClicked" Gesture="Ctrl+O"/>
<NativeMenuItemSeperator/>
<NativeMenuItem Icon="/Assets/github_icon.png" Header="Recent">
<NativeMenu/>
</NativeMenuItem>
<NativeMenuItemSeperator/>
<NativeMenuItem Header="{x:Static local:MainWindow.MenuQuitHeader}"
Gesture="{x:Static local:MainWindow.MenuQuitGesture}"
Clicked="OnCloseClicked" />
</NativeMenu>
</NativeMenuItem>
<NativeMenuItem Header="Edit">
<NativeMenuItem.Menu>
<NativeMenu>
<NativeMenuItem Header="Copy"/>
<NativeMenuItem Header="Paste"/>
</NativeMenu>
</NativeMenuItem.Menu>
<NativeMenu>
<NativeMenuItem Header="Copy"/>
<NativeMenuItem Header="Paste"/>
</NativeMenu>
</NativeMenuItem>
<NativeMenuItem Header="Options">
<NativeMenuItem.Menu>
<NativeMenu>
<NativeMenuItem Header="Check Me (None)"
Command="{Binding ToggleMenuItemCheckedCommand}"
ToggleType="None"
IsChecked="{Binding IsMenuItemChecked}" />
<NativeMenuItem Header="Check Me (CheckBox)"
Command="{Binding ToggleMenuItemCheckedCommand}"
ToggleType="CheckBox"
IsChecked="{Binding IsMenuItemChecked}" />
<NativeMenuItem Header="Check Me (Radio)"
Command="{Binding ToggleMenuItemCheckedCommand}"
ToggleType="Radio"
IsChecked="{Binding IsMenuItemChecked}" />
</NativeMenu>
</NativeMenuItem.Menu>
<NativeMenu>
<NativeMenuItem Header="Check Me (None)"
Command="{Binding ToggleMenuItemCheckedCommand}"
ToggleType="None"
IsChecked="{Binding IsMenuItemChecked}" />
<NativeMenuItem Header="Check Me (CheckBox)"
Command="{Binding ToggleMenuItemCheckedCommand}"
ToggleType="CheckBox"
IsChecked="{Binding IsMenuItemChecked}" />
<NativeMenuItem Header="Check Me (Radio)"
Command="{Binding ToggleMenuItemCheckedCommand}"
ToggleType="Radio"
IsChecked="{Binding IsMenuItemChecked}" />
</NativeMenu>
</NativeMenuItem>
</NativeMenu>
</NativeMenu.Menu>

24
src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs

@ -1,5 +1,8 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
#nullable enable
namespace Avalonia.Utilities
{
@ -9,12 +12,14 @@ namespace Avalonia.Utilities
/// <typeparam name="TValue">Stored value type.</typeparam>
internal sealed class AvaloniaPropertyValueStore<TValue>
{
// The last item in the list is always int.MaxValue.
private static readonly Entry[] s_emptyEntries = { new Entry { PropertyId = int.MaxValue, Value = default! } };
private Entry[] _entries;
public AvaloniaPropertyValueStore()
{
// The last item in the list is always int.MaxValue
_entries = new[] { new Entry { PropertyId = int.MaxValue, Value = default } };
_entries = s_emptyEntries;
}
private (int, bool) TryFindEntry(int propertyId)
@ -86,7 +91,7 @@ namespace Avalonia.Utilities
return (0, false);
}
public bool TryGetValue(AvaloniaProperty property, out TValue value)
public bool TryGetValue(AvaloniaProperty property, [MaybeNull] out TValue value)
{
(int index, bool found) = TryFindEntry(property.Id);
if (!found)
@ -132,7 +137,18 @@ namespace Avalonia.Utilities
if (found)
{
Entry[] entries = new Entry[_entries.Length - 1];
var newLength = _entries.Length - 1;
// Special case - one element left means that value store is empty so we can just reuse our "empty" array.
if (newLength == 1)
{
_entries = s_emptyEntries;
return;
}
var entries = new Entry[newLength];
int ix = 0;
for (int i = 0; i < _entries.Length; ++i)

11
src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<TargetFrameworks Condition="$(Configuration) == 'Debug'">netstandard2.0;netcoreapp2.0</TargetFrameworks>
<TargetFrameworks Condition="$(Configuration) == 'Debug'">netstandard2.0;netcoreapp3.1</TargetFrameworks>
<OutputType>exe</OutputType>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<BuildOutputTargetFolder>tools</BuildOutputTargetFolder>
@ -81,6 +81,15 @@
<Compile Include="../Avalonia.Visuals/CornerRadius.cs">
<Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
</Compile>
<Compile Include="../Avalonia.Visuals/Media/Color.cs">
<Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
</Compile>
<Compile Include="../Avalonia.Visuals/Media/KnownColors.cs">
<Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
</Compile>
<Compile Include="../Avalonia.Controls/GridLength.cs">
<Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
</Compile>
<Compile Remove="../Markup/Avalonia.Markup.Xaml.Loader\xamlil.github\**\obj\**\*.cs" />
<Compile Remove="../Markup/Avalonia.Markup.Xaml.Loader\xamlil.github\src\XamlX\IL\SreTypeSystem.cs" />
<PackageReference Include="Mono.Cecil" Version="0.11.2" />

4
src/Avalonia.Build.Tasks/SpanCompat.cs

@ -1,3 +1,4 @@
#if !NETCOREAPP3_1
namespace System
{
// This is a hack to enable our span code to work inside MSBuild task without referencing System.Memory
@ -63,6 +64,8 @@ namespace System
}
public override string ToString() => _length == 0 ? string.Empty : _s.Substring(_start, _length);
public static implicit operator ReadOnlySpan<T>(char[] arr) => new ReadOnlySpan<T>(new string(arr));
}
static class SpanCompatExtensions
@ -71,3 +74,4 @@ namespace System
}
}
#endif

10
src/Avalonia.Controls/GridLength.cs

@ -8,7 +8,10 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the valid units for a <see cref="GridLength"/>.
/// </summary>
public enum GridUnitType
#if !BUILDTASK
public
#endif
enum GridUnitType
{
/// <summary>
/// The row or column is auto-sized to fit its content.
@ -29,7 +32,10 @@ namespace Avalonia.Controls
/// <summary>
/// Holds the width or height of a <see cref="Grid"/>'s column and row definitions.
/// </summary>
public struct GridLength : IEquatable<GridLength>
#if !BUILDTASK
public
#endif
struct GridLength : IEquatable<GridLength>
{
private readonly GridUnitType _type;

3
src/Avalonia.Controls/NativeMenuBar.cs

@ -1,7 +1,6 @@
using System;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Styling;
namespace Avalonia.Controls
{
@ -16,7 +15,7 @@ namespace Avalonia.Controls
EnableMenuItemClickForwardingProperty.Changed.Subscribe(args =>
{
var item = (MenuItem)args.Sender;
if (args.NewValue.Equals(true))
if (args.NewValue.GetValueOrDefault<bool>())
item.Click += OnMenuItemClick;
else
item.Click -= OnMenuItemClick;

18
src/Avalonia.Controls/NativeMenuItem.cs

@ -2,6 +2,7 @@ using System;
using System.Windows.Input;
using Avalonia.Input;
using Avalonia.Media.Imaging;
using Avalonia.Metadata;
using Avalonia.Utilities;
namespace Avalonia.Controls
@ -62,6 +63,7 @@ namespace Avalonia.Controls
public static readonly DirectProperty<NativeMenuItem, NativeMenu> MenuProperty =
AvaloniaProperty.RegisterDirect<NativeMenuItem, NativeMenu>(nameof(Menu), o => o.Menu, (o, v) => o.Menu = v);
[Content]
public NativeMenu Menu
{
get => _menu;
@ -151,7 +153,7 @@ namespace Avalonia.Controls
IsEnabled = _command?.CanExecute(null) ?? true;
}
public bool HasClickHandlers => Clicked != null;
public bool HasClickHandlers => Click != null;
public ICommand Command
{
@ -182,11 +184,21 @@ namespace Avalonia.Controls
set { SetValue(CommandParameterProperty, value); }
}
public event EventHandler Clicked;
/// <summary>
/// Occurs when a <see cref="NativeMenuItem"/> is clicked.
/// </summary>
public event EventHandler Click;
[Obsolete("Use Click event.")]
public event EventHandler Clicked
{
add => Click += value;
remove => Click -= value;
}
void INativeMenuItemExporterEventsImplBridge.RaiseClicked()
{
Clicked?.Invoke(this, new EventArgs());
Click?.Invoke(this, new EventArgs());
if (Command?.CanExecute(CommandParameter) == true)
{

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

@ -231,7 +231,7 @@ namespace Avalonia.Controls.Platform
{
var direction = e.Key.ToNavigationDirection();
if (direction.HasValue)
if (direction?.IsDirectional() == true)
{
if (item == null && _isContextMenu)
{

11
src/Avalonia.Controls/Slider.cs

@ -3,6 +3,7 @@ using Avalonia.Collections;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
@ -94,6 +95,8 @@ namespace Avalonia.Controls
Thumb.DragStartedEvent.AddClassHandler<Slider>((x, e) => x.OnThumbDragStarted(e), RoutingStrategies.Bubble);
Thumb.DragCompletedEvent.AddClassHandler<Slider>((x, e) => x.OnThumbDragCompleted(e),
RoutingStrategies.Bubble);
ValueProperty.OverrideMetadata<Slider>(new DirectPropertyMetadata<double>(enableDataValidation: true));
}
/// <summary>
@ -225,6 +228,14 @@ namespace Avalonia.Controls
Value = IsSnapToTickEnabled ? SnapToTick(finalValue) : finalValue;
}
protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
{
if (property == ValueProperty)
{
DataValidationErrors.SetError(this, value.Error);
}
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(change);

2
src/Avalonia.Input/NavigationDirection.cs

@ -83,7 +83,7 @@ namespace Avalonia.Input
/// </returns>
public static bool IsDirectional(this NavigationDirection direction)
{
return direction > NavigationDirection.Previous ||
return direction > NavigationDirection.Previous &&
direction <= NavigationDirection.PageDown;
}

2
src/Avalonia.Native/AvaloniaNativeMenuExporter.cs

@ -60,7 +60,7 @@ namespace Avalonia.Native
Header = "About Avalonia",
};
aboutItem.Clicked += async (sender, e) =>
aboutItem.Click += async (sender, e) =>
{
var dialog = new AboutAvaloniaDialog();

8
src/Avalonia.Native/Extensions.cs

@ -0,0 +1,8 @@
namespace Avalonia.Native
{
internal static class Extensions
{
public static int AsComBool(this bool b) => b ? 1 : 0;
public static bool FromComBool(this int b) => b != 0;
}
}

2
src/Avalonia.Native/IAvnMenuItem.cs

@ -24,7 +24,7 @@ namespace Avalonia.Native.Interop.Impl
private void UpdateTitle(string title) => SetTitle(title ?? "");
private void UpdateIsChecked(bool isChecked) => SetIsChecked(isChecked);
private void UpdateIsChecked(bool isChecked) => SetIsChecked(isChecked.AsComBool());
private void UpdateToggleType(NativeMenuItemToggleType toggleType)
{

6
src/Avalonia.Native/PlatformThreadingInterface.cs

@ -33,9 +33,9 @@ namespace Avalonia.Native
_parent = parent;
}
public void Signaled(int priority, bool priorityContainsMeaningfulValue)
public void Signaled(int priority, int priorityContainsMeaningfulValue)
{
_parent.Signaled?.Invoke(priorityContainsMeaningfulValue ? (DispatcherPriority?)priority : null);
_parent.Signaled?.Invoke(priorityContainsMeaningfulValue.FromComBool() ? (DispatcherPriority?)priority : null);
}
}
@ -50,7 +50,7 @@ namespace Avalonia.Native
_native.SetSignaledCallback(cb);
}
public bool CurrentThreadIsLoopThread => _native.CurrentThreadIsLoopThread;
public bool CurrentThreadIsLoopThread => _native.CurrentThreadIsLoopThread.FromComBool();
public event Action<DispatcherPriority?> Signaled;

4
src/Avalonia.Native/PopupImpl.cs

@ -50,9 +50,9 @@ namespace Avalonia.Native
// NOP on Popup
}
bool IAvnWindowEvents.Closing()
int IAvnWindowEvents.Closing()
{
return true;
return true.AsComBool();
}
void IAvnWindowEvents.WindowStateChanged(AvnWindowState state)

4
src/Avalonia.Native/PredicateCallback.cs

@ -12,9 +12,9 @@ namespace Avalonia.Native
_predicate = predicate;
}
bool IAvnPredicateCallback.Evaluate()
int IAvnPredicateCallback.Evaluate()
{
return _predicate();
return _predicate().AsComBool();
}
}
}

2
src/Avalonia.Native/ScreenImpl.cs

@ -33,7 +33,7 @@ namespace Avalonia.Native
screen.PixelDensity,
screen.Bounds.ToAvaloniaPixelRect(),
screen.WorkingArea.ToAvaloniaPixelRect(),
screen.Primary);
screen.Primary.FromComBool());
}
return result;

2
src/Avalonia.Native/SystemDialogs.cs

@ -26,7 +26,7 @@ namespace Avalonia.Native
if (dialog is OpenFileDialog ofd)
{
_native.OpenFileDialog(nativeParent,
events, ofd.AllowMultiple,
events, ofd.AllowMultiple.AsComBool(),
ofd.Title ?? "",
ofd.Directory ?? "",
ofd.InitialFileName ?? "",

12
src/Avalonia.Native/WindowImpl.cs

@ -42,14 +42,14 @@ namespace Avalonia.Native
_parent = parent;
}
bool IAvnWindowEvents.Closing()
int IAvnWindowEvents.Closing()
{
if (_parent.Closing != null)
{
return _parent.Closing();
return _parent.Closing().AsComBool();
}
return true;
return true.AsComBool();
}
void IAvnWindowEvents.WindowStateChanged(AvnWindowState state)
@ -69,7 +69,7 @@ namespace Avalonia.Native
public void CanResize(bool value)
{
_native.SetCanResize(value);
_native.SetCanResize(value.AsComBool());
}
public void SetSystemDecorations(Controls.SystemDecorations enabled)
@ -145,7 +145,7 @@ namespace Avalonia.Native
{
_isExtended = extendIntoClientAreaHint;
_native.SetExtendClientArea(extendIntoClientAreaHint);
_native.SetExtendClientArea(extendIntoClientAreaHint.AsComBool());
InvalidateExtendedMargins();
}
@ -198,7 +198,7 @@ namespace Avalonia.Native
public void SetEnabled(bool enable)
{
_native.SetEnabled(enable);
_native.SetEnabled(enable.AsComBool());
}
}
}

12
src/Avalonia.Native/WindowImplBase.cs

@ -192,14 +192,14 @@ namespace Avalonia.Native
_parent.RawMouseEvent(type, timeStamp, modifiers, point, delta);
}
bool IAvnWindowBaseEvents.RawKeyEvent(AvnRawKeyEventType type, uint timeStamp, AvnInputModifiers modifiers, uint key)
int IAvnWindowBaseEvents.RawKeyEvent(AvnRawKeyEventType type, uint timeStamp, AvnInputModifiers modifiers, uint key)
{
return _parent.RawKeyEvent(type, timeStamp, modifiers, key);
return _parent.RawKeyEvent(type, timeStamp, modifiers, key).AsComBool();
}
bool IAvnWindowBaseEvents.RawTextInputEvent(uint timeStamp, string text)
int IAvnWindowBaseEvents.RawTextInputEvent(uint timeStamp, string text)
{
return _parent.RawTextInputEvent(timeStamp, text);
return _parent.RawTextInputEvent(timeStamp, text).AsComBool();
}
@ -388,7 +388,7 @@ namespace Avalonia.Native
public void SetTopmost(bool value)
{
_native.SetTopMost(value);
_native.SetTopMost(value.AsComBool());
}
public double RenderScaling => _native?.Scaling ?? 1;
@ -456,7 +456,7 @@ namespace Avalonia.Native
TransparencyLevel = transparencyLevel;
_native?.SetBlurEnabled(TransparencyLevel >= WindowTransparencyLevel.Blur);
_native?.SetBlurEnabled((TransparencyLevel >= WindowTransparencyLevel.Blur).AsComBool());
TransparencyLevelChanged?.Invoke(TransparencyLevel);
}
}

1
src/Avalonia.Native/avn.idl

@ -1,5 +1,6 @@
@clr-namespace Avalonia.Native.Interop
@clr-access internal
@clr-map bool int
@cpp-preamble @@
#include "com.h"
#include "key.h"

3
src/Avalonia.Styling/ApiCompatBaseline.txt

@ -0,0 +1,3 @@
Compat issues with assembly Avalonia.Styling:
MembersMustExist : Member 'public System.IObservable<System.Object> Avalonia.Controls.ResourceNodeExtensions.GetResourceObservable(Avalonia.IStyledElement, System.Object, System.Func<System.Object, System.Object>)' does not exist in the implementation but it does exist in the contract.
Total Issues: 1

13
src/Avalonia.Styling/Controls/ResourceNodeExtensions.cs

@ -60,7 +60,7 @@ namespace Avalonia.Controls
}
public static IObservable<object?> GetResourceObservable(
this IStyledElement control,
this IResourceHost control,
object key,
Func<object?, object?>? converter = null)
{
@ -83,11 +83,11 @@ namespace Avalonia.Controls
private class ResourceObservable : LightweightObservableBase<object?>
{
private readonly IStyledElement _target;
private readonly IResourceHost _target;
private readonly object _key;
private readonly Func<object?, object?>? _converter;
public ResourceObservable(IStyledElement target, object key, Func<object?, object?>? converter)
public ResourceObservable(IResourceHost target, object key, Func<object?, object?>? converter)
{
_target = target;
_key = key;
@ -147,13 +147,16 @@ namespace Avalonia.Controls
{
if (_target.Owner is object)
{
observer.OnNext(Convert(_target.Owner?.FindResource(_key)));
observer.OnNext(Convert(_target.Owner.FindResource(_key)));
}
}
private void PublishNext()
{
PublishNext(Convert(_target.Owner?.FindResource(_key)));
if (_target.Owner is object)
{
PublishNext(Convert(_target.Owner.FindResource(_key)));
}
}
private void OwnerChanged(object sender, EventArgs e)

15
src/Avalonia.Themes.Default/PathIcon.xaml

@ -2,16 +2,19 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style Selector="PathIcon">
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundColor}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="Height" Value="{DynamicResource IconElementThemeHeight}" />
<Setter Property="Width" Value="{DynamicResource IconElementThemeWidth}" />
<Setter Property="Template">
<ControlTemplate>
<Viewbox Height="{TemplateBinding Height}"
Width="{TemplateBinding Width}">
<Path Fill="{TemplateBinding Foreground}"
Data="{TemplateBinding Data}"
Stretch="Uniform" />
</Viewbox>
<Border Background="{TemplateBinding Background}">
<Viewbox Height="{TemplateBinding Height}"
Width="{TemplateBinding Width}">
<Path Fill="{TemplateBinding Foreground}"
Data="{TemplateBinding Data}"
Stretch="Uniform" />
</Viewbox>
</Border>
</ControlTemplate>
</Setter>
</Style>

2
src/Avalonia.Themes.Default/ToggleSwitch.xaml

@ -5,7 +5,7 @@
<Thickness x:Key="ToggleSwitchTopHeaderMargin">0,0,0,6</Thickness>
<GridLength x:Key="ToggleSwitchPreContentMargin">6</GridLength>
<GridLength x:Key="ToggleSwitchPostContentMargin">6</GridLength>
<x:Double x:Key="ToggleSwitchThemeMinWidth">154</x:Double>
<x:Double x:Key="ToggleSwitchThemeMinWidth">0</x:Double>
<Thickness x:Key="ToggleSwitchOnStrokeThickness">0</Thickness>
<Thickness x:Key="ToggleSwitchOuterBorderStrokeThickness">1</Thickness>
<SolidColorBrush x:Key="ToggleSwitchContentForeground" Color="{DynamicResource ThemeForegroundColor}" />

15
src/Avalonia.Themes.Fluent/PathIcon.xaml

@ -10,16 +10,19 @@
</Design.PreviewWith>
<Style Selector="PathIcon">
<Setter Property="Foreground" Value="{DynamicResource TextControlForeground}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="Height" Value="{DynamicResource IconElementThemeHeight}" />
<Setter Property="Width" Value="{DynamicResource IconElementThemeWidth}" />
<Setter Property="Template">
<ControlTemplate>
<Viewbox Height="{TemplateBinding Height}"
Width="{TemplateBinding Width}">
<Path Fill="{TemplateBinding Foreground}"
Data="{TemplateBinding Data}"
Stretch="Uniform" />
</Viewbox>
<Border Background="{TemplateBinding Background}">
<Viewbox Height="{TemplateBinding Height}"
Width="{TemplateBinding Width}">
<Path Fill="{TemplateBinding Foreground}"
Data="{TemplateBinding Data}"
Stretch="Uniform" />
</Viewbox>
</Border>
</ControlTemplate>
</Setter>
</Style>

2
src/Avalonia.Themes.Fluent/ToggleSwitch.xaml

@ -5,7 +5,7 @@
<Thickness x:Key="ToggleSwitchTopHeaderMargin">0,0,0,6</Thickness>
<GridLength x:Key="ToggleSwitchPreContentMargin">6</GridLength>
<GridLength x:Key="ToggleSwitchPostContentMargin">6</GridLength>
<x:Double x:Key="ToggleSwitchThemeMinWidth">154</x:Double>
<x:Double x:Key="ToggleSwitchThemeMinWidth">0</x:Double>
</Styles.Resources>
<Design.PreviewWith>
<StackPanel Margin="20" Width="250" Spacing="24" >

42
src/Avalonia.Visuals/ApiCompatBaseline.txt

@ -1,39 +1,5 @@
Compat issues with assembly Avalonia.Visuals:
MembersMustExist : Member 'public void Avalonia.Media.DrawingContext.DrawGlyphRun(Avalonia.Media.IBrush, Avalonia.Media.GlyphRun, Avalonia.Point)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Media.Typeface Avalonia.Media.FontManager.GetOrAddTypeface(Avalonia.Media.FontFamily, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Media.Typeface Avalonia.Media.FontManager.MatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Rect Avalonia.Media.Geometry.GetRenderBounds(Avalonia.Media.Pen)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public System.Boolean Avalonia.Media.Geometry.StrokeContains(Avalonia.Media.Pen, Avalonia.Point)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Rect Avalonia.Media.GlyphRun.Bounds.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.StyledProperty<Avalonia.Point> Avalonia.StyledProperty<Avalonia.Point> Avalonia.Media.GlyphRunDrawing.BaselineOriginProperty' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Point Avalonia.Media.GlyphRunDrawing.BaselineOrigin.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.GlyphRunDrawing.BaselineOrigin.set(Avalonia.Point)' does not exist in the implementation but it does exist in the contract.
CannotSealType : Type 'Avalonia.Media.Typeface' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract.
TypeCannotChangeClassification : Type 'Avalonia.Media.Typeface' is a 'struct' in the implementation but is a 'class' in the contract.
CannotMakeMemberNonVirtual : Member 'public System.Boolean Avalonia.Media.Typeface.Equals(System.Object)' is non-virtual in the implementation but is virtual in the contract.
CannotMakeMemberNonVirtual : Member 'public System.Int32 Avalonia.Media.Typeface.GetHashCode()' is non-virtual in the implementation but is virtual in the contract.
TypesMustExist : Type 'Avalonia.Media.Fonts.FontKey' does not exist in the implementation but it does exist in the contract.
CannotAddAbstractMembers : Member 'public Avalonia.Size Avalonia.Media.TextFormatting.DrawableTextRun.Size' is abstract in the implementation but is missing in the contract.
MembersMustExist : Member 'public Avalonia.Rect Avalonia.Media.TextFormatting.DrawableTextRun.Bounds.get()' does not exist in the implementation but it does exist in the contract.
CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext)' is abstract in the implementation but is missing in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' does not exist in the implementation but it does exist in the contract.
CannotAddAbstractMembers : Member 'public Avalonia.Size Avalonia.Media.TextFormatting.DrawableTextRun.Size.get()' is abstract in the implementation but is missing in the contract.
MembersMustExist : Member 'public Avalonia.Rect Avalonia.Media.TextFormatting.ShapedTextCharacters.Bounds.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.ShapedTextCharacters.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLayout.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' does not exist in the implementation but it does exist in the contract.
CannotAddAbstractMembers : Member 'public Avalonia.Media.TextFormatting.TextLineBreak Avalonia.Media.TextFormatting.TextLine.TextLineBreak' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.TextLine.Draw(Avalonia.Media.DrawingContext)' is abstract in the implementation but is missing in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLine.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextLineBreak Avalonia.Media.TextFormatting.TextLine.LineBreak.get()' does not exist in the implementation but it does exist in the contract.
CannotAddAbstractMembers : Member 'public Avalonia.Media.TextFormatting.TextLineBreak Avalonia.Media.TextFormatting.TextLine.TextLineBreak.get()' is abstract in the implementation but is missing in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IDrawingContextLayerImpl Avalonia.Platform.IDrawingContextImpl.CreateLayer(Avalonia.Size)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IRenderTargetBitmapImpl Avalonia.Platform.IDrawingContextImpl.CreateLayer(Avalonia.Size)' is present in the contract but not in the implementation.
MembersMustExist : Member 'public Avalonia.Platform.IRenderTargetBitmapImpl Avalonia.Platform.IDrawingContextImpl.CreateLayer(Avalonia.Size)' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.DrawGlyphRun(Avalonia.Media.IBrush, Avalonia.Media.GlyphRun)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.DrawGlyphRun(Avalonia.Media.IBrush, Avalonia.Media.GlyphRun, Avalonia.Point)' is present in the contract but not in the implementation.
MembersMustExist : Member 'public void Avalonia.Platform.IDrawingContextImpl.DrawGlyphRun(Avalonia.Media.IBrush, Avalonia.Media.GlyphRun, Avalonia.Point)' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo, Avalonia.Media.Fonts.FontKey)' is present in the contract but not in the implementation.
MembersMustExist : Member 'public System.Boolean Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo, Avalonia.Media.Fonts.FontKey)' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo, Avalonia.Media.Typeface)' is present in the implementation but not in the contract.
MembersMustExist : Member 'public Avalonia.Utilities.IRef<Avalonia.Platform.IRenderTargetBitmapImpl> Avalonia.Rendering.RenderLayer.Bitmap.get()' does not exist in the implementation but it does exist in the contract.
Total Issues: 37
MembersMustExist : Member 'public Avalonia.StyledProperty<System.Collections.Generic.IReadOnlyList<System.Double>> Avalonia.StyledProperty<System.Collections.Generic.IReadOnlyList<System.Double>> Avalonia.Media.DashStyle.DashesProperty' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public System.Collections.Generic.IReadOnlyList<System.Double> Avalonia.Media.DashStyle.Dashes.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.DashStyle.Dashes.set(System.Collections.Generic.IReadOnlyList<System.Double>)' does not exist in the implementation but it does exist in the contract.
Total Issues: 3

14
src/Avalonia.Visuals/Media/Color.cs

@ -1,17 +1,24 @@
using System;
using System.Globalization;
#if !BUILDTASK
using Avalonia.Animation.Animators;
#endif
namespace Avalonia.Media
{
/// <summary>
/// An ARGB color.
/// </summary>
public readonly struct Color : IEquatable<Color>
#if !BUILDTASK
public
#endif
readonly struct Color : IEquatable<Color>
{
static Color()
{
#if !BUILDTASK
Animation.Animation.RegisterAnimator<ColorAnimator>(prop => typeof(Color).IsAssignableFrom(prop.PropertyType));
#endif
}
/// <summary>
@ -223,7 +230,12 @@ namespace Avalonia.Media
if (input.Length == 3 || input.Length == 4)
{
var extendedLength = 2 * input.Length;
#if !BUILDTASK
Span<char> extended = stackalloc char[extendedLength];
#else
char[] extended = new char[extendedLength];
#endif
for (int i = 0; i < input.Length; i++)
{

75
src/Avalonia.Visuals/Media/DashStyle.cs

@ -1,11 +1,14 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using Avalonia.Animation;
using Avalonia.Collections;
using Avalonia.Media.Immutable;
#nullable enable
namespace Avalonia.Media
{
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Animation;
using Avalonia.Media.Immutable;
/// <summary>
/// Represents the sequence of dashes and gaps that will be applied by a <see cref="Pen"/>.
/// </summary>
@ -14,8 +17,8 @@ namespace Avalonia.Media
/// <summary>
/// Defines the <see cref="Dashes"/> property.
/// </summary>
public static readonly StyledProperty<IReadOnlyList<double>> DashesProperty =
AvaloniaProperty.Register<DashStyle, IReadOnlyList<double>>(nameof(Dashes));
public static readonly StyledProperty<AvaloniaList<double>> DashesProperty =
AvaloniaProperty.Register<DashStyle, AvaloniaList<double>>(nameof(Dashes));
/// <summary>
/// Defines the <see cref="Offset"/> property.
@ -23,10 +26,10 @@ namespace Avalonia.Media
public static readonly StyledProperty<double> OffsetProperty =
AvaloniaProperty.Register<DashStyle, double>(nameof(Offset));
private static ImmutableDashStyle s_dash;
private static ImmutableDashStyle s_dot;
private static ImmutableDashStyle s_dashDot;
private static ImmutableDashStyle s_dashDotDot;
private static ImmutableDashStyle? s_dash;
private static ImmutableDashStyle? s_dot;
private static ImmutableDashStyle? s_dashDot;
private static ImmutableDashStyle? s_dashDotDot;
/// <summary>
/// Initializes a new instance of the <see cref="DashStyle"/> class.
@ -41,9 +44,9 @@ namespace Avalonia.Media
/// </summary>
/// <param name="dashes">The dashes collection.</param>
/// <param name="offset">The dash sequence offset.</param>
public DashStyle(IEnumerable<double> dashes, double offset)
public DashStyle(IEnumerable<double>? dashes, double offset)
{
Dashes = (IReadOnlyList<double>)dashes?.ToList() ?? Array.Empty<double>();
Dashes = (dashes as AvaloniaList<double>) ?? new AvaloniaList<double>(dashes ?? Array.Empty<double>());
Offset = offset;
}
@ -61,31 +64,27 @@ namespace Avalonia.Media
/// <summary>
/// Represents a dashed <see cref="DashStyle"/>.
/// </summary>
public static IDashStyle Dash =>
s_dash ?? (s_dash = new ImmutableDashStyle(new double[] { 2, 2 }, 1));
public static IDashStyle Dash => s_dash ??= new ImmutableDashStyle(new double[] { 2, 2 }, 1);
/// <summary>
/// Represents a dotted <see cref="DashStyle"/>.
/// </summary>
public static IDashStyle Dot =>
s_dot ?? (s_dot = new ImmutableDashStyle(new double[] { 0, 2 }, 0));
public static IDashStyle Dot => s_dot ??= new ImmutableDashStyle(new double[] { 0, 2 }, 0);
/// <summary>
/// Represents a dashed dotted <see cref="DashStyle"/>.
/// </summary>
public static IDashStyle DashDot =>
s_dashDot ?? (s_dashDot = new ImmutableDashStyle(new double[] { 2, 2, 0, 2 }, 1));
public static IDashStyle DashDot => s_dashDot ??= new ImmutableDashStyle(new double[] { 2, 2, 0, 2 }, 1);
/// <summary>
/// Represents a dashed double dotted <see cref="DashStyle"/>.
/// </summary>
public static IDashStyle DashDotDot =>
s_dashDotDot ?? (s_dashDotDot = new ImmutableDashStyle(new double[] { 2, 2, 0, 2, 0, 2 }, 1));
public static IDashStyle DashDotDot => s_dashDotDot ??= new ImmutableDashStyle(new double[] { 2, 2, 0, 2, 0, 2 }, 1);
/// <summary>
/// Gets or sets the length of alternating dashes and gaps.
/// </summary>
public IReadOnlyList<double> Dashes
public AvaloniaList<double> Dashes
{
get => GetValue(DashesProperty);
set => SetValue(DashesProperty, value);
@ -100,15 +99,43 @@ namespace Avalonia.Media
set => SetValue(OffsetProperty, value);
}
IReadOnlyList<double> IDashStyle.Dashes => Dashes;
/// <summary>
/// Raised when the dash style changes.
/// </summary>
public event EventHandler Invalidated;
public event EventHandler? Invalidated;
/// <summary>
/// Returns an immutable clone of the <see cref="DashStyle"/>.
/// </summary>
/// <returns></returns>
public ImmutableDashStyle ToImmutable() => new ImmutableDashStyle(Dashes, Offset);
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(change);
if (change.Property == DashesProperty)
{
var oldValue = change.OldValue.GetValueOrDefault<AvaloniaList<double>>();
var newValue = change.NewValue.GetValueOrDefault<AvaloniaList<double>>();
if (oldValue is object)
{
oldValue.CollectionChanged -= DashesChanged;
}
if (newValue is object)
{
newValue.CollectionChanged += DashesChanged;
}
}
}
private void DashesChanged(object sender, NotifyCollectionChangedEventArgs e)
{
Invalidated?.Invoke(this, e);
}
}
}

56
src/Avalonia.Visuals/Media/EllipseGeometry.cs

@ -12,10 +12,28 @@ namespace Avalonia.Media
/// </summary>
public static readonly StyledProperty<Rect> RectProperty =
AvaloniaProperty.Register<EllipseGeometry, Rect>(nameof(Rect));
/// <summary>
/// Defines the <see cref="RadiusX"/> property.
/// </summary>
public static readonly StyledProperty<double> RadiusXProperty =
AvaloniaProperty.Register<EllipseGeometry, double>(nameof(RadiusX));
/// <summary>
/// Defines the <see cref="RadiusY"/> property.
/// </summary>
public static readonly StyledProperty<double> RadiusYProperty =
AvaloniaProperty.Register<EllipseGeometry, double>(nameof(RadiusY));
/// <summary>
/// Defines the <see cref="Center"/> property.
/// </summary>
public static readonly StyledProperty<Point> CenterProperty =
AvaloniaProperty.Register<EllipseGeometry, Point>(nameof(Center));
static EllipseGeometry()
{
AffectsGeometry(RectProperty);
AffectsGeometry(RectProperty, RadiusXProperty, RadiusYProperty, CenterProperty);
}
/// <summary>
@ -43,6 +61,33 @@ namespace Avalonia.Media
set => SetValue(RectProperty, value);
}
/// <summary>
/// Gets or sets a double that defines the radius in the X-axis of the ellipse.
/// </summary>
public double RadiusX
{
get => GetValue(RadiusXProperty);
set => SetValue(RadiusXProperty, value);
}
/// <summary>
/// Gets or sets a double that defines the radius in the Y-axis of the ellipse.
/// </summary>
public double RadiusY
{
get => GetValue(RadiusYProperty);
set => SetValue(RadiusYProperty, value);
}
/// <summary>
/// Gets or sets a point that defines the center of the ellipse.
/// </summary>
public Point Center
{
get => GetValue(CenterProperty);
set => SetValue(CenterProperty, value);
}
/// <inheritdoc/>
public override Geometry Clone()
{
@ -54,7 +99,14 @@ namespace Avalonia.Media
{
var factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
return factory.CreateEllipseGeometry(Rect);
if (Rect != default) return factory.CreateEllipseGeometry(Rect);
var originX = Center.X - RadiusX;
var originY = Center.Y - RadiusY;
var width = RadiusX * 2;
var height = RadiusY * 2;
return factory.CreateEllipseGeometry(new Rect(originX, originY, width, height));
}
}
}

9
src/Avalonia.Visuals/Media/KnownColors.cs

@ -8,7 +8,9 @@ namespace Avalonia.Media
{
private static readonly IReadOnlyDictionary<string, KnownColor> _knownColorNames;
private static readonly IReadOnlyDictionary<uint, string> _knownColors;
#if !BUILDTASK
private static readonly Dictionary<KnownColor, ISolidColorBrush> _knownBrushes;
#endif
static KnownColors()
{
@ -32,14 +34,19 @@ namespace Avalonia.Media
_knownColorNames = knownColorNames;
_knownColors = knownColors;
#if !BUILDTASK
_knownBrushes = new Dictionary<KnownColor, ISolidColorBrush>();
#endif
}
#if !BUILDTASK
public static ISolidColorBrush GetKnownBrush(string s)
{
var color = GetKnownColor(s);
return color != KnownColor.None ? color.ToBrush() : null;
}
#endif
public static KnownColor GetKnownColor(string s)
{
@ -61,6 +68,7 @@ namespace Avalonia.Media
return Color.FromUInt32((uint)color);
}
#if !BUILDTASK
public static ISolidColorBrush ToBrush(this KnownColor color)
{
lock (_knownBrushes)
@ -74,6 +82,7 @@ namespace Avalonia.Media
return brush;
}
}
#endif
}
internal enum KnownColor : uint

72
src/Avalonia.Visuals/Media/PathFigure.cs

@ -1,3 +1,7 @@
#nullable enable
using System;
using System.Linq;
using Avalonia.Collections;
using Avalonia.Metadata;
namespace Avalonia.Media
@ -8,22 +12,36 @@ namespace Avalonia.Media
/// Defines the <see cref="IsClosed"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsClosedProperty
= AvaloniaProperty.Register<PathFigure, bool>(nameof(IsClosed), true);
= AvaloniaProperty.Register<PathFigure, bool>(nameof(IsClosed), true);
/// <summary>
/// Defines the <see cref="IsFilled"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsFilledProperty
= AvaloniaProperty.Register<PathFigure, bool>(nameof(IsFilled), true);
= AvaloniaProperty.Register<PathFigure, bool>(nameof(IsFilled), true);
/// <summary>
/// Defines the <see cref="Segments"/> property.
/// </summary>
public static readonly DirectProperty<PathFigure, PathSegments> SegmentsProperty
= AvaloniaProperty.RegisterDirect<PathFigure, PathSegments>(nameof(Segments), f => f.Segments, (f, s) => f.Segments = s);
public static readonly DirectProperty<PathFigure, PathSegments?> SegmentsProperty
= AvaloniaProperty.RegisterDirect<PathFigure, PathSegments?>(
nameof(Segments),
f => f.Segments,
(f, s) => f.Segments = s);
/// <summary>
/// Defines the <see cref="StartPoint"/> property.
/// </summary>
public static readonly StyledProperty<Point> StartPointProperty
= AvaloniaProperty.Register<PathFigure, Point>(nameof(StartPoint));
= AvaloniaProperty.Register<PathFigure, Point>(nameof(StartPoint));
internal event EventHandler? SegmentsInvalidated;
private PathSegments? _segments;
private IDisposable? _segmentsDisposable;
private IDisposable? _segmentsPropertiesDisposable;
/// <summary>
/// Initializes a new instance of the <see cref="PathFigure"/> class.
@ -33,6 +51,31 @@ namespace Avalonia.Media
Segments = new PathSegments();
}
static PathFigure()
{
SegmentsProperty.Changed.AddClassHandler<PathFigure>(
(s, e) =>
s.OnSegmentsChanged());
}
private void OnSegmentsChanged()
{
_segmentsDisposable?.Dispose();
_segmentsPropertiesDisposable?.Dispose();
_segmentsDisposable = _segments?.ForEachItem(
_ => InvalidateSegments(),
_ => InvalidateSegments(),
InvalidateSegments);
_segmentsPropertiesDisposable = _segments?.TrackItemPropertyChanged(_ => InvalidateSegments());
}
private void InvalidateSegments()
{
SegmentsInvalidated?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Gets or sets a value indicating whether this instance is closed.
/// </summary>
@ -64,7 +107,7 @@ namespace Avalonia.Media
/// The segments.
/// </value>
[Content]
public PathSegments Segments
public PathSegments? Segments
{
get { return _segments; }
set { SetAndRaise(SegmentsProperty, ref _segments, value); }
@ -81,22 +124,23 @@ namespace Avalonia.Media
get { return GetValue(StartPointProperty); }
set { SetValue(StartPointProperty, value); }
}
public override string ToString()
=> $"M {StartPoint} {string.Join(" ", _segments ?? Enumerable.Empty<PathSegment>())}{(IsClosed ? "Z" : "")}";
internal void ApplyTo(StreamGeometryContext ctx)
{
ctx.BeginFigure(StartPoint, IsFilled);
foreach (var segment in Segments)
if (Segments != null)
{
segment.ApplyTo(ctx);
foreach (var segment in Segments)
{
segment.ApplyTo(ctx);
}
}
ctx.EndFigure(IsClosed);
}
private PathSegments _segments;
public override string ToString()
=> $"M {StartPoint} {string.Join(" ", _segments)}{(IsClosed ? "Z" : "")}";
}
}
}

20
src/Avalonia.Visuals/Media/PathGeometry.cs

@ -104,12 +104,26 @@ namespace Avalonia.Media
_figuresPropertiesObserver?.Dispose();
_figuresObserver = figures?.ForEachItem(
_ => InvalidateGeometry(),
_ => InvalidateGeometry(),
() => InvalidateGeometry());
s =>
{
s.SegmentsInvalidated += InvalidateGeometryFromSegments;
InvalidateGeometry();
},
s =>
{
s.SegmentsInvalidated -= InvalidateGeometryFromSegments;
InvalidateGeometry();
},
InvalidateGeometry);
_figuresPropertiesObserver = figures?.TrackItemPropertyChanged(_ => InvalidateGeometry());
}
private void InvalidateGeometryFromSegments(object _, EventArgs __)
{
InvalidateGeometry();
}
public override string ToString()
=> $"{(FillRule != FillRule.EvenOdd ? "F1 " : "")}{(string.Join(" ", Figures))}";

2
src/Avalonia.X11/XI2Manager.cs

@ -351,7 +351,7 @@ namespace Avalonia.X11
if (state.HasFlag(XModifierMask.Mod4Mask))
Modifiers |= RawInputModifiers.Meta;
Modifiers = ParseButtonState(ev->buttons.MaskLen, ev->buttons.Mask);
Modifiers |= ParseButtonState(ev->buttons.MaskLen, ev->buttons.Mask);
Valuators = new Dictionary<int, double>();
Position = new Point(ev->event_x, ev->event_y);

34
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AstNodes/AvaloniaXamlIlGridLengthAstNode.cs

@ -0,0 +1,34 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers;
using XamlX.Ast;
using XamlX.Emit;
using XamlX.IL;
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.AstNodes
{
class AvaloniaXamlIlGridLengthAstNode : XamlAstNode, IXamlAstValueNode, IXamlAstILEmitableNode
{
private readonly AvaloniaXamlIlWellKnownTypes _types;
private readonly GridLength _gridLength;
public AvaloniaXamlIlGridLengthAstNode(IXamlLineInfo lineInfo, AvaloniaXamlIlWellKnownTypes types, GridLength gridLength) : base(lineInfo)
{
_types = types;
_gridLength = gridLength;
Type = new XamlAstClrTypeReference(lineInfo, types.GridLength, false);
}
public IXamlAstTypeReference Type { get; }
public XamlILNodeEmitResult Emit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen)
{
codeGen
.Ldc_R8(_gridLength.Value)
.Ldc_I4((int)_gridLength.GridUnitType)
.Newobj(_types.GridLengthConstructorValueType);
return XamlILNodeEmitResult.Type(0, Type.GetClrType());
}
}
}

147
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs

@ -2,8 +2,10 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.AstNodes;
using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers;
using Avalonia.Media;
using XamlX;
using XamlX.Ast;
using XamlX.Emit;
@ -205,67 +207,152 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
result = new AvaloniaXamlIlFontFamilyAstNode(types, text, node);
return true;
}
if (type.Equals(types.Thickness))
{
var thickness = Thickness.Parse(text);
result = new AvaloniaXamlIlVectorLikeConstantAstNode(node, types, types.Thickness, types.ThicknessFullConstructor,
new[] { thickness.Left, thickness.Top, thickness.Right, thickness.Bottom });
try
{
var thickness = Thickness.Parse(text);
result = new AvaloniaXamlIlVectorLikeConstantAstNode(node, types, types.Thickness, types.ThicknessFullConstructor,
new[] { thickness.Left, thickness.Top, thickness.Right, thickness.Bottom });
return true;
return true;
}
catch
{
throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a thickness", node);
}
}
if (type.Equals(types.Point))
{
var point = Point.Parse(text);
result = new AvaloniaXamlIlVectorLikeConstantAstNode(node, types, types.Point, types.PointFullConstructor,
new[] { point.X, point.Y });
try
{
var point = Point.Parse(text);
result = new AvaloniaXamlIlVectorLikeConstantAstNode(node, types, types.Point, types.PointFullConstructor,
new[] { point.X, point.Y });
return true;
return true;
}
catch
{
throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a point", node);
}
}
if (type.Equals(types.Vector))
{
var vector = Vector.Parse(text);
result = new AvaloniaXamlIlVectorLikeConstantAstNode(node, types, types.Vector, types.VectorFullConstructor,
new[] { vector.X, vector.Y });
try
{
var vector = Vector.Parse(text);
result = new AvaloniaXamlIlVectorLikeConstantAstNode(node, types, types.Vector, types.VectorFullConstructor,
new[] { vector.X, vector.Y });
return true;
return true;
}
catch
{
throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a vector", node);
}
}
if (type.Equals(types.Size))
{
var size = Size.Parse(text);
result = new AvaloniaXamlIlVectorLikeConstantAstNode(node, types, types.Size, types.SizeFullConstructor,
new[] { size.Width, size.Height });
try
{
var size = Size.Parse(text);
return true;
result = new AvaloniaXamlIlVectorLikeConstantAstNode(node, types, types.Size, types.SizeFullConstructor,
new[] { size.Width, size.Height });
return true;
}
catch
{
throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a size", node);
}
}
if (type.Equals(types.Matrix))
{
var matrix = Matrix.Parse(text);
result = new AvaloniaXamlIlVectorLikeConstantAstNode(node, types, types.Matrix, types.MatrixFullConstructor,
new[] { matrix.M11, matrix.M12, matrix.M21, matrix.M22, matrix.M31, matrix.M32 });
try
{
var matrix = Matrix.Parse(text);
result = new AvaloniaXamlIlVectorLikeConstantAstNode(node, types, types.Matrix, types.MatrixFullConstructor,
new[] { matrix.M11, matrix.M12, matrix.M21, matrix.M22, matrix.M31, matrix.M32 });
return true;
return true;
}
catch
{
throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a matrix", node);
}
}
if (type.Equals(types.CornerRadius))
{
var cornerRadius = CornerRadius.Parse(text);
result = new AvaloniaXamlIlVectorLikeConstantAstNode(node, types, types.CornerRadius, types.CornerRadiusFullConstructor,
new[] { cornerRadius.TopLeft, cornerRadius.TopRight, cornerRadius.BottomRight, cornerRadius.BottomLeft });
try
{
var cornerRadius = CornerRadius.Parse(text);
result = new AvaloniaXamlIlVectorLikeConstantAstNode(node, types, types.CornerRadius, types.CornerRadiusFullConstructor,
new[] { cornerRadius.TopLeft, cornerRadius.TopRight, cornerRadius.BottomRight, cornerRadius.BottomLeft });
return true;
}
catch
{
throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a corner radius", node);
}
}
if (type.Equals(types.Color))
{
if (!Color.TryParse(text, out Color color))
{
throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a color", node);
}
result = new XamlStaticOrTargetedReturnMethodCallNode(node,
type.GetMethod(
new FindMethodMethodSignature("FromUInt32", type, types.UInt) { IsStatic = true }),
new[] { new XamlConstantNode(node, types.UInt, color.ToUint32()) });
return true;
}
if (type.Equals(types.GridLength))
{
try
{
var gridLength = GridLength.Parse(text);
result = new AvaloniaXamlIlGridLengthAstNode(node, types, gridLength);
return true;
}
catch
{
throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a grid length", node);
}
}
if (type.Equals(types.Cursor))
{
if (TypeSystemHelpers.TryGetEnumValueNode(types.StandardCursorType, text, node, out var enumConstantNode))
{
var cursorTypeRef = new XamlAstClrTypeReference(node, types.Cursor, false);
result = new XamlAstNewClrObjectNode(node, cursorTypeRef, types.CursorTypeConstructor, new List<IXamlAstValueNode> { enumConstantNode });
return true;
}
}
if (type.FullName == "Avalonia.AvaloniaProperty")
{
var scope = context.ParentNodes().OfType<AvaloniaXamlIlTargetTypeMetadataNode>().FirstOrDefault();

15
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs

@ -49,6 +49,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlType ReflectionBindingExtension { get; }
public IXamlType RelativeSource { get; }
public IXamlType UInt { get; }
public IXamlType Long { get; }
public IXamlType Uri { get; }
public IXamlType FontFamily { get; }
@ -65,6 +66,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlConstructor MatrixFullConstructor { get; }
public IXamlType CornerRadius { get; }
public IXamlConstructor CornerRadiusFullConstructor { get; }
public IXamlType GridLength { get; }
public IXamlConstructor GridLengthConstructorValueType { get; }
public IXamlType Color { get; }
public IXamlType StandardCursorType { get; }
public IXamlType Cursor { get; }
public IXamlConstructor CursorTypeConstructor { get; }
public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg)
{
@ -122,6 +129,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
ItemsRepeater = cfg.TypeSystem.GetType("Avalonia.Controls.ItemsRepeater");
ReflectionBindingExtension = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.ReflectionBindingExtension");
RelativeSource = cfg.TypeSystem.GetType("Avalonia.Data.RelativeSource");
UInt = cfg.TypeSystem.GetType("System.UInt32");
Long = cfg.TypeSystem.GetType("System.Int64");
Uri = cfg.TypeSystem.GetType("System.Uri");
FontFamily = cfg.TypeSystem.GetType("Avalonia.Media.FontFamily");
@ -141,6 +149,13 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
(Size, SizeFullConstructor) = GetNumericTypeInfo("Avalonia.Size", XamlIlTypes.Double, 2);
(Matrix, MatrixFullConstructor) = GetNumericTypeInfo("Avalonia.Matrix", XamlIlTypes.Double, 6);
(CornerRadius, CornerRadiusFullConstructor) = GetNumericTypeInfo("Avalonia.CornerRadius", XamlIlTypes.Double, 4);
GridLength = cfg.TypeSystem.GetType("Avalonia.Controls.GridLength");
GridLengthConstructorValueType = GridLength.GetConstructor(new List<IXamlType> { XamlIlTypes.Double, cfg.TypeSystem.GetType("Avalonia.Controls.GridUnitType") });
Color = cfg.TypeSystem.GetType("Avalonia.Media.Color");
StandardCursorType = cfg.TypeSystem.GetType("Avalonia.Input.StandardCursorType");
Cursor = cfg.TypeSystem.GetType("Avalonia.Input.Cursor");
CursorTypeConstructor = Cursor.GetConstructor(new List<IXamlType> { StandardCursorType });
}
}

18
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs

@ -10,8 +10,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
{
public class DynamicResourceExtension : IBinding
{
private IStyledElement? _anchor;
private IResourceProvider? _resourceProvider;
private object? _anchor;
public DynamicResourceExtension()
{
@ -30,12 +29,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
if (!(provideTarget.TargetObject is IStyledElement))
{
_anchor = serviceProvider.GetFirstParent<IStyledElement>();
if (_anchor is null)
{
_resourceProvider = serviceProvider.GetFirstParent<IResourceProvider>();
}
_anchor = serviceProvider.GetFirstParent<IStyledElement>() ??
serviceProvider.GetFirstParent<IResourceProvider>() ??
(object?)serviceProvider.GetFirstParent<IResourceHost>();
}
return this;
@ -52,16 +48,16 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
return null;
}
var control = target as IStyledElement ?? _anchor as IStyledElement;
var control = target as IResourceHost ?? _anchor as IResourceHost;
if (control != null)
{
var source = control.GetResourceObservable(ResourceKey, GetConverter(targetProperty));
return InstancedBinding.OneWay(source);
}
else if (_resourceProvider is object)
else if (_anchor is IResourceProvider resourceProvider)
{
var source = _resourceProvider.GetResourceObservable(ResourceKey, GetConverter(targetProperty));
var source = resourceProvider.GetResourceObservable(ResourceKey, GetConverter(targetProperty));
return InstancedBinding.OneWay(source);
}

2
src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs

@ -134,7 +134,7 @@ namespace Avalonia.Win32.WinRT.Composition
public static void TryCreateAndRegister(EglPlatformOpenGlInterface angle)
{
const int majorRequired = 10;
const int buildRequired = 16299;
const int buildRequired = 17134;
var majorInstalled = Win32Platform.WindowsVersion.Major;
var buildInstalled = Win32Platform.WindowsVersion.Build;

3
tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs

@ -17,7 +17,8 @@ namespace Avalonia.Benchmarks.Layout
_app = UnitTestApplication.Start(
TestServices.StyledWindow.With(
renderInterface: new NullRenderingPlatform(),
threadingInterface: new NullThreadingPlatform()));
threadingInterface: new NullThreadingPlatform(),
standardCursorFactory: new NullCursorFactory()));
_root = new TestRoot(true, null)
{

14
tests/Avalonia.Benchmarks/NullCursorFactory.cs

@ -0,0 +1,14 @@
using System;
using Avalonia.Input;
using Avalonia.Platform;
namespace Avalonia.Benchmarks
{
internal class NullCursorFactory : IStandardCursorFactory
{
public IPlatformHandle GetCursor(StandardCursorType cursorType)
{
return new PlatformHandle(IntPtr.Zero, "null");
}
}
}

17
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs

@ -345,6 +345,23 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
Assert.Equal(0xff506070, brush.Color.ToUint32());
}
[Fact]
public void DynamicResource_Can_Be_Assigned_To_Resource_Property_In_Application()
{
var xaml = @"
<Application xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Application.Resources>
<Color x:Key='color'>#ff506070</Color>
<SolidColorBrush x:Key='brush' Color='{DynamicResource color}'/>
</Application.Resources>
</Application>";
var application = (Application)AvaloniaRuntimeXamlLoader.Load(xaml);
var brush = (SolidColorBrush)application.Resources["brush"];
Assert.Equal(0xff506070, brush.Color.ToUint32());
}
[Fact]
public void DynamicResource_Can_Be_Assigned_To_ItemTemplate_Property()

24
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ShapeTests.cs

@ -0,0 +1,24 @@
using Avalonia.Controls;
using Avalonia.Media;
using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Xaml
{
public class ShapeTests : XamlTestBase
{
[Fact]
public void Can_Specify_DashStyle_In_XAML()
{
var xaml = @"
<Pen xmlns='https://github.com/avaloniaui'>
<Pen.DashStyle>
<DashStyle Offset='0' Dashes='1,3'/>
</Pen.DashStyle>
</Pen>";
var target = AvaloniaRuntimeXamlLoader.Parse<Pen>(xaml);
Assert.NotNull(target);
}
}
}

34
tests/Avalonia.Visuals.UnitTests/Media/PathSegmentTests.cs

@ -0,0 +1,34 @@
using Avalonia.Media;
using Xunit;
namespace Avalonia.Visuals.UnitTests.Media
{
public class PathSegmentTests
{
[Fact]
public void PathSegment_Triggers_Invalidation_On_Property_Change()
{
var targetSegment = new ArcSegment()
{
Size = new Size(10, 10),
Point = new Point(5, 5)
};
var target = new PathGeometry
{
Figures = new PathFigures
{
new PathFigure { IsClosed = false, Segments = new PathSegments { targetSegment } }
}
};
var changed = false;
target.Changed += (s, e) => changed = true;
targetSegment.Size = new Size(20, 20);
Assert.True(changed);
}
}
}

18
tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs

@ -1,4 +1,5 @@
using Avalonia.Media;
using Avalonia.Collections;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Xunit;
@ -39,7 +40,20 @@ namespace Avalonia.Visuals.UnitTests.Media
var raised = false;
target.Invalidated += (s, e) => raised = true;
dashes.Dashes = new[] { 0.1, 0.2 };
dashes.Dashes = new AvaloniaList<double> { 0.1, 0.2 };
Assert.True(raised);
}
[Fact]
public void Adding_DashStyle_Dashes_Raises_Invalidated()
{
var dashes = new DashStyle();
var target = new Pen { DashStyle = dashes };
var raised = false;
target.Invalidated += (s, e) => raised = true;
dashes.Dashes.Add(0.3);
Assert.True(raised);
}

Loading…
Cancel
Save