# DockLayout Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Create a fully-featured, dynamically splittable, multi-window docking layout system using WinUI 3. **Architecture:** A C# data model (node tree) drives the recursive generation of XAML `Grid` and `NavigationTabView` controls. Drag and drop events mutate the node tree, and the UI automatically reflects the changes. **Tech Stack:** C#, WinUI 3, CommunityToolkit.Mvvm (for ObservableObject). --- ### Task 1: Create Core Data Models **Files:** - Create: `src/Editor/Ghost.Editor.Core/Controls/Internal/Docking/DockNode.cs` - Create: `src/Editor/Ghost.Editor.Core/Controls/Internal/Docking/DockGroupNode.cs` - Create: `src/Editor/Ghost.Editor.Core/Controls/Internal/Docking/DockPanelNode.cs` - [ ] **Step 1: Write `DockNode` base class** ```csharp using CommunityToolkit.Mvvm.ComponentModel; namespace Ghost.Editor.Core.Controls.Internal.Docking; public abstract partial class DockNode : ObservableObject { [ObservableProperty] private DockGroupNode? _parent; } ``` - [ ] **Step 2: Write `DockGroupNode` class** ```csharp using System.Collections.ObjectModel; using Microsoft.UI.Xaml.Controls; namespace Ghost.Editor.Core.Controls.Internal.Docking; public partial class DockGroupNode : DockNode { [ObservableProperty] private Orientation _orientation = Orientation.Horizontal; public ObservableCollection Children { get; } = new(); public void AddChild(DockNode node) { node.Parent = this; Children.Add(node); } public void RemoveChild(DockNode node) { if (Children.Remove(node)) { node.Parent = null; } } } ``` - [ ] **Step 3: Write `DockPanelNode` class** ```csharp using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; namespace Ghost.Editor.Core.Controls.Internal.Docking; public partial class DockPanelNode : DockNode { public ObservableCollection Items { get; } = new(); [ObservableProperty] private int _selectedIndex = -1; [ObservableProperty] private object? _selectedItem; } ``` - [ ] **Step 4: Commit** ```bash git add src/Editor/Ghost.Editor.Core/Controls/Internal/Docking/* git commit -m "feat(dock): add core data models for docking system" ``` --- ### Task 2: Implement Tree Renderer in XAML **Files:** - Modify: `src/Editor/Ghost.Editor/View/Controls/DockLayout.cs` - Modify: `src/Editor/Ghost.Editor/View/Controls/DockLayout.xaml` - [ ] **Step 1: Add DependencyProperty for Root in DockLayout.cs** ```csharp using Ghost.Editor.Core.Controls.Internal.Docking; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; namespace Ghost.Editor.View.Controls; public sealed partial class DockLayout : Control { public DockLayout() { DefaultStyleKey = typeof(DockLayout); } public DockGroupNode? Root { get => (DockGroupNode?)GetValue(RootProperty); set => SetValue(RootProperty, value); } public static readonly DependencyProperty RootProperty = DependencyProperty.Register("Root", typeof(DockGroupNode), typeof(DockLayout), new PropertyMetadata(null, OnRootChanged)); private static void OnRootChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is DockLayout layout) { layout.RenderTree(); } } private void RenderTree() { if (GetTemplateChild("PART_RootGrid") is Grid rootGrid) { rootGrid.Children.Clear(); if (Root != null) { var ui = CreateUIForNode(Root); rootGrid.Children.Add(ui); } } } private UIElement CreateUIForNode(DockNode node) { if (node is DockGroupNode groupNode) { // Simple visualizer for now, full grid logic in next step var grid = new Grid(); foreach (var child in groupNode.Children) { grid.Children.Add(CreateUIForNode(child)); } return grid; } else if (node is DockPanelNode panelNode) { return new Ghost.Editor.Controls.NavigationTabView { ItemsSource = panelNode.Items, HorizontalAlignment = HorizontalAlignment.Stretch, VerticalAlignment = VerticalAlignment.Stretch }; } return new Grid(); // Fallback } protected override void OnApplyTemplate() { base.OnApplyTemplate(); RenderTree(); } } ``` - [ ] **Step 2: Define ControlTemplate in DockLayout.xaml** ```xml ``` - [ ] **Step 3: Commit** ```bash git add src/Editor/Ghost.Editor/View/Controls/DockLayout.* git commit -m "feat(dock): implement basic recursive tree renderer" ``` --- ### Task 3: Implement DockGroupNode Grid Builder **Files:** - Modify: `src/Editor/Ghost.Editor/View/Controls/DockLayout.cs` - [ ] **Step 1: Replace `CreateUIForNode` Group logic to generate Columns/Rows and GridSplitters** ```csharp private UIElement CreateUIForNode(DockNode node) { if (node is DockGroupNode groupNode) { var grid = new Grid(); bool isHorizontal = groupNode.Orientation == Orientation.Horizontal; int childCount = groupNode.Children.Count; for (int i = 0; i < childCount; i++) { var childNode = groupNode.Children[i]; var childUI = CreateUIForNode(childNode); if (isHorizontal) { grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); Grid.SetColumn((FrameworkElement)childUI, i * 2); } else { grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); Grid.SetRow((FrameworkElement)childUI, i * 2); } grid.Children.Add(childUI); // Add GridSplitter between children if (i < childCount - 1) { if (isHorizontal) { grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); var splitter = new CommunityToolkit.WinUI.Controls.GridSplitter { Width = 4, HorizontalAlignment = HorizontalAlignment.Center, ResizeDirection = CommunityToolkit.WinUI.Controls.GridSplitter.GridResizeDirection.Columns }; Grid.SetColumn(splitter, (i * 2) + 1); grid.Children.Add(splitter); } else { grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); var splitter = new CommunityToolkit.WinUI.Controls.GridSplitter { Height = 4, VerticalAlignment = VerticalAlignment.Center, ResizeDirection = CommunityToolkit.WinUI.Controls.GridSplitter.GridResizeDirection.Rows }; Grid.SetRow(splitter, (i * 2) + 1); grid.Children.Add(splitter); } } } // Listen to CollectionChanged to trigger re-render groupNode.Children.CollectionChanged -= GroupNode_Children_CollectionChanged; groupNode.Children.CollectionChanged += GroupNode_Children_CollectionChanged; return grid; } else if (node is DockPanelNode panelNode) { var tabView = new Ghost.Editor.Controls.NavigationTabView { TabItemsSource = panelNode.Items, HorizontalAlignment = HorizontalAlignment.Stretch, VerticalAlignment = VerticalAlignment.Stretch, CanDragTabs = true, AllowDrop = true, Tag = panelNode // Store reference to data node }; return tabView; } return new Grid(); } private void GroupNode_Children_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { // For MVP, just re-render the whole tree when a group changes structure RenderTree(); } ``` - [ ] **Step 2: Commit** ```bash git add src/Editor/Ghost.Editor/View/Controls/DockLayout.cs git commit -m "feat(dock): implement grid and gridsplitter generation for groups" ``` --- ### Task 4: Setup Visual Drop Target Overlay **Files:** - Modify: `src/Editor/Ghost.Editor/View/Controls/DockLayout.xaml` - Modify: `src/Editor/Ghost.Editor/View/Controls/DockLayout.cs` - [ ] **Step 1: Add Drop Overlay to ControlTemplate** ```xml ``` - [ ] **Step 2: Add Fields and ApplyTemplate logic in DockLayout.cs** ```csharp private Border? _dropTargetOverlay; protected override void OnApplyTemplate() { base.OnApplyTemplate(); _dropTargetOverlay = GetTemplateChild("PART_DropTargetOverlay") as Border; RenderTree(); } // Helper enum for later public enum DockPosition { Center, Top, Bottom, Left, Right, None } ``` - [ ] **Step 3: Commit** ```bash git add src/Editor/Ghost.Editor/View/Controls/DockLayout.* git commit -m "feat(dock): add visual drop target overlay" ``` --- ### Task 5: Implement Drag and Drop Calculations (Highlighting) **Files:** - Modify: `src/Editor/Ghost.Editor/View/Controls/DockLayout.cs` - [ ] **Step 1: Attach TabView Drag Events in `CreateUIForNode`** ```csharp else if (node is DockPanelNode panelNode) { var tabView = new Ghost.Editor.Controls.NavigationTabView { TabItemsSource = panelNode.Items, HorizontalAlignment = HorizontalAlignment.Stretch, VerticalAlignment = VerticalAlignment.Stretch, CanDragTabs = true, AllowDrop = true, Tag = panelNode // Store reference to data node }; tabView.DragOver += TabView_DragOver; tabView.DragLeave += TabView_DragLeave; tabView.Drop += TabView_Drop; tabView.TabDragStarting += TabView_TabDragStarting; return tabView; } ``` - [ ] **Step 2: Implement Drag Handling Logic** ```csharp private object? _draggedItem; private DockPanelNode? _sourceNode; private DockPosition _currentDropPosition = DockPosition.None; private void TabView_TabDragStarting(Microsoft.UI.Xaml.Controls.TabView sender, Microsoft.UI.Xaml.Controls.TabViewTabDragStartingEventArgs args) { _draggedItem = args.Item; _sourceNode = sender.Tag as DockPanelNode; args.Data.Properties.Add("DockTab", _draggedItem); // Identify our drag } private void TabView_DragOver(object sender, DragEventArgs e) { if (e.DataView.Properties.ContainsKey("DockTab") && sender is FrameworkElement targetElement) { e.AcceptedOperation = Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move; var position = e.GetPosition(targetElement); double width = targetElement.ActualWidth; double height = targetElement.ActualHeight; 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); } } private void TabView_DragLeave(object sender, DragEventArgs e) { if (_dropTargetOverlay != null) { _dropTargetOverlay.Visibility = Visibility.Collapsed; _currentDropPosition = DockPosition.None; } } private void UpdateDropOverlay(FrameworkElement targetElement, DockPosition position) { if (_dropTargetOverlay == null) return; if (position == DockPosition.None) { _dropTargetOverlay.Visibility = Visibility.Collapsed; return; } var transform = targetElement.TransformToVisual(this); var bounds = transform.TransformBounds(new Windows.Foundation.Rect(0, 0, targetElement.ActualWidth, targetElement.ActualHeight)); _dropTargetOverlay.Visibility = Visibility.Visible; _dropTargetOverlay.Width = double.NaN; _dropTargetOverlay.Height = double.NaN; switch (position) { case DockPosition.Center: _dropTargetOverlay.Margin = new Thickness(bounds.Left, bounds.Top, ActualWidth - bounds.Right, ActualHeight - bounds.Bottom); break; case DockPosition.Left: _dropTargetOverlay.Margin = new Thickness(bounds.Left, bounds.Top, ActualWidth - (bounds.Left + bounds.Width / 2), ActualHeight - bounds.Bottom); break; case DockPosition.Right: _dropTargetOverlay.Margin = new Thickness(bounds.Left + bounds.Width / 2, bounds.Top, ActualWidth - bounds.Right, ActualHeight - bounds.Bottom); break; case DockPosition.Top: _dropTargetOverlay.Margin = new Thickness(bounds.Left, bounds.Top, ActualWidth - bounds.Right, ActualHeight - (bounds.Top + bounds.Height / 2)); break; case DockPosition.Bottom: _dropTargetOverlay.Margin = new Thickness(bounds.Left, bounds.Top + bounds.Height / 2, ActualWidth - bounds.Right, ActualHeight - bounds.Bottom); break; } } ``` - [ ] **Step 3: Commit** ```bash git add src/Editor/Ghost.Editor/View/Controls/DockLayout.cs git commit -m "feat(dock): implement drop highlight calculations" ``` --- ### Task 6: Implement Dropping (Data Tree Mutation) **Files:** - Modify: `src/Editor/Ghost.Editor/View/Controls/DockLayout.cs` - [ ] **Step 1: Implement `TabView_Drop` logic** ```csharp private void TabView_Drop(object sender, DragEventArgs e) { if (_dropTargetOverlay != null) _dropTargetOverlay.Visibility = Visibility.Collapsed; if (_draggedItem == null || _sourceNode == null || !(sender is FrameworkElement targetElement) || !(targetElement.Tag is DockPanelNode targetNode)) return; if (_sourceNode == targetNode && _currentDropPosition == DockPosition.Center) return; // Reordering within same tab is handled natively by TabView // 1. Remove from source _sourceNode.Items.Remove(_draggedItem); CleanupEmptyNodes(_sourceNode); // 2. Add to target if (_currentDropPosition == DockPosition.Center) { targetNode.Items.Add(_draggedItem); } else { // Split scenario var parentGroup = targetNode.Parent; if (parentGroup != null) { int index = parentGroup.Children.IndexOf(targetNode); parentGroup.Children.RemoveAt(index); var newGroup = new DockGroupNode { Orientation = (_currentDropPosition == DockPosition.Left || _currentDropPosition == DockPosition.Right) ? Orientation.Horizontal : Orientation.Vertical }; var newPanel = new DockPanelNode(); newPanel.Items.Add(_draggedItem); if (_currentDropPosition == DockPosition.Left || _currentDropPosition == DockPosition.Top) { newGroup.AddChild(newPanel); newGroup.AddChild(targetNode); } else { newGroup.AddChild(targetNode); newGroup.AddChild(newPanel); } parentGroup.Children.Insert(index, newGroup); } } _draggedItem = null; _sourceNode = null; _currentDropPosition = DockPosition.None; } private void CleanupEmptyNodes(DockPanelNode panelNode) { if (panelNode.Items.Count > 0) return; var parentGroup = panelNode.Parent; if (parentGroup != null) { parentGroup.RemoveChild(panelNode); // If group only has 1 child left, collapse it if (parentGroup.Children.Count == 1) { var onlyChild = parentGroup.Children[0]; var grandParent = parentGroup.Parent; if (grandParent != null) { int index = grandParent.Children.IndexOf(parentGroup); parentGroup.RemoveChild(onlyChild); grandParent.Children.RemoveAt(index); grandParent.Children.Insert(index, onlyChild); } else if (parentGroup == Root) { // If root is collapsing, the only child becomes the new root parentGroup.RemoveChild(onlyChild); if (onlyChild is DockGroupNode newRootGroup) { Root = newRootGroup; } else { // Wrap panel in a new group to keep Root as a GroupNode var wrapperGroup = new DockGroupNode(); wrapperGroup.AddChild(onlyChild); Root = wrapperGroup; } } } } } ``` - [ ] **Step 2: Commit** ```bash git add src/Editor/Ghost.Editor/View/Controls/DockLayout.cs git commit -m "feat(dock): implement tree mutation on drop and empty node cleanup" ``` --- ### Task 7: Implement Window Tear-Off (TabDroppedOutside) **Files:** - Create: `src/Editor/Ghost.Editor/View/Windows/DockWindow.xaml` - Create: `src/Editor/Ghost.Editor/View/Windows/DockWindow.xaml.cs` - Modify: `src/Editor/Ghost.Editor/View/Controls/DockLayout.cs` - [ ] **Step 1: Create `DockWindow` wrapper** `DockWindow.xaml`: ```xml ``` `DockWindow.xaml.cs`: ```csharp using Ghost.Editor.Core.Controls.Internal.Docking; using WinUIEx; namespace Ghost.Editor.View.Windows; public sealed partial class DockWindow : WindowEx { public DockWindow(object initialTabContent) { InitializeComponent(); // Setup initial single panel layout var rootGroup = new DockGroupNode(); var panel = new DockPanelNode(); panel.Items.Add(initialTabContent); rootGroup.AddChild(panel); PART_DockLayout.Root = rootGroup; // Optional: Titlebar setup etc. } } ``` - [ ] **Step 2: Handle `TabDroppedOutside` in `DockLayout.cs`** Modify `CreateUIForNode` in `DockLayout.cs`: ```csharp // ... inside CreateUIForNode for DockPanelNode ... tabView.TabDragStarting += TabView_TabDragStarting; tabView.TabDroppedOutside += TabView_TabDroppedOutside; // NEW return tabView; ``` Add event handler: ```csharp private void TabView_TabDroppedOutside(Microsoft.UI.Xaml.Controls.TabView sender, Microsoft.UI.Xaml.Controls.TabViewTabDroppedOutsideEventArgs args) { if (_sourceNode != null && _draggedItem != null) { // Remove from current tree _sourceNode.Items.Remove(_draggedItem); CleanupEmptyNodes(_sourceNode); // Create new window var newWindow = new Ghost.Editor.View.Windows.DockWindow(_draggedItem); newWindow.Activate(); _draggedItem = null; _sourceNode = null; _currentDropPosition = DockPosition.None; if (_dropTargetOverlay != null) _dropTargetOverlay.Visibility = Visibility.Collapsed; } } ``` - [ ] **Step 3: Commit** ```bash git add src/Editor/Ghost.Editor/View/Windows/DockWindow.* src/Editor/Ghost.Editor/View/Controls/DockLayout.cs git commit -m "feat(dock): implement tab tear-off to new window" ```