using Avalonia.Controls.Metadata; 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; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; 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 /// [PseudoClasses(":open", ":closed")] [PseudoClasses(":compactoverlay", ":compactinline", ":overlay", ":inline")] [PseudoClasses(":left", ":right")] [PseudoClasses(":lightdismiss")] public class SplitView : ContentControl { /* Pseudo classes & combos :open / :closed :compactoverlay :compactinline :overlay :inline :left :right */ /// /// 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 PaneTemplateProperty = AvaloniaProperty.Register(nameof(PaneTemplate)); /// /// 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 IDisposable _pointerDisposable; 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 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 object Pane { get => GetValue(PaneProperty); set => SetValue(PaneProperty, value); } /// /// Gets or sets the data template used to display the header content of the control. /// public IDataTemplate PaneTemplate { get => GetValue(PaneTemplateProperty); set => SetValue(PaneTemplateProperty, 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 bool RegisterContentPresenter(IContentPresenter presenter) { var result = base.RegisterContentPresenter(presenter); if (presenter.Name == "PART_PanePresenter") { return true; } return result; } 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) { _pointerDisposable = window.AddDisposableHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel); } } protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { base.OnDetachedFromVisualTree(e); _pointerDisposable?.Dispose(); } 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) { // Make assumption that if Popup is in visual tree, // owning control is within pane // This works because if pane is triggered to close // when clicked anywhere else in Window, the pane // would close before the popup is opened if (src == _pane || src is PopupRoot) { closePane = false; break; } src = src.VisualParent; } if (closePane) { IsPaneOpen = false; e.Handled = true; } } 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); } } }