using System; using Avalonia.Controls.Metadata; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Metadata; using Avalonia.Reactive; namespace Avalonia.Controls { /// /// A control with two views: A collapsible pane and an area for content /// [TemplatePart("PART_PaneRoot", typeof(Panel))] [PseudoClasses(pcOpen, pcClosed)] [PseudoClasses(pcCompactOverlay, pcCompactInline, pcOverlay, pcInline)] [PseudoClasses(pcLeft, pcRight)] [PseudoClasses(pcLightDismiss)] public class SplitView : ContentControl { protected const string pcOpen = ":open"; protected const string pcClosed = ":closed"; protected const string pcCompactOverlay = ":compactoverlay"; protected const string pcCompactInline = ":compactinline"; protected const string pcOverlay = ":overlay"; protected const string pcInline = ":inline"; protected const string pcLeft = ":left"; protected const string pcRight = ":right"; protected const string pcLightDismiss = ":lightDismiss"; /// /// 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 StyledProperty IsPaneOpenProperty = AvaloniaProperty.Register( nameof(IsPaneOpen), defaultValue: false, coerce: CoerceIsPaneOpen); /// /// 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 DirectProperty TemplateSettingsProperty = AvaloniaProperty.RegisterDirect(nameof(TemplateSettings), x => x.TemplateSettings); /// /// Defines the event. /// public static readonly RoutedEvent PaneClosedEvent = RoutedEvent.Register( nameof(PaneClosed), RoutingStrategies.Bubble); /// /// Defines the event. /// public static readonly RoutedEvent PaneClosingEvent = RoutedEvent.Register( nameof(PaneClosing), RoutingStrategies.Bubble); /// /// Defines the event. /// public static readonly RoutedEvent PaneOpenedEvent = RoutedEvent.Register( nameof(PaneOpened), RoutingStrategies.Bubble); /// /// Defines the event. /// public static readonly RoutedEvent PaneOpeningEvent = RoutedEvent.Register( nameof(PaneOpening), RoutingStrategies.Bubble); private Panel? _pane; private IDisposable? _pointerDisposable; private SplitViewTemplateSettings _templateSettings = new SplitViewTemplateSettings(); private string? _lastDisplayModePseudoclass; private string? _lastPlacementPseudoclass; /// /// 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 => GetValue(IsPaneOpenProperty); set => SetValue(IsPaneOpenProperty, value); } /// /// 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 /// [DependsOn(nameof(PaneTemplate))] 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 => _templateSettings; private set => SetAndRaise(TemplateSettingsProperty, ref _templateSettings, value); } /// /// Fired when the pane is closed. /// public event EventHandler? PaneClosed { add => AddHandler(PaneClosedEvent, value); remove => RemoveHandler(PaneClosedEvent, value); } /// /// Fired when the pane is closing. /// /// /// The event args property may be set to true to cancel the event /// and keep the pane open. /// public event EventHandler? PaneClosing { add => AddHandler(PaneClosingEvent, value); remove => RemoveHandler(PaneClosingEvent, value); } /// /// Fired when the pane is opened. /// public event EventHandler? PaneOpened { add => AddHandler(PaneOpenedEvent, value); remove => RemoveHandler(PaneOpenedEvent, value); } /// /// Fired when the pane is opening. /// /// /// The event args property may be set to true to cancel the event /// and keep the pane closed. /// public event EventHandler? PaneOpening { add => AddHandler(PaneOpeningEvent, value); remove => RemoveHandler(PaneOpeningEvent, value); } 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"); UpdateVisualStateForDisplayMode(DisplayMode); } protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); // :left and :right style triggers contain the template so we need to do this as // soon as we're attached so the template applies. The other visual states can // be updated after the template applies UpdateVisualStateForPanePlacementProperty(PanePlacement); } protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { base.OnDetachedFromVisualTree(e); _pointerDisposable?.Dispose(); } /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == CompactPaneLengthProperty) { UpdateVisualStateForCompactPaneLength(change.GetNewValue()); } else if (change.Property == DisplayModeProperty) { UpdateVisualStateForDisplayMode(change.GetNewValue()); } else if (change.Property == IsPaneOpenProperty) { bool isPaneOpen = change.GetNewValue(); if (isPaneOpen) { PseudoClasses.Add(pcOpen); PseudoClasses.Remove(pcClosed); OnPaneOpened(new RoutedEventArgs(PaneOpenedEvent, this)); } else { PseudoClasses.Add(pcClosed); PseudoClasses.Remove(pcOpen); OnPaneClosed(new RoutedEventArgs(PaneClosedEvent, this)); } } else if (change.Property == PaneProperty) { if (change.OldValue is ILogical oldChild) { LogicalChildren.Remove(oldChild); } if (change.NewValue is ILogical newChild) { LogicalChildren.Add(newChild); } } else if (change.Property == PanePlacementProperty) { UpdateVisualStateForPanePlacementProperty(change.GetNewValue()); } else if (change.Property == UseLightDismissOverlayModeProperty) { var mode = change.GetNewValue(); PseudoClasses.Set(pcLightDismiss, mode); } } protected override void OnKeyDown(KeyEventArgs e) { if (!e.Handled && e.Key == Key.Escape) { if (IsPaneOpen && IsInOverlayMode()) { SetCurrentValue(IsPaneOpenProperty, false); e.Handled = true; } } base.OnKeyDown(e); } private void PointerReleasedOutside(object? sender, PointerReleasedEventArgs e) { if (!IsPaneOpen || _pane == null) { return; } var closePane = true; var src = e.Source as Visual; 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) { SetCurrentValue(IsPaneOpenProperty, false); e.Handled = true; } } private bool IsInOverlayMode() { return (DisplayMode == SplitViewDisplayMode.CompactOverlay || DisplayMode == SplitViewDisplayMode.Overlay); } protected virtual void OnPaneOpening(CancelRoutedEventArgs args) { RaiseEvent(args); } protected virtual void OnPaneOpened(RoutedEventArgs args) { EnableLightDismiss(); RaiseEvent(args); } protected virtual void OnPaneClosing(CancelRoutedEventArgs args) { RaiseEvent(args); } protected virtual void OnPaneClosed(RoutedEventArgs args) { _pointerDisposable?.Dispose(); _pointerDisposable = null; RaiseEvent(args); } /// /// Gets the appropriate PseudoClass for the given . /// private static string GetPseudoClass(SplitViewDisplayMode mode) { return mode switch { SplitViewDisplayMode.Inline => pcInline, SplitViewDisplayMode.CompactInline => pcCompactInline, SplitViewDisplayMode.Overlay => pcOverlay, SplitViewDisplayMode.CompactOverlay => pcCompactOverlay, _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null) }; } /// /// Gets the appropriate PseudoClass for the given . /// private static string GetPseudoClass(SplitViewPanePlacement placement) { return placement switch { SplitViewPanePlacement.Left => pcLeft, SplitViewPanePlacement.Right => pcRight, _ => throw new ArgumentOutOfRangeException(nameof(placement), placement, null) }; } /// /// Called when the property has to be coerced. /// /// The value to coerce. protected virtual bool OnCoerceIsPaneOpen(bool value) { CancelRoutedEventArgs eventArgs; if (value) { eventArgs = new CancelRoutedEventArgs(PaneOpeningEvent, this); OnPaneOpening(eventArgs); } else { eventArgs = new CancelRoutedEventArgs(PaneClosingEvent, this); OnPaneClosing(eventArgs); } if (eventArgs.Cancel) { return !value; } return value; } private void UpdateVisualStateForCompactPaneLength(double newLen) { 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 UpdateVisualStateForDisplayMode(SplitViewDisplayMode newValue) { if (!string.IsNullOrEmpty(_lastDisplayModePseudoclass)) { PseudoClasses.Remove(_lastDisplayModePseudoclass); } _lastDisplayModePseudoclass = GetPseudoClass(newValue); PseudoClasses.Add(_lastDisplayModePseudoclass); var (closedPaneWidth, paneColumnGridLength) = 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 UpdateVisualStateForPanePlacementProperty(SplitViewPanePlacement newValue) { if (!string.IsNullOrEmpty(_lastPlacementPseudoclass)) { PseudoClasses.Remove(_lastPlacementPseudoclass); } _lastPlacementPseudoclass = GetPseudoClass(newValue); PseudoClasses.Add(_lastPlacementPseudoclass); } private void EnableLightDismiss() { if (_pane == null) return; // If this returns false, we're not in Overlay or CompactOverlay DisplayMode // and don't need the light dismiss behavior if (!IsInOverlayMode()) return; var topLevel = TopLevel.GetTopLevel(this); if (topLevel != null) { _pointerDisposable = Disposable.Create(() => { topLevel.PointerReleased -= PointerReleasedOutside; topLevel.BackRequested -= TopLevelBackRequested; }); topLevel.PointerReleased += PointerReleasedOutside; topLevel.BackRequested += TopLevelBackRequested; } } private void TopLevelBackRequested(object? sender, RoutedEventArgs e) { if (!IsInOverlayMode()) return; SetCurrentValue(IsPaneOpenProperty, false); e.Handled = true; } /// /// Coerces/validates the property value. /// /// The instance. /// The value to coerce. /// The coerced/validated value. private static bool CoerceIsPaneOpen(AvaloniaObject instance, bool value) { if (instance is SplitView splitView) { return splitView.OnCoerceIsPaneOpen(value); } return value; } } }