From a2c2198715e3d0c3662b45650c4c426fe57bc6d4 Mon Sep 17 00:00:00 2001 From: Misaki Date: Sat, 28 Mar 2026 14:24:30 +0900 Subject: [PATCH] fix(dock): address reviewer feedback on drag-and-drop highlighting --- .../Ghost.Editor/View/Controls/DockLayout.cs | 48 +++++++++----- src/Test/Ghost.UnitTest/DockLayoutTest.cs | 62 +++++++++++++++++++ src/Test/Ghost.UnitTest/Ghost.UnitTest.csproj | 1 + 3 files changed, 96 insertions(+), 15 deletions(-) create mode 100644 src/Test/Ghost.UnitTest/DockLayoutTest.cs diff --git a/src/Editor/Ghost.Editor/View/Controls/DockLayout.cs b/src/Editor/Ghost.Editor/View/Controls/DockLayout.cs index 70f6ce4..f821488 100644 --- a/src/Editor/Ghost.Editor/View/Controls/DockLayout.cs +++ b/src/Editor/Ghost.Editor/View/Controls/DockLayout.cs @@ -17,13 +17,15 @@ public sealed partial class DockLayout : Control { private const string PART_ROOT_GRID = "PART_RootGrid"; private const string PART_DROP_TARGET_OVERLAY = "PART_DropTargetOverlay"; + private const string DRAG_PROPERTY_DOCK_TAB = "DockTab"; private const double MIN_PANE_SIZE = 100; private const double SPLITTER_THICKNESS = 4; + private const double DROP_EDGE_THRESHOLD = 0.25; private FrameworkElement? _dropTargetOverlay; private readonly HashSet _subscribedNodes = new(); - internal enum DockPosition { Center, Top, Bottom, Left, Right, None } + public enum DockPosition { Center, Top, Bottom, Left, Right, None } public DockLayout() { @@ -32,6 +34,19 @@ public sealed partial class DockLayout : Control Unloaded += OnUnloaded; } + /// + /// Calculates the dock position based on the relative position within a target element. + /// Precedence: Left/Right win over Top/Bottom at corners. + /// + public static DockPosition CalculateDockPosition(double width, double height, double x, double y, double threshold) + { + if (x < width * threshold) return DockPosition.Left; + if (x > width * (1 - threshold)) return DockPosition.Right; + if (y < height * threshold) return DockPosition.Top; + if (y > height * (1 - threshold)) return DockPosition.Bottom; + return DockPosition.Center; + } + private void OnLoaded(object sender, RoutedEventArgs e) { if (Root != null) @@ -302,38 +317,40 @@ public sealed partial class DockLayout : Control { _draggedItem = args.Item; _sourceNode = sender.Tag as DockPanelNode; - args.Data.Properties.Add("DockTab", _draggedItem); // Identify our drag + args.Data.Properties.Add(DRAG_PROPERTY_DOCK_TAB, _draggedItem); // Identify our drag } private void TabView_DragOver(object sender, DragEventArgs e) { - if (e.DataView.Properties.ContainsKey("DockTab") && sender is FrameworkElement targetElement) + if (e.DataView.Properties.ContainsKey(DRAG_PROPERTY_DOCK_TAB) && sender is FrameworkElement targetElement) { e.AcceptedOperation = global::Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move; var position = e.GetPosition(targetElement); - double width = targetElement.ActualWidth; - double height = targetElement.ActualHeight; + var newPosition = CalculateDockPosition(targetElement.ActualWidth, targetElement.ActualHeight, position.X, position.Y, DROP_EDGE_THRESHOLD); - double edgeThreshold = 0.25; // 25% of edge triggers split - - if (position.X < width * edgeThreshold) _currentDropPosition = DockPosition.Left; - else if (position.X > width * (1 - edgeThreshold)) _currentDropPosition = DockPosition.Right; - else if (position.Y < height * edgeThreshold) _currentDropPosition = DockPosition.Top; - else if (position.Y > height * (1 - edgeThreshold)) _currentDropPosition = DockPosition.Bottom; - else _currentDropPosition = DockPosition.Center; - - UpdateDropOverlay(targetElement, _currentDropPosition); + if (newPosition != _currentDropPosition) + { + _currentDropPosition = newPosition; + UpdateDropOverlay(targetElement, _currentDropPosition); + } } } private void TabView_DragLeave(object sender, DragEventArgs e) + { + ClearDragState(); + } + + private void ClearDragState() { if (_dropTargetOverlay != null) { _dropTargetOverlay.Visibility = Visibility.Collapsed; - _currentDropPosition = DockPosition.None; } + _currentDropPosition = DockPosition.None; + _draggedItem = null; + _sourceNode = null; } private void UpdateDropOverlay(FrameworkElement targetElement, DockPosition position) @@ -375,6 +392,7 @@ public sealed partial class DockLayout : Control // Add a dummy TabView_Drop method so it compiles, we will implement it in Task 6 private void TabView_Drop(object sender, DragEventArgs e) { + ClearDragState(); } protected override void OnApplyTemplate() diff --git a/src/Test/Ghost.UnitTest/DockLayoutTest.cs b/src/Test/Ghost.UnitTest/DockLayoutTest.cs new file mode 100644 index 0000000..7af1161 --- /dev/null +++ b/src/Test/Ghost.UnitTest/DockLayoutTest.cs @@ -0,0 +1,62 @@ +using Ghost.Editor.View.Controls; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Ghost.UnitTest; + +[TestClass] +public class DockLayoutTest +{ + private const double THRESHOLD = 0.25; + + [TestMethod] + public void TestCalculateDockPosition_Center() + { + // 100x100, threshold 0.25 -> Center is [25, 75] + var pos = DockLayout.CalculateDockPosition(100, 100, 50, 50, THRESHOLD); + Assert.AreEqual(DockLayout.DockPosition.Center, pos); + } + + [TestMethod] + public void TestCalculateDockPosition_Left() + { + var pos = DockLayout.CalculateDockPosition(100, 100, 10, 50, THRESHOLD); + Assert.AreEqual(DockLayout.DockPosition.Left, pos); + } + + [TestMethod] + public void TestCalculateDockPosition_Right() + { + var pos = DockLayout.CalculateDockPosition(100, 100, 90, 50, THRESHOLD); + Assert.AreEqual(DockLayout.DockPosition.Right, pos); + } + + [TestMethod] + public void TestCalculateDockPosition_Top() + { + var pos = DockLayout.CalculateDockPosition(100, 100, 50, 10, THRESHOLD); + Assert.AreEqual(DockLayout.DockPosition.Top, pos); + } + + [TestMethod] + public void TestCalculateDockPosition_Bottom() + { + var pos = DockLayout.CalculateDockPosition(100, 100, 50, 90, THRESHOLD); + Assert.AreEqual(DockLayout.DockPosition.Bottom, pos); + } + + [TestMethod] + public void TestCalculateDockPosition_CornerPrecedence_LeftTop() + { + // (10, 10) is in both Left and Top zones. + // Current implementation: Left/Right win over Top/Bottom. + var pos = DockLayout.CalculateDockPosition(100, 100, 10, 10, THRESHOLD); + Assert.AreEqual(DockLayout.DockPosition.Left, pos); + } + + [TestMethod] + public void TestCalculateDockPosition_CornerPrecedence_RightBottom() + { + var pos = DockLayout.CalculateDockPosition(100, 100, 90, 90, THRESHOLD); + Assert.AreEqual(DockLayout.DockPosition.Right, pos); + } +} diff --git a/src/Test/Ghost.UnitTest/Ghost.UnitTest.csproj b/src/Test/Ghost.UnitTest/Ghost.UnitTest.csproj index 0b2d506..4cce861 100644 --- a/src/Test/Ghost.UnitTest/Ghost.UnitTest.csproj +++ b/src/Test/Ghost.UnitTest/Ghost.UnitTest.csproj @@ -16,6 +16,7 @@ +