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