diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index 488062f5b6..ec7a11067c 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -29,6 +29,9 @@
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
+
+
+
@@ -54,6 +57,7 @@
+
diff --git a/samples/ControlCatalog/Pages/DateTimePickerPage.xaml b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml
new file mode 100644
index 0000000000..af6b6e8605
--- /dev/null
+++ b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml
@@ -0,0 +1,123 @@
+
+
+ DatePicker and TimePicker
+
+
+
+
+ A simple DatePicker with a header
+
+
+
+
+
+
+
+
+ <DatePicker Header="Pick a date" />
+
+
+
+
+
+
+ A DatePicker with day formatted and year hidden.
+
+
+
+
+
+
+
+
+ <DatePicker DayFormat="d (ddd)" YearVisible="False" />
+
+
+
+
+
+
+
+
+ A simple TimePicker.
+
+
+
+
+
+
+
+
+ <TimePicker />
+
+
+
+
+
+
+ A TimePicker with a header and minute increments specified.
+
+
+
+
+
+
+
+
+ <TimePicker Header="Arrival time" MinuteIncrement="15" />
+
+
+
+
+
+
+ A TimePicker using a 12-hour clock.
+
+
+
+
+
+
+
+
+ <TimePicker ClockIdentifier="12HourClock" Header="12 hour clock" />
+
+
+
+
+
+
+ A TimePicker using a 24-hour clock.
+
+
+
+
+
+
+
+
+ <TimePicker ClockIdentifier="24HourClock" Header="24 hour clock" />
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/DateTimePickerPage.xaml.cs b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml.cs
new file mode 100644
index 0000000000..6c7ae3437e
--- /dev/null
+++ b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml.cs
@@ -0,0 +1,30 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace ControlCatalog.Pages
+{
+ public class DateTimePickerPage : UserControl
+ {
+ public DateTimePickerPage()
+ {
+ this.InitializeComponent();
+ this.FindControl("DatePickerDesc").Text = "Use a DatePicker to let users set a date in your app, " +
+ "for example to schedule an appointment. The DatePicker displays three controls for month, day, and year. " +
+ "These controls are easy to use with touch or mouse, and they can be styled and configured in several different ways. " +
+ "Order of month, day, and year is dynamically set based on user date settings";
+
+ this.FindControl("TimePickerDesc").Text = "Use a TimePicker to let users set a time in your app, for example " +
+ "to set a reminder. The TimePicker displays three controls for hour, minute, and AM / PM(if necessary).These controls " +
+ "are easy to use with touch or mouse, and they can be styled and configured in several different ways. " +
+ "12 - hour or 24 - hour clock and visiblility of AM / PM is dynamically set based on user time settings, or can be overridden.";
+
+
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
diff --git a/samples/ControlCatalog/Pages/ProgressBarPage.xaml b/samples/ControlCatalog/Pages/ProgressBarPage.xaml
index 13bae59805..2ec0b48c76 100644
--- a/samples/ControlCatalog/Pages/ProgressBarPage.xaml
+++ b/samples/ControlCatalog/Pages/ProgressBarPage.xaml
@@ -1,29 +1,19 @@
-
+
ProgressBar
A progress bar control
-
-
-
+
+
+
-
-
+
-
-
+
-
-
+
+
diff --git a/samples/ControlCatalog/Pages/SplitViewPage.xaml b/samples/ControlCatalog/Pages/SplitViewPage.xaml
new file mode 100644
index 0000000000..7e629db2da
--- /dev/null
+++ b/samples/ControlCatalog/Pages/SplitViewPage.xaml
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Inline
+ CompactInline
+ Overlay
+ CompactOverlay
+
+
+
+
+ SystemControlBackgroundChromeMediumLowBrush
+ Red
+ Blue
+ Green
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/SplitViewPage.xaml.cs b/samples/ControlCatalog/Pages/SplitViewPage.xaml.cs
new file mode 100644
index 0000000000..cbf217c94a
--- /dev/null
+++ b/samples/ControlCatalog/Pages/SplitViewPage.xaml.cs
@@ -0,0 +1,21 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using ControlCatalog.ViewModels;
+
+namespace ControlCatalog.Pages
+{
+ public class SplitViewPage : UserControl
+ {
+ public SplitViewPage()
+ {
+ this.InitializeComponent();
+ DataContext = new SplitViewPageViewModel();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
diff --git a/samples/ControlCatalog/ViewModels/SplitViewPageViewModel.cs b/samples/ControlCatalog/ViewModels/SplitViewPageViewModel.cs
new file mode 100644
index 0000000000..f27f605a8b
--- /dev/null
+++ b/samples/ControlCatalog/ViewModels/SplitViewPageViewModel.cs
@@ -0,0 +1,46 @@
+using System;
+using Avalonia.Controls;
+using ReactiveUI;
+
+namespace ControlCatalog.ViewModels
+{
+ public class SplitViewPageViewModel : ReactiveObject
+ {
+ private bool _isLeft = true;
+ private int _displayMode = 3; //CompactOverlay
+
+ public bool IsLeft
+ {
+ get => _isLeft;
+ set
+ {
+ this.RaiseAndSetIfChanged(ref _isLeft, value);
+ this.RaisePropertyChanged(nameof(PanePlacement));
+ }
+ }
+
+ public int DisplayMode
+ {
+ get => _displayMode;
+ set
+ {
+ this.RaiseAndSetIfChanged(ref _displayMode, value);
+ this.RaisePropertyChanged(nameof(CurrentDisplayMode));
+ }
+ }
+
+ public SplitViewPanePlacement PanePlacement => _isLeft ? SplitViewPanePlacement.Left : SplitViewPanePlacement.Right;
+
+ public SplitViewDisplayMode CurrentDisplayMode
+ {
+ get
+ {
+ if (Enum.IsDefined(typeof(SplitViewDisplayMode), _displayMode))
+ {
+ return (SplitViewDisplayMode)_displayMode;
+ }
+ return SplitViewDisplayMode.CompactOverlay;
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Animation/Easing/Easing.cs b/src/Avalonia.Animation/Easing/Easing.cs
index 5b0dea6c60..e006459652 100644
--- a/src/Avalonia.Animation/Easing/Easing.cs
+++ b/src/Avalonia.Animation/Easing/Easing.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
+using System.Globalization;
using System.Linq;
namespace Avalonia.Animation.Easings
@@ -25,6 +26,11 @@ namespace Avalonia.Animation.Easings
/// Returns the instance of the parsed type.
public static Easing Parse(string e)
{
+ if (e.Contains(','))
+ {
+ return new SplineEasing(KeySpline.Parse(e, CultureInfo.InvariantCulture));
+ }
+
if (_easingTypes == null)
{
_easingTypes = new Dictionary();
diff --git a/src/Avalonia.Animation/Easing/SplineEasing.cs b/src/Avalonia.Animation/Easing/SplineEasing.cs
new file mode 100644
index 0000000000..975fcc4746
--- /dev/null
+++ b/src/Avalonia.Animation/Easing/SplineEasing.cs
@@ -0,0 +1,85 @@
+namespace Avalonia.Animation.Easings
+{
+ ///
+ /// Eases a value
+ /// using a user-defined cubic bezier curve.
+ /// Good for custom easing functions that doesn't quite
+ /// fit with the built-in ones.
+ ///
+ public class SplineEasing : Easing
+ {
+ ///
+ /// X coordinate of the first control point
+ ///
+ public double X1
+ {
+ get => _internalKeySpline.ControlPointX1;
+ set
+ {
+ _internalKeySpline.ControlPointX1 = value;
+ }
+ }
+
+ ///
+ /// Y coordinate of the first control point
+ ///
+ public double Y1
+ {
+ get => _internalKeySpline.ControlPointY1;
+ set
+ {
+ _internalKeySpline.ControlPointY1 = value;
+ }
+ }
+
+ ///
+ /// X coordinate of the second control point
+ ///
+ public double X2
+ {
+ get => _internalKeySpline.ControlPointX2;
+ set
+ {
+ _internalKeySpline.ControlPointX2 = value;
+ }
+ }
+
+ ///
+ /// Y coordinate of the second control point
+ ///
+ public double Y2
+ {
+ get => _internalKeySpline.ControlPointY2;
+ set
+ {
+ _internalKeySpline.ControlPointY2 = value;
+ }
+ }
+
+ private readonly KeySpline _internalKeySpline;
+
+ public SplineEasing(double x1 = 0d, double y1 = 0d, double x2 = 1d, double y2 = 1d)
+ {
+ _internalKeySpline = new KeySpline();
+
+ this.X1 = x1;
+ this.Y1 = y1;
+ this.X2 = x2;
+ this.Y1 = y2;
+ }
+
+ public SplineEasing(KeySpline keySpline)
+ {
+ _internalKeySpline = keySpline;
+ }
+
+ public SplineEasing()
+ {
+ _internalKeySpline = new KeySpline();
+ }
+
+ ///
+ public override double Ease(double progress) =>
+ _internalKeySpline.GetSplineProgress(progress);
+ }
+}
diff --git a/src/Avalonia.Animation/KeySpline.cs b/src/Avalonia.Animation/KeySpline.cs
index 5a4f7a15a3..a6e9769186 100644
--- a/src/Avalonia.Animation/KeySpline.cs
+++ b/src/Avalonia.Animation/KeySpline.cs
@@ -81,7 +81,10 @@ namespace Avalonia.Animation
/// A with the appropriate values set
public static KeySpline Parse(string value, CultureInfo culture)
{
- using (var tokenizer = new StringTokenizer((string)value, CultureInfo.InvariantCulture, exceptionMessage: "Invalid KeySpline."))
+ if (culture is null)
+ culture = CultureInfo.InvariantCulture;
+
+ using (var tokenizer = new StringTokenizer((string)value, culture, exceptionMessage: $"Invalid KeySpline string: \"{value}\"."))
{
return new KeySpline(tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble());
}
@@ -98,6 +101,7 @@ namespace Avalonia.Animation
if (IsValidXValue(value))
{
_controlPointX1 = value;
+ _isDirty = true;
}
else
{
@@ -112,7 +116,11 @@ namespace Avalonia.Animation
public double ControlPointY1
{
get => _controlPointY1;
- set => _controlPointY1 = value;
+ set
+ {
+ _controlPointY1 = value;
+ _isDirty = true;
+ }
}
///
@@ -126,6 +134,7 @@ namespace Avalonia.Animation
if (IsValidXValue(value))
{
_controlPointX2 = value;
+ _isDirty = true;
}
else
{
@@ -140,7 +149,11 @@ namespace Avalonia.Animation
public double ControlPointY2
{
get => _controlPointY2;
- set => _controlPointY2 = value;
+ set
+ {
+ _controlPointY2 = value;
+ _isDirty = true;
+ }
}
///
@@ -330,20 +343,4 @@ namespace Avalonia.Animation
}
}
}
-
- ///
- /// Converts string values to values
- ///
- public class KeySplineTypeConverter : TypeConverter
- {
- public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
- {
- return sourceType == typeof(string);
- }
-
- public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
- {
- return KeySpline.Parse((string)value, culture);
- }
- }
}
diff --git a/src/Avalonia.Animation/KeySplineTypeConverter.cs b/src/Avalonia.Animation/KeySplineTypeConverter.cs
new file mode 100644
index 0000000000..cd7427a37d
--- /dev/null
+++ b/src/Avalonia.Animation/KeySplineTypeConverter.cs
@@ -0,0 +1,25 @@
+using System;
+using System.ComponentModel;
+using System.Globalization;
+
+// Ported from WPF open-source code.
+// https://github.com/dotnet/wpf/blob/ae1790531c3b993b56eba8b1f0dd395a3ed7de75/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Media/Animation/KeySpline.cs
+
+namespace Avalonia.Animation
+{
+ ///
+ /// Converts string values to values
+ ///
+ public class KeySplineTypeConverter : TypeConverter
+ {
+ public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
+ {
+ return sourceType == typeof(string);
+ }
+
+ public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
+ {
+ return KeySpline.Parse((string)value, culture);
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Data/Core/LogicalNotNode.cs b/src/Avalonia.Base/Data/Core/LogicalNotNode.cs
index 7c402f42f6..6d09befeba 100644
--- a/src/Avalonia.Base/Data/Core/LogicalNotNode.cs
+++ b/src/Avalonia.Base/Data/Core/LogicalNotNode.cs
@@ -12,8 +12,19 @@ namespace Avalonia.Data.Core
base.NextValueChanged(Negate(value));
}
- private static object Negate(object v)
+ private static object Negate(object value)
{
+ var notification = value as BindingNotification;
+ var v = BindingNotification.ExtractValue(value);
+
+ BindingNotification GenerateError(Exception e)
+ {
+ notification ??= new BindingNotification(AvaloniaProperty.UnsetValue);
+ notification.AddError(e, BindingErrorType.Error);
+ notification.ClearValue();
+ return notification;
+ }
+
if (v != AvaloniaProperty.UnsetValue)
{
var s = v as string;
@@ -28,9 +39,7 @@ namespace Avalonia.Data.Core
}
else
{
- return new BindingNotification(
- new InvalidCastException($"Unable to convert '{s}' to bool."),
- BindingErrorType.Error);
+ return GenerateError(new InvalidCastException($"Unable to convert '{s}' to bool."));
}
}
else
@@ -38,24 +47,31 @@ namespace Avalonia.Data.Core
try
{
var boolean = Convert.ToBoolean(v, CultureInfo.InvariantCulture);
- return !boolean;
+
+ if (notification is object)
+ {
+ notification.SetValue(!boolean);
+ return notification;
+ }
+ else
+ {
+ return !boolean;
+ }
}
catch (InvalidCastException)
{
// The error message here is "Unable to cast object of type 'System.Object'
// to type 'System.IConvertible'" which is kinda useless so provide our own.
- return new BindingNotification(
- new InvalidCastException($"Unable to convert '{v}' to bool."),
- BindingErrorType.Error);
+ return GenerateError(new InvalidCastException($"Unable to convert '{v}' to bool."));
}
catch (Exception e)
{
- return new BindingNotification(e, BindingErrorType.Error);
+ return GenerateError(e);
}
}
}
- return AvaloniaProperty.UnsetValue;
+ return notification ?? AvaloniaProperty.UnsetValue;
}
public object Transform(object value)
diff --git a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs
new file mode 100644
index 0000000000..5d3311e8c6
--- /dev/null
+++ b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs
@@ -0,0 +1,412 @@
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Shapes;
+using Avalonia.Controls.Templates;
+using Avalonia.Interactivity;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// A control to allow the user to select a date
+ ///
+ public class DatePicker : TemplatedControl
+ {
+ ///
+ /// Define the Property
+ ///
+ public static readonly DirectProperty DayFormatProperty =
+ AvaloniaProperty.RegisterDirect(nameof(DayFormat),
+ x => x.DayFormat, (x, v) => x.DayFormat = v);
+
+ ///
+ /// Defines the Property
+ ///
+ public static readonly DirectProperty DayVisibleProperty =
+ AvaloniaProperty.RegisterDirect(nameof(DayVisible),
+ x => x.DayVisible, (x, v) => x.DayVisible = v);
+
+ ///
+ /// Defines the Property
+ ///
+ public static readonly StyledProperty
public class ProgressBar : RangeBase
{
+ public class ProgressBarTemplateProperties : AvaloniaObject
+ {
+ private double _container2Width;
+ private double _containerWidth;
+ private double _containerAnimationStartPosition;
+ private double _containerAnimationEndPosition;
+ private double _container2AnimationStartPosition;
+ private double _container2AnimationEndPosition;
+
+ public static readonly DirectProperty ContainerAnimationStartPositionProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(ContainerAnimationStartPosition),
+ p => p.ContainerAnimationStartPosition,
+ (p, o) => p.ContainerAnimationStartPosition = o, 0d);
+
+ public static readonly DirectProperty ContainerAnimationEndPositionProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(ContainerAnimationEndPosition),
+ p => p.ContainerAnimationEndPosition,
+ (p, o) => p.ContainerAnimationEndPosition = o, 0d);
+
+ public static readonly DirectProperty Container2AnimationStartPositionProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(Container2AnimationStartPosition),
+ p => p.Container2AnimationStartPosition,
+ (p, o) => p.Container2AnimationStartPosition = o, 0d);
+
+ public static readonly DirectProperty Container2AnimationEndPositionProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(Container2AnimationEndPosition),
+ p => p.Container2AnimationEndPosition,
+ (p, o) => p.Container2AnimationEndPosition = o);
+
+ public static readonly DirectProperty Container2WidthProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(Container2Width),
+ p => p.Container2Width,
+ (p, o) => p.Container2Width = o);
+
+ public static readonly DirectProperty ContainerWidthProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(ContainerWidth),
+ p => p.ContainerWidth,
+ (p, o) => p.ContainerWidth = o);
+
+ public double ContainerAnimationStartPosition
+ {
+ get => _containerAnimationStartPosition;
+ set => SetAndRaise(ContainerAnimationStartPositionProperty, ref _containerAnimationStartPosition, value);
+ }
+
+ public double ContainerAnimationEndPosition
+ {
+ get => _containerAnimationEndPosition;
+ set => SetAndRaise(ContainerAnimationEndPositionProperty, ref _containerAnimationEndPosition, value);
+ }
+
+ public double Container2AnimationStartPosition
+ {
+ get => _container2AnimationStartPosition;
+ set => SetAndRaise(Container2AnimationStartPositionProperty, ref _container2AnimationStartPosition, value);
+ }
+
+ public double Container2Width
+ {
+ get => _container2Width;
+ set => SetAndRaise(Container2WidthProperty, ref _container2Width, value);
+ }
+
+ public double ContainerWidth
+ {
+ get => _containerWidth;
+ set => SetAndRaise(ContainerWidthProperty, ref _containerWidth, value);
+ }
+
+ public double Container2AnimationEndPosition
+ {
+ get => _container2AnimationEndPosition;
+ set => SetAndRaise(Container2AnimationEndPositionProperty, ref _container2AnimationEndPosition, value);
+ }
+ }
+
+ private double _indeterminateStartingOffset;
+ private double _indeterminateEndingOffset;
+ private Border _indicator;
+
public static readonly StyledProperty IsIndeterminateProperty =
AvaloniaProperty.Register(nameof(IsIndeterminate));
@@ -20,19 +105,33 @@ namespace Avalonia.Controls
public static readonly StyledProperty OrientationProperty =
AvaloniaProperty.Register(nameof(Orientation), Orientation.Horizontal);
- private static readonly DirectProperty IndeterminateStartingOffsetProperty =
+ [Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")]
+ public static readonly DirectProperty IndeterminateStartingOffsetProperty =
AvaloniaProperty.RegisterDirect(
nameof(IndeterminateStartingOffset),
p => p.IndeterminateStartingOffset,
(p, o) => p.IndeterminateStartingOffset = o);
- private static readonly DirectProperty IndeterminateEndingOffsetProperty =
+ [Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")]
+ public static readonly DirectProperty IndeterminateEndingOffsetProperty =
AvaloniaProperty.RegisterDirect(
nameof(IndeterminateEndingOffset),
p => p.IndeterminateEndingOffset,
(p, o) => p.IndeterminateEndingOffset = o);
- private Border _indicator;
+ [Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")]
+ public double IndeterminateStartingOffset
+ {
+ get => _indeterminateStartingOffset;
+ set => SetAndRaise(IndeterminateStartingOffsetProperty, ref _indeterminateStartingOffset, value);
+ }
+
+ [Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")]
+ public double IndeterminateEndingOffset
+ {
+ get => _indeterminateEndingOffset;
+ set => SetAndRaise(IndeterminateEndingOffsetProperty, ref _indeterminateEndingOffset, value);
+ }
static ProgressBar()
{
@@ -45,6 +144,8 @@ namespace Avalonia.Controls
UpdatePseudoClasses(IsIndeterminate, Orientation);
}
+ public ProgressBarTemplateProperties TemplateProperties { get; } = new ProgressBarTemplateProperties();
+
public bool IsIndeterminate
{
get => GetValue(IsIndeterminateProperty);
@@ -62,19 +163,6 @@ namespace Avalonia.Controls
get => GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}
- private double _indeterminateStartingOffset;
- private double IndeterminateStartingOffset
- {
- get => _indeterminateStartingOffset;
- set => SetAndRaise(IndeterminateStartingOffsetProperty, ref _indeterminateStartingOffset, value);
- }
-
- private double _indeterminateEndingOffset;
- private double IndeterminateEndingOffset
- {
- get => _indeterminateEndingOffset;
- set => SetAndRaise(IndeterminateEndingOffsetProperty, ref _indeterminateEndingOffset, value);
- }
///
protected override Size ArrangeOverride(Size finalSize)
@@ -111,21 +199,33 @@ namespace Avalonia.Controls
{
if (IsIndeterminate)
{
- if (Orientation == Orientation.Horizontal)
- {
- var width = bounds.Width / 5.0;
- IndeterminateStartingOffset = -width;
- _indicator.Width = width;
- IndeterminateEndingOffset = bounds.Width;
+ // Pulled from ModernWPF.
- }
- else
- {
- var height = bounds.Height / 5.0;
- IndeterminateStartingOffset = -bounds.Height;
- _indicator.Height = height;
- IndeterminateEndingOffset = height;
- }
+ var dim = Orientation == Orientation.Horizontal ? bounds.Width : bounds.Height;
+ var barIndicatorWidth = dim * 0.4; // Indicator width at 40% of ProgressBar
+ var barIndicatorWidth2 = dim * 0.6; // Indicator width at 60% of ProgressBar
+
+ TemplateProperties.ContainerWidth = barIndicatorWidth;
+ TemplateProperties.Container2Width = barIndicatorWidth2;
+
+ TemplateProperties.ContainerAnimationStartPosition = barIndicatorWidth * -1.8; // Position at -180%
+ TemplateProperties.ContainerAnimationEndPosition = barIndicatorWidth * 3.0; // Position at 300%
+
+ TemplateProperties.Container2AnimationStartPosition = barIndicatorWidth2 * -1.5; // Position at -150%
+ TemplateProperties.Container2AnimationEndPosition = barIndicatorWidth2 * 1.66; // Position at 166%
+
+ // Remove these properties when we switch to fluent as default and removed the old one.
+ IndeterminateStartingOffset = -(dim / 5d);
+ IndeterminateEndingOffset = dim;
+
+ var padding = Padding;
+ var rectangle = new RectangleGeometry(
+ new Rect(
+ padding.Left,
+ padding.Top,
+ bounds.Width - (padding.Right + padding.Left),
+ bounds.Height - (padding.Bottom + padding.Top)
+ ));
}
else
{
diff --git a/src/Avalonia.Controls/SplitView.cs b/src/Avalonia.Controls/SplitView.cs
new file mode 100644
index 0000000000..fc3ff51f24
--- /dev/null
+++ b/src/Avalonia.Controls/SplitView.cs
@@ -0,0 +1,487 @@
+using Avalonia.Controls.Primitives;
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+using Avalonia.Interactivity;
+using Avalonia.Media;
+using Avalonia.Metadata;
+using Avalonia.Platform;
+using Avalonia.VisualTree;
+using System;
+using System.Reactive.Disposables;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Defines constants for how the SplitView Pane should display
+ ///
+ public enum SplitViewDisplayMode
+ {
+ ///
+ /// Pane is displayed next to content, and does not auto collapse
+ /// when tapped outside
+ ///
+ Inline,
+ ///
+ /// Pane is displayed next to content. When collapsed, pane is still
+ /// visible according to CompactPaneLength. Pane does not auto collapse
+ /// when tapped outside
+ ///
+ CompactInline,
+ ///
+ /// Pane is displayed above content. Pane collapses when tapped outside
+ ///
+ Overlay,
+ ///
+ /// Pane is displayed above content. When collapsed, pane is still
+ /// visible according to CompactPaneLength. Pane collapses when tapped outside
+ ///
+ CompactOverlay
+ }
+
+ ///
+ /// Defines constants for where the Pane should appear
+ ///
+ public enum SplitViewPanePlacement
+ {
+ Left,
+ Right
+ }
+
+ public class SplitViewTemplateSettings : AvaloniaObject
+ {
+ internal SplitViewTemplateSettings() { }
+
+ public static readonly StyledProperty ClosedPaneWidthProperty =
+ AvaloniaProperty.Register(nameof(ClosedPaneWidth), 0d);
+
+ public static readonly StyledProperty PaneColumnGridLengthProperty =
+ AvaloniaProperty.Register(nameof(PaneColumnGridLength));
+
+ public double ClosedPaneWidth
+ {
+ get => GetValue(ClosedPaneWidthProperty);
+ internal set => SetValue(ClosedPaneWidthProperty, value);
+ }
+
+ public GridLength PaneColumnGridLength
+ {
+ get => GetValue(PaneColumnGridLengthProperty);
+ internal set => SetValue(PaneColumnGridLengthProperty, value);
+ }
+ }
+
+ ///
+ /// A control with two views: A collapsible pane and an area for content
+ ///
+ public class SplitView : TemplatedControl
+ {
+ /*
+ Pseudo classes & combos
+ :open / :closed
+ :compactoverlay :compactinline :overlay :inline
+ :left :right
+ */
+
+ ///
+ /// Defines the property
+ ///
+ public static readonly StyledProperty ContentProperty =
+ AvaloniaProperty.Register(nameof(Content));
+
+ ///
+ /// Defines the property
+ ///
+ public static readonly StyledProperty CompactPaneLengthProperty =
+ AvaloniaProperty.Register(nameof(CompactPaneLength), defaultValue: 48);
+
+ ///
+ /// Defines the property
+ ///
+ public static readonly StyledProperty DisplayModeProperty =
+ AvaloniaProperty.Register(nameof(DisplayMode), defaultValue: SplitViewDisplayMode.Overlay);
+
+ ///
+ /// Defines the property
+ ///
+ public static readonly DirectProperty IsPaneOpenProperty =
+ AvaloniaProperty.RegisterDirect(nameof(IsPaneOpen),
+ x => x.IsPaneOpen, (x, v) => x.IsPaneOpen = v);
+
+ ///
+ /// Defines the property
+ ///
+ public static readonly StyledProperty OpenPaneLengthProperty =
+ AvaloniaProperty.Register(nameof(OpenPaneLength), defaultValue: 320);
+
+ ///
+ /// Defines the property
+ ///
+ public static readonly StyledProperty PaneBackgroundProperty =
+ AvaloniaProperty.Register(nameof(PaneBackground));
+
+ ///
+ /// Defines the property
+ ///
+ public static readonly StyledProperty PanePlacementProperty =
+ AvaloniaProperty.Register(nameof(PanePlacement));
+
+ ///
+ /// Defines the property
+ ///
+ public static readonly StyledProperty PaneProperty =
+ AvaloniaProperty.Register(nameof(Pane));
+
+ ///
+ /// Defines the property
+ ///
+ public static readonly StyledProperty UseLightDismissOverlayModeProperty =
+ AvaloniaProperty.Register(nameof(UseLightDismissOverlayMode));
+
+ ///
+ /// Defines the property
+ ///
+ public static readonly StyledProperty TemplateSettingsProperty =
+ AvaloniaProperty.Register(nameof(TemplateSettings));
+
+ private bool _isPaneOpen;
+ private Panel _pane;
+ private CompositeDisposable _pointerDisposables;
+
+ public SplitView()
+ {
+ PseudoClasses.Add(":overlay");
+ PseudoClasses.Add(":left");
+
+ TemplateSettings = new SplitViewTemplateSettings();
+ }
+
+ static SplitView()
+ {
+ UseLightDismissOverlayModeProperty.Changed.AddClassHandler((x, v) => x.OnUseLightDismissChanged(v));
+ CompactPaneLengthProperty.Changed.AddClassHandler((x, v) => x.OnCompactPaneLengthChanged(v));
+ PanePlacementProperty.Changed.AddClassHandler((x, v) => x.OnPanePlacementChanged(v));
+ DisplayModeProperty.Changed.AddClassHandler((x, v) => x.OnDisplayModeChanged(v));
+ }
+
+ ///
+ /// Gets or sets the content of the SplitView
+ ///
+ [Content]
+ public IControl Content
+ {
+ get => GetValue(ContentProperty);
+ set => SetValue(ContentProperty, value);
+ }
+
+ ///
+ /// Gets or sets the length of the pane when in
+ /// or mode
+ ///
+ public double CompactPaneLength
+ {
+ get => GetValue(CompactPaneLengthProperty);
+ set => SetValue(CompactPaneLengthProperty, value);
+ }
+
+ ///
+ /// Gets or sets the for the SplitView
+ ///
+ public SplitViewDisplayMode DisplayMode
+ {
+ get => GetValue(DisplayModeProperty);
+ set => SetValue(DisplayModeProperty, value);
+ }
+
+ ///
+ /// Gets or sets whether the pane is open or closed
+ ///
+ public bool IsPaneOpen
+ {
+ get => _isPaneOpen;
+ set
+ {
+ if (value == _isPaneOpen)
+ {
+ return;
+ }
+
+ if (value)
+ {
+ OnPaneOpening(this, null);
+ SetAndRaise(IsPaneOpenProperty, ref _isPaneOpen, value);
+
+ PseudoClasses.Add(":open");
+ PseudoClasses.Remove(":closed");
+ OnPaneOpened(this, null);
+ }
+ else
+ {
+ SplitViewPaneClosingEventArgs args = new SplitViewPaneClosingEventArgs(false);
+ OnPaneClosing(this, args);
+ if (!args.Cancel)
+ {
+ SetAndRaise(IsPaneOpenProperty, ref _isPaneOpen, value);
+
+ PseudoClasses.Add(":closed");
+ PseudoClasses.Remove(":open");
+ OnPaneClosed(this, null);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the length of the pane when open
+ ///
+ public double OpenPaneLength
+ {
+ get => GetValue(OpenPaneLengthProperty);
+ set => SetValue(OpenPaneLengthProperty, value);
+ }
+
+ ///
+ /// Gets or sets the background of the pane
+ ///
+ public IBrush PaneBackground
+ {
+ get => GetValue(PaneBackgroundProperty);
+ set => SetValue(PaneBackgroundProperty, value);
+ }
+
+ ///
+ /// Gets or sets the for the SplitView
+ ///
+ public SplitViewPanePlacement PanePlacement
+ {
+ get => GetValue(PanePlacementProperty);
+ set => SetValue(PanePlacementProperty, value);
+ }
+
+ ///
+ /// Gets or sets the Pane for the SplitView
+ ///
+ public IControl Pane
+ {
+ get => GetValue(PaneProperty);
+ set => SetValue(PaneProperty, value);
+ }
+
+ ///
+ /// Gets or sets whether WinUI equivalent LightDismissOverlayMode is enabled
+ /// When enabled, and the pane is open in Overlay or CompactOverlay mode,
+ /// the contents of the splitview are darkened to visually separate the open pane
+ /// and the rest of the SplitView
+ ///
+ public bool UseLightDismissOverlayMode
+ {
+ get => GetValue(UseLightDismissOverlayModeProperty);
+ set => SetValue(UseLightDismissOverlayModeProperty, value);
+ }
+
+ ///
+ /// Gets or sets the TemplateSettings for the SplitView
+ ///
+ public SplitViewTemplateSettings TemplateSettings
+ {
+ get => GetValue(TemplateSettingsProperty);
+ set => SetValue(TemplateSettingsProperty, value);
+ }
+
+ ///
+ /// Fired when the pane is closed
+ ///
+ public event EventHandler PaneClosed;
+
+ ///
+ /// Fired when the pane is closing
+ ///
+ public event EventHandler PaneClosing;
+
+ ///
+ /// Fired when the pane is opened
+ ///
+ public event EventHandler PaneOpened;
+
+ ///
+ /// Fired when the pane is opening
+ ///
+ public event EventHandler PaneOpening;
+
+ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+ {
+ base.OnApplyTemplate(e);
+ _pane = e.NameScope.Find("PART_PaneRoot");
+ }
+
+ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ base.OnAttachedToVisualTree(e);
+
+ var topLevel = this.VisualRoot;
+ if (topLevel is Window window)
+ {
+ //Logic adapted from Popup
+ //Basically if we're using an overlay DisplayMode, close the pane if we don't click on the pane
+ IDisposable subscribeToEventHandler(T target, TEventHandler handler,
+ Action subscribe, Action unsubscribe)
+ {
+ subscribe(target, handler);
+ return Disposable.Create((unsubscribe, target, handler), state => state.unsubscribe(state.target, state.handler));
+ }
+
+ _pointerDisposables = new CompositeDisposable(
+ window.AddDisposableHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel),
+ InputManager.Instance?.Process.Subscribe(OnNonClientClick),
+ subscribeToEventHandler(window, Window_Deactivated,
+ (x, handler) => x.Deactivated += handler, (x, handler) => x.Deactivated -= handler),
+ subscribeToEventHandler(window.PlatformImpl, OnWindowLostFocus,
+ (x, handler) => x.LostFocus += handler, (x, handler) => x.LostFocus -= handler));
+ }
+ }
+
+ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ base.OnDetachedFromVisualTree(e);
+ _pointerDisposables?.Dispose();
+ }
+
+ private void OnWindowLostFocus()
+ {
+ if (IsPaneOpen && ShouldClosePane())
+ {
+ IsPaneOpen = false;
+ }
+ }
+
+ private void PointerPressedOutside(object sender, PointerPressedEventArgs e)
+ {
+ if (!IsPaneOpen)
+ {
+ return;
+ }
+
+ //If we click within the Pane, don't do anything
+ //Otherwise, ClosePane if open & using an overlay display mode
+ bool closePane = ShouldClosePane();
+ if (!closePane)
+ {
+ return;
+ }
+
+ var src = e.Source as IVisual;
+ while (src != null)
+ {
+ if (src == _pane)
+ {
+ closePane = false;
+ break;
+ }
+
+ src = src.VisualParent;
+ }
+ if (closePane)
+ {
+ IsPaneOpen = false;
+ e.Handled = true;
+ }
+ }
+
+ private void OnNonClientClick(RawInputEventArgs obj)
+ {
+ if (!IsPaneOpen)
+ {
+ return;
+ }
+
+ var mouse = obj as RawPointerEventArgs;
+ if (mouse?.Type == RawPointerEventType.NonClientLeftButtonDown)
+
+ {
+ if (ShouldClosePane())
+ IsPaneOpen = false;
+ }
+ }
+
+ private void Window_Deactivated(object sender, EventArgs e)
+ {
+ if (IsPaneOpen && ShouldClosePane())
+ {
+ IsPaneOpen = false;
+ }
+ }
+
+ private bool ShouldClosePane()
+ {
+ return (DisplayMode == SplitViewDisplayMode.CompactOverlay || DisplayMode == SplitViewDisplayMode.Overlay);
+ }
+
+ protected virtual void OnPaneOpening(SplitView sender, EventArgs args)
+ {
+ PaneOpening?.Invoke(sender, args);
+ }
+
+ protected virtual void OnPaneOpened(SplitView sender, EventArgs args)
+ {
+ PaneOpened?.Invoke(sender, args);
+ }
+
+ protected virtual void OnPaneClosing(SplitView sender, SplitViewPaneClosingEventArgs args)
+ {
+ PaneClosing?.Invoke(sender, args);
+ }
+
+ protected virtual void OnPaneClosed(SplitView sender, EventArgs args)
+ {
+ PaneClosed?.Invoke(sender, args);
+ }
+
+ private void OnCompactPaneLengthChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ var newLen = (double)e.NewValue;
+ var displayMode = DisplayMode;
+ if (displayMode == SplitViewDisplayMode.CompactInline)
+ {
+ TemplateSettings.ClosedPaneWidth = newLen;
+ }
+ else if (displayMode == SplitViewDisplayMode.CompactOverlay)
+ {
+ TemplateSettings.ClosedPaneWidth = newLen;
+ TemplateSettings.PaneColumnGridLength = new GridLength(newLen, GridUnitType.Pixel);
+ }
+ }
+
+ private void OnPanePlacementChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ var oldState = e.OldValue.ToString().ToLower();
+ var newState = e.NewValue.ToString().ToLower();
+ PseudoClasses.Remove($":{oldState}");
+ PseudoClasses.Add($":{newState}");
+ }
+
+ private void OnDisplayModeChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ var oldState = e.OldValue.ToString().ToLower();
+ var newState = e.NewValue.ToString().ToLower();
+
+ PseudoClasses.Remove($":{oldState}");
+ PseudoClasses.Add($":{newState}");
+
+ var (closedPaneWidth, paneColumnGridLength) = (SplitViewDisplayMode)e.NewValue switch
+ {
+ SplitViewDisplayMode.Overlay => (0, new GridLength(0, GridUnitType.Pixel)),
+ SplitViewDisplayMode.CompactOverlay => (CompactPaneLength, new GridLength(CompactPaneLength, GridUnitType.Pixel)),
+ SplitViewDisplayMode.Inline => (0, new GridLength(0, GridUnitType.Auto)),
+ SplitViewDisplayMode.CompactInline => (CompactPaneLength, new GridLength(0, GridUnitType.Auto)),
+ _ => throw new NotImplementedException(),
+ };
+ TemplateSettings.ClosedPaneWidth = closedPaneWidth;
+ TemplateSettings.PaneColumnGridLength = paneColumnGridLength;
+ }
+
+ private void OnUseLightDismissChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ var mode = (bool)e.NewValue;
+ PseudoClasses.Set(":lightdismiss", mode);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/SplitViewPaneClosingEventArgs.cs b/src/Avalonia.Controls/SplitViewPaneClosingEventArgs.cs
new file mode 100644
index 0000000000..46fb2d161b
--- /dev/null
+++ b/src/Avalonia.Controls/SplitViewPaneClosingEventArgs.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace Avalonia.Controls
+{
+ public class SplitViewPaneClosingEventArgs : EventArgs
+ {
+ public bool Cancel { get; set; }
+
+ public SplitViewPaneClosingEventArgs(bool cancel)
+ {
+ Cancel = cancel;
+ }
+ }
+}
diff --git a/src/Avalonia.Themes.Default/ProgressBar.xaml b/src/Avalonia.Themes.Default/ProgressBar.xaml
index d3c2f0c784..43a0752bc8 100644
--- a/src/Avalonia.Themes.Default/ProgressBar.xaml
+++ b/src/Avalonia.Themes.Default/ProgressBar.xaml
@@ -1,4 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml
index b5de500093..88040a69e7 100644
--- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml
+++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml
@@ -248,6 +248,14 @@
+
+ 0.6
+ 4
+ 0
+
+
+
+
1
@@ -656,6 +664,67 @@
8,5,8,7
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
XamlAutoFontFamily
XamlAutoFontFamily
@@ -817,5 +886,7 @@
1
32
+
+
diff --git a/src/Avalonia.Themes.Fluent/DatePicker.xaml b/src/Avalonia.Themes.Fluent/DatePicker.xaml
new file mode 100644
index 0000000000..ce29869ed3
--- /dev/null
+++ b/src/Avalonia.Themes.Fluent/DatePicker.xaml
@@ -0,0 +1,338 @@
+
+
+
+
+ 0,0,0,4
+ 40
+ 40
+ 41
+ 296
+ 456
+ 0,3,0,6
+ 9,3,0,6
+ 0,3,0,6
+ 9,3,0,6
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml b/src/Avalonia.Themes.Fluent/FluentTheme.xaml
index 49b2d9561b..0cbbb77d38 100644
--- a/src/Avalonia.Themes.Fluent/FluentTheme.xaml
+++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml
@@ -53,4 +53,7 @@
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/FocusAdorner.xaml b/src/Avalonia.Themes.Fluent/FocusAdorner.xaml
index 2d5e369573..f20dc2e650 100644
--- a/src/Avalonia.Themes.Fluent/FocusAdorner.xaml
+++ b/src/Avalonia.Themes.Fluent/FocusAdorner.xaml
@@ -1,10 +1,34 @@
-
+
+
+ 0
+ 2
+ 1
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/ProgressBar.xaml b/src/Avalonia.Themes.Fluent/ProgressBar.xaml
index d3c2f0c784..ac5dd7c0ed 100644
--- a/src/Avalonia.Themes.Fluent/ProgressBar.xaml
+++ b/src/Avalonia.Themes.Fluent/ProgressBar.xaml
@@ -1,81 +1,176 @@
-
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
-
+
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/SplitView.xaml b/src/Avalonia.Themes.Fluent/SplitView.xaml
new file mode 100644
index 0000000000..71e92459f1
--- /dev/null
+++ b/src/Avalonia.Themes.Fluent/SplitView.xaml
@@ -0,0 +1,219 @@
+
+
+
+ 320
+ 48
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/TimePicker.xaml b/src/Avalonia.Themes.Fluent/TimePicker.xaml
new file mode 100644
index 0000000000..4a411796d8
--- /dev/null
+++ b/src/Avalonia.Themes.Fluent/TimePicker.xaml
@@ -0,0 +1,284 @@
+
+
+
+
+ 40
+ 1
+ 1
+ 0,0,0,4
+ 40
+ 41
+ 242
+ 456
+ 0,3,0,6
+ 0,3,0,6
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs b/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs
index df7c0693e1..fa2ed61e65 100644
--- a/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs
+++ b/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs
@@ -1,4 +1,5 @@
using System;
+using Avalonia.Animation.Easings;
using Avalonia.Controls.Shapes;
using Avalonia.Media;
using Avalonia.Styling;
@@ -25,6 +26,16 @@ namespace Avalonia.Animation.UnitTests
Assert.Equal(4, keySpline.ControlPointY2);
}
+ [Theory]
+ [InlineData("1,2F,3,4")]
+ [InlineData("Foo,Bar,Fee,Buzz")]
+ public void Can_Handle_Invalid_String_KeySpline_Via_TypeConverter(string input)
+ {
+ var conv = new KeySplineTypeConverter();
+
+ Assert.ThrowsAny(() => (KeySpline)conv.ConvertFrom(input));
+ }
+
[Theory]
[InlineData(0.00)]
[InlineData(0.50)]
@@ -46,6 +57,22 @@ namespace Avalonia.Animation.UnitTests
Assert.Throws(() => keySpline.ControlPointX2 = input);
}
+ [Fact]
+ public void SplineEasing_Can_Be_Mutated()
+ {
+ var easing = new SplineEasing();
+
+ Assert.Equal(0, easing.Ease(0));
+ Assert.Equal(1, easing.Ease(1));
+
+ easing.X1 = 0.25;
+ easing.Y1 = 0.5;
+ easing.X2 = 0.75;
+ easing.Y2 = 1.0;
+
+ Assert.NotEqual(0.5, easing.Ease(0.5));
+ }
+
/*
To get the test values for the KeySpline test, you can:
1) Grab the WPF sample for KeySpline animations from https://github.com/microsoft/WPF-Samples/tree/master/Animation/KeySplineAnimations
@@ -141,5 +168,73 @@ namespace Avalonia.Animation.UnitTests
expected = 1.8016358493761722;
Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance);
}
+
+ [Fact]
+ public void Check_KeySpline_Parsing_Is_Correct()
+ {
+ var keyframe1 = new KeyFrame()
+ {
+ Setters =
+ {
+ new Setter(RotateTransform.AngleProperty, -2.5d),
+ },
+ KeyTime = TimeSpan.FromSeconds(0)
+ };
+
+ var keyframe2 = new KeyFrame()
+ {
+ Setters =
+ {
+ new Setter(RotateTransform.AngleProperty, 2.5d),
+ },
+ KeyTime = TimeSpan.FromSeconds(5),
+ };
+
+ var animation = new Animation()
+ {
+ Duration = TimeSpan.FromSeconds(5),
+ Children =
+ {
+ keyframe1,
+ keyframe2
+ },
+ IterationCount = new IterationCount(5),
+ PlaybackDirection = PlaybackDirection.Alternate,
+ Easing = Easing.Parse("0.1123555056179775,0.657303370786517,0.8370786516853934,0.499999999999999999")
+ };
+
+ var rotateTransform = new RotateTransform(-2.5);
+ var rect = new Rectangle()
+ {
+ RenderTransform = rotateTransform
+ };
+
+ var clock = new TestClock();
+ var animationRun = animation.RunAsync(rect, clock);
+
+ // position is what you'd expect at end and beginning
+ clock.Step(TimeSpan.Zero);
+ Assert.Equal(rotateTransform.Angle, -2.5);
+ clock.Step(TimeSpan.FromSeconds(5));
+ Assert.Equal(rotateTransform.Angle, 2.5);
+
+ // test some points in between end and beginning
+ var tolerance = 0.01;
+ clock.Step(TimeSpan.Parse("00:00:10.0153932"));
+ var expected = -2.4122350198982545;
+ Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance);
+
+ clock.Step(TimeSpan.Parse("00:00:11.2655407"));
+ expected = -0.37153223002125113;
+ Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance);
+
+ clock.Step(TimeSpan.Parse("00:00:12.6158773"));
+ expected = 0.3967885416786294;
+ Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance);
+
+ clock.Step(TimeSpan.Parse("00:00:14.6495256"));
+ expected = 1.8016358493761722;
+ Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance);
+ }
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs b/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs
new file mode 100644
index 0000000000..7c95f02a7a
--- /dev/null
+++ b/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs
@@ -0,0 +1,253 @@
+using System;
+using System.Linq;
+using Avalonia.Controls.Shapes;
+using Avalonia.Controls.Templates;
+using Avalonia.Platform;
+using Avalonia.UnitTests;
+using Avalonia.VisualTree;
+using Moq;
+using Xunit;
+
+namespace Avalonia.Controls.UnitTests
+{
+ public class DatePickerTests
+ {
+ [Fact]
+ public void SelectedDateChanged_Should_Fire_When_SelectedDate_Set()
+ {
+ using (UnitTestApplication.Start(Services))
+ {
+ bool handled = false;
+ DatePicker datePicker = new DatePicker();
+ datePicker.SelectedDateChanged += (s, e) =>
+ {
+ handled = true;
+ };
+ DateTimeOffset value = new DateTimeOffset(2000, 10, 10, 0, 0, 0, TimeSpan.Zero);
+ datePicker.SelectedDate = value;
+ Threading.Dispatcher.UIThread.RunJobs();
+ Assert.True(handled);
+ }
+ }
+
+ [Fact]
+ public void DayVisible_False_Should_Hide_Day()
+ {
+ using (UnitTestApplication.Start(Services))
+ {
+ DatePicker datePicker = new DatePicker
+ {
+ Template = CreateTemplate(),
+ DayVisible = false
+ };
+ datePicker.ApplyTemplate();
+ Threading.Dispatcher.UIThread.RunJobs();
+
+ var desc = datePicker.GetVisualDescendants();
+ Assert.True(desc.Count() > 1);//Should be layoutroot grid & button
+ TextBlock dayText = null;
+ Grid container = null;
+
+ Assert.True(desc.ElementAt(1) is Button);
+
+ container = (desc.ElementAt(1) as Button).Content as Grid;
+ Assert.True(container != null);
+
+ for(int i = 0; i < container.Children.Count; i++)
+ {
+ if(container.Children[i] is TextBlock tb && tb.Name == "DayText")
+ {
+ dayText = tb;
+ break;
+ }
+ }
+
+ Assert.True(dayText != null);
+ Assert.True(!dayText.IsVisible);
+ Assert.True(container.ColumnDefinitions.Count == 3);
+ }
+ }
+
+ [Fact]
+ public void MonthVisible_False_Should_Hide_Month()
+ {
+ using (UnitTestApplication.Start(Services))
+ {
+ DatePicker datePicker = new DatePicker
+ {
+ Template = CreateTemplate(),
+ MonthVisible = false
+ };
+ datePicker.ApplyTemplate();
+ Threading.Dispatcher.UIThread.RunJobs();
+
+ var desc = datePicker.GetVisualDescendants();
+ Assert.True(desc.Count() > 1);//Should be layoutroot grid & button
+ TextBlock monthText = null;
+ Grid container = null;
+
+ Assert.True(desc.ElementAt(1) is Button);
+
+ container = (desc.ElementAt(1) as Button).Content as Grid;
+ Assert.True(container != null);
+
+ for (int i = 0; i < container.Children.Count; i++)
+ {
+ if (container.Children[i] is TextBlock tb && tb.Name == "MonthText")
+ {
+ monthText = tb;
+ break;
+ }
+ }
+
+ Assert.True(monthText != null);
+ Assert.True(!monthText.IsVisible);
+ Assert.True(container.ColumnDefinitions.Count == 3);
+ }
+ }
+
+ [Fact]
+ public void YearVisible_False_Should_Hide_Year()
+ {
+ using (UnitTestApplication.Start(Services))
+ {
+ DatePicker datePicker = new DatePicker
+ {
+ Template = CreateTemplate(),
+ YearVisible = false
+ };
+ datePicker.ApplyTemplate();
+ Threading.Dispatcher.UIThread.RunJobs();
+
+ var desc = datePicker.GetVisualDescendants();
+ Assert.True(desc.Count() > 1);//Should be layoutroot grid & button
+ TextBlock yearText = null;
+ Grid container = null;
+
+ Assert.True(desc.ElementAt(1) is Button);
+
+ container = (desc.ElementAt(1) as Button).Content as Grid;
+ Assert.True(container != null);
+
+ for (int i = 0; i < container.Children.Count; i++)
+ {
+ if (container.Children[i] is TextBlock tb && tb.Name == "YearText")
+ {
+ yearText = tb;
+ break;
+ }
+ }
+
+ Assert.True(yearText != null);
+ Assert.True(!yearText.IsVisible);
+ Assert.True(container.ColumnDefinitions.Count == 3);
+ }
+ }
+
+ [Fact]
+ public void SelectedDate_null_Should_Use_Placeholders()
+ {
+ using (UnitTestApplication.Start(Services))
+ {
+ DatePicker datePicker = new DatePicker
+ {
+ Template = CreateTemplate(),
+ YearVisible = false
+ };
+ datePicker.ApplyTemplate();
+ Threading.Dispatcher.UIThread.RunJobs();
+
+ var desc = datePicker.GetVisualDescendants();
+ Assert.True(desc.Count() > 1);//Should be layoutroot grid & button
+ TextBlock yearText = null;
+ TextBlock monthText = null;
+ TextBlock dayText = null;
+ Grid container = null;
+
+ Assert.True(desc.ElementAt(1) is Button);
+
+ container = (desc.ElementAt(1) as Button).Content as Grid;
+ Assert.True(container != null);
+
+ for (int i = 0; i < container.Children.Count; i++)
+ {
+ if (container.Children[i] is TextBlock tb && tb.Name == "YearText")
+ {
+ yearText = tb;
+ }
+ else if (container.Children[i] is TextBlock tb1 && tb1.Name == "MonthText")
+ {
+ monthText = tb1;
+ }
+ else if (container.Children[i] is TextBlock tb2 && tb2.Name == "DayText")
+ {
+ dayText = tb2;
+ }
+ }
+
+ DateTimeOffset value = new DateTimeOffset(2000, 10, 10, 0, 0, 0, TimeSpan.Zero);
+ datePicker.SelectedDate = value;
+
+ Assert.False(dayText.Text == "day");
+ Assert.False(monthText.Text == "month");
+ Assert.False(yearText.Text == "year");
+ Assert.False(datePicker.Classes.Contains(":hasnodate"));
+
+ datePicker.SelectedDate = null;
+
+ Assert.True(dayText.Text == "day");
+ Assert.True(monthText.Text == "month");
+ Assert.True(yearText.Text == "year");
+ Assert.True(datePicker.Classes.Contains(":hasnodate"));
+ }
+ }
+
+ private static TestServices Services => TestServices.MockThreadingInterface.With(
+ standardCursorFactory: Mock.Of());
+
+ private IControlTemplate CreateTemplate()
+ {
+ return new FuncControlTemplate((control, scope) =>
+ {
+ var layoutRoot = new Grid
+ {
+ Name = "LayoutRoot"
+ }.RegisterInNameScope(scope);
+ //Skip contentpresenter
+ var flyoutButton = new Button
+ {
+ Name = "FlyoutButton"
+ }.RegisterInNameScope(scope);
+ var contentGrid = new Grid
+ {
+ Name = "ButtonContentGrid"
+ }.RegisterInNameScope(scope);
+ var dayText = new TextBlock
+ {
+ Name = "DayText"
+ }.RegisterInNameScope(scope);
+ var monthText = new TextBlock
+ {
+ Name = "MonthText"
+ }.RegisterInNameScope(scope);
+ var yearText = new TextBlock
+ {
+ Name = "YearText"
+ }.RegisterInNameScope(scope);
+ var firstSpacer = new Rectangle
+ {
+ Name = "FirstSpacer"
+ }.RegisterInNameScope(scope);
+ var secondSpacer = new Rectangle
+ {
+ Name = "SecondSpacer"
+ }.RegisterInNameScope(scope);
+
+ contentGrid.Children.AddRange(new IControl[] { dayText, monthText, yearText, firstSpacer, secondSpacer });
+ flyoutButton.Content = contentGrid;
+ layoutRoot.Children.Add(flyoutButton);
+ return layoutRoot;
+ });
+ }
+ }
+}
diff --git a/tests/Avalonia.Controls.UnitTests/SplitViewTests.cs b/tests/Avalonia.Controls.UnitTests/SplitViewTests.cs
new file mode 100644
index 0000000000..03653ec42c
--- /dev/null
+++ b/tests/Avalonia.Controls.UnitTests/SplitViewTests.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Xunit;
+
+namespace Avalonia.Controls.UnitTests
+{
+
+ public class SplitViewTests
+ {
+ [Fact]
+ public void SplitView_PaneOpening_Should_Fire_Before_PaneOpened()
+ {
+ var splitView = new SplitView();
+
+ bool handledOpening = false;
+ splitView.PaneOpening += (x, e) =>
+ {
+ handledOpening = true;
+ };
+
+ splitView.PaneOpened += (x, e) =>
+ {
+ Assert.True(handledOpening);
+ };
+
+ splitView.IsPaneOpen = true;
+ }
+
+ [Fact]
+ public void SplitView_PaneClosing_Should_Fire_Before_PaneClosed()
+ {
+ var splitView = new SplitView();
+ splitView.IsPaneOpen = true;
+
+ bool handledClosing = false;
+ splitView.PaneClosing += (x, e) =>
+ {
+ handledClosing = true;
+ };
+
+ splitView.PaneClosed += (x, e) =>
+ {
+ Assert.True(handledClosing);
+ };
+
+ splitView.IsPaneOpen = false;
+ }
+
+ [Fact]
+ public void SplitView_Cancel_Close_Should_Prevent_Pane_From_Closing()
+ {
+ var splitView = new SplitView();
+ splitView.IsPaneOpen = true;
+
+ splitView.PaneClosing += (x, e) =>
+ {
+ e.Cancel = true;
+ };
+
+ splitView.IsPaneOpen = false;
+
+ Assert.True(splitView.IsPaneOpen);
+ }
+ }
+}
diff --git a/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs b/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs
new file mode 100644
index 0000000000..c803b93364
--- /dev/null
+++ b/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs
@@ -0,0 +1,172 @@
+using System;
+using System.Linq;
+using Avalonia.Controls.Shapes;
+using Avalonia.Controls.Templates;
+using Avalonia.Platform;
+using Avalonia.UnitTests;
+using Avalonia.VisualTree;
+using Moq;
+using Xunit;
+
+namespace Avalonia.Controls.UnitTests
+{
+ public class TimePickerTests
+ {
+ [Fact(Skip = "FIX ME ASAP")]
+ public void SelectedTimeChanged_Should_Fire_When_SelectedTime_Set()
+ {
+ using (UnitTestApplication.Start(Services))
+ {
+ bool handled = false;
+ TimePicker timePicker = new TimePicker();
+ timePicker.SelectedTimeChanged += (s, e) =>
+ {
+ handled = true;
+ };
+ TimeSpan value = TimeSpan.FromHours(10);
+ timePicker.SelectedTime = value;
+ Threading.Dispatcher.UIThread.RunJobs();
+ Assert.True(handled);
+ }
+ }
+
+ [Fact]
+ public void Using_24HourClock_Should_Hide_Period()
+ {
+ using (UnitTestApplication.Start(Services))
+ {
+ TimePicker timePicker = new TimePicker()
+ {
+ ClockIdentifier = "12HourClock",
+ Template = CreateTemplate()
+ };
+ timePicker.ApplyTemplate();
+
+ var desc = timePicker.GetVisualDescendants();
+ Assert.True(desc.Count() > 1);//Should be layoutroot grid & button
+ Grid container = null;
+
+ Assert.True(desc.ElementAt(1) is Button);
+
+ container = (desc.ElementAt(1) as Button).Content as Grid;
+ Assert.True(container != null);
+
+ var periodTextHost = container.Children[4] as Border;
+ Assert.True(periodTextHost != null);
+ Assert.True(periodTextHost.IsVisible);
+
+ timePicker.ClockIdentifier = "24HourClock";
+ Assert.False(periodTextHost.IsVisible);
+ }
+ }
+
+ [Fact]
+ public void SelectedTime_null_Should_Use_Placeholders()
+ {
+ using (UnitTestApplication.Start(Services))
+ {
+ TimePicker timePicker = new TimePicker()
+ {
+ Template = CreateTemplate()
+ };
+ timePicker.ApplyTemplate();
+
+ var desc = timePicker.GetVisualDescendants();
+ Assert.True(desc.Count() > 1);//Should be layoutroot grid & button
+ Grid container = null;
+
+ Assert.True(desc.ElementAt(1) is Button);
+
+ container = (desc.ElementAt(1) as Button).Content as Grid;
+ Assert.True(container != null);
+
+ var hourTextHost = container.Children[0] as Border;
+ Assert.True(hourTextHost != null);
+ var hourText = hourTextHost.Child as TextBlock;
+ var minuteTextHost = container.Children[2] as Border;
+ Assert.True(minuteTextHost != null);
+ var minuteText = minuteTextHost.Child as TextBlock;
+
+ TimeSpan ts = TimeSpan.FromHours(10);
+ timePicker.SelectedTime = ts;
+ Assert.False(hourText.Text == "hour");
+ Assert.False(minuteText.Text == "minute");
+
+ timePicker.SelectedTime = null;
+ Assert.True(hourText.Text == "hour");
+ Assert.True(minuteText.Text == "minute");
+ }
+ }
+
+
+ private static TestServices Services => TestServices.MockThreadingInterface.With(
+ standardCursorFactory: Mock.Of());
+
+ private IControlTemplate CreateTemplate()
+ {
+ return new FuncControlTemplate((control, scope) =>
+ {
+ var layoutRoot = new Grid
+ {
+ Name = "LayoutRoot"
+ }.RegisterInNameScope(scope);
+ //Skip contentpresenter
+ var flyoutButton = new Button
+ {
+ Name = "FlyoutButton"
+ }.RegisterInNameScope(scope);
+ var contentGrid = new Grid
+ {
+ Name = "FlyoutButtonContentGrid"
+ }.RegisterInNameScope(scope);
+
+ var firstPickerHost = new Border
+ {
+ Name = "FirstPickerHost",
+ Child = new TextBlock
+ {
+ Name = "HourTextBlock"
+ }.RegisterInNameScope(scope)
+ }.RegisterInNameScope(scope);
+ Grid.SetColumn(firstPickerHost, 0);
+
+ var secondPickerHost = new Border
+ {
+ Name = "SecondPickerHost",
+ Child = new TextBlock
+ {
+ Name = "MinuteTextBlock"
+ }.RegisterInNameScope(scope)
+ }.RegisterInNameScope(scope);
+ Grid.SetColumn(secondPickerHost, 2);
+
+ var thirdPickerHost = new Border
+ {
+ Name = "ThirdPickerHost",
+ Child = new TextBlock
+ {
+ Name = "PeriodTextBlock"
+ }.RegisterInNameScope(scope)
+ }.RegisterInNameScope(scope);
+ Grid.SetColumn(thirdPickerHost, 4);
+
+ var firstSpacer = new Rectangle
+ {
+ Name = "FirstColumnDivider"
+ }.RegisterInNameScope(scope);
+ Grid.SetColumn(firstSpacer, 1);
+
+ var secondSpacer = new Rectangle
+ {
+ Name = "SecondColumnDivider"
+ }.RegisterInNameScope(scope);
+ Grid.SetColumn(secondSpacer, 3);
+
+ contentGrid.Children.AddRange(new IControl[] { firstPickerHost, firstSpacer, secondPickerHost, secondSpacer, thirdPickerHost });
+ flyoutButton.Content = contentGrid;
+ layoutRoot.Children.Add(flyoutButton);
+ return layoutRoot;
+ });
+ }
+ }
+}
diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Negation.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Negation.cs
index 757cd628b7..0be3bbbb9f 100644
--- a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Negation.cs
+++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Negation.cs
@@ -1,4 +1,6 @@
using System;
+using System.Collections;
+using System.ComponentModel;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Data;
@@ -89,6 +91,69 @@ namespace Avalonia.Markup.UnitTests.Parsers
GC.KeepAlive(data);
}
+ [Fact]
+ public async Task Should_Negate_BindingNotification_Value()
+ {
+ var data = new { Foo = true };
+ var target = ExpressionObserverBuilder.Build(data, "!Foo", enableDataValidation: true);
+ var result = await target.Take(1);
+
+ Assert.Equal(new BindingNotification(false), result);
+
+ GC.KeepAlive(data);
+ }
+
+ [Fact]
+ public async Task Should_Pass_Through_BindingNotification_Error()
+ {
+ var data = new { };
+ var target = ExpressionObserverBuilder.Build(data, "!Foo", enableDataValidation: true);
+ var result = await target.Take(1);
+
+ Assert.Equal(
+ new BindingNotification(
+ new MissingMemberException("Could not find a matching property accessor for 'Foo' on '{ }'"),
+ BindingErrorType.Error),
+ result);
+
+ GC.KeepAlive(data);
+ }
+
+ [Fact]
+ public async Task Should_Negate_BindingNotification_Error_FallbackValue()
+ {
+ var data = new Test { DataValidationError = "Test error" };
+ var target = ExpressionObserverBuilder.Build(data, "!Foo", enableDataValidation: true);
+ var result = await target.Take(1);
+
+ Assert.Equal(
+ new BindingNotification(
+ new DataValidationException("Test error"),
+ BindingErrorType.DataValidationError,
+ true),
+ result);
+
+ GC.KeepAlive(data);
+ }
+
+ [Fact]
+ public async Task Should_Add_Error_To_BindingNotification_For_FallbackValue_Not_Convertible_To_Boolean()
+ {
+ var data = new Test { Bar = new object(), DataValidationError = "Test error" };
+ var target = ExpressionObserverBuilder.Build(data, "!Bar", enableDataValidation: true);
+ var result = await target.Take(1);
+
+ Assert.Equal(
+ new BindingNotification(
+ new AggregateException(
+ new DataValidationException("Test error"),
+ new InvalidCastException($"Unable to convert 'System.Object' to bool.")),
+ BindingErrorType.Error),
+ result);
+
+ GC.KeepAlive(data);
+ }
+
[Fact]
public void SetValue_Should_Return_False_For_Invalid_Value()
{
@@ -101,9 +166,20 @@ namespace Avalonia.Markup.UnitTests.Parsers
GC.KeepAlive(data);
}
- private class Test
+ private class Test : INotifyDataErrorInfo
{
public bool Foo { get; set; }
+ public object Bar { get; set; }
+
+ public string DataValidationError { get; set; }
+ public bool HasErrors => !string.IsNullOrWhiteSpace(DataValidationError);
+
+ public event EventHandler ErrorsChanged;
+
+ public IEnumerable GetErrors(string propertyName)
+ {
+ return DataValidationError is object ? new[] { DataValidationError } : null;
+ }
}
}
}