diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index 27905a44d9..ec7a11067c 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -57,6 +57,7 @@
+
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.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.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml
index d68ed19574..5ab50f5b73 100644
--- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml
+++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml
@@ -889,5 +889,7 @@
1
32
+
+
diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml
index bbf68d46ef..88040a69e7 100644
--- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml
+++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml
@@ -886,5 +886,7 @@
1
32
+
+
diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml b/src/Avalonia.Themes.Fluent/FluentTheme.xaml
index cef9625e1d..0cbbb77d38 100644
--- a/src/Avalonia.Themes.Fluent/FluentTheme.xaml
+++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml
@@ -53,6 +53,7 @@
+
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/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/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);
+ }
+ }
+}