diff --git a/src/Avalonia.Controls/SplitView.cs b/src/Avalonia.Controls/SplitView.cs
new file mode 100644
index 0000000000..df3db324a3
--- /dev/null
+++ b/src/Avalonia.Controls/SplitView.cs
@@ -0,0 +1,476 @@
+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);
+ if (_pointerDisposables != null)
+ _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 state = (SplitViewDisplayMode)e.NewValue;
+ //PaneColumn Specs: Width/ColumnWidth
+ // Overlay - 0px/0px
+ // CompactOverlay - CompactPaneLength/CompactPaneLength
+ // Inline - 0px/Auto
+ // CompactInline - CompactPaneLength/Auto
+ TemplateSettings.ClosedPaneWidth = (state == SplitViewDisplayMode.CompactInline ||
+ state == SplitViewDisplayMode.CompactOverlay) ? CompactPaneLength : 0;
+
+ TemplateSettings.PaneColumnGridLength = (state == SplitViewDisplayMode.CompactInline ||
+ state == SplitViewDisplayMode.Inline) ? new GridLength(0, GridUnitType.Auto) :
+ state == SplitViewDisplayMode.Overlay ? new GridLength(0, GridUnitType.Pixel) :
+ new GridLength(CompactPaneLength, GridUnitType.Pixel);
+ }
+
+ 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;
+ }
+ }
+}