diff --git a/src/Avalonia.Controls/Calendar/CalendarItem.cs b/src/Avalonia.Controls/Calendar/CalendarItem.cs index e9ea942142..e830717b95 100644 --- a/src/Avalonia.Controls/Calendar/CalendarItem.cs +++ b/src/Avalonia.Controls/Calendar/CalendarItem.cs @@ -36,11 +36,7 @@ namespace Avalonia.Controls.Primitives private Button _headerButton; private Button _nextButton; private Button _previousButton; - private Grid _monthView; - private Grid _yearView; private ITemplate _dayTitleTemplate; - private CalendarButton _lastCalendarButton; - private CalendarDayButton _lastCalendarDayButton; private DateTime _currentMonth; private bool _isMouseLeftButtonDown = false; @@ -160,38 +156,12 @@ namespace Avalonia.Controls.Primitives /// /// Gets the Grid that hosts the content when in month mode. /// - internal Grid MonthView - { - get { return _monthView; } - private set - { - if (_monthView != null) - _monthView.PointerLeave -= MonthView_MouseLeave; - - _monthView = value; - - if (_monthView != null) - _monthView.PointerLeave += MonthView_MouseLeave; - } - } + internal Grid MonthView { get; set; } /// /// Gets the Grid that hosts the content when in year or decade mode. /// - internal Grid YearView - { - get { return _yearView; } - private set - { - if (_yearView != null) - _yearView.PointerLeave -= YearView_MouseLeave; - - _yearView = value; - - if (_yearView != null) - _yearView.PointerLeave += YearView_MouseLeave; - } - } - + internal Grid YearView { get; set; } + private void PopulateGrids() { if (MonthView != null) @@ -226,7 +196,6 @@ namespace Avalonia.Controls.Primitives cell.CalendarDayButtonMouseDown += Cell_MouseLeftButtonDown; cell.CalendarDayButtonMouseUp += Cell_MouseLeftButtonUp; cell.PointerEnter += Cell_MouseEnter; - cell.PointerLeave += Cell_MouseLeave; cell.Click += Cell_Click; children.Add(cell); } @@ -256,7 +225,6 @@ namespace Avalonia.Controls.Primitives month.CalendarLeftMouseButtonDown += Month_CalendarButtonMouseDown; month.CalendarLeftMouseButtonUp += Month_CalendarButtonMouseUp; month.PointerEnter += Month_MouseEnter; - month.PointerLeave += Month_MouseLeave; children.Add(month); } } @@ -937,17 +905,7 @@ namespace Avalonia.Controls.Primitives } } } - internal void Cell_MouseLeave(object sender, PointerEventArgs e) - { - if (_isMouseLeftButtonDown) - { - CalendarDayButton b = (CalendarDayButton)sender; - // The button is in Pressed state. Change the state to normal. - if (e.Pointer.Captured == b) - e.Pointer.Capture(null); - _lastCalendarDayButton = b; - } - } + internal void Cell_MouseLeftButtonDown(object sender, PointerPressedEventArgs e) { if (Owner != null) @@ -1207,35 +1165,6 @@ namespace Avalonia.Controls.Primitives } } - private void Month_MouseLeave(object sender, PointerEventArgs e) - { - if (_isMouseLeftButtonDownYearView) - { - CalendarButton b = (CalendarButton)sender; - // The button is in Pressed state. Change the state to normal. - if (e.Pointer.Captured == b) - e.Pointer.Capture(null); - //b.ReleaseMouseCapture(); - - _lastCalendarButton = b; - } - } - private void MonthView_MouseLeave(object sender, PointerEventArgs e) - { - if (_lastCalendarDayButton != null) - { - e.Pointer.Capture(_lastCalendarDayButton); - } - } - - private void YearView_MouseLeave(object sender, PointerEventArgs e) - { - if (_lastCalendarButton != null) - { - e.Pointer.Capture(_lastCalendarButton); - } - } - internal void UpdateDisabled(bool isEnabled) { PseudoClasses.Set(":calendardisabled", !isEnabled); diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 3d8ab3ae48..7d4fef009d 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -101,7 +101,7 @@ namespace Avalonia.Controls private ICommand? _command; private bool _commandCanExecute = true; - private Popup _popup; + private Popup? _popup; /// /// Initializes static members of the class. @@ -145,7 +145,7 @@ namespace Avalonia.Controls { var parent = x as Control; return parent?.GetObservable(DefinitionBase.PrivateSharedSizeScopeProperty) ?? - Observable.Return(null); + Observable.Return(null); }); this.Bind(DefinitionBase.PrivateSharedSizeScopeProperty, parentSharedSizeScope); diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index 6d6398bcda..a54d1ce308 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -148,6 +148,7 @@ namespace Avalonia.Controls.Platform { case Key.Up: case Key.Down: + { if (item?.IsTopLevel == true) { if (item.HasSubMenu && !item.IsSubMenuOpen) @@ -161,8 +162,10 @@ namespace Avalonia.Controls.Platform goto default; } break; + } case Key.Left: + { if (item?.Parent is IMenuItem parent && !parent.IsTopLevel && parent.IsSubMenuOpen) { parent.Close(); @@ -174,8 +177,10 @@ namespace Avalonia.Controls.Platform goto default; } break; + } case Key.Right: + { if (item != null && !item.IsTopLevel && item.HasSubMenu) { Open(item, true); @@ -186,8 +191,10 @@ namespace Avalonia.Controls.Platform goto default; } break; + } case Key.Enter: + { if (item != null) { if (!item.HasSubMenu) @@ -202,12 +209,14 @@ namespace Avalonia.Controls.Platform e.Handled = true; } break; + } case Key.Escape: - if (item?.Parent != null) + { + if (item?.Parent is IMenuElement parent) { - item.Parent.Close(); - item.Parent.Focus(); + parent.Close(); + parent.Focus(); } else { @@ -216,8 +225,10 @@ namespace Avalonia.Controls.Platform e.Handled = true; break; + } default: + { var direction = e.Key.ToNavigationDirection(); if (direction.HasValue) @@ -246,6 +257,7 @@ namespace Avalonia.Controls.Platform } break; + } } if (!e.Handled && item?.Parent is IMenuItem parentItem) diff --git a/src/Avalonia.Visuals/Animation/CompositePageTransition.cs b/src/Avalonia.Visuals/Animation/CompositePageTransition.cs new file mode 100644 index 0000000000..9489914c97 --- /dev/null +++ b/src/Avalonia.Visuals/Animation/CompositePageTransition.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Avalonia.Metadata; + +namespace Avalonia.Animation +{ + /// + /// Defines a composite page transition that can be used to combine multiple transitions. + /// + /// + /// + /// Instantiate the in XAML and initialize the + /// property in order to have many animations triggered at once. + /// For example, you can combine and . + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// ]]> + /// + /// + /// + public class CompositePageTransition : IPageTransition + { + /// + /// Gets or sets the transitions to be executed. Can be defined from XAML. + /// + [Content] + public List PageTransitions { get; set; } = new List(); + + /// + /// Starts the animation. + /// + /// + /// The control that is being transitioned away from. May be null. + /// + /// + /// The control that is being transitioned to. May be null. + /// + /// + /// Defines the direction of the transition. + /// + /// + /// A that tracks the progress of the animation. + /// + public Task Start(Visual from, Visual to, bool forward) + { + var transitionTasks = PageTransitions + .Select(transition => transition.Start(from, to, forward)) + .ToList(); + return Task.WhenAll(transitionTasks); + } + } +} diff --git a/src/Avalonia.Visuals/Animation/CrossFade.cs b/src/Avalonia.Visuals/Animation/CrossFade.cs index 640f401418..0615b854da 100644 --- a/src/Avalonia.Visuals/Animation/CrossFade.cs +++ b/src/Avalonia.Visuals/Animation/CrossFade.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Avalonia.Animation.Easings; using Avalonia.Styling; using Avalonia.VisualTree; @@ -74,14 +75,26 @@ namespace Avalonia.Animation /// public TimeSpan Duration { - get - { - return _fadeOutAnimation.Duration; - } - set - { - _fadeOutAnimation.Duration = _fadeInAnimation.Duration = value; - } + get => _fadeOutAnimation.Duration; + set => _fadeOutAnimation.Duration = _fadeInAnimation.Duration = value; + } + + /// + /// Gets or sets element entrance easing. + /// + public Easing FadeInEasing + { + get => _fadeInAnimation.Easing; + set => _fadeInAnimation.Easing = value; + } + + /// + /// Gets or sets element exit easing. + /// + public Easing FadeOutEasing + { + get => _fadeOutAnimation.Easing; + set => _fadeOutAnimation.Easing = value; } /// diff --git a/src/Avalonia.Visuals/Animation/PageSlide.cs b/src/Avalonia.Visuals/Animation/PageSlide.cs index 501c8c0ba4..dd5d598e12 100644 --- a/src/Avalonia.Visuals/Animation/PageSlide.cs +++ b/src/Avalonia.Visuals/Animation/PageSlide.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Avalonia.Animation.Easings; using Avalonia.Media; using Avalonia.Styling; using Avalonia.VisualTree; @@ -48,6 +49,16 @@ namespace Avalonia.Animation /// Gets the duration of the animation. /// public SlideAxis Orientation { get; set; } + + /// + /// Gets or sets element entrance easing. + /// + public Easing SlideInEasing { get; set; } = new LinearEasing(); + + /// + /// Gets or sets element exit easing. + /// + public Easing SlideOutEasing { get; set; } = new LinearEasing(); /// /// Starts the animation. @@ -75,18 +86,12 @@ namespace Avalonia.Animation { var animation = new Animation { - Children = + Easing = SlideOutEasing, + Children = { new KeyFrame { - Setters = - { - new Setter - { - Property = translateProperty, - Value = 0d - } - }, + Setters = { new Setter { Property = translateProperty, Value = 0d } }, Cue = new Cue(0d) }, new KeyFrame @@ -100,10 +105,10 @@ namespace Avalonia.Animation } }, Cue = new Cue(1d) - } - } + } + }, + Duration = Duration }; - animation.Duration = Duration; tasks.Add(animation.RunAsync(from)); } @@ -112,9 +117,9 @@ namespace Avalonia.Animation to.IsVisible = true; var animation = new Animation { + Easing = SlideInEasing, Children = { - new KeyFrame { Setters = @@ -129,19 +134,12 @@ namespace Avalonia.Animation }, new KeyFrame { - Setters = - { - new Setter - { - Property = translateProperty, - Value = 0d - } - }, + Setters = { new Setter { Property = translateProperty, Value = 0d } }, Cue = new Cue(1d) } - } + }, + Duration = Duration }; - animation.Duration = Duration; tasks.Add(animation.RunAsync(to)); } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 98528a128a..05a8daaec8 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -583,14 +583,45 @@ namespace Avalonia.Skia var center = radialGradient.Center.ToPixels(targetSize).ToSKPoint(); var radius = (float)(radialGradient.Radius * targetSize.Width); - // TODO: There is no SetAlpha in SkiaSharp - //paint.setAlpha(128); + var origin = radialGradient.GradientOrigin.ToPixels(targetSize).ToSKPoint(); - // would be nice to cache these shaders possibly? - using (var shader = - SKShader.CreateRadialGradient(center, radius, stopColors, stopOffsets, tileMode)) + if (origin.Equals(center)) { - paintWrapper.Paint.Shader = shader; + // when the origin is the same as the center the Skia RadialGradient acts the same as D2D + using (var shader = + SKShader.CreateRadialGradient(center, radius, stopColors, stopOffsets, tileMode)) + { + paintWrapper.Paint.Shader = shader; + } + } + else + { + // when the origin is different to the center use a two point ConicalGradient to match the behaviour of D2D + + // reverse the order of the stops to match D2D + var reversedColors = new SKColor[stopColors.Length]; + Array.Copy(stopColors, reversedColors, stopColors.Length); + Array.Reverse(reversedColors); + + // and then reverse the reference point of the stops + var reversedStops = new float[stopOffsets.Length]; + for (var i = 0; i < stopOffsets.Length; i++) + { + reversedStops[i] = stopOffsets[i]; + if (reversedStops[i] > 0 && reversedStops[i] < 1) + { + reversedStops[i] = Math.Abs(1 - stopOffsets[i]); + } + } + + // compose with a background colour of the final stop to match D2D's behaviour of filling with the final color + using (var shader = SKShader.CreateCompose( + SKShader.CreateColor(reversedColors[0]), + SKShader.CreateTwoPointConicalGradient(center, radius, origin, 0, reversedColors, reversedStops, tileMode) + )) + { + paintWrapper.Paint.Shader = shader; + } } break; diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 25a34561fc..a03e1ffc22 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -200,6 +200,10 @@ namespace Avalonia.Win32 DipFromLParam(lParam), GetMouseModifiers(wParam)); break; } + // Mouse capture is lost + case WindowsMessage.WM_CANCELMODE: + _mouseDevice.Capture(null); + break; case WindowsMessage.WM_MOUSEMOVE: { diff --git a/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs b/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs index 9beb860bee..2941b8dc34 100644 --- a/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs @@ -18,7 +18,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media public RadialGradientBrushTests() : base(@"Media\RadialGradientBrush") { } - + [Fact] public async Task RadialGradientBrush_RedBlue() { @@ -43,5 +43,127 @@ namespace Avalonia.Direct2D1.RenderTests.Media await RenderToFile(target); CompareImages(); } + + /// + /// Tests using a GradientOrigin that falls inside of the circle described by Center/Radius. + /// + [Fact] + public async Task RadialGradientBrush_RedBlue_Offset_Inside() + { + Decorator target = new Decorator + { + Padding = new Thickness(8), + Width = 200, + Height = 200, + Child = new Border + { + Background = new RadialGradientBrush + { + GradientStops = + { + new GradientStop { Color = Colors.Red, Offset = 0 }, + new GradientStop { Color = Colors.Blue, Offset = 1 } + }, + GradientOrigin = new RelativePoint(0.25, 0.25, RelativeUnit.Relative) + } + } + }; + + await RenderToFile(target); + CompareImages(); + } + + /// + /// Tests using a GradientOrigin that falls outside of the circle described by Center/Radius. + /// + [Fact] + public async Task RadialGradientBrush_RedBlue_Offset_Outside() + { + Decorator target = new Decorator + { + Padding = new Thickness(8), + Width = 200, + Height = 200, + Child = new Border + { + Background = new RadialGradientBrush + { + GradientStops = + { + new GradientStop { Color = Colors.Red, Offset = 0 }, + new GradientStop { Color = Colors.Blue, Offset = 1 } + }, + GradientOrigin = new RelativePoint(0.1, 0.1, RelativeUnit.Relative) + } + } + }; + + await RenderToFile(target); + CompareImages(); + } + + /// + /// Tests using a GradientOrigin that falls inside of the circle described by Center/Radius. + /// + [Fact] + public async Task RadialGradientBrush_RedGreenBlue_Offset_Inside() + { + Decorator target = new Decorator + { + Padding = new Thickness(8), + Width = 200, + Height = 200, + Child = new Border + { + Background = new RadialGradientBrush + { + GradientStops = + { + new GradientStop { Color = Colors.Red, Offset = 0 }, + new GradientStop { Color = Colors.Green, Offset = 0.5 }, + new GradientStop { Color = Colors.Blue, Offset = 1 } + }, + GradientOrigin = new RelativePoint(0.25, 0.25, RelativeUnit.Relative), + Center = new RelativePoint(0.5, 0.5, RelativeUnit.Relative), + Radius = 0.5 + } + } + }; + + await RenderToFile(target); + CompareImages(); + } + + /// + /// Tests using a GradientOrigin that falls outside of the circle described by Center/Radius. + /// + [Fact] + public async Task RadialGradientBrush_RedGreenBlue_Offset_Outside() + { + Decorator target = new Decorator + { + Padding = new Thickness(8), + Width = 200, + Height = 200, + Child = new Border + { + Background = new RadialGradientBrush + { + GradientStops = + { + new GradientStop { Color = Colors.Red, Offset = 0 }, + new GradientStop { Color = Colors.Green, Offset = 0.25 }, + new GradientStop { Color = Colors.Blue, Offset = 1 } + }, + GradientOrigin = new RelativePoint(0.1, 0.1, RelativeUnit.Relative), + Center = new RelativePoint(0.5, 0.5, RelativeUnit.Relative), + Radius = 0.5 + } + } + }; + + await RenderToFile(target); + CompareImages(); + } } } diff --git a/tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_RedBlue_Offset_Inside.expected.png b/tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_RedBlue_Offset_Inside.expected.png new file mode 100644 index 0000000000..4d690abee5 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_RedBlue_Offset_Inside.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_RedBlue_Offset_Outside.expected.png b/tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_RedBlue_Offset_Outside.expected.png new file mode 100644 index 0000000000..667aa9151e Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_RedBlue_Offset_Outside.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_RedGreenBlue_Offset_Inside.expected.png b/tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_RedGreenBlue_Offset_Inside.expected.png new file mode 100644 index 0000000000..1d660f79d1 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_RedGreenBlue_Offset_Inside.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_RedGreenBlue_Offset_Outside.expected.png b/tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_RedGreenBlue_Offset_Outside.expected.png new file mode 100644 index 0000000000..ea1009b54a Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_RedGreenBlue_Offset_Outside.expected.png differ diff --git a/tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_RedBlue_Offset_Inside.expected.png b/tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_RedBlue_Offset_Inside.expected.png new file mode 100644 index 0000000000..4d690abee5 Binary files /dev/null and b/tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_RedBlue_Offset_Inside.expected.png differ diff --git a/tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_RedBlue_Offset_Outside.expected.png b/tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_RedBlue_Offset_Outside.expected.png new file mode 100644 index 0000000000..667aa9151e Binary files /dev/null and b/tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_RedBlue_Offset_Outside.expected.png differ diff --git a/tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_RedGreenBlue_Offset_Inside.expected.png b/tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_RedGreenBlue_Offset_Inside.expected.png new file mode 100644 index 0000000000..1d660f79d1 Binary files /dev/null and b/tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_RedGreenBlue_Offset_Inside.expected.png differ diff --git a/tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_RedGreenBlue_Offset_Outside.expected.png b/tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_RedGreenBlue_Offset_Outside.expected.png new file mode 100644 index 0000000000..ea1009b54a Binary files /dev/null and b/tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_RedGreenBlue_Offset_Outside.expected.png differ