fix(dock): migrate primary editor to DockLayout and add persistent sizing

This commit is contained in:
2026-03-28 17:57:54 +09:00
parent ea7d3fad26
commit 9a1b8dcab0
5 changed files with 83 additions and 103 deletions

View File

@@ -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
/// </summary>
public ReadOnlyObservableCollection<DockNode> Children { get; }
/// <summary>
/// Gets the collection of sizes for the children.
/// </summary>
public ObservableCollection<Microsoft.UI.Xaml.GridLength> Sizes { get; } = new();
/// <summary>
/// Initializes a new instance of the <see cref="DockGroupNode"/> class.
/// </summary>
public DockGroupNode()
{
Children = new ReadOnlyObservableCollection<DockNode>(_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));
}
}
}
/// <summary>
/// Adds a child node to this group, enforcing tree invariants.
/// </summary>

View File

@@ -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();
}

View File

@@ -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)

View File

@@ -66,85 +66,7 @@
<!-- Editor -->
<Grid Grid.Row="2">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ghost:NavigationTabView
x:Name="PART_HierarchyTabView"
Grid.Column="0"
Width="350"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
TabDroppedOutside="OnTabDroppedOutside">
<ghost:NavigationTabView.TabItems>
<TabViewItem Header="Hierarchy">
<TabViewItem.IconSource>
<FontIconSource Glyph="&#xE8A4;" />
</TabViewItem.IconSource>
<controls:Hierarchy />
</TabViewItem>
</ghost:NavigationTabView.TabItems>
</ghost:NavigationTabView>
<ghost:NavigationTabView
x:Name="PART_SceneTabView"
Grid.Column="1"
TabDroppedOutside="OnTabDroppedOutside">
<ghost:NavigationTabView.TabItems>
<ee:ScenePage Header="Scene">
<ee:ScenePage.IconSource>
<FontIconSource Glyph="&#xF159;" />
</ee:ScenePage.IconSource>
</ee:ScenePage>
</ghost:NavigationTabView.TabItems>
</ghost:NavigationTabView>
<ghost:NavigationTabView
x:Name="PART_InspectorTabView"
Grid.Column="2"
Width="350"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
TabDroppedOutside="OnTabDroppedOutside">
<ghost:NavigationTabView.TabItems>
<ee:InspectorPage Header="Inspector">
<ee:InspectorPage.IconSource>
<FontIconSource Glyph="&#xEC7A;" />
</ee:InspectorPage.IconSource>
</ee:InspectorPage>
</ghost:NavigationTabView.TabItems>
</ghost:NavigationTabView>
</Grid>
<ghost:NavigationTabView
x:Name="PART_BottomTabView"
Grid.Row="1"
Height="350"
TabDroppedOutside="OnTabDroppedOutside">
<ghost:NavigationTabView.TabItems>
<TabViewItem Header="Project">
<TabViewItem.IconSource>
<FontIconSource Glyph="&#xEC50;" />
</TabViewItem.IconSource>
<controls:ProjectBrowser />
</TabViewItem>
<TabViewItem Header="Console">
<TabViewItem.IconSource>
<FontIconSource Glyph="&#xE756;" />
</TabViewItem.IconSource>
<ee:ConsolePage />
</TabViewItem>
</ghost:NavigationTabView.TabItems>
</ghost:NavigationTabView>
<controls:DockLayout x:Name="PART_DockLayout" />
</Grid>
<!-- Status Bar -->

View File

@@ -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)