diff --git a/Documentation/build.md b/Documentation/build.md index a7d68eb599..9f5436e68e 100644 --- a/Documentation/build.md +++ b/Documentation/build.md @@ -6,6 +6,7 @@ Avalonia requires at least Visual Studio 2019 and .NET Core SDK 3.1 to build on ``` git clone https://github.com/AvaloniaUI/Avalonia.git +cd Avalonia git submodule update --init ``` diff --git a/packages/Avalonia/AvaloniaBuildTasks.targets b/packages/Avalonia/AvaloniaBuildTasks.targets index 45a7f1aa44..de3830ffea 100644 --- a/packages/Avalonia/AvaloniaBuildTasks.targets +++ b/packages/Avalonia/AvaloniaBuildTasks.targets @@ -42,12 +42,24 @@ - $(BuildAvaloniaResourcesDependsOn);AddAvaloniaResources;ResolveReferences + $(BuildAvaloniaResourcesDependsOn);AddAvaloniaResources;ResolveReferences;_GenerateAvaloniaResourcesDependencyCache + + + + + + + + + + + + diff --git a/src/Avalonia.Base/Metadata/TemplateContent.cs b/src/Avalonia.Base/Metadata/TemplateContent.cs index fcd7d69e7b..7f9e878419 100644 --- a/src/Avalonia.Base/Metadata/TemplateContent.cs +++ b/src/Avalonia.Base/Metadata/TemplateContent.cs @@ -8,5 +8,6 @@ namespace Avalonia.Metadata [AttributeUsage(AttributeTargets.Property)] public class TemplateContentAttribute : Attribute { + public Type TemplateResultType { get; set; } } } diff --git a/src/Avalonia.Controls/Design.cs b/src/Avalonia.Controls/Design.cs index 0d05e19e53..07d2918a88 100644 --- a/src/Avalonia.Controls/Design.cs +++ b/src/Avalonia.Controls/Design.cs @@ -60,6 +60,19 @@ namespace Avalonia.Controls return target.GetValue(PreviewWithProperty); } + public static readonly AttachedProperty DesignStyleProperty = AvaloniaProperty + .RegisterAttached("DesignStyle", typeof(Design)); + + public static void SetDesignStyle(Control control, IStyle value) + { + control.SetValue(DesignStyleProperty, value); + } + + public static IStyle GetDesignStyle(Control control) + { + return control.GetValue(DesignStyleProperty); + } + public static void ApplyDesignModeProperties(Control target, Control source) { if (source.IsSet(WidthProperty)) @@ -68,6 +81,8 @@ namespace Avalonia.Controls target.Height = source.GetValue(HeightProperty); if (source.IsSet(DataContextProperty)) target.DataContext = source.GetValue(DataContextProperty); + if (source.IsSet(DesignStyleProperty)) + target.Styles.Add(source.GetValue(DesignStyleProperty)); } } } diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index 230b4954fe..4b903d056c 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -215,11 +215,6 @@ namespace Avalonia.Controls.Primitives } } - if (CancelOpening()) - { - return false; - } - if (Popup.Parent != null && Popup.Parent != placementTarget) { ((ISetLogicalParent)Popup).SetParent(null); @@ -236,6 +231,11 @@ namespace Avalonia.Controls.Primitives Popup.Child = CreatePresenter(); } + if (CancelOpening()) + { + return false; + } + PositionPopup(showAtPointer); IsOpen = Popup.IsOpen = true; OnOpened(); diff --git a/src/Avalonia.Controls/ItemsSourceView.cs b/src/Avalonia.Controls/ItemsSourceView.cs index b2663f3213..c2d20495ef 100644 --- a/src/Avalonia.Controls/ItemsSourceView.cs +++ b/src/Avalonia.Controls/ItemsSourceView.cs @@ -32,8 +32,8 @@ namespace Avalonia.Controls /// public static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty()); - private protected readonly IList _inner; - private INotifyCollectionChanged? _notifyCollectionChanged; + private IList? _inner; + private NotifyCollectionChangedEventHandler? _collectionChanged; /// /// Initializes a new instance of the ItemsSourceView class for the specified data source. @@ -42,27 +42,22 @@ namespace Avalonia.Controls public ItemsSourceView(IEnumerable source) { source = source ?? throw new ArgumentNullException(nameof(source)); - - if (source is IList list) - { - _inner = list; - } - else if (source is IEnumerable objectEnumerable) + _inner = source switch { - _inner = new List(objectEnumerable); - } - else - { - _inner = new List(source.Cast()); - } - - ListenToCollectionChanges(); + ItemsSourceView _ => throw new ArgumentException("Cannot wrap an existing ItemsSourceView.", nameof(source)), + IList list => list, + INotifyCollectionChanged _ => throw new ArgumentException( + "Collection implements INotifyCollectionChanged by not IList.", + nameof(source)), + IEnumerable iObj => new List(iObj), + _ => new List(source.Cast()) + }; } /// /// Gets the number of items in the collection. /// - public int Count => _inner.Count; + public int Count => Inner.Count; /// /// Gets a value that indicates whether the items source can provide a unique key for each item. @@ -72,6 +67,19 @@ namespace Avalonia.Controls /// public bool HasKeyIndexMapping => false; + /// + /// Gets the inner collection. + /// + public IList Inner + { + get + { + if (_inner is null) + ThrowDisposed(); + return _inner!; + } + } + /// /// Retrieves the item at the specified index. /// @@ -82,15 +90,38 @@ namespace Avalonia.Controls /// /// Occurs when the collection has changed to indicate the reason for the change and which items changed. /// - public event NotifyCollectionChangedEventHandler? CollectionChanged; + public event NotifyCollectionChangedEventHandler? CollectionChanged + { + add + { + if (_collectionChanged is null && Inner is INotifyCollectionChanged incc) + { + incc.CollectionChanged += OnCollectionChanged; + } + + _collectionChanged += value; + } + + remove + { + _collectionChanged -= value; + + if (_collectionChanged is null && Inner is INotifyCollectionChanged incc) + { + incc.CollectionChanged -= OnCollectionChanged; + } + } + } /// public void Dispose() { - if (_notifyCollectionChanged != null) + if (_inner is INotifyCollectionChanged incc) { - _notifyCollectionChanged.CollectionChanged -= OnCollectionChanged; + incc.CollectionChanged -= OnCollectionChanged; } + + _inner = null; } /// @@ -98,9 +129,9 @@ namespace Avalonia.Controls /// /// The index. /// The item. - public object? GetAt(int index) => _inner[index]; + public object? GetAt(int index) => Inner[index]; - public int IndexOf(object? item) => _inner.IndexOf(item); + public int IndexOf(object? item) => Inner.IndexOf(item); public static ItemsSourceView GetOrCreate(IEnumerable? items) { @@ -146,7 +177,7 @@ namespace Avalonia.Controls internal void AddListener(ICollectionChangedListener listener) { - if (_inner is INotifyCollectionChanged incc) + if (Inner is INotifyCollectionChanged incc) { CollectionChangedEventManager.Instance.AddListener(incc, listener); } @@ -154,7 +185,7 @@ namespace Avalonia.Controls internal void RemoveListener(ICollectionChangedListener listener) { - if (_inner is INotifyCollectionChanged incc) + if (Inner is INotifyCollectionChanged incc) { CollectionChangedEventManager.Instance.RemoveListener(incc, listener); } @@ -162,22 +193,15 @@ namespace Avalonia.Controls protected void OnItemsSourceChanged(NotifyCollectionChangedEventArgs args) { - CollectionChanged?.Invoke(this, args); - } - - private void ListenToCollectionChanges() - { - if (_inner is INotifyCollectionChanged incc) - { - incc.CollectionChanged += OnCollectionChanged; - _notifyCollectionChanged = incc; - } + _collectionChanged?.Invoke(this, args); } private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { OnItemsSourceChanged(e); } + + private void ThrowDisposed() => throw new ObjectDisposedException(nameof(ItemsSourceView)); } public class ItemsSourceView : ItemsSourceView, IReadOnlyList @@ -216,10 +240,10 @@ namespace Avalonia.Controls /// The index. /// The item. [return: MaybeNull] - public new T GetAt(int index) => (T)_inner[index]; + public new T GetAt(int index) => (T)Inner[index]; - public IEnumerator GetEnumerator() => _inner.Cast().GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => _inner.GetEnumerator(); + public IEnumerator GetEnumerator() => Inner.Cast().GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => Inner.GetEnumerator(); public static new ItemsSourceView GetOrCreate(IEnumerable? items) { diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index 209feb351c..e361e7b736 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -275,7 +275,7 @@ namespace Avalonia.Controls.Platform return; } - if (item.HasSubMenu) + if (item.HasSubMenu && item.IsEffectivelyEnabled) { Open(item, true); } @@ -303,7 +303,8 @@ namespace Avalonia.Controls.Platform { item.Parent.SelectedItem.Close(); SelectItemAndAncestors(item); - Open(item, false); + if (item.HasSubMenu) + Open(item, false); } else { diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 1ed3896dd3..a5cdeefb0e 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -53,6 +53,7 @@ namespace Avalonia.Controls.Primitives AvaloniaProperty.Register( nameof(PlacementConstraintAdjustment), PopupPositionerConstraintAdjustment.FlipX | PopupPositionerConstraintAdjustment.FlipY | + PopupPositionerConstraintAdjustment.SlideX | PopupPositionerConstraintAdjustment.SlideY | PopupPositionerConstraintAdjustment.ResizeX | PopupPositionerConstraintAdjustment.ResizeY); /// diff --git a/src/Avalonia.Controls/Templates/IControlTemplate.cs b/src/Avalonia.Controls/Templates/IControlTemplate.cs index 7414f438a1..ab46884402 100644 --- a/src/Avalonia.Controls/Templates/IControlTemplate.cs +++ b/src/Avalonia.Controls/Templates/IControlTemplate.cs @@ -1,3 +1,4 @@ +using System; using Avalonia.Controls.Primitives; using Avalonia.Styling; @@ -10,18 +11,16 @@ namespace Avalonia.Controls.Templates { } - public class ControlTemplateResult + public class ControlTemplateResult : TemplateResult { public IControl Control { get; } - public INameScope NameScope { get; } - public ControlTemplateResult(IControl control, INameScope nameScope) + public ControlTemplateResult(IControl control, INameScope nameScope) : base(control, nameScope) { Control = control; - NameScope = nameScope; } - public void Deconstruct(out IControl control, out INameScope scope) + public new void Deconstruct(out IControl control, out INameScope scope) { control = Control; scope = NameScope; diff --git a/src/Avalonia.Controls/Templates/TemplateResult.cs b/src/Avalonia.Controls/Templates/TemplateResult.cs new file mode 100644 index 0000000000..770aecc329 --- /dev/null +++ b/src/Avalonia.Controls/Templates/TemplateResult.cs @@ -0,0 +1,20 @@ +namespace Avalonia.Controls.Templates +{ + public class TemplateResult + { + public T Result { get; } + public INameScope NameScope { get; } + + public TemplateResult(T result, INameScope nameScope) + { + Result = result; + NameScope = nameScope; + } + + public void Deconstruct(out T result, out INameScope scope) + { + result = Result; + scope = NameScope; + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index ea06c33e4d..73d867bf10 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -160,13 +160,19 @@ namespace Avalonia.Diagnostics.Views return; } + var root = Root; + if (root is null) + { + return; + } + switch (e.Modifiers) { case RawInputModifiers.Control | RawInputModifiers.Shift: { IControl? control = null; - foreach (var popupRoot in GetPopupRoots(Root)) + foreach (var popupRoot in GetPopupRoots(root)) { control = GetHoveredControl(popupRoot); @@ -176,7 +182,7 @@ namespace Avalonia.Diagnostics.Views } } - control ??= GetHoveredControl(Root); + control ??= GetHoveredControl(root); if (control != null) { @@ -190,7 +196,7 @@ namespace Avalonia.Diagnostics.Views { vm.FreezePopups = !vm.FreezePopups; - foreach (var popupRoot in GetPopupRoots(Root)) + foreach (var popupRoot in GetPopupRoots(root)) { if (popupRoot.Parent is Popup popup) { diff --git a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs index 55e30396e1..5d7619d184 100644 --- a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs +++ b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs @@ -30,13 +30,13 @@ namespace Avalonia.Dialogs } else { - using (Process process = Process.Start(new ProcessStartInfo + using Process process = Process.Start(new ProcessStartInfo { FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? url : "open", Arguments = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? $"{url}" : "", CreateNoWindow = true, UseShellExecute = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - })); + }); } } diff --git a/src/Avalonia.Dialogs/ManagedFileChooser.cs b/src/Avalonia.Dialogs/ManagedFileChooser.cs index f9f38ac474..9058c405a3 100644 --- a/src/Avalonia.Dialogs/ManagedFileChooser.cs +++ b/src/Avalonia.Dialogs/ManagedFileChooser.cs @@ -1,13 +1,11 @@ using System; using System.Linq; using System.Threading.Tasks; -using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.LogicalTree; -using Avalonia.Markup.Xaml; namespace Avalonia.Dialogs { @@ -35,7 +33,9 @@ namespace Avalonia.Dialogs if (_quickLinksRoot != null) { var isQuickLink = _quickLinksRoot.IsLogicalAncestorOf(e.Source as Control); +#pragma warning disable CS0618 // Type or member is obsolete if (e.ClickCount == 2 || isQuickLink) +#pragma warning restore CS0618 // Type or member is obsolete { if (model.ItemType == ManagedFileChooserItemType.File) { diff --git a/src/Avalonia.Dialogs/ManagedFileChooserSources.cs b/src/Avalonia.Dialogs/ManagedFileChooserSources.cs index 050d618ce1..a217a67bc6 100644 --- a/src/Avalonia.Dialogs/ManagedFileChooserSources.cs +++ b/src/Avalonia.Dialogs/ManagedFileChooserSources.cs @@ -67,7 +67,7 @@ namespace Avalonia.Dialogs { Directory.GetFiles(x.VolumePath); } - catch (Exception _) + catch (Exception) { return null; } diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 268171d467..63cbfb2dbe 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -47,6 +47,8 @@ namespace Avalonia.Headless } public IStreamGeometryImpl CreateStreamGeometry() => new HeadlessStreamingGeometryStub(); + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => throw new NotImplementedException(); + public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => throw new NotImplementedException(); public IRenderTarget CreateRenderTarget(IEnumerable surfaces) => new HeadlessRenderTarget(); diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs index f2cc9e9072..8d74001309 100644 --- a/src/Avalonia.Input/Gestures.cs +++ b/src/Avalonia.Input/Gestures.cs @@ -81,17 +81,21 @@ namespace Avalonia.Input var e = (PointerPressedEventArgs)ev; var visual = (IVisual)ev.Source; - if (e.ClickCount <= 1) +#pragma warning disable CS0618 // Type or member is obsolete + var clickCount = e.ClickCount; +#pragma warning restore CS0618 // Type or member is obsolete + if (clickCount <= 1) { s_lastPress = new WeakReference(ev.Source); } - else if (s_lastPress != null && e.ClickCount == 2 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed) + else if (s_lastPress != null && clickCount == 2 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed) { if (s_lastPress.TryGetTarget(out var target) && target == e.Source) { e.Source.RaiseEvent(new TappedEventArgs(DoubleTappedEvent, e)); } } + } } diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index cfa3690daf..401c6cb2ac 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -75,7 +75,9 @@ namespace Avalonia.Input throw new InvalidOperationException("Control is not attached to visual tree."); } +#pragma warning disable CS0618 // Type or member is obsolete var rootPoint = relativeTo.VisualRoot.PointToClient(Position); +#pragma warning restore CS0618 // Type or member is obsolete var transform = relativeTo.VisualRoot.TransformToVisual(relativeTo); return rootPoint * transform!.Value; } diff --git a/src/Avalonia.Themes.Default/Expander.xaml b/src/Avalonia.Themes.Default/Expander.xaml index 5e0958c54c..7df65677b6 100644 --- a/src/Avalonia.Themes.Default/Expander.xaml +++ b/src/Avalonia.Themes.Default/Expander.xaml @@ -101,6 +101,7 @@ Grid.Column="1" Background="Transparent" Content="{TemplateBinding Content}" + ContentTemplate="{Binding $parent[Expander].HeaderTemplate}" VerticalAlignment="Center" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" diff --git a/src/Avalonia.Themes.Fluent/Controls/NotificationCard.xaml b/src/Avalonia.Themes.Fluent/Controls/NotificationCard.xaml index c06b501f82..924d977eb5 100644 --- a/src/Avalonia.Themes.Fluent/Controls/NotificationCard.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/NotificationCard.xaml @@ -17,7 +17,7 @@ - + - + - - + + diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt index 39a4c3004c..e3f9f9a070 100644 --- a/src/Avalonia.Visuals/ApiCompatBaseline.txt +++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt @@ -67,6 +67,8 @@ InterfacesShouldHaveSameMembers : Interface member 'public System.Double Avaloni InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IGeometryImpl.TryGetPointAndTangentAtDistance(System.Double, Avalonia.Point, Avalonia.Point)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IGeometryImpl.TryGetPointAtDistance(System.Double, Avalonia.Point)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IGeometryImpl.TryGetSegment(System.Double, System.Double, System.Boolean, Avalonia.Platform.IGeometryImpl)' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGeometryImpl Avalonia.Platform.IPlatformRenderInterface.CreateCombinedGeometry(Avalonia.Media.GeometryCombineMode, Avalonia.Media.Geometry, Avalonia.Media.Geometry)' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGeometryImpl Avalonia.Platform.IPlatformRenderInterface.CreateGeometryGroup(Avalonia.Media.FillRule, System.Collections.Generic.IReadOnlyList)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun, System.Double)' is present in the contract but not in the implementation. MembersMustExist : Member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun, System.Double)' does not exist in the implementation but it does exist in the contract. @@ -74,4 +76,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWr InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmap(System.String)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmapToHeight(System.IO.Stream, System.Int32, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmapToWidth(System.IO.Stream, System.Int32, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode)' is present in the implementation but not in the contract. -Total Issues: 75 +Total Issues: 77 diff --git a/src/Avalonia.Visuals/Media/CombinedGeometry.cs b/src/Avalonia.Visuals/Media/CombinedGeometry.cs new file mode 100644 index 0000000000..2202030b7a --- /dev/null +++ b/src/Avalonia.Visuals/Media/CombinedGeometry.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Platform; + +#nullable enable + +namespace Avalonia.Media +{ + public enum GeometryCombineMode + { + /// + /// The two regions are combined by taking the union of both. The resulting geometry is + /// geometry A + geometry B. + /// + Union, + + /// + /// The two regions are combined by taking their intersection. The new area consists of the + /// overlapping region between the two geometries. + /// + Intersect, + + /// + /// The two regions are combined by taking the area that exists in the first region but not + /// the second and the area that exists in the second region but not the first. The new + /// region consists of (A-B) + (B-A), where A and B are geometries. + /// + Xor, + + /// + /// The second region is excluded from the first. Given two geometries, A and B, the area of + /// geometry B is removed from the area of geometry A, producing a region that is A-B. + /// + Exclude, + } + + /// + /// Represents a 2-D geometric shape defined by the combination of two Geometry objects. + /// + public class CombinedGeometry : Geometry + { + /// + /// Defines the property. + /// + public static readonly StyledProperty Geometry1Property = + AvaloniaProperty.Register(nameof(Geometry1)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty Geometry2Property = + AvaloniaProperty.Register(nameof(Geometry2)); + /// + /// Defines the property. + /// + public static readonly StyledProperty GeometryCombineModeProperty = + AvaloniaProperty.Register(nameof(GeometryCombineMode)); + + /// + /// Initializes a new instance of the class. + /// + public CombinedGeometry() + { + } + + /// + /// Initializes a new instance of the class with the + /// specified objects. + /// + /// The first geometry to combine. + /// The second geometry to combine. + public CombinedGeometry(Geometry geometry1, Geometry geometry2) + { + Geometry1 = geometry1; + Geometry2 = geometry2; + } + + /// + /// Initializes a new instance of the class with the + /// specified objects and . + /// + /// The method by which geometry1 and geometry2 are combined. + /// The first geometry to combine. + /// The second geometry to combine. + public CombinedGeometry(GeometryCombineMode combineMode, Geometry? geometry1, Geometry? geometry2) + { + Geometry1 = geometry1; + Geometry2 = geometry2; + GeometryCombineMode = combineMode; + } + + /// + /// Initializes a new instance of the class with the + /// specified objects, and + /// . + /// + /// The method by which geometry1 and geometry2 are combined. + /// The first geometry to combine. + /// The second geometry to combine. + /// The transform applied to the geometry. + public CombinedGeometry( + GeometryCombineMode combineMode, + Geometry? geometry1, + Geometry? geometry2, + Transform? transform) + { + Geometry1 = geometry1; + Geometry2 = geometry2; + GeometryCombineMode = combineMode; + Transform = transform; + } + + /// + /// Gets or sets the first object of this + /// object. + /// + public Geometry? Geometry1 + { + get => GetValue(Geometry1Property); + set => SetValue(Geometry1Property, value); + } + + /// + /// Gets or sets the second object of this + /// object. + /// + public Geometry? Geometry2 + { + get => GetValue(Geometry2Property); + set => SetValue(Geometry2Property, value); + } + + /// + /// Gets or sets the method by which the two geometries (specified by the + /// and properties) are combined. The + /// default value is . + /// + public GeometryCombineMode GeometryCombineMode + { + get => GetValue(GeometryCombineModeProperty); + set => SetValue(GeometryCombineModeProperty, value); + } + + public override Geometry Clone() + { + return new CombinedGeometry(GeometryCombineMode, Geometry1, Geometry2, Transform); + } + + protected override IGeometryImpl? CreateDefiningGeometry() + { + var g1 = Geometry1; + var g2 = Geometry2; + + if (g1 is object && g2 is object) + { + var factory = AvaloniaLocator.Current.GetService(); + return factory.CreateCombinedGeometry(GeometryCombineMode, g1, g2); + } + else if (GeometryCombineMode == GeometryCombineMode.Intersect) + return null; + else if (g1 is object) + return g1.PlatformImpl; + else if (g2 is object) + return g2.PlatformImpl; + else + return null; + } + } +} diff --git a/src/Avalonia.Visuals/Media/FormattedText.cs b/src/Avalonia.Visuals/Media/FormattedText.cs index a843be4c4b..ddffbe7500 100644 --- a/src/Avalonia.Visuals/Media/FormattedText.cs +++ b/src/Avalonia.Visuals/Media/FormattedText.cs @@ -200,7 +200,7 @@ namespace Avalonia.Media private void Set(ref T field, T value) { - if (field != null && field.Equals(value)) + if (EqualityComparer.Default.Equals(field, value)) { return; } diff --git a/src/Avalonia.Visuals/Media/GeometryCollection.cs b/src/Avalonia.Visuals/Media/GeometryCollection.cs new file mode 100644 index 0000000000..0bd02d5438 --- /dev/null +++ b/src/Avalonia.Visuals/Media/GeometryCollection.cs @@ -0,0 +1,37 @@ +using System.Collections; +using System.Collections.Generic; +using Avalonia.Animation; + +#nullable enable + +namespace Avalonia.Media +{ + public class GeometryCollection : Animatable, IList, IReadOnlyList + { + private List _inner; + + public GeometryCollection() => _inner = new List(); + public GeometryCollection(IEnumerable collection) => _inner = new List(collection); + public GeometryCollection(int capacity) => _inner = new List(capacity); + + public Geometry this[int index] + { + get => _inner[index]; + set => _inner[index] = value; + } + + public int Count => _inner.Count; + public bool IsReadOnly => false; + + public void Add(Geometry item) => _inner.Add(item); + public void Clear() => _inner.Clear(); + public bool Contains(Geometry item) => _inner.Contains(item); + public void CopyTo(Geometry[] array, int arrayIndex) => _inner.CopyTo(array, arrayIndex); + public IEnumerator GetEnumerator() => _inner.GetEnumerator(); + public int IndexOf(Geometry item) => _inner.IndexOf(item); + public void Insert(int index, Geometry item) => _inner.Insert(index, item); + public bool Remove(Geometry item) => _inner.Remove(item); + public void RemoveAt(int index) => _inner.RemoveAt(index); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/src/Avalonia.Visuals/Media/GeometryGroup.cs b/src/Avalonia.Visuals/Media/GeometryGroup.cs new file mode 100644 index 0000000000..edbe63d4bb --- /dev/null +++ b/src/Avalonia.Visuals/Media/GeometryGroup.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; +using System.Linq; +using Avalonia.Metadata; +using Avalonia.Platform; + +#nullable enable + +namespace Avalonia.Media +{ + /// + /// Represents a composite geometry, composed of other objects. + /// + public class GeometryGroup : Geometry + { + public static readonly DirectProperty ChildrenProperty = + AvaloniaProperty.RegisterDirect ( + nameof(Children), + o => o.Children, + (o, v) => o.Children = v); + + public static readonly StyledProperty FillRuleProperty = + AvaloniaProperty.Register(nameof(FillRule)); + + private GeometryCollection? _children; + private bool _childrenSet; + + /// + /// Gets or sets the collection that contains the child geometries. + /// + [Content] + public GeometryCollection? Children + { + get => _children ??= (!_childrenSet ? new GeometryCollection() : null); + set + { + SetAndRaise(ChildrenProperty, ref _children, value); + _childrenSet = true; + } + } + + /// + /// Gets or sets how the intersecting areas of the objects contained in this + /// are combined. The default is . + /// + public FillRule FillRule + { + get => GetValue(FillRuleProperty); + set => SetValue(FillRuleProperty, value); + } + + public override Geometry Clone() + { + var result = new GeometryGroup { FillRule = FillRule, Transform = Transform }; + if (_children?.Count > 0) + result.Children = new GeometryCollection(_children); + return result; + } + + protected override IGeometryImpl? CreateDefiningGeometry() + { + if (_children?.Count > 0) + { + var factory = AvaloniaLocator.Current.GetService(); + return factory.CreateGeometryGroup(FillRule, _children); + } + + return null; + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ChildrenProperty || change.Property == FillRuleProperty) + { + InvalidateGeometry(); + } + } + } +} diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index 9fefcb6645..8b9d0833db 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -496,12 +496,18 @@ namespace Avalonia.Media private bool ReadBool(ref ReadOnlySpan span) { - if (!ReadArgument(ref span, out var boolValue) || boolValue.Length != 1) + span = SkipWhitespace(span); + + if (span.IsEmpty) { throw new InvalidDataException("Invalid bool rule."); } - switch (boolValue[0]) + var c = span[0]; + + span = span.Slice(1); + + switch (c) { case '0': return false; diff --git a/src/Avalonia.Visuals/Media/RotateTransform.cs b/src/Avalonia.Visuals/Media/RotateTransform.cs index 653d38eb45..126bb7c274 100644 --- a/src/Avalonia.Visuals/Media/RotateTransform.cs +++ b/src/Avalonia.Visuals/Media/RotateTransform.cs @@ -14,6 +14,18 @@ namespace Avalonia.Media public static readonly StyledProperty AngleProperty = AvaloniaProperty.Register(nameof(Angle)); + /// + /// Defines the property. + /// + public static readonly StyledProperty CenterXProperty = + AvaloniaProperty.Register(nameof(CenterX)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty CenterYProperty = + AvaloniaProperty.Register(nameof(CenterY)); + /// /// Initializes a new instance of the class. /// @@ -32,18 +44,52 @@ namespace Avalonia.Media Angle = angle; } + /// + /// Initializes a new instance of the class. + /// + /// The angle, in degrees. + /// The x-coordinate of the center point for the rotation. + /// The y-coordinate of the center point for the rotation. + public RotateTransform(double angle, double centerX, double centerY) + : this() + { + Angle = angle; + CenterX = centerX; + CenterY = centerY; + } + /// /// Gets or sets the angle of rotation, in degrees. /// public double Angle { - get { return GetValue(AngleProperty); } - set { SetValue(AngleProperty, value); } + get => GetValue(AngleProperty); + set => SetValue(AngleProperty, value); + } + + /// + /// Gets or sets the x-coordinate of the rotation center point. The default is 0. + /// + public double CenterX + { + get => GetValue(CenterXProperty); + set => SetValue(CenterXProperty, value); + } + + /// + /// Gets or sets the y-coordinate of the rotation center point. The default is 0. + /// + public double CenterY + { + get => GetValue(CenterYProperty); + set => SetValue(CenterYProperty, value); } /// /// Gets the transform's . /// - public override Matrix Value => Matrix.CreateRotation(Matrix.ToRadians(Angle)); + public override Matrix Value => Matrix.CreateTranslation(-CenterX, -CenterY) * + Matrix.CreateRotation(Matrix.ToRadians(Angle)) * + Matrix.CreateTranslation(CenterX, CenterY); } } diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs index de67aca5a8..772f1ac9f3 100644 --- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs @@ -59,6 +59,23 @@ namespace Avalonia.Platform /// An . IStreamGeometryImpl CreateStreamGeometry(); + /// + /// Creates a geometry group implementation. + /// + /// The fill rule. + /// The geometries to group. + /// A combined geometry. + IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children); + + /// + /// Creates a geometry group implementation. + /// + /// The combine mode + /// The first geometry. + /// The second geometry. + /// A combined geometry. + IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2); + /// /// Creates a renderer. /// diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index 6c84cfd55c..fe63fdec46 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -279,13 +279,13 @@ namespace Avalonia.Rendering /// Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush) { - return (_currentDraw.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual]?.Size ?? Size.Empty; + return (_currentDraw?.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual]?.Size ?? Size.Empty; } /// void IVisualBrushRenderer.RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush) { - var childScene = (_currentDraw.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual]; + var childScene = (_currentDraw?.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual]; if (childScene != null) { diff --git a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs index 85feb06c44..52427c4ae6 100644 --- a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs @@ -289,11 +289,14 @@ namespace Avalonia.Rendering using (context.PushPostTransform(m)) using (context.PushOpacity(opacity)) - using (clipToBounds - ? visual is IVisualWithRoundRectClip roundClipVisual + using (clipToBounds +#pragma warning disable CS0618 // Type or member is obsolete + ? visual is IVisualWithRoundRectClip roundClipVisual ? context.PushClip(new RoundedRect(bounds, roundClipVisual.ClipToBoundsRadius)) : context.PushClip(bounds) : default(DrawingContext.PushedState)) +#pragma warning restore CS0618 // Type or member is obsolete + using (visual.Clip != null ? context.PushGeometryClip(visual.Clip) : default(DrawingContext.PushedState)) using (visual.OpacityMask != null ? context.PushOpacityMask(visual.OpacityMask, bounds) : default(DrawingContext.PushedState)) using (context.PushTransformContainer()) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/CustomDrawOperation.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/CustomDrawOperation.cs index 15e5660671..b7311936d3 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/CustomDrawOperation.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/CustomDrawOperation.cs @@ -17,7 +17,12 @@ namespace Avalonia.Rendering.SceneGraph public override bool HitTest(Point p) { - return Custom.HitTest(p * Transform); + if (Transform.HasInverse) + { + return Custom.HitTest(p * Transform.Invert()); + } + + return false; } public override void Render(IDrawingContextImpl context) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index c6cdf474bb..b9131c26f4 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs @@ -164,10 +164,12 @@ namespace Avalonia.Rendering.SceneGraph var visual = node.Visual; var opacity = visual.Opacity; var clipToBounds = visual.ClipToBounds; +#pragma warning disable CS0618 // Type or member is obsolete var clipToBoundsRadius = visual is IVisualWithRoundRectClip roundRectClip ? roundRectClip.ClipToBoundsRadius : default; - +#pragma warning restore CS0618 // Type or member is obsolete + var bounds = new Rect(visual.Bounds.Size); var contextImpl = (DeferredDrawingContextImpl)context.PlatformImpl; diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs index 79c4202be4..810530066f 100644 --- a/src/Avalonia.Visuals/Vector.cs +++ b/src/Avalonia.Visuals/Vector.cs @@ -175,7 +175,7 @@ namespace Avalonia MathUtilities.AreClose(_y, other._y); } - public override bool Equals(object obj) => obj is Vector other && Equals(other); + public override bool Equals(object? obj) => obj is Vector other && Equals(other); public override int GetHashCode() { diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index f95d4320fe..d677ababef 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -25,7 +25,7 @@ namespace Avalonia.X11 { {StandardCursorType.Arrow, CursorFontShape.XC_top_left_arrow}, {StandardCursorType.Cross, CursorFontShape.XC_cross}, - {StandardCursorType.Hand, CursorFontShape.XC_hand1}, + {StandardCursorType.Hand, CursorFontShape.XC_hand2}, {StandardCursorType.Help, CursorFontShape.XC_question_arrow}, {StandardCursorType.Ibeam, CursorFontShape.XC_xterm}, {StandardCursorType.No, CursorFontShape.XC_X_cursor}, diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index e39be6fc04..7bc8872fa7 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -192,11 +192,6 @@ namespace Avalonia.X11 if (platform.Options.UseDBusMenu) NativeMenuExporter = DBusMenuExporter.TryCreateTopLevelNativeMenu(_handle); NativeControlHost = new X11NativeControlHost(_platform, this); - DispatcherTimer.Run(() => - { - Paint?.Invoke(default); - return _handle != IntPtr.Zero; - }, TimeSpan.FromMilliseconds(100)); InitializeIme(); } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index b097e0917f..8688671d3b 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -30,10 +30,9 @@ namespace Avalonia.LinuxFramebuffer public IRenderer CreateRenderer(IRenderRoot root) { - return new DeferredRenderer(root, AvaloniaLocator.Current.GetService()) - { - - }; + var factory = AvaloniaLocator.Current.GetService(); + var renderLoop = AvaloniaLocator.Current.GetService(); + return factory?.Create(root, renderLoop) ?? new DeferredRenderer(root, renderLoop); } public void Dispose() @@ -41,7 +40,7 @@ namespace Avalonia.LinuxFramebuffer throw new NotSupportedException(); } - + public void Invalidate(Rect rect) { } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index 89f81a7649..f4db6bf48a 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -37,16 +37,17 @@ namespace Avalonia.LinuxFramebuffer Threading = new InternalPlatformThreadingInterface(); if (_fb is IGlOutputBackend gl) AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl.PlatformOpenGlInterface); + + var opts = AvaloniaLocator.Current.GetService(); + AvaloniaLocator.CurrentMutable .Bind().ToConstant(Threading) - .Bind().ToConstant(new DefaultRenderTimer(60)) + .Bind().ToConstant(new DefaultRenderTimer(opts?.Fps ?? 60)) .Bind().ToConstant(new RenderLoop()) .Bind().ToTransient() .Bind().ToConstant(new KeyboardDevice()) .Bind().ToSingleton() - .Bind().ToConstant(new RenderLoop()) .Bind().ToSingleton(); - } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatformOptions.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatformOptions.cs new file mode 100644 index 0000000000..bf925bbd75 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatformOptions.cs @@ -0,0 +1,14 @@ +namespace Avalonia.LinuxFramebuffer +{ + /// + /// Platform-specific options which apply to the Linux framebuffer. + /// + public class LinuxFramebufferPlatformOptions + { + /// + /// Gets or sets the number of frames per second at which the renderer should run. + /// Default 60. + /// + public int Fps { get; set; } = 60; + } +} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs b/src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs deleted file mode 100644 index 87c7b64c26..0000000000 --- a/src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using Avalonia.Platform; - -namespace Avalonia.LinuxFramebuffer -{ - unsafe class LockedFramebuffer : ILockedFramebuffer - { - private readonly int _fb; - private readonly fb_fix_screeninfo _fixedInfo; - private fb_var_screeninfo _varInfo; - private readonly IntPtr _address; - - public LockedFramebuffer(int fb, fb_fix_screeninfo fixedInfo, fb_var_screeninfo varInfo, IntPtr address, Vector dpi) - { - _fb = fb; - _fixedInfo = fixedInfo; - _varInfo = varInfo; - _address = address; - Dpi = dpi; - //Use double buffering to avoid flicker - Address = Marshal.AllocHGlobal(RowBytes * Size.Height); - } - - - void VSync() - { - NativeUnsafeMethods.ioctl(_fb, FbIoCtl.FBIO_WAITFORVSYNC, null); - } - - public void Dispose() - { - VSync(); - NativeUnsafeMethods.memcpy(_address, Address, new IntPtr(RowBytes * Size.Height)); - - Marshal.FreeHGlobal(Address); - Address = IntPtr.Zero; - } - - public IntPtr Address { get; private set; } - public PixelSize Size => new PixelSize((int)_varInfo.xres, (int) _varInfo.yres); - public int RowBytes => (int) _fixedInfo.line_length; - public Vector Dpi { get; } - public PixelFormat Format => _varInfo.bits_per_pixel == 16 ? PixelFormat.Rgb565 : _varInfo.blue.offset == 16 ? PixelFormat.Rgba8888 : PixelFormat.Bgra8888; - } -} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevBackBuffer.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevBackBuffer.cs new file mode 100644 index 0000000000..7afad13bb6 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevBackBuffer.cs @@ -0,0 +1,70 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; +using Avalonia.Platform; + +namespace Avalonia.LinuxFramebuffer.Output +{ + internal unsafe class FbDevBackBuffer : IDisposable + { + private readonly int _fb; + private readonly fb_fix_screeninfo _fixedInfo; + private readonly fb_var_screeninfo _varInfo; + private readonly IntPtr _targetAddress; + private readonly object _lock = new object(); + + public FbDevBackBuffer(int fb, fb_fix_screeninfo fixedInfo, fb_var_screeninfo varInfo, IntPtr targetAddress) + { + _fb = fb; + _fixedInfo = fixedInfo; + _varInfo = varInfo; + _targetAddress = targetAddress; + Address = Marshal.AllocHGlobal(RowBytes * Size.Height); + } + + + public void Dispose() + { + if (Address != IntPtr.Zero) + { + Marshal.FreeHGlobal(Address); + Address = IntPtr.Zero; + } + } + + public ILockedFramebuffer Lock(Vector dpi) + { + Monitor.Enter(_lock); + try + { + return new LockedFramebuffer(Address, + new PixelSize((int)_varInfo.xres, (int)_varInfo.yres), + (int)_fixedInfo.line_length, dpi, + _varInfo.bits_per_pixel == 16 ? PixelFormat.Rgb565 + : _varInfo.blue.offset == 16 ? PixelFormat.Rgba8888 + : PixelFormat.Bgra8888, + () => + { + try + { + NativeUnsafeMethods.ioctl(_fb, FbIoCtl.FBIO_WAITFORVSYNC, null); + NativeUnsafeMethods.memcpy(_targetAddress, Address, new IntPtr(RowBytes * Size.Height)); + } + finally + { + Monitor.Exit(_lock); + } + }); + } + catch + { + Monitor.Exit(_lock); + throw; + } + } + + public IntPtr Address { get; private set; } + public PixelSize Size => new PixelSize((int)_varInfo.xres, (int) _varInfo.yres); + public int RowBytes => (int) _fixedInfo.line_length; + } +} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs index 61f00b2795..f3f9a12ac8 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs @@ -14,6 +14,7 @@ namespace Avalonia.LinuxFramebuffer private fb_var_screeninfo _varInfo; private IntPtr _mappedLength; private IntPtr _mappedAddress; + private FbDevBackBuffer _backBuffer; public double Scaling { get; set; } /// @@ -146,7 +147,9 @@ namespace Avalonia.LinuxFramebuffer { if (_fd <= 0) throw new ObjectDisposedException("LinuxFramebuffer"); - return new LockedFramebuffer(_fd, _fixedInfo, _varInfo, _mappedAddress, new Vector(96, 96) * Scaling); + return (_backBuffer ??= + new FbDevBackBuffer(_fd, _fixedInfo, _varInfo, _mappedAddress)) + .Lock(new Vector(96, 96) * Scaling); } @@ -165,6 +168,8 @@ namespace Avalonia.LinuxFramebuffer public void Dispose() { + _backBuffer?.Dispose(); + _backBuffer = null; ReleaseUnmanagedResources(); GC.SuppressFinalize(this); } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index a191dc59fb..1ca7be67a7 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -14,7 +14,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions { class AvaloniaXamlIlCompiler : XamlILCompiler { - private readonly TransformerConfiguration _configuration; private readonly IXamlType _contextType; private readonly AvaloniaXamlIlDesignPropertiesTransformer _designTransformer; private readonly AvaloniaBindingExtensionTransformer _bindingTransformer; @@ -22,8 +21,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions private AvaloniaXamlIlCompiler(TransformerConfiguration configuration, XamlLanguageEmitMappings emitMappings) : base(configuration, emitMappings, true) { - _configuration = configuration; - void InsertAfter(params IXamlAstTransformer[] t) => Transformers.InsertRange(Transformers.FindIndex(x => x is T) + 1, t); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs index a82f5b9e60..1db0208310 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs @@ -49,8 +49,13 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions XmlNamespaceInfoProvider = typeSystem.GetType("Avalonia.Markup.Xaml.XamlIl.Runtime.IAvaloniaXamlIlXmlNamespaceInfoProvider"), DeferredContentPropertyAttributes = {typeSystem.GetType("Avalonia.Metadata.TemplateContentAttribute")}, + DeferredContentExecutorCustomizationDefaultTypeParameter = typeSystem.GetType("Avalonia.Controls.IControl"), + DeferredContentExecutorCustomizationTypeParameterDeferredContentAttributePropertyNames = new List + { + "TemplateResultType" + }, DeferredContentExecutorCustomization = - runtimeHelpers.FindMethod(m => m.Name == "DeferredTransformationFactoryV1"), + runtimeHelpers.FindMethod(m => m.Name == "DeferredTransformationFactoryV2"), UsableDuringInitializationAttributes = { typeSystem.GetType("Avalonia.Metadata.UsableDuringInitializationAttribute"), diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github index f4ac681b91..8e20d65eb5 160000 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github @@ -1 +1 @@ -Subproject commit f4ac681b91a9dc7a7a095d1050a683de23d86b72 +Subproject commit 8e20d65eb5f1efbae08e49b18f39bfdce32df7b3 diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs index 483a1a5d06..07c79d7077 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs @@ -7,6 +7,7 @@ namespace Avalonia.Markup.Xaml.Templates public static class TemplateContent { public static ControlTemplateResult Load(object templateContent) + { if (templateContent is Func direct) { @@ -20,5 +21,16 @@ namespace Avalonia.Markup.Xaml.Templates throw new ArgumentException(nameof(templateContent)); } + + public static TemplateResult Load(object templateContent) + { + if (templateContent is Func direct) + return (TemplateResult)direct(null); + + if (templateContent is null) + return null; + + throw new ArgumentException(nameof(templateContent)); + } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs index 83d70122b3..c48f386ffd 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs @@ -15,6 +15,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime { public static Func DeferredTransformationFactoryV1(Func builder, IServiceProvider provider) + { + return DeferredTransformationFactoryV2(builder, provider); + } + + public static Func DeferredTransformationFactoryV2(Func builder, + IServiceProvider provider) { var resourceNodes = provider.GetService().Parents .OfType().ToList(); @@ -25,7 +31,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime var scope = parentScope != null ? new ChildNameScope(parentScope) : (INameScope)new NameScope(); var obj = builder(new DeferredParentServiceProvider(sp, resourceNodes, rootObject, scope)); scope.Complete(); - return new ControlTemplateResult((IControl)obj, scope); + + if(typeof(T) == typeof(IControl)) + return new ControlTemplateResult((IControl)obj, scope); + + return new TemplateResult((T)obj, scope); }; } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs index 250eca1852..bf11a02fee 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs @@ -184,6 +184,9 @@ namespace Avalonia.Markup.Parsers } + // Don't need to override GetHashCode as the ISyntax objects will not be stored in a hash; the + // only reason they have overridden Equals methods is for unit testing. +#pragma warning disable CS0659 // Type overrides Object.Equals(object o) but does not override Object.GetHashCode() public class PropertySyntax : ISyntax { public string Name { get; set; } = string.Empty; @@ -205,7 +208,7 @@ namespace Avalonia.Markup.Parsers && other.TypeName == TypeName && other.TypeNamespace == TypeNamespace; } - + public class ChildTraversalSyntax : ISyntax { public static ChildTraversalSyntax Instance { get; } = new ChildTraversalSyntax(); @@ -231,5 +234,6 @@ namespace Avalonia.Markup.Parsers && other.TypeName == TypeName && other.TypeNamespace == TypeNamespace; } +#pragma warning restore CS0659 // Type overrides Object.Equals(object o) but does not override Object.GetHashCode() } } diff --git a/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs b/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs new file mode 100644 index 0000000000..40d7e10ae3 --- /dev/null +++ b/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using Avalonia.Media; +using SkiaSharp; + +#nullable enable + +namespace Avalonia.Skia +{ + /// + /// A Skia implementation of a . + /// + internal class CombinedGeometryImpl : GeometryImpl + { + public CombinedGeometryImpl(GeometryCombineMode combineMode, Geometry g1, Geometry g2) + { + var path1 = ((GeometryImpl)g1.PlatformImpl).EffectivePath; + var path2 = ((GeometryImpl)g2.PlatformImpl).EffectivePath; + var op = combineMode switch + { + GeometryCombineMode.Intersect => SKPathOp.Intersect, + GeometryCombineMode.Xor => SKPathOp.Xor, + GeometryCombineMode.Exclude => SKPathOp.Difference, + _ => SKPathOp.Union, + }; + + var path = path1.Op(path2, op); + + EffectivePath = path; + Bounds = path.Bounds.ToAvaloniaRect(); + } + + public override Rect Bounds { get; } + public override SKPath EffectivePath { get; } + } +} diff --git a/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs b/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs new file mode 100644 index 0000000000..d6f19612c1 --- /dev/null +++ b/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using Avalonia.Media; +using SkiaSharp; + +#nullable enable + +namespace Avalonia.Skia +{ + /// + /// A Skia implementation of a . + /// + internal class GeometryGroupImpl : GeometryImpl + { + public GeometryGroupImpl(FillRule fillRule, IReadOnlyList children) + { + var path = new SKPath + { + FillType = fillRule == FillRule.NonZero ? SKPathFillType.Winding : SKPathFillType.EvenOdd, + }; + + var count = children.Count; + + for (var i = 0; i < count; ++i) + { + if (children[i]?.PlatformImpl is GeometryImpl child) + path.AddPath(child.EffectivePath); + } + + EffectivePath = path; + Bounds = path.Bounds.ToAvaloniaRect(); + } + + public override Rect Bounds { get; } + public override SKPath EffectivePath { get; } + } +} diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 7bc83ec85b..e2175f1145 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -62,6 +62,16 @@ namespace Avalonia.Skia return new StreamGeometryImpl(); } + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) + { + return new GeometryGroupImpl(fillRule, children); + } + + public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) + { + return new CombinedGeometryImpl(combineMode, g1, g2); + } + /// public IBitmapImpl LoadBitmap(string fileName) { diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index f50167b39a..eef4416101 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -175,6 +175,8 @@ namespace Avalonia.Direct2D1 public IGeometryImpl CreateLineGeometry(Point p1, Point p2) => new LineGeometryImpl(p1, p2); public IGeometryImpl CreateRectangleGeometry(Rect rect) => new RectangleGeometryImpl(rect); public IStreamGeometryImpl CreateStreamGeometry() => new StreamGeometryImpl(); + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => new GeometryGroupImpl(fillRule, children); + public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2); /// public IBitmapImpl LoadBitmap(string fileName) diff --git a/src/Windows/Avalonia.Direct2D1/Media/CombinedGeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/CombinedGeometryImpl.cs new file mode 100644 index 0000000000..5a13c10bbc --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/Media/CombinedGeometryImpl.cs @@ -0,0 +1,36 @@ +using SharpDX.Direct2D1; +using AM = Avalonia.Media; + +namespace Avalonia.Direct2D1.Media +{ + /// + /// A Direct2D implementation of a . + /// + internal class CombinedGeometryImpl : GeometryImpl + { + /// + /// Initializes a new instance of the class. + /// + public CombinedGeometryImpl( + AM.GeometryCombineMode combineMode, + AM.Geometry geometry1, + AM.Geometry geometry2) + : base(CreateGeometry(combineMode, geometry1, geometry2)) + { + } + + private static Geometry CreateGeometry( + AM.GeometryCombineMode combineMode, + AM.Geometry geometry1, + AM.Geometry geometry2) + { + var g1 = ((GeometryImpl)geometry1.PlatformImpl).Geometry; + var g2 = ((GeometryImpl)geometry2.PlatformImpl).Geometry; + var dest = new PathGeometry(Direct2D1Platform.Direct2D1Factory); + using var sink = dest.Open(); + g1.Combine(g2, (CombineMode)combineMode, sink); + sink.Close(); + return dest; + } + } +} diff --git a/src/Windows/Avalonia.Direct2D1/Media/GeometryGroupImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GeometryGroupImpl.cs new file mode 100644 index 0000000000..352708bf03 --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/Media/GeometryGroupImpl.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using SharpDX.Direct2D1; +using AM = Avalonia.Media; + +namespace Avalonia.Direct2D1.Media +{ + /// + /// A Direct2D implementation of a . + /// + internal class GeometryGroupImpl : GeometryImpl + { + /// + /// Initializes a new instance of the class. + /// + public GeometryGroupImpl(AM.FillRule fillRule, IReadOnlyList geometry) + : base(CreateGeometry(fillRule, geometry)) + { + } + + private static Geometry CreateGeometry(AM.FillRule fillRule, IReadOnlyList children) + { + var count = children.Count; + var c = new Geometry[count]; + + for (var i = 0; i < count; ++i) + { + c[i] = ((GeometryImpl)children[i].PlatformImpl).Geometry; + } + + return new GeometryGroup(Direct2D1Platform.Direct2D1Factory, (FillMode)fillRule, c); + } + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 22f46ae5cb..7057199c52 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -808,6 +808,31 @@ namespace Avalonia.Win32.Interop MNC_SELECT = 3 } + public enum SysCommands + { + SC_SIZE = 0xF000, + SC_MOVE = 0xF010, + SC_MINIMIZE = 0xF020, + SC_MAXIMIZE = 0xF030, + SC_NEXTWINDOW = 0xF040, + SC_PREVWINDOW = 0xF050, + SC_CLOSE = 0xF060, + SC_VSCROLL = 0xF070, + SC_HSCROLL = 0xF080, + SC_MOUSEMENU = 0xF090, + SC_KEYMENU = 0xF100, + SC_ARRANGE = 0xF110, + SC_RESTORE = 0xF120, + SC_TASKLIST = 0xF130, + SC_SCREENSAVE = 0xF140, + SC_HOTKEY = 0xF150, + SC_DEFAULT = 0xF160, + SC_MONITORPOWER = 0xF170, + SC_CONTEXTHELP = 0xF180, + SC_SEPARATOR = 0xF00F, + SCF_ISSECURE = 0x00000001, + } + [StructLayout(LayoutKind.Sequential)] public struct RGBQUAD { diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index d163b3d068..f7ed2f215f 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -123,6 +123,12 @@ namespace Avalonia.Win32 break; } + case WindowsMessage.WM_SYSCOMMAND: + // Disable system handling of Alt/F10 menu keys. + if ((SysCommands)wParam == SysCommands.SC_KEYMENU && HighWord(ToInt32(lParam)) <= 0) + return IntPtr.Zero; + break; + case WindowsMessage.WM_MENUCHAR: { // mute the system beep diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs index 876a0de643..3e11c74e1c 100644 --- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs +++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs @@ -36,6 +36,16 @@ namespace Avalonia.Benchmarks return new MockStreamGeometryImpl(); } + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) + { + throw new NotImplementedException(); + } + + public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) + { + throw new NotImplementedException(); + } + public IRenderTarget CreateRenderTarget(IEnumerable surfaces) { throw new NotImplementedException(); diff --git a/tests/Avalonia.Controls.UnitTests/ItemsSourceViewTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsSourceViewTests.cs new file mode 100644 index 0000000000..529b3b1aa8 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/ItemsSourceViewTests.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Text; +using Avalonia.Collections; +using Avalonia.Diagnostics; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class ItemsSourceViewTests + { + [Fact] + public void Only_Subscribes_To_Source_CollectionChanged_When_CollectionChanged_Subscribed() + { + var source = new AvaloniaList(); + var target = new ItemsSourceView(source); + var debug = (INotifyCollectionChangedDebug)source; + + Assert.Null(debug.GetCollectionChangedSubscribers()); + + void Handler(object sender, NotifyCollectionChangedEventArgs e) { } + target.CollectionChanged += Handler; + + Assert.NotNull(debug.GetCollectionChangedSubscribers()); + Assert.Equal(1, debug.GetCollectionChangedSubscribers().Length); + + target.CollectionChanged -= Handler; + + Assert.Null(debug.GetCollectionChangedSubscribers()); + } + + [Fact] + public void Cannot_Wrap_An_ItemsSourceView_In_Another() + { + var source = new ItemsSourceView(new string[0]); + Assert.Throws(() => new ItemsSourceView(source)); + } + + [Fact] + public void Cannot_Create_ItemsSourceView_With_Collection_That_Implements_INCC_But_Not_List() + { + var source = new InvalidCollection(); + Assert.Throws(() => new ItemsSourceView(source)); + } + + private class InvalidCollection : INotifyCollectionChanged, IEnumerable + { + public event NotifyCollectionChangedEventHandler CollectionChanged; + + public IEnumerator GetEnumerator() + { + yield break; + } + + IEnumerator IEnumerable.GetEnumerator() + { + yield break; + } + } + } +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/GenericTemplateTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/GenericTemplateTests.cs new file mode 100644 index 0000000000..9fee5285aa --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/GenericTemplateTests.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Markup.Xaml.Templates; +using Avalonia.Metadata; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Xaml +{ + public class SampleTemplatedObject : StyledElement + { + [Content] public List Content { get; set; } = new List(); + public string Foo { get; set; } + } + + public class SampleTemplatedObjectTemplate + { + [Content] + [TemplateContent(TemplateResultType = typeof(SampleTemplatedObject))] + public object Content { get; set; } + } + + public class SampleTemplatedObjectContainer + { + public SampleTemplatedObjectTemplate Template { get; set; } + } + + public class GenericTemplateTests + { + [Fact] + public void DataTemplate_Can_Be_Empty() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + + +"; + var container = + (SampleTemplatedObjectContainer)AvaloniaRuntimeXamlLoader.Load(xaml, + typeof(GenericTemplateTests).Assembly); + var res = TemplateContent.Load(container.Template.Content); + Assert.Equal(res.Result, res.NameScope.Find("root")); + Assert.Equal(res.Result.Content[0], res.NameScope.Find("child1")); + Assert.Equal(res.Result.Content[1], res.NameScope.Find("child2")); + Assert.Equal("foo", res.Result.Content[0].Foo); + Assert.Equal("bar", res.Result.Content[1].Foo); + } + } + } +} diff --git a/tests/Avalonia.RenderTests/Media/CombinedGeometryTests.cs b/tests/Avalonia.RenderTests/Media/CombinedGeometryTests.cs new file mode 100644 index 0000000000..9c5c0248cf --- /dev/null +++ b/tests/Avalonia.RenderTests/Media/CombinedGeometryTests.cs @@ -0,0 +1,89 @@ +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Controls.Shapes; +using Avalonia.Media; +using Xunit; + +#if AVALONIA_SKIA +namespace Avalonia.Skia.RenderTests +#else +namespace Avalonia.Direct2D1.RenderTests.Media +#endif +{ + public class CombinedGeometryTests : TestBase + { + public CombinedGeometryTests() + : base(@"Media\CombinedGeometry") + { + } + + [Theory] + [InlineData(Avalonia.Media.GeometryCombineMode.Union)] + [InlineData(Avalonia.Media.GeometryCombineMode.Intersect)] + [InlineData(Avalonia.Media.GeometryCombineMode.Xor)] + [InlineData(Avalonia.Media.GeometryCombineMode.Exclude)] + public async Task GeometryCombineMode(GeometryCombineMode mode) + { + var target = new Border + { + Width = 200, + Height = 200, + Background = Brushes.White, + Child = new Path + { + Data = new CombinedGeometry + { + GeometryCombineMode = mode, + Geometry1 = new RectangleGeometry(new Rect(25, 25, 100, 100)), + Geometry2 = new EllipseGeometry + { + Center = new Point(125, 125), + RadiusX = 50, + RadiusY = 50, + } + }, + Fill = Brushes.Blue, + Stroke = Brushes.Red, + StrokeThickness = 1, + } + }; + + var testName = $"{nameof(GeometryCombineMode)}_{mode}"; + await RenderToFile(target, testName); + CompareImages(testName); + } + + [Fact] + public async Task Geometry1_Transform() + { + var target = new Border + { + Width = 200, + Height = 200, + Background = Brushes.White, + Child = new Path + { + Data = new CombinedGeometry + { + Geometry1 = new RectangleGeometry(new Rect(25, 25, 100, 100)) + { + Transform = new RotateTransform(45, 75, 75) + }, + Geometry2 = new EllipseGeometry + { + Center = new Point(125, 125), + RadiusX = 50, + RadiusY = 50, + } + }, + Fill = Brushes.Blue, + Stroke = Brushes.Red, + StrokeThickness = 1, + } + }; + + await RenderToFile(target); + CompareImages(); + } + } +} diff --git a/tests/Avalonia.RenderTests/Media/GeometryGroupTests.cs b/tests/Avalonia.RenderTests/Media/GeometryGroupTests.cs new file mode 100644 index 0000000000..9ebbd30e05 --- /dev/null +++ b/tests/Avalonia.RenderTests/Media/GeometryGroupTests.cs @@ -0,0 +1,95 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Controls.Shapes; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Xunit; + +#if AVALONIA_SKIA +namespace Avalonia.Skia.RenderTests +#else +namespace Avalonia.Direct2D1.RenderTests.Media +#endif +{ + public class GeometryGroupTests : TestBase + { + public GeometryGroupTests() + : base(@"Media\GeometryGroup") + { + } + + [Theory] + [InlineData(FillRule.EvenOdd)] + [InlineData(FillRule.NonZero)] + public async Task FillRule_Stroke(FillRule fillRule) + { + var target = new Border + { + Width = 200, + Height = 200, + Background = Brushes.White, + Child = new Path + { + Data = new GeometryGroup + { + FillRule = fillRule, + Children = + { + new RectangleGeometry(new Rect(25, 25, 100, 100)), + new EllipseGeometry + { + Center = new Point(125, 125), + RadiusX = 50, + RadiusY = 50, + }, + } + }, + Fill = Brushes.Blue, + Stroke = Brushes.Red, + StrokeThickness = 1, + } + }; + + var testName = $"{nameof(FillRule_Stroke)}_{fillRule}"; + await RenderToFile(target, testName); + CompareImages(testName); + } + + [Fact] + public async Task Child_Transform() + { + var target = new Border + { + Width = 200, + Height = 200, + Background = Brushes.White, + Child = new Path + { + Data = new GeometryGroup + { + Children = + { + new RectangleGeometry(new Rect(25, 25, 100, 100)) + { + Transform = new RotateTransform(45, 75, 75) + }, + new EllipseGeometry + { + Center = new Point(125, 125), + RadiusX = 50, + RadiusY = 50, + }, + } + }, + Fill = Brushes.Blue, + Stroke = Brushes.Red, + StrokeThickness = 1, + } + }; + + await RenderToFile(target); + CompareImages(); + } + } +} diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index 74366f9e26..1f632034be 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -52,6 +52,16 @@ namespace Avalonia.UnitTests return new MockStreamGeometryImpl(); } + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) + { + return Mock.Of(); + } + + public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) + { + return Mock.Of(); + } + public IWriteableBitmapImpl CreateWriteableBitmap( PixelSize size, Vector dpi, diff --git a/tests/Avalonia.Visuals.UnitTests/Media/GeometryGroupTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/GeometryGroupTests.cs new file mode 100644 index 0000000000..8f80238903 --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/Media/GeometryGroupTests.cs @@ -0,0 +1,26 @@ +using Avalonia.Media; +using Xunit; + +namespace Avalonia.Visuals.UnitTests.Media +{ + public class GeometryGroupTests + { + [Fact] + public void Children_Should_Have_Initial_Collection() + { + var target = new GeometryGroup(); + + Assert.NotNull(target.Children); + } + + [Fact] + public void Children_Can_Be_Set_To_Null() + { + var target = new GeometryGroup(); + + target.Children = null; + + Assert.Null(target.Children); + } + } +} diff --git a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs index c5ad705654..ba8c490829 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs @@ -297,5 +297,28 @@ namespace Avalonia.Visuals.UnitTests.Media Assert.Equal(new Point(20, 20), figure.StartPoint); } } + + [Fact] + public void Should_Parse_Flags_Without_Separator() + { + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) + { + parser.Parse("a.898.898 0 01.27.188"); + + var figure = pathGeometry.Figures[0]; + + var segments = figure.Segments; + + Assert.NotNull(segments); + + Assert.Equal(1, segments.Count); + + var arcSegment = segments[0]; + + Assert.IsType(arcSegment); + } + } } } diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs index 51ea1e893f..229bb8aef3 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs @@ -37,6 +37,16 @@ namespace Avalonia.Visuals.UnitTests.VisualTree return new MockStreamGeometry(); } + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) + { + throw new NotImplementedException(); + } + + public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) + { + throw new NotImplementedException(); + } + public IBitmapImpl LoadBitmap(Stream stream) { throw new NotImplementedException(); diff --git a/tests/TestFiles/Direct2D1/Media/CombinedGeometry/Geometry1_Transform.expected.png b/tests/TestFiles/Direct2D1/Media/CombinedGeometry/Geometry1_Transform.expected.png new file mode 100644 index 0000000000..34976f3de6 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/CombinedGeometry/Geometry1_Transform.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Exclude.expected.png b/tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Exclude.expected.png new file mode 100644 index 0000000000..2c4aa99eeb Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Exclude.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Intersect.expected.png b/tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Intersect.expected.png new file mode 100644 index 0000000000..abb3cea270 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Intersect.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Union.expected.png b/tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Union.expected.png new file mode 100644 index 0000000000..f431ef4403 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Union.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Xor.expected.png b/tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Xor.expected.png new file mode 100644 index 0000000000..4dd547c4bf Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Xor.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/GeometryGroup/Child_Transform.expected.png b/tests/TestFiles/Direct2D1/Media/GeometryGroup/Child_Transform.expected.png new file mode 100644 index 0000000000..d8e4c54924 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/GeometryGroup/Child_Transform.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/GeometryGroup/FillRule_Stroke_EvenOdd.expected.png b/tests/TestFiles/Direct2D1/Media/GeometryGroup/FillRule_Stroke_EvenOdd.expected.png new file mode 100644 index 0000000000..4dd547c4bf Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/GeometryGroup/FillRule_Stroke_EvenOdd.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/GeometryGroup/FillRule_Stroke_NonZero.expected.png b/tests/TestFiles/Direct2D1/Media/GeometryGroup/FillRule_Stroke_NonZero.expected.png new file mode 100644 index 0000000000..3ab700dc04 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/GeometryGroup/FillRule_Stroke_NonZero.expected.png differ diff --git a/tests/TestFiles/Skia/Media/CombinedGeometry/Geometry1_Transform.expected.png b/tests/TestFiles/Skia/Media/CombinedGeometry/Geometry1_Transform.expected.png new file mode 100644 index 0000000000..2b98a79049 Binary files /dev/null and b/tests/TestFiles/Skia/Media/CombinedGeometry/Geometry1_Transform.expected.png differ diff --git a/tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Exclude.expected.png b/tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Exclude.expected.png new file mode 100644 index 0000000000..2c4aa99eeb Binary files /dev/null and b/tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Exclude.expected.png differ diff --git a/tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Intersect.expected.png b/tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Intersect.expected.png new file mode 100644 index 0000000000..abb3cea270 Binary files /dev/null and b/tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Intersect.expected.png differ diff --git a/tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Union.expected.png b/tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Union.expected.png new file mode 100644 index 0000000000..f431ef4403 Binary files /dev/null and b/tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Union.expected.png differ diff --git a/tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Xor.expected.png b/tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Xor.expected.png new file mode 100644 index 0000000000..4dd547c4bf Binary files /dev/null and b/tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Xor.expected.png differ diff --git a/tests/TestFiles/Skia/Media/GeometryGroup/Child_Transform.expected.png b/tests/TestFiles/Skia/Media/GeometryGroup/Child_Transform.expected.png new file mode 100644 index 0000000000..7182602fce Binary files /dev/null and b/tests/TestFiles/Skia/Media/GeometryGroup/Child_Transform.expected.png differ diff --git a/tests/TestFiles/Skia/Media/GeometryGroup/FillRule_Stroke_EvenOdd.expected.png b/tests/TestFiles/Skia/Media/GeometryGroup/FillRule_Stroke_EvenOdd.expected.png new file mode 100644 index 0000000000..80b91d1209 Binary files /dev/null and b/tests/TestFiles/Skia/Media/GeometryGroup/FillRule_Stroke_EvenOdd.expected.png differ diff --git a/tests/TestFiles/Skia/Media/GeometryGroup/FillRule_Stroke_NonZero.expected.png b/tests/TestFiles/Skia/Media/GeometryGroup/FillRule_Stroke_NonZero.expected.png new file mode 100644 index 0000000000..a101525cb3 Binary files /dev/null and b/tests/TestFiles/Skia/Media/GeometryGroup/FillRule_Stroke_NonZero.expected.png differ