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); } } }