From 882b90eaefc2cfee3d7a76b01600d03347370045 Mon Sep 17 00:00:00 2001 From: chinboy Date: Thu, 1 Jan 2026 20:50:43 +0800 Subject: [PATCH] Refactor Splitter for Attached property --- .../Views/Layout/SplitterShowCase.axaml | 187 +- .../Views/Layout/SplitterShowCase.axaml.cs | 11 +- .../Splitter/Splitter.cs | 1868 ++-------------- .../Splitter/SplitterPanel.cs | 1928 ++++++++++++++++- .../Splitter/Themes/SplitterHandleTheme.axaml | 2 +- .../Splitter/Themes/SplitterPanelTheme.axaml | 23 - .../Splitter/Themes/SplitterTheme.axaml | 13 + .../Splitter/Themes/SplitterThemeConstants.cs | 1 + .../Splitter/Themes/SplitterThemes.axaml | 1 - 9 files changed, 2056 insertions(+), 1978 deletions(-) delete mode 100644 src/AtomUI.Desktop.Controls/Splitter/Themes/SplitterPanelTheme.axaml diff --git a/controlgallery/AtomUIGallery/ShowCases/Views/Layout/SplitterShowCase.axaml b/controlgallery/AtomUIGallery/ShowCases/Views/Layout/SplitterShowCase.axaml index a122d20b4..0e209acf4 100644 --- a/controlgallery/AtomUIGallery/ShowCases/Views/Layout/SplitterShowCase.axaml +++ b/controlgallery/AtomUIGallery/ShowCases/Views/Layout/SplitterShowCase.axaml @@ -34,16 +34,12 @@ - - - First - - - - - Second - - + + First + + + Second + @@ -51,71 +47,50 @@ - - - Top - - - - - Bottom - - + + Top + + + Bottom + - - + - + + Left + + - Left + Top - - - - - - - Top - - - - - Bottom - - - + + Bottom - + - + - - - Resizable - - - - - Not Resizable - - - - - Resizable - - + + Resizable + + + Not Resizable + + + Resizable + - + @@ -135,83 +110,63 @@ IsCheckedChanged="HandleShowCollapsibleIconChanged">False - - - First - - - - - Second - - - - - Third - - + + First + + + Second + + + Third + - - - + - - - A - - - - - B - - - - - C - - - - - D - - + + A + + + B + + + C + + + D + - + - - - First - - - - - Second - - + + First + + + Second + - - - First - - - - - Second - - + + First + + + Second + diff --git a/controlgallery/AtomUIGallery/ShowCases/Views/Layout/SplitterShowCase.axaml.cs b/controlgallery/AtomUIGallery/ShowCases/Views/Layout/SplitterShowCase.axaml.cs index 4e1bab125..1ca774add 100644 --- a/controlgallery/AtomUIGallery/ShowCases/Views/Layout/SplitterShowCase.axaml.cs +++ b/controlgallery/AtomUIGallery/ShowCases/Views/Layout/SplitterShowCase.axaml.cs @@ -1,9 +1,10 @@ -using System; using AtomUI.Desktop.Controls; using AtomUIGallery.ShowCases.ViewModels; +using Avalonia.Controls; using Avalonia.Interactivity; using ReactiveUI; using ReactiveUI.Avalonia; +using RadioButton = AtomUI.Desktop.Controls.RadioButton; namespace AtomUIGallery.ShowCases.Views; @@ -55,12 +56,12 @@ public partial class SplitterShowCase : ReactiveUserControl ApplyShowMode(ShowCollapsiblePanelThird, mode); } - private static void ApplyShowMode(SplitterPanel panel, SplitterCollapsibleIconDisplayMode mode) + private static void ApplyShowMode(Control panel, SplitterCollapsibleIconDisplayMode mode) { - panel.Collapsible = new SplitterPanelCollapsible + Splitter.SetCollapsible(panel, new SplitterPanelCollapsible() { - IsEnabled = true, + IsEnabled = true, ShowCollapsibleIcon = mode - }; + }); } } diff --git a/src/AtomUI.Desktop.Controls/Splitter/Splitter.cs b/src/AtomUI.Desktop.Controls/Splitter/Splitter.cs index 56d61872f..b162162f1 100644 --- a/src/AtomUI.Desktop.Controls/Splitter/Splitter.cs +++ b/src/AtomUI.Desktop.Controls/Splitter/Splitter.cs @@ -1,24 +1,21 @@ using System.Collections.Specialized; using AtomUI.Controls; -using AtomUI.Data; -using AtomUI.Desktop.Controls.DesignTokens; +using AtomUI.Desktop.Controls.Themes; using AtomUI.Theme; -using AtomUI.Utils; using Avalonia; using Avalonia.Controls; -using Avalonia.Controls.Metadata; -using Avalonia.Input; +using Avalonia.Controls.Primitives; +using Avalonia.Data; using Avalonia.Layout; -using Avalonia.Media; -using Avalonia.VisualTree; +using Avalonia.Metadata; namespace AtomUI.Desktop.Controls; -[PseudoClasses(StdPseudoClass.Vertical, StdPseudoClass.Horizontal)] -public class Splitter : Panel, IControlSharedTokenResourcesHost +using ControlList = Avalonia.Controls.Controls; + +public class Splitter : TemplatedControl, IControlSharedTokenResourcesHost { #region 公共属性定义 - public static readonly StyledProperty OrientationProperty = AvaloniaProperty.Register(nameof(Orientation), Orientation.Vertical); @@ -63,6 +60,35 @@ public class Splitter : Panel, IControlSharedTokenResourcesHost get => GetValue(CollapseNextIconProperty); set => SetValue(CollapseNextIconProperty, value); } + + [Content] + public ControlList Children { get; } = new (); + + #endregion + + #region 布局项附加属性 + + public static readonly AttachedProperty SizeProperty = + AvaloniaProperty.RegisterAttached("Size", + defaultBindingMode: BindingMode.TwoWay); + + public static readonly AttachedProperty DefaultSizeProperty = + AvaloniaProperty.RegisterAttached("DefaultSize"); + + public static readonly AttachedProperty MinSizeProperty = + AvaloniaProperty.RegisterAttached("MinSize"); + + public static readonly AttachedProperty MaxSizeProperty = + AvaloniaProperty.RegisterAttached("MaxSize"); + + public static readonly AttachedProperty IsResizableProperty = + AvaloniaProperty.RegisterAttached("IsResizable", true); + + public static readonly AttachedProperty CollapsibleProperty = + AvaloniaProperty.RegisterAttached("Collapsible"); + + public static readonly AttachedProperty IsCollapsedProperty = + AvaloniaProperty.RegisterAttached("IsCollapsed", defaultBindingMode: BindingMode.TwoWay); #endregion @@ -73,1795 +99,147 @@ public class Splitter : Panel, IControlSharedTokenResourcesHost public event EventHandler? ResizeCompleted; #endregion - + #region 内部属性定义 Control IControlSharedTokenResourcesHost.HostControl => this; string IControlSharedTokenResourcesHost.TokenId => SplitterToken.ID; #endregion - - private readonly List _panels = new(); - private readonly List _handles = new(); - private readonly HashSet _trackedPanels = new(); - - private bool _suppressChildrenChanged; - private bool _suppressPanelChange; - private Size _lastLayoutSize; - private DragContext? _dragContext; - - static Splitter() - { - AffectsMeasure(OrientationProperty, HandleSizeProperty); - } - + + private SplitterPanel? _splitterPanel; + public Splitter() { - Children.CollectionChanged += HandleChildrenChanged; - UpdatePseudoClasses(); - this.RegisterResources(); + Children.CollectionChanged += ChildrenChanged; } - - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + + public static Dimension? GetSize(Control control) { - base.OnPropertyChanged(change); - if (change.Property == OrientationProperty) - { - UpdatePseudoClasses(); - UpdateHandleLayouts(); - InvalidateMeasure(); - } - - if (change.Property == HandleSizeProperty) - { - UpdateHandleLayouts(); - } - - if (change.Property == CollapsePreviousIconProperty || - change.Property == CollapseNextIconProperty) - { - UpdateHandleLayouts(); - } + return control.GetValue(SizeProperty); } - - private void HandleChildrenChanged(object? sender, NotifyCollectionChangedEventArgs e) + + public static void SetSize(Control control, Dimension? value) { - if (_suppressChildrenChanged) - { - return; - } - - RefreshPanelsAndHandles(); + control.SetValue(SizeProperty, value); } - - private void RefreshPanelsAndHandles() + + public static Dimension? GetDefaultSize(Control control) { - _suppressChildrenChanged = true; - try - { - foreach (var handle in _handles) - { - handle.DragStarted -= HandleDragStarted; - handle.DragDelta -= HandleDragDelta; - handle.DragCompleted -= HandleDragCompleted; - handle.CollapsePreviousRequested -= HandleCollapsePreviousRequested; - handle.CollapseNextRequested -= HandleCollapseNextRequested; - Children.Remove(handle); - } - _handles.Clear(); - - var panels = Children.OfType().ToList(); - SyncTrackedPanels(panels); - _panels.Clear(); - _panels.AddRange(panels); - - for (var i = 0; i < _panels.Count - 1; i++) - { - var handle = CreateHandle(i); - _handles.Add(handle); - Children.Add(handle); - } - } - finally - { - _suppressChildrenChanged = false; - } - - InvalidateMeasure(); + return control.GetValue(DefaultSizeProperty); } - - private void SyncTrackedPanels(IReadOnlyList panels) + + public static void SetDefaultSize(Control control, Dimension? value) { - foreach (var panel in _trackedPanels.ToList()) - { - if (!panels.Contains(panel)) - { - panel.PropertyChanged -= HandlePanelPropertyChanged; - panel.Owner = null; - _trackedPanels.Remove(panel); - } - } - - foreach (var panel in panels) - { - if (_trackedPanels.Add(panel)) - { - panel.Owner = this; - panel.UpdatePseudoClasses(); - panel.PropertyChanged += HandlePanelPropertyChanged; - } - } + control.SetValue(DefaultSizeProperty, value); } - - private SplitterHandle CreateHandle(int index) + + public static Dimension? GetMinSize(Control control) { - var handle = new SplitterHandle - { - Orientation = Orientation, - HandleIndex = index - }; - - handle.DragStarted += HandleDragStarted; - handle.DragDelta += HandleDragDelta; - handle.DragCompleted += HandleDragCompleted; - handle.CollapsePreviousRequested += HandleCollapsePreviousRequested; - handle.CollapseNextRequested += HandleCollapseNextRequested; - - UpdateHandleState(index, handle); - return handle; + return control.GetValue(MinSizeProperty); } - - private void HandlePanelPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + + public static void SetMinSize(Control control, Dimension? value) { - if (sender is not SplitterPanel panel) - { - return; - } - - if (e.Property == SplitterPanel.IsCollapsedProperty) - { - if (_suppressPanelChange) - { - return; - } - ApplyPanelCollapse(panel, panel.IsCollapsed); - return; - } - - if (e.Property == SplitterPanel.IsResizableProperty || - e.Property == SplitterPanel.CollapsibleProperty) - { - UpdateHandlesForPanel(panel); - return; - } - - if (e.Property == SplitterPanel.SizeProperty || - e.Property == SplitterPanel.DefaultSizeProperty || - e.Property == SplitterPanel.MinProperty || - e.Property == SplitterPanel.MaxProperty) - { - InvalidateMeasure(); - } + control.SetValue(MinSizeProperty, value); } - - private void UpdateHandlesForPanel(SplitterPanel panel) + + public static Dimension? GetMaxSize(Control control) { - var index = _panels.IndexOf(panel); - if (index < 0) - { - return; - } - - if (index > 0) - { - UpdateHandleState(index - 1); - } - - if (index < _handles.Count) - { - UpdateHandleState(index); - } + return control.GetValue(MaxSizeProperty); } - - private void UpdateHandleLayouts() + + public static void SetMaxSize(Control control, Dimension? value) { - for (var i = 0; i < _handles.Count; i++) - { - var handle = _handles[i]; - handle.Orientation = Orientation; - UpdateHandleState(i, handle); - } + control.SetValue(MaxSizeProperty, value); } - - private void UpdateHandleState(int index) + + public static bool GetIsResizable(Control control) { - if (index < 0 || index >= _handles.Count) - { - return; - } - - UpdateHandleState(index, _handles[index]); + return control.GetValue(IsResizableProperty); } - - private void UpdateHandleState(int index, SplitterHandle handle) + + public static void SetIsResizable(Control control, bool value) { - if (index < 0 || index + 1 >= _panels.Count) - { - return; - } - - var previous = _panels[index]; - var next = _panels[index + 1]; - var availableLength = GetAvailablePanelLength(GetLayoutLength(_lastLayoutSize)); - - var canDrag = CanDragBetween(previous, next, availableLength); - if (!canDrag) - { - if (previous.IsCollapsed && index - 1 >= 0) - { - canDrag = CanDragBetween(_panels[index - 1], previous, availableLength); - } - - if (!canDrag && next.IsCollapsed && index + 2 < _panels.Count) - { - canDrag = CanDragBetween(next, _panels[index + 2], availableLength); - } - } - handle.IsDragEnabled = canDrag; - handle.Cursor = canDrag - ? Orientation == Orientation.Vertical - ? new Cursor(StandardCursorType.SizeWestEast) - : new Cursor(StandardCursorType.SizeNorthSouth) - : Cursor.Default; - - handle.PreviousButtonControlsNext = false; - handle.NextButtonControlsPrevious = false; - handle.PreviousIconTemplate = CollapsePreviousIcon; - handle.NextIconTemplate = CollapseNextIcon; - - if (!TryGetBoundaryForHandle(index, out var leftVisible, out var rightVisible)) - { - handle.IsPreviousCollapsible = false; - handle.IsNextCollapsible = false; - handle.ShowPreviousButton = false; - handle.ShowNextButton = false; - handle.IsPreviousCollapsed = false; - handle.IsNextCollapsed = false; - handle.UpdateCollapseButtons(); - return; - } - - var hasLeftButton = TryGetExpandLeftState(leftVisible, rightVisible, out var leftShowMode); - var hasRightButton = TryGetExpandRightState(leftVisible, rightVisible, out var rightShowMode); - var showBothOnHover = leftVisible >= 0 && rightVisible >= 0 && rightVisible == leftVisible + 1; - - handle.IsPreviousCollapsible = hasLeftButton; - handle.IsNextCollapsible = hasRightButton; - handle.ShowPreviousButton = hasLeftButton; - handle.ShowNextButton = hasRightButton; - handle.ShowBothButtonsOnHover = showBothOnHover; - handle.PreviousShowMode = leftShowMode; - handle.NextShowMode = rightShowMode; - handle.IsPreviousCollapsed = false; - handle.IsNextCollapsed = false; - - handle.UpdateCollapseButtons(); + control.SetValue(IsResizableProperty, value); } - - private bool CanDragBetween(SplitterPanel previous, SplitterPanel next, double availableLength) + + public static SplitterPanelCollapsible? GetCollapsible(Control control) { - if (!previous.IsResizable || !next.IsResizable) - { - return false; - } - - if (previous.IsCollapsed && !CanExpandByDrag(previous, availableLength)) - { - return false; - } - - if (next.IsCollapsed && !CanExpandByDrag(next, availableLength)) - { - return false; - } - - return true; + return control.GetValue(CollapsibleProperty); } - - private bool CanExpandByDrag(SplitterPanel panel, double availableLength) + + public static void SetCollapsible(Control control, SplitterPanelCollapsible? value) { - var min = GetPanelMin(panel, availableLength); - return min <= 0.0; + control.SetValue(CollapsibleProperty, value); } - - private void HandleCollapsePreviousRequested(object? sender, EventArgs e) + + public static bool GetIsCollapsed(Control control) { - if (sender is SplitterHandle handle) - { - ExpandLeftAtHandle(handle); - } + return control.GetValue(IsCollapsedProperty); } - - private void HandleCollapseNextRequested(object? sender, EventArgs e) + + public static void SetIsCollapsed(Control control, bool value) { - if (sender is SplitterHandle handle) - { - ExpandRightAtHandle(handle); - } + control.SetValue(IsCollapsedProperty, value); } - - private void ExpandLeftAtHandle(SplitterHandle handle) + + protected virtual void ChildrenChanged(object? sender, NotifyCollectionChangedEventArgs e) { - if (!TryGetBoundaryForHandle(handle.HandleIndex, out var leftVisible, out var rightVisible)) + switch (e.Action) { - return; - } - - if (!IsPrimaryHandleForBoundary(handle.HandleIndex, leftVisible, rightVisible)) - { - return; - } - - if (leftVisible < 0) - { - return; - } - - var availableLength = GetAvailablePanelLength(GetLayoutLength(_lastLayoutSize)); - var restoreIndex = FindRestoreCandidateLeft(leftVisible, rightVisible); - if (restoreIndex.HasValue) - { - var ownerIndex = ResolveOwnerIndex(restoreIndex.Value); - if (ownerIndex >= 0 && - _panels[restoreIndex.Value].IsResizable && - _panels[ownerIndex].IsResizable) - { - ApplyCollapseForIndices(restoreIndex.Value, ownerIndex, availableLength); - } - return; - } - - if (rightVisible < 0) - { - return; - } - - if (!_panels[leftVisible].IsResizable || !_panels[rightVisible].IsResizable) - { - return; - } - - ApplyCollapseForIndices(leftVisible, rightVisible, availableLength); - } - - private void ExpandRightAtHandle(SplitterHandle handle) - { - if (!TryGetBoundaryForHandle(handle.HandleIndex, out var leftVisible, out var rightVisible)) - { - return; - } - - if (!IsPrimaryHandleForBoundary(handle.HandleIndex, leftVisible, rightVisible)) - { - return; - } - - if (rightVisible < 0) - { - return; - } - - var availableLength = GetAvailablePanelLength(GetLayoutLength(_lastLayoutSize)); - var restoreIndex = FindRestoreCandidateRight(leftVisible, rightVisible); - if (restoreIndex.HasValue) - { - var ownerIndex = ResolveOwnerIndex(restoreIndex.Value); - if (ownerIndex >= 0 && - _panels[restoreIndex.Value].IsResizable && - _panels[ownerIndex].IsResizable) - { - ApplyCollapseForIndices(restoreIndex.Value, ownerIndex, availableLength); - } - return; - } - - if (leftVisible < 0) - { - return; - } - - if (!_panels[leftVisible].IsResizable || !_panels[rightVisible].IsResizable) - { - return; - } - - ApplyCollapseForIndices(rightVisible, leftVisible, availableLength); - } - - private bool TryGetBoundaryForHandle(int handleIndex, out int leftVisible, out int rightVisible) - { - leftVisible = FindLeftVisibleIndex(handleIndex); - rightVisible = FindRightVisibleIndex(handleIndex); - return leftVisible != -1 || rightVisible != -1; - } - - private int FindLeftVisibleIndex(int handleIndex) - { - for (var i = handleIndex; i >= 0; i--) - { - if (!_panels[i].IsCollapsed) - { - return i; - } - } - - return -1; - } - - private int FindRightVisibleIndex(int handleIndex) - { - for (var i = handleIndex + 1; i < _panels.Count; i++) - { - if (!_panels[i].IsCollapsed) - { - return i; - } - } - - return -1; - } - - private bool IsPrimaryHandleForBoundary(int handleIndex, int leftVisible, int rightVisible) - { - if (rightVisible >= 0) - { - return handleIndex == rightVisible - 1; - } - - if (leftVisible >= 0) - { - return handleIndex == leftVisible; - } - - return false; - } - - private bool TryGetExpandLeftState( - int leftVisible, - int rightVisible, - out SplitterCollapsibleIconDisplayMode showMode) - { - showMode = SplitterCollapsibleIconDisplayMode.Hover; - if (leftVisible < 0) - { - return false; - } - - var restoreIndex = FindRestoreCandidateLeft(leftVisible, rightVisible); - if (restoreIndex.HasValue) - { - var ownerIndex = ResolveOwnerIndex(restoreIndex.Value); - if (ownerIndex < 0) - { - return false; - } - - if (!_panels[restoreIndex.Value].IsResizable || !_panels[ownerIndex].IsResizable) - { - return false; - } - } - else if (rightVisible < 0) - { - return false; - } - - if (!restoreIndex.HasValue && - (!_panels[leftVisible].IsResizable || !_panels[rightVisible].IsResizable)) - { - return false; - } - - var targetIndex = restoreIndex ?? leftVisible; - if (targetIndex < 0) - { - return false; - } - - var targetPanel = _panels[targetIndex]; - if (!targetPanel.IsCollapsible || !targetPanel.IsResizable) - { - return false; - } - - showMode = targetPanel.Collapsible?.ShowCollapsibleIcon ?? SplitterCollapsibleIconDisplayMode.Hover; - return true; - } - - private bool TryGetExpandRightState( - int leftVisible, - int rightVisible, - out SplitterCollapsibleIconDisplayMode showMode) - { - showMode = SplitterCollapsibleIconDisplayMode.Hover; - if (rightVisible < 0) - { - return false; - } - - var restoreIndex = FindRestoreCandidateRight(leftVisible, rightVisible); - if (restoreIndex.HasValue) - { - var ownerIndex = ResolveOwnerIndex(restoreIndex.Value); - if (ownerIndex < 0) - { - return false; - } - - if (!_panels[restoreIndex.Value].IsResizable || !_panels[ownerIndex].IsResizable) - { - return false; - } - } - else if (leftVisible < 0) - { - return false; - } - - if (!restoreIndex.HasValue && - (!_panels[leftVisible].IsResizable || !_panels[rightVisible].IsResizable)) - { - return false; - } - - var targetIndex = restoreIndex ?? rightVisible; - if (targetIndex < 0) - { - return false; - } - - var targetPanel = _panels[targetIndex]; - if (!targetPanel.IsCollapsible || !targetPanel.IsResizable) - { - return false; - } - - showMode = targetPanel.Collapsible?.ShowCollapsibleIcon ?? SplitterCollapsibleIconDisplayMode.Hover; - return true; - } - - private int? FindRestoreCandidateLeft(int leftVisible, int rightVisible) - { - if (leftVisible < 0) - { - return null; - } - - if (rightVisible >= 0) - { - var start = rightVisible - 1; - var end = leftVisible; - - var owner = leftVisible; - for (var i = start; i > end; i--) - { - var panel = _panels[i]; - if (!panel.IsCollapsed || !panel.IsCollapsible) - { - continue; - } - - if (!panel.LastCollapsedIntoIndex.HasValue || panel.LastCollapsedIntoIndex.Value != owner) - { - continue; - } - - return i; - } - } - else - { - var start = _panels.Count - 1; - var end = leftVisible + 1; - - var owner = leftVisible; - for (var i = start; i >= end; i--) - { - var panel = _panels[i]; - if (!panel.IsCollapsed || !panel.IsCollapsible) - { - continue; - } - - if (!panel.LastCollapsedIntoIndex.HasValue || panel.LastCollapsedIntoIndex.Value != owner) - { - continue; - } - - return i; - } - } - - return null; - } - - private int? FindRestoreCandidateRight(int leftVisible, int rightVisible) - { - if (rightVisible < 0) - { - return null; - } - - if (leftVisible >= 0) - { - var start = leftVisible + 1; - var end = rightVisible; - - var owner = rightVisible; - for (var i = start; i < end; i++) - { - var panel = _panels[i]; - if (!panel.IsCollapsed || !panel.IsCollapsible) - { - continue; - } - - if (!panel.LastCollapsedIntoIndex.HasValue || panel.LastCollapsedIntoIndex.Value != owner) - { - continue; - } - - return i; - } - } - else - { - var start = 0; - var end = rightVisible; - - var owner = rightVisible; - for (var i = start; i < end; i++) - { - var panel = _panels[i]; - if (!panel.IsCollapsed || !panel.IsCollapsible) - { - continue; - } - - if (!panel.LastCollapsedIntoIndex.HasValue || panel.LastCollapsedIntoIndex.Value != owner) - { - continue; - } - - return i; - } - } - - return null; - } - - private int ResolveOwnerIndex(int panelIndex) - { - if (panelIndex < 0 || panelIndex >= _panels.Count) - { - return -1; - } - - var index = panelIndex; - var guard = 0; - while (index >= 0 && index < _panels.Count && _panels[index].IsCollapsed) - { - var next = _panels[index].LastCollapsedIntoIndex; - if (!next.HasValue || next.Value == index) - { - return -1; - } - - index = next.Value; - guard++; - if (guard > _panels.Count) - { - return -1; - } - } - - return index; - } - - private void ApplyCollapseForIndices(int collapseIndex, int partnerIndex, double availableLength) - { - if (collapseIndex < 0 || collapseIndex >= _panels.Count || - partnerIndex < 0 || partnerIndex >= _panels.Count || - collapseIndex == partnerIndex) - { - return; - } - - if (collapseIndex < partnerIndex) - { - var previous = _panels[collapseIndex]; - var next = _panels[partnerIndex]; - if (!previous.IsCollapsed && previous.EffectiveSize > 0) - { - previous.LastNonCollapsedSize = previous.EffectiveSize; - } - - var targetPrevious = previous.IsCollapsed - ? GetRestoreSize(previous, availableLength) - : 0; - var targetDelta = targetPrevious - previous.EffectiveSize; - ApplyCollapseDelta(previous, next, targetDelta, availableLength, collapseIndex, partnerIndex); - } - else - { - var previous = _panels[partnerIndex]; - var next = _panels[collapseIndex]; - if (!next.IsCollapsed && next.EffectiveSize > 0) - { - next.LastNonCollapsedSize = next.EffectiveSize; - } - - var targetNext = next.IsCollapsed - ? GetRestoreSize(next, availableLength) - : 0; - var targetDelta = next.EffectiveSize - targetNext; - ApplyCollapseDelta(previous, next, targetDelta, availableLength, partnerIndex, collapseIndex); - } - } - - private double GetRestoreSize(SplitterPanel panel, double availableLength) - { - var restoreSize = panel.LastNonCollapsedSize - ?? ResolvePanelInitialSize(panel, availableLength) - ?? GetFallbackPanelSize(panel, availableLength); - return ClampPanelSize(panel, restoreSize, availableLength); - } - - private void ApplyCollapseDelta( - SplitterPanel previous, - SplitterPanel next, - double targetDelta, - double availableLength, - int previousIndex, - int nextIndex) - { - var prevSize = previous.EffectiveSize; - var nextSize = next.EffectiveSize; - var prevMin = GetPanelMin(previous, availableLength); - var prevMax = GetPanelMax(previous, availableLength); - if (prevMax < prevMin) - { - prevMax = prevMin; - } - - var nextMin = GetPanelMin(next, availableLength); - var nextMax = GetPanelMax(next, availableLength); - if (nextMax < nextMin) - { - nextMax = nextMin; - } - - var minDelta = Math.Max(prevMin - prevSize, nextSize - nextMax); - var maxDelta = Math.Min(prevMax - prevSize, nextSize - nextMin); - if (minDelta > maxDelta) - { - return; - } - - var clampedDelta = Math.Clamp(targetDelta, minDelta, maxDelta); - var newPrev = prevSize + clampedDelta; - var newNext = nextSize - clampedDelta; - - if (prevSize > 0 && newPrev <= 0) - { - previous.LastNonCollapsedSize = prevSize; - } - - if (nextSize > 0 && newNext <= 0) - { - next.LastNonCollapsedSize = nextSize; - } - - if (newPrev > 0) - { - previous.LastCollapsedIntoIndex = null; - } - else if (prevSize > 0 && newPrev <= 0) - { - previous.LastCollapsedIntoIndex = nextIndex; - } - - if (newNext > 0) - { - next.LastCollapsedIntoIndex = null; - } - else if (nextSize > 0 && newNext <= 0) - { - next.LastCollapsedIntoIndex = previousIndex; - } - - SetPanelSize(previous, newPrev, availableLength, updateLastSize: newPrev > 0); - SetPanelSize(next, newNext, availableLength, updateLastSize: newNext > 0); - - UpdatePanelCollapsedState(previous, newPrev); - UpdatePanelCollapsedState(next, newNext); - - InvalidateArrange(); - } - - private void SetPanelCollapsed(SplitterPanel panel, bool collapsed) - { - _suppressPanelChange = true; - try - { - ApplyPanelCollapse(panel, collapsed); - panel.SetCurrentValue(SplitterPanel.IsCollapsedProperty, collapsed); - panel.UpdatePseudoClasses(); - } - finally - { - _suppressPanelChange = false; - } - } - - private void ApplyPanelCollapse(SplitterPanel panel, bool collapsed) - { - var availableLength = GetAvailablePanelLength(GetLayoutLength(_lastLayoutSize)); - if (collapsed) - { - if (panel.EffectiveSize > 0) - { - panel.LastNonCollapsedSize = panel.EffectiveSize; - } - - SetPanelSize(panel, 0, availableLength, updateLastSize: false); - } - else - { - var restoreSize = panel.LastNonCollapsedSize - ?? ResolvePanelInitialSize(panel, availableLength) - ?? GetFallbackPanelSize(panel, availableLength); - restoreSize = ClampPanelSize(panel, restoreSize, availableLength); - SetPanelSize(panel, restoreSize, availableLength, updateLastSize: true); - } - - UpdateHandlesForPanel(panel); - InvalidateMeasure(); - } - - protected override Size MeasureOverride(Size availableSize) - { - if (_panels.Count == 0) - { - return default; - } - - var isVertical = Orientation == Orientation.Vertical; - var availableLength = GetLayoutLength(availableSize); - var availableCross = GetCrossLength(availableSize); - - if (double.IsInfinity(availableLength)) - { - return MeasureWithInfiniteLength(availableSize); - } - - var sizes = ComputePanelSizes(availableLength); - for (var i = 0; i < _panels.Count; i++) - { - var panel = _panels[i]; - var panelSize = sizes[i]; - var panelAvailable = isVertical - ? new Size(panelSize, availableCross) - : new Size(availableCross, panelSize); - panel.Measure(panelAvailable); - } - - foreach (var handle in _handles) - { - var handleAvailable = isVertical - ? new Size(HandleSize, availableCross) - : new Size(availableCross, HandleSize); - handle.Measure(handleAvailable); - } - - return availableSize; - } - - private Size MeasureWithInfiniteLength(Size availableSize) - { - var isVertical = Orientation == Orientation.Vertical; - var availableCross = GetCrossLength(availableSize); - var length = 0d; - var cross = 0d; - - foreach (var panel in _panels) - { - var panelAvailable = isVertical - ? new Size(double.PositiveInfinity, availableCross) - : new Size(availableCross, double.PositiveInfinity); - panel.Measure(panelAvailable); - var desired = panel.DesiredSize; - length += isVertical ? desired.Width : desired.Height; - cross = Math.Max(cross, isVertical ? desired.Height : desired.Width); - } - - length += Math.Max(0, GetEffectiveHandleCount()) * GetHandleSpacing(); - - return isVertical - ? new Size(length, double.IsInfinity(availableCross) ? cross : availableCross) - : new Size(double.IsInfinity(availableCross) ? cross : availableCross, length); - } - - protected override Size ArrangeOverride(Size finalSize) - { - _lastLayoutSize = finalSize; - - if (_panels.Count == 0) - { - return finalSize; - } - - var isVertical = Orientation == Orientation.Vertical; - var availableLength = GetLayoutLength(finalSize); - var sizes = ComputePanelSizes(availableLength); - var handleSpacing = GetHandleSpacing(); - var handleOffset = (HandleSize - handleSpacing) / 2d; - - var offset = 0d; - for (var i = 0; i < _panels.Count; i++) - { - var panel = _panels[i]; - var panelSize = sizes[i]; - panel.EffectiveSize = panelSize; - - var rect = isVertical - ? new Rect(offset, 0, panelSize, finalSize.Height) - : new Rect(0, offset, finalSize.Width, panelSize); - panel.Arrange(rect); - offset += panelSize; - - if (i < _handles.Count) - { - var handlePosition = offset - handleOffset; - var handleRect = isVertical - ? new Rect(handlePosition, 0, HandleSize, finalSize.Height) - : new Rect(0, handlePosition, finalSize.Width, HandleSize); - _handles[i].Arrange(handleRect); - _handles[i].IsVisible = IsHandleVisibleForLayout(i); - offset += GetHandleSpacingForIndex(i, handleSpacing); - } - } - - UpdateHandleLayouts(); - return finalSize; - } - - private void HandleDragStarted(object? sender, VectorEventArgs e) - { - if (sender is not SplitterHandle handle) - { - return; - } - - if (!TryCreateBoundaryDragContext(handle, e.Vector, out var context, out var index)) - { - index = ResolveHandleIndexForDrag(handle, e.Vector); - context = CreateDragContext(handle, index, index, index + 1); - if (context == null) - { - var fallbackIndex = FindFallbackDragIndex(index); - if (fallbackIndex.HasValue) - { - index = fallbackIndex.Value; - context = CreateDragContext(handle, index, index, index + 1); - } - } - } - if (context == null) - { - return; - } - - _dragContext = context; - - handle.SetDragging(true); - RaiseResizeStarted(index, BuildSizesWithDelta(context.StartPreviousSize, context.StartNextSize, 0)); - } - - private bool TryCreateBoundaryDragContext( - SplitterHandle handle, - Vector startVector, - out DragContext? context, - out int handleIndex) - { - context = null; - handleIndex = handle.HandleIndex; - - if (!TryGetBoundaryForHandle(handle.HandleIndex, out var leftVisible, out var rightVisible)) - { - return false; - } - - if (leftVisible < 0 || rightVisible < 0 || rightVisible == leftVisible + 1) - { - return false; - } - - var preferLeft = !IsPointerOnNextSide(handle, startVector); - var leftCandidate = FindRestoreCandidateLeft(leftVisible, rightVisible); - var rightCandidate = FindRestoreCandidateRight(leftVisible, rightVisible); - - if (!leftCandidate.HasValue && !rightCandidate.HasValue) - { - return false; - } - - var candidates = preferLeft - ? new[] { leftCandidate, rightCandidate } - : new[] { rightCandidate, leftCandidate }; - - foreach (var candidate in candidates) - { - if (!candidate.HasValue) - { - continue; - } - - var ownerIndex = ResolveOwnerIndex(candidate.Value); - if (ownerIndex < 0) - { - continue; - } - - var previousIndex = Math.Min(ownerIndex, candidate.Value); - var nextIndex = Math.Max(ownerIndex, candidate.Value); - var dragContext = CreateDragContext(handle, handle.HandleIndex, previousIndex, nextIndex, false); - if (dragContext == null) - { - continue; - } - - context = dragContext; - handleIndex = previousIndex; - return true; - } - - return false; - } - - private bool IsPointerOnNextSide(SplitterHandle handle, Vector startVector) - { - var root = handle.GetVisualRoot() as Visual ?? handle; - var origin = handle.TranslatePoint(new Point(0, 0), root); - if (!origin.HasValue) - { - return true; - } - - var local = new Point(startVector.X - origin.Value.X, startVector.Y - origin.Value.Y); - var compare = Orientation == Orientation.Vertical ? local.X : local.Y; - var center = Orientation == Orientation.Vertical ? handle.Bounds.Width * 0.5 : handle.Bounds.Height * 0.5; - return compare >= center; - } - - private int? FindFallbackDragIndex(int handleIndex) - { - if (handleIndex < 0 || handleIndex + 1 >= _panels.Count) - { - return null; - } - - var availableLength = GetAvailablePanelLength(GetLayoutLength(_lastLayoutSize)); - - if (_panels[handleIndex].IsCollapsed && handleIndex - 1 >= 0) - { - if (CanDragBetween(_panels[handleIndex - 1], _panels[handleIndex], availableLength)) - { - return handleIndex - 1; - } - } - - if (handleIndex + 2 < _panels.Count && _panels[handleIndex + 1].IsCollapsed) - { - if (CanDragBetween(_panels[handleIndex + 1], _panels[handleIndex + 2], availableLength)) - { - return handleIndex + 1; - } - } - - return null; - } - - private void HandleDragDelta(object? sender, VectorEventArgs e) - { - if (_dragContext == null) - { - return; - } - - var handle = _dragContext.Handle; - if (handle != sender) - { - return; - } - - var delta = Orientation == Orientation.Vertical ? e.Vector.X : e.Vector.Y; - if (TrySwitchDragContext(handle, delta)) - { - delta = 0; - } - var clampedDelta = Math.Clamp(delta, _dragContext.MinDelta, _dragContext.MaxDelta); - var adjust = delta - clampedDelta; - if (Math.Abs(adjust) > 0.001) - { - handle.AdjustDrag(Orientation == Orientation.Vertical - ? new Vector(adjust, 0) - : new Vector(0, adjust)); - } - - var previewSizes = BuildSizesWithDelta(_dragContext.StartPreviousSize, _dragContext.StartNextSize, clampedDelta); - RaiseResizeDelta(_dragContext.HandleIndex, previewSizes); - - if (IsLazy) - { - _dragContext.CurrentDelta = clampedDelta; - ApplyHandlePreview(handle, clampedDelta); - return; - } - - ApplyResize(_dragContext, clampedDelta); - } - - private void HandleDragCompleted(object? sender, VectorEventArgs e) - { - if (_dragContext == null) - { - return; - } - - var handle = _dragContext.Handle; - if (handle != sender) - { - return; - } - - var delta = IsLazy ? _dragContext.CurrentDelta : - Math.Clamp(Orientation == Orientation.Vertical ? e.Vector.X : e.Vector.Y, - _dragContext.MinDelta, _dragContext.MaxDelta); - - if (IsLazy) - { - ClearHandlePreview(handle); - ApplyResize(_dragContext, delta); - } - - handle.SetDragging(false); - FinalizeResizeSizes(_dragContext); - var completedSizes = BuildSizesWithDelta(_dragContext.StartPreviousSize, _dragContext.StartNextSize, delta); - RaiseResizeCompleted(_dragContext.HandleIndex, completedSizes); - _dragContext = null; - } - - private void ApplyResize(DragContext context, double delta) - { - var previousSize = context.StartPreviousSize + delta; - var nextSize = context.StartNextSize - delta; - previousSize = ClampPanelSize(context.PreviousPanel, previousSize, context.AvailableLength); - nextSize = ClampPanelSize(context.NextPanel, nextSize, context.AvailableLength); - - UpdateCollapseRecordForResize(context, previousSize, nextSize); - - SetPanelSize(context.PreviousPanel, previousSize, context.AvailableLength, updateLastSize: false); - SetPanelSize(context.NextPanel, nextSize, context.AvailableLength, updateLastSize: false); - - UpdatePanelCollapsedState(context.PreviousPanel, previousSize); - UpdatePanelCollapsedState(context.NextPanel, nextSize); - - InvalidateArrange(); - } - - private void ApplyHandlePreview(SplitterHandle handle, double delta) - { - handle.RenderTransform = Orientation == Orientation.Vertical - ? new TranslateTransform(delta, 0) - : new TranslateTransform(0, delta); - } - - private void ClearHandlePreview(SplitterHandle handle) - { - handle.RenderTransform = null; - } - - private void UpdatePanelCollapsedState(SplitterPanel panel, double size) - { - var collapsed = size <= 0.0; - if (panel.IsCollapsed == collapsed) - { - return; - } - - _suppressPanelChange = true; - try - { - panel.SetCurrentValue(SplitterPanel.IsCollapsedProperty, collapsed); - panel.UpdatePseudoClasses(); - } - finally - { - _suppressPanelChange = false; - } - - UpdateHandlesForPanel(panel); - } - - private void UpdateCollapseRecordForResize(DragContext context, double newPrev, double newNext) - { - var previous = context.PreviousPanel; - var next = context.NextPanel; - var prevSize = previous.EffectiveSize; - var nextSize = next.EffectiveSize; - - if (prevSize > 0 && newPrev <= 0) - { - previous.LastNonCollapsedSize = context.StartPreviousSize; - previous.LastCollapsedIntoIndex = context.NextIndex; - } - else if (newPrev > 0) - { - previous.LastCollapsedIntoIndex = null; - } - - if (nextSize > 0 && newNext <= 0) - { - next.LastNonCollapsedSize = context.StartNextSize; - next.LastCollapsedIntoIndex = context.PreviousIndex; - } - else if (newNext > 0) - { - next.LastCollapsedIntoIndex = null; - } - } - - private void FinalizeResizeSizes(DragContext context) - { - var previous = context.PreviousPanel; - var next = context.NextPanel; - - if (previous.EffectiveSize > 0) - { - previous.LastNonCollapsedSize = previous.EffectiveSize; - } - - if (next.EffectiveSize > 0) - { - next.LastNonCollapsedSize = next.EffectiveSize; - } - } - - private void SetPanelSize(SplitterPanel panel, double size, double availableLength, bool updateLastSize) - { - if (double.IsNaN(size) || double.IsInfinity(size)) - { - return; - } - - size = Math.Max(0, size); - var unit = panel.PreferredSizeUnit; - Dimension dimension; - if (unit == DimensionUnitType.Percentage && availableLength > 0) - { - var percentValue = Math.Clamp(size / availableLength * 100.0, 0, 100); - dimension = new Dimension(percentValue, DimensionUnitType.Percentage); - } - else - { - dimension = new Dimension(size, DimensionUnitType.Pixel); - } - - panel.SetCurrentValue(SplitterPanel.SizeProperty, dimension); - panel.EffectiveSize = size; - if (updateLastSize && size > 0) - { - panel.LastNonCollapsedSize = size; - } - } - - private double? ResolvePanelInitialSize(SplitterPanel panel, double availableLength) - { - if (panel.Size.HasValue) - { - return panel.Size.Value.Resolve(availableLength); - } - - if (panel.DefaultSize.HasValue) - { - return panel.DefaultSize.Value.Resolve(availableLength); - } - - return null; - } - - private double GetFallbackPanelSize(SplitterPanel panel, double availableLength) - { - if (_panels.Count == 0) - { - return 0; - } - - var remaining = availableLength; - foreach (var other in _panels) - { - if (other == panel) - { - continue; - } - - var size = ResolvePanelInitialSize(other, availableLength); - if (size.HasValue) - { - remaining -= size.Value; - } - } - - if (remaining < 0) - { - remaining = availableLength / _panels.Count; - } - - return remaining; - } - - private List ComputePanelSizes(double totalLength) - { - var handleSpace = Math.Max(0, GetEffectiveHandleCount()) * GetHandleSpacing(); - var availableLength = Math.Max(0, totalLength - handleSpace); - var sizes = new List(_panels.Count); - var flexibleIndices = new List(); - var minSizes = new double[_panels.Count]; - var maxSizes = new double[_panels.Count]; - - var sumExplicit = 0d; - for (var i = 0; i < _panels.Count; i++) - { - var panel = _panels[i]; - var min = GetPanelMin(panel, availableLength); - var max = GetPanelMax(panel, availableLength); - if (max < min) - { - max = min; - } - minSizes[i] = min; - maxSizes[i] = max; - - if (panel.IsCollapsed) - { - sizes.Add(0); - sumExplicit += 0; - continue; - } - - var initial = ResolvePanelInitialSize(panel, availableLength); - if (initial.HasValue) - { - var clamped = Math.Clamp(initial.Value, min, max); - sizes.Add(clamped); - sumExplicit += clamped; - } - else if (panel.EffectiveSize > 0) - { - var clamped = Math.Clamp(panel.EffectiveSize, min, max); - sizes.Add(clamped); - sumExplicit += clamped; - } - else - { - sizes.Add(double.NaN); - flexibleIndices.Add(i); - } - } - - var remaining = availableLength - sumExplicit; - if (flexibleIndices.Count > 0) - { - var each = remaining / flexibleIndices.Count; - foreach (var index in flexibleIndices) - { - sizes[index] = each; - } - } - else if (_panels.Count > 0 && Math.Abs(remaining) > 0.001) - { - var index = _panels.Count - 1; - sizes[index] = Math.Max(0, sizes[index] + remaining); - } - - for (var i = 0; i < sizes.Count; i++) - { - if (_panels[i].IsCollapsed) - { - sizes[i] = 0; - continue; - } - - sizes[i] = Math.Clamp(sizes[i], minSizes[i], maxSizes[i]); - } - - NormalizeSizes(sizes, minSizes, maxSizes, availableLength); - return sizes; - } - - private void NormalizeSizes(List sizes, double[] minSizes, double[] maxSizes, double availableLength) - { - var delta = availableLength - sizes.Sum(); - var guard = 0; - while (Math.Abs(delta) > 0.5 && guard < 100) - { - var indices = new List(); - for (var i = 0; i < sizes.Count; i++) - { - if (_panels[i].IsCollapsed) - { - continue; - } - - if (delta > 0 && sizes[i] < maxSizes[i]) - { - indices.Add(i); - } - else if (delta < 0 && sizes[i] > minSizes[i]) - { - indices.Add(i); - } - } - - if (indices.Count == 0) - { + case NotifyCollectionChangedAction.Add: + _splitterPanel?.Children.InsertRange(e.NewStartingIndex, e.NewItems!.OfType().ToList()); break; - } - var per = delta / indices.Count; - var adjusted = 0d; + case NotifyCollectionChangedAction.Move: + _splitterPanel?.Children.MoveRange(e.OldStartingIndex, e.OldItems!.Count, e.NewStartingIndex); + break; - foreach (var index in indices) + case NotifyCollectionChangedAction.Remove: + _splitterPanel?.Children.RemoveAll(e.OldItems!.OfType().ToList()); + break; + + case NotifyCollectionChangedAction.Replace: + for (var i = 0; i < e.OldItems!.Count; ++i) + { + var index = i + e.OldStartingIndex; + var child = (Control)e.NewItems![i]!; + _splitterPanel?.Children[index] = child; + } + break; + + case NotifyCollectionChangedAction.Reset: + throw new NotSupportedException(); + } + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + _splitterPanel = e.NameScope.Find(SplitterThemeConstants.SplitterPanelPart); + if (_splitterPanel != null) + { + _splitterPanel.OwnerSplitter = this; + foreach (var child in Children) { - var target = sizes[index] + per; - var newSize = delta > 0 - ? Math.Min(target, maxSizes[index]) - : Math.Max(target, minSizes[index]); - var diff = newSize - sizes[index]; - sizes[index] = newSize; - adjusted += diff; - } - - delta -= adjusted; - guard++; - } - } - - private double ClampPanelSize(SplitterPanel panel, double size, double availableLength) - { - var min = GetPanelMin(panel, availableLength); - var max = GetPanelMax(panel, availableLength); - if (max < min) - { - max = min; - } - return Math.Clamp(size, min, max); - } - - private double GetPanelMin(SplitterPanel panel, double availableLength) - { - if (panel.Min.HasValue) - { - return Math.Max(0, panel.Min.Value.Resolve(availableLength)); - } - - return 0; - } - - private double GetPanelMax(SplitterPanel panel, double availableLength) - { - if (panel.Max.HasValue) - { - var max = panel.Max.Value.Resolve(availableLength); - if (max < 0) - { - return 0; - } - return max; - } - - return double.PositiveInfinity; - } - - private double GetLayoutLength(Size size) - { - return Orientation == Orientation.Vertical ? size.Width : size.Height; - } - - private double GetCrossLength(Size size) - { - return Orientation == Orientation.Vertical ? size.Height : size.Width; - } - - private double GetAvailablePanelLength(double totalLength) - { - if (_panels.Count <= 1) - { - return Math.Max(0, totalLength); - } - - var handleSpace = Math.Max(0, GetEffectiveHandleCount()) * GetHandleSpacing(); - return Math.Max(0, totalLength - handleSpace); - } - - private DragContext? CreateDragContext( - SplitterHandle handle, - int handleIndex, - int previousIndex, - int nextIndex, - bool? allowSwitchOverride = null) - { - if (previousIndex < 0 || nextIndex >= _panels.Count || previousIndex == nextIndex) - { - return null; - } - - var availableLength = GetAvailablePanelLength(GetLayoutLength(_lastLayoutSize)); - var previous = _panels[previousIndex]; - var next = _panels[nextIndex]; - - if (!CanDragBetween(previous, next, availableLength)) - { - return null; - } - - var previousSize = previous.EffectiveSize; - var nextSize = next.EffectiveSize; - var prevMin = GetPanelMin(previous, availableLength); - var prevMax = GetPanelMax(previous, availableLength); - var nextMin = GetPanelMin(next, availableLength); - var nextMax = GetPanelMax(next, availableLength); - if (prevMax < prevMin) - { - prevMax = prevMin; - } - if (nextMax < nextMin) - { - nextMax = nextMin; - } - - var minDelta = Math.Max(prevMin - previousSize, nextSize - nextMax); - var maxDelta = Math.Min(prevMax - previousSize, nextSize - nextMin); - if (minDelta > maxDelta) - { - return null; - } - - var allowSwitch = allowSwitchOverride ?? (previous.IsCollapsed || next.IsCollapsed); - return new DragContext - { - Handle = handle, - HandleIndex = handleIndex, - PreviousIndex = previousIndex, - NextIndex = nextIndex, - PreviousPanel = previous, - NextPanel = next, - StartPreviousSize = previousSize, - StartNextSize = nextSize, - MinDelta = minDelta, - MaxDelta = maxDelta, - AvailableLength = availableLength, - AllowSwitch = allowSwitch - }; - } - - private bool TrySwitchDragContext(SplitterHandle handle, double delta) - { - if (_dragContext == null || !_dragContext.AllowSwitch || Math.Abs(delta) < 0.001) - { - return false; - } - - var baseIndex = handle.HandleIndex; - var prevIndex = _dragContext.PreviousIndex; - var nextIndex = _dragContext.NextIndex; - - var newPrevIndex = -1; - var newNextIndex = -1; - - if (delta > 0 && - _dragContext.NextPanel.IsCollapsed && - nextIndex + 1 < _panels.Count && - _dragContext.MaxDelta <= 0) - { - newPrevIndex = nextIndex; - newNextIndex = nextIndex + 1; - } - else if (delta < 0 && - _dragContext.PreviousPanel.IsCollapsed && - prevIndex - 1 >= 0 && - _dragContext.MinDelta >= 0) - { - newPrevIndex = prevIndex - 1; - newNextIndex = prevIndex; - } - - if (newPrevIndex < 0 || newNextIndex < 0 || - (newPrevIndex == prevIndex && newNextIndex == nextIndex)) - { - return false; - } - - var context = CreateDragContext(handle, baseIndex, newPrevIndex, newNextIndex, false); - if (context == null) - { - return false; - } - - _dragContext = context; - handle.AdjustDrag(Orientation == Orientation.Vertical - ? new Vector(delta, 0) - : new Vector(0, delta)); - - return true; - } - - - private int ResolveHandleIndexForDrag(SplitterHandle handle, Vector startVector) - { - var index = handle.HandleIndex; - if (index < 0 || index + 1 >= _panels.Count) - { - return index; - } - - var root = handle.GetVisualRoot() as Visual ?? handle; - var origin = handle.TranslatePoint(new Point(0, 0), root); - if (!origin.HasValue) - { - return index; - } - - var local = new Point(startVector.X - origin.Value.X, startVector.Y - origin.Value.Y); - var compare = Orientation == Orientation.Vertical ? local.X : local.Y; - var center = Orientation == Orientation.Vertical ? handle.Bounds.Width * 0.5 : handle.Bounds.Height * 0.5; - var isNextSide = compare >= center; - - if (isNextSide && index + 2 < _panels.Count && _panels[index + 1].IsCollapsed) - { - return index + 1; - } - - if (!isNextSide && index - 1 >= 0 && _panels[index].IsCollapsed) - { - return index - 1; - } - - return index; - } - - private double GetHandleSpacing() - { - var value = TokenResourceUtils.FindTokenResource(this, SplitterTokenKey.SplitBarSize); - if (value is double size && !double.IsNaN(size) && !double.IsInfinity(size)) - { - return Math.Max(0, size); - } - - return Math.Max(0, HandleSize); - } - - private int GetEffectiveHandleCount() - { - if (_panels.Count <= 1) - { - return 0; - } - - var count = 0; - for (var i = 0; i < _panels.Count - 1; i++) - { - if (IsHandleVisibleForLayout(i)) - { - count++; + _splitterPanel?.Children.Add(child); } } - - return count; } - - private double GetHandleSpacingForIndex(int handleIndex, double spacing) - { - if (handleIndex < 0 || handleIndex >= _handles.Count) - { - return 0; - } - - if (!IsHandleVisibleForLayout(handleIndex)) - { - return 0; - } - - return spacing; - } - - private bool IsHandleVisibleForLayout(int handleIndex) - { - if (handleIndex < 0 || handleIndex + 1 >= _panels.Count) - { - return false; - } - - if (!TryGetBoundaryForHandle(handleIndex, out var leftVisible, out var rightVisible)) - { - return false; - } - - return IsPrimaryHandleForBoundary(handleIndex, leftVisible, rightVisible); - } - - private IReadOnlyList BuildSizesWithDelta(double previousSize, double nextSize, double delta) - { - var sizes = GetCurrentSizes().ToArray(); - if (_dragContext == null) - { - return sizes; - } - - var previousIndex = _dragContext.PreviousIndex; - var nextIndex = _dragContext.NextIndex; - if (previousIndex < 0 || nextIndex < 0 || - previousIndex >= sizes.Length || nextIndex >= sizes.Length) - { - return sizes; - } - - sizes[previousIndex] = previousSize + delta; - sizes[nextIndex] = nextSize - delta; - return sizes; - } - - private IReadOnlyList GetCurrentSizes() - { - return _panels.Select(p => p.EffectiveSize).ToArray(); - } - - private void RaiseResizeStarted(int handleIndex, IReadOnlyList sizes) + + internal void RaiseResizeStarted(int handleIndex, IReadOnlyList sizes) { ResizeStarted?.Invoke(this, new SplitterResizeEventArgs(handleIndex, sizes)); } - private void RaiseResizeDelta(int handleIndex, IReadOnlyList sizes) + internal void RaiseResizeDelta(int handleIndex, IReadOnlyList sizes) { ResizeDelta?.Invoke(this, new SplitterResizeEventArgs(handleIndex, sizes)); } - private void RaiseResizeCompleted(int handleIndex, IReadOnlyList sizes) + internal void RaiseResizeCompleted(int handleIndex, IReadOnlyList sizes) { ResizeCompleted?.Invoke(this, new SplitterResizeEventArgs(handleIndex, sizes)); } - - private void UpdatePseudoClasses() - { - PseudoClasses.Set(StdPseudoClass.Vertical, Orientation == Orientation.Vertical); - PseudoClasses.Set(StdPseudoClass.Horizontal, Orientation == Orientation.Horizontal); - } - - private class DragContext - { - public required SplitterHandle Handle { get; init; } - public required int HandleIndex { get; init; } - public required int PreviousIndex { get; init; } - public required int NextIndex { get; init; } - public required SplitterPanel PreviousPanel { get; init; } - public required SplitterPanel NextPanel { get; init; } - public required double StartPreviousSize { get; init; } - public required double StartNextSize { get; init; } - public required double MinDelta { get; init; } - public required double MaxDelta { get; init; } - public required double AvailableLength { get; init; } - public bool AllowSwitch { get; set; } - public double CurrentDelta { get; set; } - } -} +} \ No newline at end of file diff --git a/src/AtomUI.Desktop.Controls/Splitter/SplitterPanel.cs b/src/AtomUI.Desktop.Controls/Splitter/SplitterPanel.cs index 8559a9521..aa386bbb4 100644 --- a/src/AtomUI.Desktop.Controls/Splitter/SplitterPanel.cs +++ b/src/AtomUI.Desktop.Controls/Splitter/SplitterPanel.cs @@ -1,131 +1,1885 @@ +using System.Collections.Specialized; +using AtomUI.Controls; +using AtomUI.Data; +using AtomUI.Desktop.Controls.DesignTokens; using Avalonia; using Avalonia.Controls; -using Avalonia.Controls.Metadata; -using Avalonia.Data; +using Avalonia.Input; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.VisualTree; namespace AtomUI.Desktop.Controls; -internal static class SplitterPanelPseudoClass -{ - public const string Collapsed = ":collapsed"; - public const string NotResizable = ":not-resizable"; -} - -[PseudoClasses(SplitterPanelPseudoClass.Collapsed, SplitterPanelPseudoClass.NotResizable)] -public class SplitterPanel : ContentControl +internal class SplitterPanel : Panel { #region 公共属性定义 + public static readonly StyledProperty OrientationProperty = + Splitter.OrientationProperty.AddOwner(); - public static readonly StyledProperty SizeProperty = - AvaloniaProperty.Register(nameof(Size), - defaultBindingMode: BindingMode.TwoWay); + public static readonly StyledProperty IsLazyProperty = + Splitter.IsLazyProperty.AddOwner(); - public static readonly StyledProperty DefaultSizeProperty = - AvaloniaProperty.Register(nameof(DefaultSize)); + public static readonly StyledProperty HandleSizeProperty = + Splitter.HandleSizeProperty.AddOwner(); - public static readonly StyledProperty MinProperty = - AvaloniaProperty.Register(nameof(Min)); + public static readonly StyledProperty CollapsePreviousIconProperty = + Splitter.CollapsePreviousIconProperty.AddOwner(); - public static readonly StyledProperty MaxProperty = - AvaloniaProperty.Register(nameof(Max)); - - public static readonly StyledProperty IsResizableProperty = - AvaloniaProperty.Register(nameof(IsResizable), true); - - public static readonly StyledProperty CollapsibleProperty = - AvaloniaProperty.Register(nameof(Collapsible)); - - public static readonly StyledProperty IsCollapsedProperty = - AvaloniaProperty.Register(nameof(IsCollapsed), - defaultValue: false, - inherits: false, - defaultBindingMode: BindingMode.TwoWay); - - public Dimension? Size + public static readonly StyledProperty CollapseNextIconProperty = + Splitter.CollapseNextIconProperty.AddOwner(); + + public Orientation Orientation { - get => GetValue(SizeProperty); - set => SetValue(SizeProperty, value); + get => GetValue(OrientationProperty); + set => SetValue(OrientationProperty, value); } - public Dimension? DefaultSize + public bool IsLazy { - get => GetValue(DefaultSizeProperty); - set => SetValue(DefaultSizeProperty, value); + get => GetValue(IsLazyProperty); + set => SetValue(IsLazyProperty, value); } - public Dimension? Min + public double HandleSize { - get => GetValue(MinProperty); - set => SetValue(MinProperty, value); + get => GetValue(HandleSizeProperty); + set => SetValue(HandleSizeProperty, value); } - public Dimension? Max + public IconTemplate? CollapsePreviousIcon { - get => GetValue(MaxProperty); - set => SetValue(MaxProperty, value); + get => GetValue(CollapsePreviousIconProperty); + set => SetValue(CollapsePreviousIconProperty, value); } - public bool IsResizable + public IconTemplate? CollapseNextIcon { - get => GetValue(IsResizableProperty); - set => SetValue(IsResizableProperty, value); + get => GetValue(CollapseNextIconProperty); + set => SetValue(CollapseNextIconProperty, value); } - - public SplitterPanelCollapsible? Collapsible - { - get => GetValue(CollapsibleProperty); - set => SetValue(CollapsibleProperty, value); - } - - public bool IsCollapsed - { - get => GetValue(IsCollapsedProperty); - set => SetValue(IsCollapsedProperty, value); - } - #endregion - - #region 内部属性定义 - - internal double EffectiveSize { get; set; } - internal double? LastNonCollapsedSize { get; set; } - internal int? LastCollapsedIntoIndex { get; set; } - internal Splitter? Owner { get; set; } - - internal bool IsCollapsible => Collapsible?.IsEnabled == true; - - internal DimensionUnitType PreferredSizeUnit + + static SplitterPanel() { - get + AffectsMeasure(OrientationProperty, HandleSizeProperty); + } + + public SplitterPanel() + { + Children.CollectionChanged += HandleChildrenChanged; + } + + internal Splitter? OwnerSplitter; + private readonly List _panels = new(); + private readonly List _handles = new(); + private readonly HashSet _trackedPanels = new(); + private readonly Dictionary _partContexts = new(); + + private bool _suppressChildrenChanged; + private bool _suppressPanelChange; + private Size _lastLayoutSize; + private DragContext? _dragContext; + + private void HandleChildrenChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (_suppressChildrenChanged) { - if (Size.HasValue) + return; + } + + RefreshPanelsAndHandles(); + } + + private void RefreshPanelsAndHandles() + { + _suppressChildrenChanged = true; + try + { + foreach (var handle in _handles) { - return Size.Value.UnitType; + handle.DragStarted -= HandleDragStarted; + handle.DragDelta -= HandleDragDelta; + handle.DragCompleted -= HandleDragCompleted; + handle.CollapsePreviousRequested -= HandleCollapsePreviousRequested; + handle.CollapseNextRequested -= HandleCollapseNextRequested; + Children.Remove(handle); + } + _handles.Clear(); + + var panels = Children.ToList(); + SyncTrackedPanels(panels); + _panels.Clear(); + _panels.AddRange(panels); + _partContexts.Clear(); + + for (var i = 0; i < _panels.Count - 1; i++) + { + var handle = CreateHandle(i); + _handles.Add(handle); + Children.Add(handle); } - if (DefaultSize.HasValue) + foreach (var panel in _panels) { - return DefaultSize.Value.UnitType; + _partContexts[panel] = new SplitterPartContext(); } + } + finally + { + _suppressChildrenChanged = false; + } - return DimensionUnitType.Pixel; + InvalidateMeasure(); + } + + private void SyncTrackedPanels(IReadOnlyList panels) + { + foreach (var panel in _trackedPanels.ToList()) + { + if (!panels.Contains(panel)) + { + panel.PropertyChanged -= HandlePanelPropertyChanged; + _trackedPanels.Remove(panel); + } + } + + foreach (var panel in panels) + { + if (_trackedPanels.Add(panel)) + { + panel.PropertyChanged += HandlePanelPropertyChanged; + } + } + } + + private void HandlePanelPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + + if (sender is not Control panel) + { + return; + } + + if (e.Property == Splitter.IsCollapsedProperty) + { + if (_suppressPanelChange) + { + return; + } + ApplyPanelCollapse(panel, Splitter.GetIsCollapsed(panel)); + return; + } + + if (e.Property == Splitter.IsResizableProperty || + e.Property == Splitter.CollapsibleProperty) + { + UpdateHandlesForPanel(panel); + return; + } + + if (e.Property == Splitter.SizeProperty || + e.Property == Splitter.DefaultSizeProperty || + e.Property == Splitter.MinSizeProperty || + e.Property == Splitter.MaxSizeProperty) + { + InvalidateMeasure(); } } - #endregion - - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + private SplitterHandle CreateHandle(int index) { - base.OnPropertyChanged(change); - if (change.Property == IsResizableProperty || change.Property == IsCollapsedProperty) + var handle = new SplitterHandle { - UpdatePseudoClasses(); + Orientation = Orientation, + HandleIndex = index + }; + + handle.DragStarted += HandleDragStarted; + handle.DragDelta += HandleDragDelta; + handle.DragCompleted += HandleDragCompleted; + handle.CollapsePreviousRequested += HandleCollapsePreviousRequested; + handle.CollapseNextRequested += HandleCollapseNextRequested; + + UpdateHandleState(index, handle); + return handle; + } + + private void UpdateHandlesForPanel(Control panel) + { + var index = _panels.IndexOf(panel); + if (index < 0) + { + return; + } + + if (index > 0) + { + UpdateHandleState(index - 1); + } + + if (index < _handles.Count) + { + UpdateHandleState(index); } } - internal void UpdatePseudoClasses() + private void UpdateHandleLayouts() { - PseudoClasses.Set(SplitterPanelPseudoClass.Collapsed, IsCollapsed); - PseudoClasses.Set(SplitterPanelPseudoClass.NotResizable, !IsResizable); + for (var i = 0; i < _handles.Count; i++) + { + var handle = _handles[i]; + handle.Orientation = Orientation; + UpdateHandleState(i, handle); + } } -} + + private void UpdateHandleState(int index) + { + if (index < 0 || index >= _handles.Count) + { + return; + } + + UpdateHandleState(index, _handles[index]); + } + + private void UpdateHandleState(int index, SplitterHandle handle) + { + if (index < 0 || index + 1 >= _panels.Count) + { + return; + } + + var previous = _panels[index]; + var next = _panels[index + 1]; + var availableLength = GetAvailablePanelLength(GetLayoutLength(_lastLayoutSize)); + + var canDrag = CanDragBetween(previous, next, availableLength); + if (!canDrag) + { + if (Splitter.GetIsCollapsed(previous) && index - 1 >= 0) + { + canDrag = CanDragBetween(_panels[index - 1], previous, availableLength); + } + + if (!canDrag && Splitter.GetIsCollapsed(next) && index + 2 < _panels.Count) + { + canDrag = CanDragBetween(next, _panels[index + 2], availableLength); + } + } + handle.IsDragEnabled = canDrag; + handle.Cursor = canDrag + ? Orientation == Orientation.Vertical + ? new Cursor(StandardCursorType.SizeWestEast) + : new Cursor(StandardCursorType.SizeNorthSouth) + : Cursor.Default; + + handle.PreviousButtonControlsNext = false; + handle.NextButtonControlsPrevious = false; + handle.PreviousIconTemplate = CollapsePreviousIcon; + handle.NextIconTemplate = CollapseNextIcon; + + if (!TryGetBoundaryForHandle(index, out var leftVisible, out var rightVisible)) + { + handle.IsPreviousCollapsible = false; + handle.IsNextCollapsible = false; + handle.ShowPreviousButton = false; + handle.ShowNextButton = false; + handle.IsPreviousCollapsed = false; + handle.IsNextCollapsed = false; + handle.UpdateCollapseButtons(); + return; + } + + var hasLeftButton = TryGetExpandLeftState(leftVisible, rightVisible, out var leftShowMode); + var hasRightButton = TryGetExpandRightState(leftVisible, rightVisible, out var rightShowMode); + var showBothOnHover = leftVisible >= 0 && rightVisible >= 0 && rightVisible == leftVisible + 1; + + handle.IsPreviousCollapsible = hasLeftButton; + handle.IsNextCollapsible = hasRightButton; + handle.ShowPreviousButton = hasLeftButton; + handle.ShowNextButton = hasRightButton; + handle.ShowBothButtonsOnHover = showBothOnHover; + handle.PreviousShowMode = leftShowMode; + handle.NextShowMode = rightShowMode; + handle.IsPreviousCollapsed = false; + handle.IsNextCollapsed = false; + + handle.UpdateCollapseButtons(); + } + + private bool CanDragBetween(Control previous, Control next, double availableLength) + { + if (!Splitter.GetIsResizable(previous) || !Splitter.GetIsResizable(next)) + { + return false; + } + + if (Splitter.GetIsCollapsed(previous) && !CanExpandByDrag(previous, availableLength)) + { + return false; + } + + if (Splitter.GetIsCollapsed(next) && !CanExpandByDrag(next, availableLength)) + { + return false; + } + + return true; + } + + private bool CanExpandByDrag(Control panel, double availableLength) + { + var min = GetPanelMin(panel, availableLength); + return min <= 0.0; + } + + private void HandleCollapsePreviousRequested(object? sender, EventArgs e) + { + if (sender is SplitterHandle handle) + { + ExpandLeftAtHandle(handle); + } + } + + private void HandleCollapseNextRequested(object? sender, EventArgs e) + { + if (sender is SplitterHandle handle) + { + ExpandRightAtHandle(handle); + } + } + + private void ExpandLeftAtHandle(SplitterHandle handle) + { + if (!TryGetBoundaryForHandle(handle.HandleIndex, out var leftVisible, out var rightVisible)) + { + return; + } + + if (!IsPrimaryHandleForBoundary(handle.HandleIndex, leftVisible, rightVisible)) + { + return; + } + + if (leftVisible < 0) + { + return; + } + + var availableLength = GetAvailablePanelLength(GetLayoutLength(_lastLayoutSize)); + var restoreIndex = FindRestoreCandidateLeft(leftVisible, rightVisible); + if (restoreIndex.HasValue) + { + var ownerIndex = ResolveOwnerIndex(restoreIndex.Value); + if (ownerIndex >= 0 && + Splitter.GetIsResizable(_panels[restoreIndex.Value]) && + Splitter.GetIsResizable(_panels[ownerIndex])) + { + ApplyCollapseForIndices(restoreIndex.Value, ownerIndex, availableLength); + } + return; + } + + if (rightVisible < 0) + { + return; + } + + if (!Splitter.GetIsResizable(_panels[leftVisible]) || !Splitter.GetIsResizable(_panels[rightVisible])) + { + return; + } + + ApplyCollapseForIndices(leftVisible, rightVisible, availableLength); + } + + private void ExpandRightAtHandle(SplitterHandle handle) + { + if (!TryGetBoundaryForHandle(handle.HandleIndex, out var leftVisible, out var rightVisible)) + { + return; + } + + if (!IsPrimaryHandleForBoundary(handle.HandleIndex, leftVisible, rightVisible)) + { + return; + } + + if (rightVisible < 0) + { + return; + } + + var availableLength = GetAvailablePanelLength(GetLayoutLength(_lastLayoutSize)); + var restoreIndex = FindRestoreCandidateRight(leftVisible, rightVisible); + if (restoreIndex.HasValue) + { + var ownerIndex = ResolveOwnerIndex(restoreIndex.Value); + if (ownerIndex >= 0 && + Splitter.GetIsResizable(_panels[restoreIndex.Value]) && + Splitter.GetIsResizable(_panels[ownerIndex])) + { + ApplyCollapseForIndices(restoreIndex.Value, ownerIndex, availableLength); + } + return; + } + + if (leftVisible < 0) + { + return; + } + + if (!Splitter.GetIsResizable(_panels[leftVisible]) || !Splitter.GetIsResizable(_panels[rightVisible])) + { + return; + } + + ApplyCollapseForIndices(rightVisible, leftVisible, availableLength); + } + + private bool TryGetBoundaryForHandle(int handleIndex, out int leftVisible, out int rightVisible) + { + leftVisible = FindLeftVisibleIndex(handleIndex); + rightVisible = FindRightVisibleIndex(handleIndex); + return leftVisible != -1 || rightVisible != -1; + } + + private int FindLeftVisibleIndex(int handleIndex) + { + for (var i = handleIndex; i >= 0; i--) + { + if (!Splitter.GetIsCollapsed(_panels[i])) + { + return i; + } + } + + return -1; + } + + private int FindRightVisibleIndex(int handleIndex) + { + for (var i = handleIndex + 1; i < _panels.Count; i++) + { + if (!Splitter.GetIsCollapsed(_panels[i])) + { + return i; + } + } + + return -1; + } + + private bool IsPrimaryHandleForBoundary(int handleIndex, int leftVisible, int rightVisible) + { + if (rightVisible >= 0) + { + return handleIndex == rightVisible - 1; + } + + if (leftVisible >= 0) + { + return handleIndex == leftVisible; + } + + return false; + } + + private bool TryGetExpandLeftState( + int leftVisible, + int rightVisible, + out SplitterCollapsibleIconDisplayMode showMode) + { + showMode = SplitterCollapsibleIconDisplayMode.Hover; + if (leftVisible < 0) + { + return false; + } + + var restoreIndex = FindRestoreCandidateLeft(leftVisible, rightVisible); + if (restoreIndex.HasValue) + { + var ownerIndex = ResolveOwnerIndex(restoreIndex.Value); + if (ownerIndex < 0) + { + return false; + } + + if (!Splitter.GetIsResizable(_panels[restoreIndex.Value]) || !Splitter.GetIsResizable(_panels[ownerIndex])) + { + return false; + } + } + else if (rightVisible < 0) + { + return false; + } + + if (!restoreIndex.HasValue && + (!Splitter.GetIsResizable(_panels[leftVisible]) || !Splitter.GetIsResizable(_panels[rightVisible]))) + { + return false; + } + + var targetIndex = restoreIndex ?? leftVisible; + if (targetIndex < 0) + { + return false; + } + + var targetPanel = _panels[targetIndex]; + if (!IsPanelCollapsible(targetPanel) || !Splitter.GetIsResizable(targetPanel)) + { + return false; + } + + showMode = Splitter.GetCollapsible(targetPanel)?.ShowCollapsibleIcon ?? SplitterCollapsibleIconDisplayMode.Hover; + return true; + } + + private static bool IsPanelCollapsible(Control panel) + { + return Splitter.GetCollapsible(panel)?.IsEnabled == true; + } + + private bool TryGetExpandRightState( + int leftVisible, + int rightVisible, + out SplitterCollapsibleIconDisplayMode showMode) + { + showMode = SplitterCollapsibleIconDisplayMode.Hover; + if (rightVisible < 0) + { + return false; + } + + var restoreIndex = FindRestoreCandidateRight(leftVisible, rightVisible); + if (restoreIndex.HasValue) + { + var ownerIndex = ResolveOwnerIndex(restoreIndex.Value); + if (ownerIndex < 0) + { + return false; + } + + if (!Splitter.GetIsResizable(_panels[restoreIndex.Value]) || !Splitter.GetIsResizable(_panels[ownerIndex])) + { + return false; + } + } + else if (leftVisible < 0) + { + return false; + } + + if (!restoreIndex.HasValue && + (!Splitter.GetIsResizable(_panels[leftVisible]) || !Splitter.GetIsResizable(_panels[rightVisible]))) + { + return false; + } + + var targetIndex = restoreIndex ?? rightVisible; + if (targetIndex < 0) + { + return false; + } + + var targetPanel = _panels[targetIndex]; + if (!IsPanelCollapsible(targetPanel) || !Splitter.GetIsResizable(targetPanel)) + { + return false; + } + + showMode = Splitter.GetCollapsible(targetPanel)?.ShowCollapsibleIcon ?? SplitterCollapsibleIconDisplayMode.Hover; + return true; + } + + private int? FindRestoreCandidateLeft(int leftVisible, int rightVisible) + { + if (leftVisible < 0) + { + return null; + } + + if (rightVisible >= 0) + { + var start = rightVisible - 1; + var end = leftVisible; + + var owner = leftVisible; + for (var i = start; i > end; i--) + { + var panel = _panels[i]; + if (!Splitter.GetIsCollapsed(panel) || !IsPanelCollapsible(panel)) + { + continue; + } + + if (_partContexts.TryGetValue(panel, out var context)) + { + if (context.LastCollapsedIntoIndex.HasValue && context.LastCollapsedIntoIndex.Value == owner) + { + return i; + } + } + } + } + else + { + var start = _panels.Count - 1; + var end = leftVisible + 1; + + var owner = leftVisible; + for (var i = start; i >= end; i--) + { + var panel = _panels[i]; + if (!Splitter.GetIsCollapsed(panel) || !IsPanelCollapsible(panel)) + { + continue; + } + + if (_partContexts.TryGetValue(panel, out var context)) + { + if (context.LastCollapsedIntoIndex.HasValue && context.LastCollapsedIntoIndex.Value == owner) + { + return i; + } + } + } + } + + return null; + } + + private int? FindRestoreCandidateRight(int leftVisible, int rightVisible) + { + if (rightVisible < 0) + { + return null; + } + + if (leftVisible >= 0) + { + var start = leftVisible + 1; + var end = rightVisible; + + var owner = rightVisible; + for (var i = start; i < end; i++) + { + var panel = _panels[i]; + if (!Splitter.GetIsCollapsed(panel) || !IsPanelCollapsible(panel)) + { + continue; + } + + if (_partContexts.TryGetValue(panel, out var context)) + { + if (context.LastCollapsedIntoIndex.HasValue && context.LastCollapsedIntoIndex.Value == owner) + { + return i; + } + } + } + } + else + { + var start = 0; + var end = rightVisible; + + var owner = rightVisible; + for (var i = start; i < end; i++) + { + var panel = _panels[i]; + if (!Splitter.GetIsCollapsed(panel) || !IsPanelCollapsible(panel)) + { + continue; + } + + if (_partContexts.TryGetValue(panel, out var context)) + { + if (context.LastCollapsedIntoIndex.HasValue && context.LastCollapsedIntoIndex.Value == owner) + { + return i; + } + } + } + } + + return null; + } + + private int ResolveOwnerIndex(int panelIndex) + { + if (panelIndex < 0 || panelIndex >= _panels.Count) + { + return -1; + } + + var index = panelIndex; + var guard = 0; + while (index >= 0 && index < _panels.Count && Splitter.GetIsCollapsed(_panels[index])) + { + if (_partContexts.TryGetValue(_panels[index], out var context)) + { + var next = context.LastCollapsedIntoIndex; + if (!next.HasValue || next.Value == index) + { + return -1; + } + + index = next.Value; + guard++; + if (guard > _panels.Count) + { + return -1; + } + } + } + + return index; + } + + private void ApplyCollapseForIndices(int collapseIndex, int partnerIndex, double availableLength) + { + if (collapseIndex < 0 || collapseIndex >= _panels.Count || + partnerIndex < 0 || partnerIndex >= _panels.Count || + collapseIndex == partnerIndex) + { + return; + } + + if (collapseIndex < partnerIndex) + { + var previous = _panels[collapseIndex]; + var next = _panels[partnerIndex]; + + if (_partContexts.TryGetValue(previous, out var context)) + { + if (!Splitter.GetIsCollapsed(previous) && context.EffectiveSize > 0) + { + context.LastNonCollapsedSize = context.EffectiveSize; + } + + var targetPrevious = Splitter.GetIsCollapsed(previous) + ? GetRestoreSize(previous, availableLength) + : 0; + var targetDelta = targetPrevious - context.EffectiveSize; + ApplyCollapseDelta(previous, next, targetDelta, availableLength, collapseIndex, partnerIndex); + } + } + else + { + var previous = _panels[partnerIndex]; + var next = _panels[collapseIndex]; + + if (_partContexts.TryGetValue(next, out var context)) + { + if (!Splitter.GetIsCollapsed(next) && context.EffectiveSize > 0) + { + context.LastNonCollapsedSize = context.EffectiveSize; + } + + var targetNext = Splitter.GetIsCollapsed(next) + ? GetRestoreSize(next, availableLength) + : 0; + var targetDelta = context.EffectiveSize - targetNext; + ApplyCollapseDelta(previous, next, targetDelta, availableLength, partnerIndex, collapseIndex); + } + } + } + + private double GetRestoreSize(Control panel, double availableLength) + { + double? lastNonCollapsedSize = null; + if (_partContexts.TryGetValue(panel, out var context)) + { + lastNonCollapsedSize = context.LastNonCollapsedSize; + } + var restoreSize = lastNonCollapsedSize + ?? ResolvePanelInitialSize(panel, availableLength) + ?? GetFallbackPanelSize(panel, availableLength); + return ClampPanelSize(panel, restoreSize, availableLength); + } + + private void ApplyCollapseDelta( + Control previous, + Control next, + double targetDelta, + double availableLength, + int previousIndex, + int nextIndex) + { + var previousContext = _partContexts[previous]; + var nextContext = _partContexts[next]; + var prevSize = previousContext.EffectiveSize; + var nextSize = nextContext.EffectiveSize; + var prevMin = GetPanelMin(previous, availableLength); + var prevMax = GetPanelMax(previous, availableLength); + if (prevMax < prevMin) + { + prevMax = prevMin; + } + + var nextMin = GetPanelMin(next, availableLength); + var nextMax = GetPanelMax(next, availableLength); + if (nextMax < nextMin) + { + nextMax = nextMin; + } + + var minDelta = Math.Max(prevMin - prevSize, nextSize - nextMax); + var maxDelta = Math.Min(prevMax - prevSize, nextSize - nextMin); + if (minDelta > maxDelta) + { + return; + } + + var clampedDelta = Math.Clamp(targetDelta, minDelta, maxDelta); + var newPrev = prevSize + clampedDelta; + var newNext = nextSize - clampedDelta; + + if (prevSize > 0 && newPrev <= 0) + { + previousContext.LastNonCollapsedSize = prevSize; + } + + if (nextSize > 0 && newNext <= 0) + { + nextContext.LastNonCollapsedSize = nextSize; + } + + if (newPrev > 0) + { + previousContext.LastCollapsedIntoIndex = null; + } + else if (prevSize > 0 && newPrev <= 0) + { + previousContext.LastCollapsedIntoIndex = nextIndex; + } + + if (newNext > 0) + { + nextContext.LastCollapsedIntoIndex = null; + } + else if (nextSize > 0 && newNext <= 0) + { + nextContext.LastCollapsedIntoIndex = previousIndex; + } + + SetPanelSize(previous, newPrev, availableLength, updateLastSize: newPrev > 0); + SetPanelSize(next, newNext, availableLength, updateLastSize: newNext > 0); + + UpdatePanelCollapsedState(previous, newPrev); + UpdatePanelCollapsedState(next, newNext); + + InvalidateArrange(); + } + + private void SetPanelCollapsed(Control panel, bool collapsed) + { + _suppressPanelChange = true; + try + { + ApplyPanelCollapse(panel, collapsed); + Splitter.SetIsCollapsed(panel, collapsed); + } + finally + { + _suppressPanelChange = false; + } + } + + private void ApplyPanelCollapse(Control panel, bool collapsed) + { + var availableLength = GetAvailablePanelLength(GetLayoutLength(_lastLayoutSize)); + if (collapsed) + { + if (_partContexts.TryGetValue(panel, out var context)) + { + if (context.EffectiveSize > 0) + { + context.LastNonCollapsedSize = context.EffectiveSize; + } + } + + SetPanelSize(panel, 0, availableLength, updateLastSize: false); + } + else + { + double? lastNonCollapsedSize = null; + if (_partContexts.TryGetValue(panel, out var context)) + { + lastNonCollapsedSize = context.LastNonCollapsedSize; + } + var restoreSize = lastNonCollapsedSize + ?? ResolvePanelInitialSize(panel, availableLength) + ?? GetFallbackPanelSize(panel, availableLength); + restoreSize = ClampPanelSize(panel, restoreSize, availableLength); + SetPanelSize(panel, restoreSize, availableLength, updateLastSize: true); + } + + UpdateHandlesForPanel(panel); + InvalidateMeasure(); + } + + protected override Size MeasureOverride(Size availableSize) + { + if (_panels.Count == 0) + { + return default; + } + + var isVertical = Orientation == Orientation.Vertical; + var availableLength = GetLayoutLength(availableSize); + var availableCross = GetCrossLength(availableSize); + + if (double.IsInfinity(availableLength)) + { + return MeasureWithInfiniteLength(availableSize); + } + + var sizes = ComputePanelSizes(availableLength); + for (var i = 0; i < _panels.Count; i++) + { + var panel = _panels[i]; + var panelSize = sizes[i]; + var panelAvailable = isVertical + ? new Size(panelSize, availableCross) + : new Size(availableCross, panelSize); + panel.Measure(panelAvailable); + } + + foreach (var handle in _handles) + { + var handleAvailable = isVertical + ? new Size(HandleSize, availableCross) + : new Size(availableCross, HandleSize); + handle.Measure(handleAvailable); + } + + return availableSize; + } + + private Size MeasureWithInfiniteLength(Size availableSize) + { + var isVertical = Orientation == Orientation.Vertical; + var availableCross = GetCrossLength(availableSize); + var length = 0d; + var cross = 0d; + + foreach (var panel in _panels) + { + var panelAvailable = isVertical + ? new Size(double.PositiveInfinity, availableCross) + : new Size(availableCross, double.PositiveInfinity); + panel.Measure(panelAvailable); + var desired = panel.DesiredSize; + length += isVertical ? desired.Width : desired.Height; + cross = Math.Max(cross, isVertical ? desired.Height : desired.Width); + } + + length += Math.Max(0, GetEffectiveHandleCount()) * GetHandleSpacing(); + + return isVertical + ? new Size(length, double.IsInfinity(availableCross) ? cross : availableCross) + : new Size(double.IsInfinity(availableCross) ? cross : availableCross, length); + } + + protected override Size ArrangeOverride(Size finalSize) + { + _lastLayoutSize = finalSize; + + if (_panels.Count == 0) + { + return finalSize; + } + + var isVertical = Orientation == Orientation.Vertical; + var availableLength = GetLayoutLength(finalSize); + var sizes = ComputePanelSizes(availableLength); + var handleSpacing = GetHandleSpacing(); + var handleOffset = (HandleSize - handleSpacing) / 2d; + + var offset = 0d; + for (var i = 0; i < _panels.Count; i++) + { + var panel = _panels[i]; + var panelSize = sizes[i]; + + if (_partContexts.TryGetValue(panel, out var context)) + { + context.EffectiveSize = panelSize; + + var rect = isVertical + ? new Rect(offset, 0, panelSize, finalSize.Height) + : new Rect(0, offset, finalSize.Width, panelSize); + panel.Arrange(rect); + offset += panelSize; + + if (i < _handles.Count) + { + var handlePosition = offset - handleOffset; + var handleRect = isVertical + ? new Rect(handlePosition, 0, HandleSize, finalSize.Height) + : new Rect(0, handlePosition, finalSize.Width, HandleSize); + _handles[i].Arrange(handleRect); + _handles[i].IsVisible = IsHandleVisibleForLayout(i); + offset += GetHandleSpacingForIndex(i, handleSpacing); + } + } + } + + UpdateHandleLayouts(); + return finalSize; + } + + private void HandleDragStarted(object? sender, VectorEventArgs e) + { + if (sender is not SplitterHandle handle) + { + return; + } + + if (!TryCreateBoundaryDragContext(handle, e.Vector, out var context, out var index)) + { + index = ResolveHandleIndexForDrag(handle, e.Vector); + context = CreateDragContext(handle, index, index, index + 1); + if (context == null) + { + var fallbackIndex = FindFallbackDragIndex(index); + if (fallbackIndex.HasValue) + { + index = fallbackIndex.Value; + context = CreateDragContext(handle, index, index, index + 1); + } + } + } + if (context == null) + { + return; + } + + _dragContext = context; + + handle.SetDragging(true); + OwnerSplitter?.RaiseResizeStarted(index, BuildSizesWithDelta(context.StartPreviousSize, context.StartNextSize, 0)); + } + + private bool TryCreateBoundaryDragContext( + SplitterHandle handle, + Vector startVector, + out DragContext? context, + out int handleIndex) + { + context = null; + handleIndex = handle.HandleIndex; + + if (!TryGetBoundaryForHandle(handle.HandleIndex, out var leftVisible, out var rightVisible)) + { + return false; + } + + if (leftVisible < 0 || rightVisible < 0 || rightVisible == leftVisible + 1) + { + return false; + } + + var preferLeft = !IsPointerOnNextSide(handle, startVector); + var leftCandidate = FindRestoreCandidateLeft(leftVisible, rightVisible); + var rightCandidate = FindRestoreCandidateRight(leftVisible, rightVisible); + + if (!leftCandidate.HasValue && !rightCandidate.HasValue) + { + return false; + } + + var candidates = preferLeft + ? new[] { leftCandidate, rightCandidate } + : new[] { rightCandidate, leftCandidate }; + + foreach (var candidate in candidates) + { + if (!candidate.HasValue) + { + continue; + } + + var ownerIndex = ResolveOwnerIndex(candidate.Value); + if (ownerIndex < 0) + { + continue; + } + + var previousIndex = Math.Min(ownerIndex, candidate.Value); + var nextIndex = Math.Max(ownerIndex, candidate.Value); + var dragContext = CreateDragContext(handle, handle.HandleIndex, previousIndex, nextIndex, false); + if (dragContext == null) + { + continue; + } + + context = dragContext; + handleIndex = previousIndex; + return true; + } + + return false; + } + + private bool IsPointerOnNextSide(SplitterHandle handle, Vector startVector) + { + var root = handle.GetVisualRoot() as Visual ?? handle; + var origin = handle.TranslatePoint(new Point(0, 0), root); + if (!origin.HasValue) + { + return true; + } + + var local = new Point(startVector.X - origin.Value.X, startVector.Y - origin.Value.Y); + var compare = Orientation == Orientation.Vertical ? local.X : local.Y; + var center = Orientation == Orientation.Vertical ? handle.Bounds.Width * 0.5 : handle.Bounds.Height * 0.5; + return compare >= center; + } + + private int? FindFallbackDragIndex(int handleIndex) + { + if (handleIndex < 0 || handleIndex + 1 >= _panels.Count) + { + return null; + } + + var availableLength = GetAvailablePanelLength(GetLayoutLength(_lastLayoutSize)); + + if (Splitter.GetIsCollapsed(_panels[handleIndex]) && handleIndex - 1 >= 0) + { + if (CanDragBetween(_panels[handleIndex - 1], _panels[handleIndex], availableLength)) + { + return handleIndex - 1; + } + } + + if (handleIndex + 2 < _panels.Count && Splitter.GetIsCollapsed(_panels[handleIndex + 1])) + { + if (CanDragBetween(_panels[handleIndex + 1], _panels[handleIndex + 2], availableLength)) + { + return handleIndex + 1; + } + } + + return null; + } + + private void HandleDragDelta(object? sender, VectorEventArgs e) + { + if (_dragContext == null) + { + return; + } + + var handle = _dragContext.Handle; + if (handle != sender) + { + return; + } + + var delta = Orientation == Orientation.Vertical ? e.Vector.X : e.Vector.Y; + if (TrySwitchDragContext(handle, delta)) + { + delta = 0; + } + var clampedDelta = Math.Clamp(delta, _dragContext.MinDelta, _dragContext.MaxDelta); + var adjust = delta - clampedDelta; + if (Math.Abs(adjust) > 0.001) + { + handle.AdjustDrag(Orientation == Orientation.Vertical + ? new Vector(adjust, 0) + : new Vector(0, adjust)); + } + + var previewSizes = BuildSizesWithDelta(_dragContext.StartPreviousSize, _dragContext.StartNextSize, clampedDelta); + OwnerSplitter?.RaiseResizeDelta(_dragContext.HandleIndex, previewSizes); + + if (IsLazy) + { + _dragContext.CurrentDelta = clampedDelta; + ApplyHandlePreview(handle, clampedDelta); + return; + } + + ApplyResize(_dragContext, clampedDelta); + } + + private void HandleDragCompleted(object? sender, VectorEventArgs e) + { + if (_dragContext == null) + { + return; + } + + var handle = _dragContext.Handle; + if (handle != sender) + { + return; + } + + var delta = IsLazy ? _dragContext.CurrentDelta : + Math.Clamp(Orientation == Orientation.Vertical ? e.Vector.X : e.Vector.Y, + _dragContext.MinDelta, _dragContext.MaxDelta); + + if (IsLazy) + { + ClearHandlePreview(handle); + ApplyResize(_dragContext, delta); + } + + handle.SetDragging(false); + FinalizeResizeSizes(_dragContext); + var completedSizes = BuildSizesWithDelta(_dragContext.StartPreviousSize, _dragContext.StartNextSize, delta); + OwnerSplitter?.RaiseResizeCompleted(_dragContext.HandleIndex, completedSizes); + _dragContext = null; + } + + private void ApplyResize(DragContext context, double delta) + { + var previousSize = context.StartPreviousSize + delta; + var nextSize = context.StartNextSize - delta; + previousSize = ClampPanelSize(context.PreviousPanel, previousSize, context.AvailableLength); + nextSize = ClampPanelSize(context.NextPanel, nextSize, context.AvailableLength); + + UpdateCollapseRecordForResize(context, previousSize, nextSize); + + SetPanelSize(context.PreviousPanel, previousSize, context.AvailableLength, updateLastSize: false); + SetPanelSize(context.NextPanel, nextSize, context.AvailableLength, updateLastSize: false); + + UpdatePanelCollapsedState(context.PreviousPanel, previousSize); + UpdatePanelCollapsedState(context.NextPanel, nextSize); + + InvalidateArrange(); + } + + private void ApplyHandlePreview(SplitterHandle handle, double delta) + { + handle.RenderTransform = Orientation == Orientation.Vertical + ? new TranslateTransform(delta, 0) + : new TranslateTransform(0, delta); + } + + private void ClearHandlePreview(SplitterHandle handle) + { + handle.RenderTransform = null; + } + + private void UpdatePanelCollapsedState(Control panel, double size) + { + var collapsed = size <= 0.0; + if (Splitter.GetIsCollapsed(panel) == collapsed) + { + return; + } + + _suppressPanelChange = true; + try + { + Splitter.SetIsCollapsed(panel, collapsed); + } + finally + { + _suppressPanelChange = false; + } + + UpdateHandlesForPanel(panel); + } + + private void UpdateCollapseRecordForResize(DragContext context, double newPrev, double newNext) + { + var previous = context.PreviousPanel; + var next = context.NextPanel; + + var previousContext = _partContexts[previous]; + var nextContext = _partContexts[next]; + + var prevSize = previousContext.EffectiveSize; + var nextSize = nextContext.EffectiveSize; + + if (prevSize > 0 && newPrev <= 0) + { + previousContext.LastNonCollapsedSize = context.StartPreviousSize; + previousContext.LastCollapsedIntoIndex = context.NextIndex; + } + else if (newPrev > 0) + { + previousContext.LastCollapsedIntoIndex = null; + } + + if (nextSize > 0 && newNext <= 0) + { + nextContext.LastNonCollapsedSize = context.StartNextSize; + nextContext.LastCollapsedIntoIndex = context.PreviousIndex; + } + else if (newNext > 0) + { + nextContext.LastCollapsedIntoIndex = null; + } + } + + private void FinalizeResizeSizes(DragContext context) + { + var previous = context.PreviousPanel; + var next = context.NextPanel; + + var previousContext = _partContexts[previous]; + var nextContext = _partContexts[next]; + + if (previousContext.EffectiveSize > 0) + { + previousContext.LastNonCollapsedSize = previousContext.EffectiveSize; + } + + if (nextContext.EffectiveSize > 0) + { + nextContext.LastNonCollapsedSize = nextContext.EffectiveSize; + } + } + + private void SetPanelSize(Control panel, double size, double availableLength, bool updateLastSize) + { + if (double.IsNaN(size) || double.IsInfinity(size)) + { + return; + } + + size = Math.Max(0, size); + + var partContext = _partContexts[panel]; + var unit = GetPartPreferredSizeUnit(panel); + Dimension dimension; + if (unit == DimensionUnitType.Percentage && availableLength > 0) + { + var percentValue = Math.Clamp(size / availableLength * 100.0, 0, 100); + dimension = new Dimension(percentValue, DimensionUnitType.Percentage); + } + else + { + dimension = new Dimension(size, DimensionUnitType.Pixel); + } + + Splitter.SetSize(panel, dimension); + partContext.EffectiveSize = size; + if (updateLastSize && size > 0) + { + partContext.LastNonCollapsedSize = size; + } + } + + private double? ResolvePanelInitialSize(Control panel, double availableLength) + { + var size = Splitter.GetSize(panel); + if (size.HasValue) + { + return size.Value.Resolve(availableLength); + } + var defaultSize = Splitter.GetDefaultSize(panel); + if (defaultSize.HasValue) + { + return defaultSize.Value.Resolve(availableLength); + } + + return null; + } + + private double GetFallbackPanelSize(Control panel, double availableLength) + { + if (_panels.Count == 0) + { + return 0; + } + + var remaining = availableLength; + foreach (var other in _panels) + { + if (other == panel) + { + continue; + } + + var size = ResolvePanelInitialSize(other, availableLength); + if (size.HasValue) + { + remaining -= size.Value; + } + } + + if (remaining < 0) + { + remaining = availableLength / _panels.Count; + } + + return remaining; + } + + private List ComputePanelSizes(double totalLength) + { + var handleSpace = Math.Max(0, GetEffectiveHandleCount()) * GetHandleSpacing(); + var availableLength = Math.Max(0, totalLength - handleSpace); + var sizes = new List(_panels.Count); + var flexibleIndices = new List(); + var minSizes = new double[_panels.Count]; + var maxSizes = new double[_panels.Count]; + + var sumExplicit = 0d; + for (var i = 0; i < _panels.Count; i++) + { + var panel = _panels[i]; + var partContext = _partContexts[panel]; + var min = GetPanelMin(panel, availableLength); + var max = GetPanelMax(panel, availableLength); + if (max < min) + { + max = min; + } + minSizes[i] = min; + maxSizes[i] = max; + + if (Splitter.GetIsCollapsed(panel)) + { + sizes.Add(0); + sumExplicit += 0; + continue; + } + + var initial = ResolvePanelInitialSize(panel, availableLength); + if (initial.HasValue) + { + var clamped = Math.Clamp(initial.Value, min, max); + sizes.Add(clamped); + sumExplicit += clamped; + } + else if (partContext.EffectiveSize > 0) + { + var clamped = Math.Clamp(partContext.EffectiveSize, min, max); + sizes.Add(clamped); + sumExplicit += clamped; + } + else + { + sizes.Add(double.NaN); + flexibleIndices.Add(i); + } + } + + var remaining = availableLength - sumExplicit; + if (flexibleIndices.Count > 0) + { + var each = remaining / flexibleIndices.Count; + foreach (var index in flexibleIndices) + { + sizes[index] = each; + } + } + else if (_panels.Count > 0 && Math.Abs(remaining) > 0.001) + { + var index = _panels.Count - 1; + sizes[index] = Math.Max(0, sizes[index] + remaining); + } + + for (var i = 0; i < sizes.Count; i++) + { + if (Splitter.GetIsCollapsed(_panels[i])) + { + sizes[i] = 0; + continue; + } + + sizes[i] = Math.Clamp(sizes[i], minSizes[i], maxSizes[i]); + } + + NormalizeSizes(sizes, minSizes, maxSizes, availableLength); + return sizes; + } + + private void NormalizeSizes(List sizes, double[] minSizes, double[] maxSizes, double availableLength) + { + var delta = availableLength - sizes.Sum(); + var guard = 0; + while (Math.Abs(delta) > 0.5 && guard < 100) + { + var indices = new List(); + for (var i = 0; i < sizes.Count; i++) + { + if (Splitter.GetIsCollapsed(_panels[i])) + { + continue; + } + + if (delta > 0 && sizes[i] < maxSizes[i]) + { + indices.Add(i); + } + else if (delta < 0 && sizes[i] > minSizes[i]) + { + indices.Add(i); + } + } + + if (indices.Count == 0) + { + break; + } + + var per = delta / indices.Count; + var adjusted = 0d; + + foreach (var index in indices) + { + var target = sizes[index] + per; + var newSize = delta > 0 + ? Math.Min(target, maxSizes[index]) + : Math.Max(target, minSizes[index]); + var diff = newSize - sizes[index]; + sizes[index] = newSize; + adjusted += diff; + } + + delta -= adjusted; + guard++; + } + } + + private double ClampPanelSize(Control panel, double size, double availableLength) + { + var min = GetPanelMin(panel, availableLength); + var max = GetPanelMax(panel, availableLength); + if (max < min) + { + max = min; + } + return Math.Clamp(size, min, max); + } + + private double GetPanelMin(Control panel, double availableLength) + { + var minSize = Splitter.GetMinSize(panel); + if (minSize.HasValue) + { + return Math.Max(0, minSize.Value.Resolve(availableLength)); + } + + return 0; + } + + private double GetPanelMax(Control panel, double availableLength) + { + var maxSize = Splitter.GetMaxSize(panel); + if (maxSize.HasValue) + { + var max = maxSize.Value.Resolve(availableLength); + if (max < 0) + { + return 0; + } + return max; + } + + return double.PositiveInfinity; + } + + private double GetLayoutLength(Size size) + { + return Orientation == Orientation.Vertical ? size.Width : size.Height; + } + + private double GetCrossLength(Size size) + { + return Orientation == Orientation.Vertical ? size.Height : size.Width; + } + + private double GetAvailablePanelLength(double totalLength) + { + if (_panels.Count <= 1) + { + return Math.Max(0, totalLength); + } + + var handleSpace = Math.Max(0, GetEffectiveHandleCount()) * GetHandleSpacing(); + return Math.Max(0, totalLength - handleSpace); + } + + private DragContext? CreateDragContext( + SplitterHandle handle, + int handleIndex, + int previousIndex, + int nextIndex, + bool? allowSwitchOverride = null) + { + if (previousIndex < 0 || nextIndex >= _panels.Count || previousIndex == nextIndex) + { + return null; + } + + var availableLength = GetAvailablePanelLength(GetLayoutLength(_lastLayoutSize)); + var previous = _panels[previousIndex]; + var next = _panels[nextIndex]; + var previousContext = _partContexts[previous]; + var nextContext = _partContexts[next]; + + if (!CanDragBetween(previous, next, availableLength)) + { + return null; + } + + var previousSize = previousContext.EffectiveSize; + var nextSize = nextContext.EffectiveSize; + var prevMin = GetPanelMin(previous, availableLength); + var prevMax = GetPanelMax(previous, availableLength); + var nextMin = GetPanelMin(next, availableLength); + var nextMax = GetPanelMax(next, availableLength); + if (prevMax < prevMin) + { + prevMax = prevMin; + } + if (nextMax < nextMin) + { + nextMax = nextMin; + } + + var minDelta = Math.Max(prevMin - previousSize, nextSize - nextMax); + var maxDelta = Math.Min(prevMax - previousSize, nextSize - nextMin); + if (minDelta > maxDelta) + { + return null; + } + + var allowSwitch = allowSwitchOverride ?? (Splitter.GetIsCollapsed(previous) || Splitter.GetIsCollapsed(next)); + return new DragContext + { + Handle = handle, + HandleIndex = handleIndex, + PreviousIndex = previousIndex, + NextIndex = nextIndex, + PreviousPanel = previous, + NextPanel = next, + StartPreviousSize = previousSize, + StartNextSize = nextSize, + MinDelta = minDelta, + MaxDelta = maxDelta, + AvailableLength = availableLength, + AllowSwitch = allowSwitch + }; + } + + private bool TrySwitchDragContext(SplitterHandle handle, double delta) + { + if (_dragContext == null || !_dragContext.AllowSwitch || Math.Abs(delta) < 0.001) + { + return false; + } + + var baseIndex = handle.HandleIndex; + var prevIndex = _dragContext.PreviousIndex; + var nextIndex = _dragContext.NextIndex; + + var newPrevIndex = -1; + var newNextIndex = -1; + + if (delta > 0 && + Splitter.GetIsCollapsed(_dragContext.NextPanel) && + nextIndex + 1 < _panels.Count && + _dragContext.MaxDelta <= 0) + { + newPrevIndex = nextIndex; + newNextIndex = nextIndex + 1; + } + else if (delta < 0 && + Splitter.GetIsCollapsed(_dragContext.PreviousPanel) && + prevIndex - 1 >= 0 && + _dragContext.MinDelta >= 0) + { + newPrevIndex = prevIndex - 1; + newNextIndex = prevIndex; + } + + if (newPrevIndex < 0 || newNextIndex < 0 || + (newPrevIndex == prevIndex && newNextIndex == nextIndex)) + { + return false; + } + + var context = CreateDragContext(handle, baseIndex, newPrevIndex, newNextIndex, false); + if (context == null) + { + return false; + } + + _dragContext = context; + handle.AdjustDrag(Orientation == Orientation.Vertical + ? new Vector(delta, 0) + : new Vector(0, delta)); + + return true; + } + + + private int ResolveHandleIndexForDrag(SplitterHandle handle, Vector startVector) + { + var index = handle.HandleIndex; + if (index < 0 || index + 1 >= _panels.Count) + { + return index; + } + + var root = handle.GetVisualRoot() as Visual ?? handle; + var origin = handle.TranslatePoint(new Point(0, 0), root); + if (!origin.HasValue) + { + return index; + } + + var local = new Point(startVector.X - origin.Value.X, startVector.Y - origin.Value.Y); + var compare = Orientation == Orientation.Vertical ? local.X : local.Y; + var center = Orientation == Orientation.Vertical ? handle.Bounds.Width * 0.5 : handle.Bounds.Height * 0.5; + var isNextSide = compare >= center; + + if (isNextSide && index + 2 < _panels.Count && Splitter.GetIsCollapsed(_panels[index + 1])) + { + return index + 1; + } + + if (!isNextSide && index - 1 >= 0 && Splitter.GetIsCollapsed(_panels[index])) + { + return index - 1; + } + + return index; + } + + private double GetHandleSpacing() + { + var value = TokenResourceUtils.FindTokenResource(this, SplitterTokenKey.SplitBarSize); + if (value is double size && !double.IsNaN(size) && !double.IsInfinity(size)) + { + return Math.Max(0, size); + } + + return Math.Max(0, HandleSize); + } + + private int GetEffectiveHandleCount() + { + if (_panels.Count <= 1) + { + return 0; + } + + var count = 0; + for (var i = 0; i < _panels.Count - 1; i++) + { + if (IsHandleVisibleForLayout(i)) + { + count++; + } + } + + return count; + } + + private double GetHandleSpacingForIndex(int handleIndex, double spacing) + { + if (handleIndex < 0 || handleIndex >= _handles.Count) + { + return 0; + } + + if (!IsHandleVisibleForLayout(handleIndex)) + { + return 0; + } + + return spacing; + } + + private bool IsHandleVisibleForLayout(int handleIndex) + { + if (handleIndex < 0 || handleIndex + 1 >= _panels.Count) + { + return false; + } + + if (!TryGetBoundaryForHandle(handleIndex, out var leftVisible, out var rightVisible)) + { + return false; + } + + return IsPrimaryHandleForBoundary(handleIndex, leftVisible, rightVisible); + } + + private IReadOnlyList BuildSizesWithDelta(double previousSize, double nextSize, double delta) + { + var sizes = GetCurrentSizes().ToArray(); + if (_dragContext == null) + { + return sizes; + } + + var previousIndex = _dragContext.PreviousIndex; + var nextIndex = _dragContext.NextIndex; + if (previousIndex < 0 || nextIndex < 0 || + previousIndex >= sizes.Length || nextIndex >= sizes.Length) + { + return sizes; + } + + sizes[previousIndex] = previousSize + delta; + sizes[nextIndex] = nextSize - delta; + return sizes; + } + + private IReadOnlyList GetCurrentSizes() + { + return _panels.Select(p => + { + var context = _partContexts[p]; + return context.EffectiveSize; + }).ToArray(); + } + + private DimensionUnitType GetPartPreferredSizeUnit(Control part) + { + var size = Splitter.GetSize(part); + if (size.HasValue) + { + return size.Value.UnitType; + } + var defaultSize = Splitter.GetDefaultSize(part); + if (defaultSize.HasValue) + { + return defaultSize.Value.UnitType; + } + return DimensionUnitType.Pixel; + } + + private class DragContext + { + public required SplitterHandle Handle { get; init; } + public required int HandleIndex { get; init; } + public required int PreviousIndex { get; init; } + public required int NextIndex { get; init; } + public required Control PreviousPanel { get; init; } + public required Control NextPanel { get; init; } + public required double StartPreviousSize { get; init; } + public required double StartNextSize { get; init; } + public required double MinDelta { get; init; } + public required double MaxDelta { get; init; } + public required double AvailableLength { get; init; } + public bool AllowSwitch { get; set; } + public double CurrentDelta { get; set; } + } + + private class SplitterPartContext + { + public double EffectiveSize { get; set; } + public double? LastNonCollapsedSize { get; set; } + public int? LastCollapsedIntoIndex { get; set; } + } +} \ No newline at end of file diff --git a/src/AtomUI.Desktop.Controls/Splitter/Themes/SplitterHandleTheme.axaml b/src/AtomUI.Desktop.Controls/Splitter/Themes/SplitterHandleTheme.axaml index 7e3767107..00039be60 100644 --- a/src/AtomUI.Desktop.Controls/Splitter/Themes/SplitterHandleTheme.axaml +++ b/src/AtomUI.Desktop.Controls/Splitter/Themes/SplitterHandleTheme.axaml @@ -57,7 +57,7 @@ - + diff --git a/src/AtomUI.Desktop.Controls/Splitter/Themes/SplitterPanelTheme.axaml b/src/AtomUI.Desktop.Controls/Splitter/Themes/SplitterPanelTheme.axaml deleted file mode 100644 index 7e9a2217a..000000000 --- a/src/AtomUI.Desktop.Controls/Splitter/Themes/SplitterPanelTheme.axaml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/AtomUI.Desktop.Controls/Splitter/Themes/SplitterTheme.axaml b/src/AtomUI.Desktop.Controls/Splitter/Themes/SplitterTheme.axaml index 01796df8d..02517af6d 100644 --- a/src/AtomUI.Desktop.Controls/Splitter/Themes/SplitterTheme.axaml +++ b/src/AtomUI.Desktop.Controls/Splitter/Themes/SplitterTheme.axaml @@ -4,6 +4,19 @@ xmlns:atom="https://atomui.net" x:ClassModifier="internal"> + + + + + + + + diff --git a/src/AtomUI.Desktop.Controls/Splitter/Themes/SplitterThemeConstants.cs b/src/AtomUI.Desktop.Controls/Splitter/Themes/SplitterThemeConstants.cs index d7118bb88..750636ec5 100644 --- a/src/AtomUI.Desktop.Controls/Splitter/Themes/SplitterThemeConstants.cs +++ b/src/AtomUI.Desktop.Controls/Splitter/Themes/SplitterThemeConstants.cs @@ -7,4 +7,5 @@ internal static class SplitterThemeConstants public const string CollapseIconsHostPart = "PART_CollapseIconsHost"; public const string CollapsePrevButtonPart = "PART_CollapsePrevButton"; public const string CollapseNextButtonPart = "PART_CollapseNextButton"; + public const string SplitterPanelPart = "PART_SplitterPanel"; } \ No newline at end of file diff --git a/src/AtomUI.Desktop.Controls/Splitter/Themes/SplitterThemes.axaml b/src/AtomUI.Desktop.Controls/Splitter/Themes/SplitterThemes.axaml index c3cc94ada..3232a5408 100644 --- a/src/AtomUI.Desktop.Controls/Splitter/Themes/SplitterThemes.axaml +++ b/src/AtomUI.Desktop.Controls/Splitter/Themes/SplitterThemes.axaml @@ -3,7 +3,6 @@ x:ClassModifier="internal"> - \ No newline at end of file