From 9a1b8dcab0c44594988fcb2f8f8604552bb1c15c Mon Sep 17 00:00:00 2001 From: Misaki Date: Sat, 28 Mar 2026 17:57:54 +0900 Subject: [PATCH] fix(dock): migrate primary editor to DockLayout and add persistent sizing --- .../Internal/Docking/DockGroupNode.cs | 35 ++++++++ .../Ghost.Editor/View/Controls/DockLayout.cs | 11 ++- .../View/Windows/DockWindow.xaml.cs | 8 ++ .../View/Windows/EngineEditorWindow.xaml | 80 +------------------ .../View/Windows/EngineEditorWindow.xaml.cs | 52 +++++++----- 5 files changed, 83 insertions(+), 103 deletions(-) diff --git a/src/Editor/Ghost.Editor.Core/Controls/Internal/Docking/DockGroupNode.cs b/src/Editor/Ghost.Editor.Core/Controls/Internal/Docking/DockGroupNode.cs index 37de98f..947a631 100644 --- a/src/Editor/Ghost.Editor.Core/Controls/Internal/Docking/DockGroupNode.cs +++ b/src/Editor/Ghost.Editor.Core/Controls/Internal/Docking/DockGroupNode.cs @@ -1,5 +1,7 @@ using System.Collections.ObjectModel; +using System.Collections.Specialized; using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; namespace Ghost.Editor.Core.Controls.Internal.Docking; @@ -22,15 +24,48 @@ public partial class DockGroupNode : DockNode /// public ReadOnlyObservableCollection Children { get; } + /// + /// Gets the collection of sizes for the children. + /// + public ObservableCollection Sizes { get; } = new(); + /// /// Initializes a new instance of the class. /// public DockGroupNode() { Children = new ReadOnlyObservableCollection(_children); + _children.CollectionChanged += OnChildrenChanged; Orientation = Orientation.Horizontal; } + private void OnChildrenChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + // Maintain Sizes collection to match Children + if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems != null) + { + for (int i = 0; i < e.NewItems.Count; i++) + { + Sizes.Insert(e.NewStartingIndex + i, new Microsoft.UI.Xaml.GridLength(1, Microsoft.UI.Xaml.GridUnitType.Star)); + } + } + else if (e.Action == NotifyCollectionChangedAction.Remove && e.OldItems != null) + { + for (int i = 0; i < e.OldItems.Count; i++) + { + Sizes.RemoveAt(e.OldStartingIndex); + } + } + else if (e.Action == NotifyCollectionChangedAction.Reset) + { + Sizes.Clear(); + foreach (var _ in _children) + { + Sizes.Add(new Microsoft.UI.Xaml.GridLength(1, Microsoft.UI.Xaml.GridUnitType.Star)); + } + } + } + /// /// Adds a child node to this group, enforcing tree invariants. /// diff --git a/src/Editor/Ghost.Editor/View/Controls/DockLayout.cs b/src/Editor/Ghost.Editor/View/Controls/DockLayout.cs index 31391d7..7a41e5e 100644 --- a/src/Editor/Ghost.Editor/View/Controls/DockLayout.cs +++ b/src/Editor/Ghost.Editor/View/Controls/DockLayout.cs @@ -208,18 +208,20 @@ public sealed partial class DockLayout : Control if (isHorizontal) { + var width = (i < groupNode.Sizes.Count) ? groupNode.Sizes[i] : new GridLength(1, GridUnitType.Star); grid.ColumnDefinitions.Add(new ColumnDefinition { - Width = new GridLength(1, GridUnitType.Star), + Width = width, MinWidth = MIN_PANE_SIZE }); Grid.SetColumn((FrameworkElement)childUI, i * 2); } else { + var height = (i < groupNode.Sizes.Count) ? groupNode.Sizes[i] : new GridLength(1, GridUnitType.Star); grid.RowDefinitions.Add(new RowDefinition { - Height = new GridLength(1, GridUnitType.Star), + Height = height, MinHeight = MIN_PANE_SIZE }); Grid.SetRow((FrameworkElement)childUI, i * 2); @@ -297,6 +299,7 @@ public sealed partial class DockLayout : Control } private DockPosition _currentDropPosition = DockPosition.None; + private FrameworkElement? _lastTargetElement; private record DockDragPayload(object Item, DockPanelNode SourceNode); @@ -322,9 +325,10 @@ public sealed partial class DockLayout : Control var position = e.GetPosition(targetElement); var newPosition = DockMath.CalculateDockPosition(targetElement.ActualWidth, targetElement.ActualHeight, position.X, position.Y, DROP_EDGE_THRESHOLD); - if (newPosition != _currentDropPosition) + if (newPosition != _currentDropPosition || targetElement != _lastTargetElement) { _currentDropPosition = newPosition; + _lastTargetElement = targetElement; UpdateDropOverlay(targetElement, _currentDropPosition); } } @@ -332,6 +336,7 @@ public sealed partial class DockLayout : Control private void TabView_DragLeave(object sender, DragEventArgs e) { + _lastTargetElement = null; ClearOverlayState(); } diff --git a/src/Editor/Ghost.Editor/View/Windows/DockWindow.xaml.cs b/src/Editor/Ghost.Editor/View/Windows/DockWindow.xaml.cs index ae73433..22f2faa 100644 --- a/src/Editor/Ghost.Editor/View/Windows/DockWindow.xaml.cs +++ b/src/Editor/Ghost.Editor/View/Windows/DockWindow.xaml.cs @@ -19,6 +19,14 @@ internal sealed partial class DockWindow : WindowEx PART_DockLayout.Root = rootGroup; PART_DockLayout.TabTornOff += OnTabTornOff; + + ((System.Collections.Specialized.INotifyCollectionChanged)rootGroup.Children).CollectionChanged += (s, e) => + { + if (rootGroup.Children.Count == 0) + { + this.Close(); + } + }; } private void OnTabTornOff(object? sender, TabTornOffEventArgs e) diff --git a/src/Editor/Ghost.Editor/View/Windows/EngineEditorWindow.xaml b/src/Editor/Ghost.Editor/View/Windows/EngineEditorWindow.xaml index da94378..6b56f6e 100644 --- a/src/Editor/Ghost.Editor/View/Windows/EngineEditorWindow.xaml +++ b/src/Editor/Ghost.Editor/View/Windows/EngineEditorWindow.xaml @@ -66,85 +66,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/src/Editor/Ghost.Editor/View/Windows/EngineEditorWindow.xaml.cs b/src/Editor/Ghost.Editor/View/Windows/EngineEditorWindow.xaml.cs index c79b7f3..d0e4893 100644 --- a/src/Editor/Ghost.Editor/View/Windows/EngineEditorWindow.xaml.cs +++ b/src/Editor/Ghost.Editor/View/Windows/EngineEditorWindow.xaml.cs @@ -38,31 +38,41 @@ internal sealed partial class EngineEditorWindow : WindowEx SetTitleBar(PART_TitleBar); this.CenterOnScreen(); + + InitializeDockLayout(); + } + + private void InitializeDockLayout() + { + var root = new DockGroupNode { Orientation = Orientation.Horizontal }; + + var leftGroup = new DockGroupNode { Orientation = Orientation.Vertical }; + var hierarchyPanel = new DockPanelNode(); + hierarchyPanel.Items.Add(new TabViewItem { Header = "Hierarchy", Content = new Hierarchy() }); + leftGroup.AddChild(hierarchyPanel); + + var centerGroup = new DockGroupNode { Orientation = Orientation.Vertical }; + var scenePanel = new DockPanelNode(); + scenePanel.Items.Add(new ScenePage { Header = "Scene" }); + centerGroup.AddChild(scenePanel); + + var rightGroup = new DockGroupNode { Orientation = Orientation.Vertical }; + var inspectorPanel = new DockPanelNode(); + inspectorPanel.Items.Add(new InspectorPage { Header = "Inspector" }); + rightGroup.AddChild(inspectorPanel); + + root.AddChild(leftGroup); + root.AddChild(centerGroup); + root.AddChild(rightGroup); + + PART_DockLayout.Root = root; + PART_DockLayout.TabTornOff += (s, e) => App.CreateAndShowDockWindow(e.TabContent); } private void OnTabDroppedOutside(Microsoft.UI.Xaml.Controls.TabView sender, Microsoft.UI.Xaml.Controls.TabViewTabDroppedOutsideEventArgs args) { - // For static tabs in EngineEditorWindow, we remove the item from TabItems - if (sender.TabItems is System.Collections.IList list) - { - if (list.Contains(args.Item)) - { - var result = TabTearOffService.TryTearOffTab(list, args.Item, (tab) => - { - App.CreateAndShowDockWindow(tab); - }, sender); - - if (!result.IsSuccess) - { - Logger.LogWarning($"Tab tear-off failed: {result.Message}"); - } - } - else - { - string itemInfo = args.Item is FrameworkElement fe ? fe.GetType().Name : args.Item?.ToString() ?? "unknown"; - Logger.LogWarning($"OnTabDroppedOutside: Item '{itemInfo}' not found in source TabView (Items count: {list.Count})."); - } - } + // This is now handled by DockLayout.TabTornOff for dynamic tabs. + // If we still have static tabs, we'd handle them here. } private void MainGrid_Loaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)