feat: implement asynchronous asset management system with texture streaming support

This commit is contained in:
2026-04-20 01:09:59 +09:00
parent 4f5556ee1b
commit ed00f205b0
64 changed files with 1385 additions and 1157 deletions

View File

@@ -0,0 +1,211 @@
using System;
using System.Collections.ObjectModel;
namespace Ghost.Editor.Views.Controls.Docking;
/// <summary>
/// Base class for containers that can hold other dock modules.
/// </summary>
public abstract class DockContainer : DockModule
{
private readonly ObservableCollection<DockModule> _children = new();
private bool _isCleaningUp;
/// <summary>
/// Gets the collection of child modules.
/// </summary>
public ReadOnlyObservableCollection<DockModule> Children { get; }
protected DockContainer()
{
Children = new ReadOnlyObservableCollection<DockModule>(_children);
_children.CollectionChanged += OnChildrenChanged;
}
private void OnChildrenChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
OnChildrenUpdated();
}
/// <summary>
/// Adds a child module to the end of the container.
/// </summary>
/// <param name="module">The module to add.</param>
public virtual void AddChild(DockModule module)
{
InsertChild(_children.Count, module);
}
/// <summary>
/// Inserts a child module at the specified index.
/// </summary>
/// <remarks>
/// This method does not support reordering existing children within the same container.
/// Cross-layout moves are intentionally allowed and supported (e.g., for dragging tabs between floating windows and the main window).
/// </remarks>
/// <param name="index">The zero-based index at which the module should be inserted.</param>
/// <param name="module">The module to insert.</param>
public virtual void InsertChild(int index, DockModule module)
{
ValidateChild(module);
if (module.Owner == null && module.Root != null && module.Root != this.Root)
throw new InvalidOperationException("Cannot insert a module that is the root of another layout. Detach it first.");
if (index < 0 || index > _children.Count)
throw new ArgumentOutOfRangeException(nameof(index));
if (_children.Contains(module))
return;
module.Owner?.RemoveChild(module);
module.Owner = this;
module.Root = Root;
_children.Insert(index, module);
}
/// <summary>
/// Removes a child module from the container.
/// </summary>
/// <param name="module">The module to remove.</param>
public virtual void RemoveChild(DockModule module)
{
RemoveChildInternal(module, true);
}
internal void RemoveChildInternal(DockModule module, bool triggerCleanup)
{
ArgumentNullException.ThrowIfNull(module);
if (_children.Remove(module))
{
module.Owner = null;
module.Root = null;
if (!_isCleaningUp && triggerCleanup)
{
CheckCleanup();
}
}
}
/// <summary>
/// Replaces an existing child module with a new one.
/// </summary>
/// <remarks>
/// Cross-layout moves are intentionally allowed and supported (e.g., for dragging tabs between floating windows and the main window).
/// </remarks>
/// <param name="oldChild">The child module to be replaced.</param>
/// <param name="newChild">The new child module to insert.</param>
public virtual void ReplaceChild(DockModule oldChild, DockModule newChild)
{
ArgumentNullException.ThrowIfNull(oldChild);
ValidateChild(newChild);
if (newChild.Owner == null && newChild.Root != null && newChild.Root != this.Root)
throw new InvalidOperationException("Cannot insert a module that is the root of another layout. Detach it first.");
if (oldChild == newChild) return;
int index = _children.IndexOf(oldChild);
if (index < 0) throw new ArgumentException("oldChild not found in this container", nameof(oldChild));
// Detach newChild from its current owner if any
if (newChild.Owner == this)
{
throw new ArgumentException("newChild is already in this container", nameof(newChild));
}
var oldOwner = newChild.Owner;
newChild.Owner?.RemoveChildInternal(newChild, false);
// Remove oldChild without triggering cleanup
_isCleaningUp = true;
try
{
_children.RemoveAt(index);
oldChild.Owner = null;
oldChild.Root = null;
newChild.Owner = this;
newChild.Root = Root;
_children.Insert(index, newChild);
}
finally
{
_isCleaningUp = false;
}
CheckCleanup();
oldOwner?.CheckCleanup();
}
/// <summary>
/// Checks if the container is empty and removes it from its owner if necessary.
/// </summary>
internal virtual void CheckCleanup()
{
if (Children.Count == 0)
{
if (Owner != null)
{
Owner.RemoveChildInternal(this, true);
}
else if (Root != null && Root.RootModule == this)
{
var root = Root;
root.RootModule = null;
root.NotifyLayoutEmpty();
}
}
}
/// <summary>
/// Validates if a module can be added as a child to this container.
/// </summary>
/// <param name="module">The module to validate.</param>
protected virtual void ValidateChild(DockModule module)
{
ArgumentNullException.ThrowIfNull(module);
if (module == this)
throw new ArgumentException("Cannot add a container to itself.", nameof(module));
if (module is DockContainer container)
{
var current = Owner;
while (current != null)
{
if (current == container)
throw new ArgumentException("Cannot add a container that is an ancestor of this container.", nameof(module));
current = current.Owner;
}
}
}
/// <summary>
/// Removes all child modules from the container.
/// </summary>
public void Clear()
{
foreach (var child in _children)
{
child.Owner = null;
child.Root = null;
}
_children.Clear();
if (!_isCleaningUp)
{
CheckCleanup();
}
}
protected override void OnRootChanged()
{
foreach (var child in _children)
{
child.Root = Root;
}
}
protected virtual void OnChildrenUpdated() { }
}

View File

@@ -0,0 +1,42 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Views.Controls.Docking;
/// <summary>
/// Represents a document module in the docking system.
/// </summary>
public partial class DockDocument : DockModule
{
public static readonly DependencyProperty TitleProperty = DependencyProperty.Register(
nameof(Title), typeof(string), typeof(DockDocument), new PropertyMetadata(string.Empty));
public static readonly DependencyProperty ContentProperty = DependencyProperty.Register(
nameof(Content), typeof(object), typeof(DockDocument), new PropertyMetadata(null));
/// <summary>
/// Gets or sets the title of the document.
/// </summary>
public string? Title
{
get => (string?)GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
/// <summary>
/// Gets or sets the content of the document.
/// </summary>
public object? Content
{
get => GetValue(ContentProperty);
set => SetValue(ContentProperty, value);
}
/// <summary>
/// Initializes a new instance of the <see cref="DockDocument"/> class.
/// </summary>
public DockDocument()
{
DefaultStyleKey = typeof(DockDocument);
}
}

View File

@@ -0,0 +1,22 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Ghost.Editor.Views.Controls.Docking">
<Style TargetType="local:DockDocument">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Stretch" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:DockDocument">
<Border Background="Transparent">
<ContentPresenter
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,182 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Data;
namespace Ghost.Editor.Views.Controls.Docking;
/// <summary>
/// A container that displays its children (documents) as tabs.
/// </summary>
[TemplatePart(Name = PART_TAB_VIEW, Type = typeof(TabView))]
public partial class DockGroup : DockContainer
{
private const string PART_TAB_VIEW = "PART_TabView";
private const string DRAG_DOCUMENT_KEY = "DockDocument";
private TabView? _tabView;
public DockGroup()
{
DefaultStyleKey = typeof(DockGroup);
}
protected override void ValidateChild(DockModule module)
{
base.ValidateChild(module);
if (module is not DockDocument)
{
throw new ArgumentException($"{nameof(DockGroup)} only accepts {nameof(DockDocument)} children.", nameof(module));
}
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (_tabView != null)
{
_tabView.TabDragStarting -= OnTabDragStarting;
_tabView.TabDroppedOutside -= OnTabDroppedOutside;
_tabView.DragOver -= OnDragOver;
_tabView.Drop -= OnDrop;
_tabView.DragLeave -= OnDragLeave;
_tabView.TabCloseRequested -= OnTabCloseRequested;
}
_tabView = GetTemplateChild(PART_TAB_VIEW) as TabView;
if (_tabView != null)
{
_tabView.TabDragStarting += OnTabDragStarting;
_tabView.TabDroppedOutside += OnTabDroppedOutside;
_tabView.DragOver += OnDragOver;
_tabView.Drop += OnDrop;
_tabView.DragLeave += OnDragLeave;
_tabView.TabCloseRequested += OnTabCloseRequested;
}
UpdateTabs();
}
private void OnTabDragStarting(TabView sender, TabViewTabDragStartingEventArgs args)
{
if (args.Tab.Tag is DockDocument doc)
{
args.Data.Properties.Add(DRAG_DOCUMENT_KEY, doc);
}
}
private void OnTabDroppedOutside(TabView sender, TabViewTabDroppedOutsideEventArgs args)
{
if (args.Tab.Tag is DockDocument doc)
{
Root?.CreateFloatingWindow(doc);
}
}
private void OnDragOver(object sender, DragEventArgs e)
{
if (e.DataView.Properties.ContainsKey(DRAG_DOCUMENT_KEY))
{
e.AcceptedOperation = global::Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move;
Root?.ShowHighlight(this, e.GetPosition(this));
}
}
private void OnDrop(object sender, DragEventArgs e)
{
if (e.DataView.Properties.TryGetValue(DRAG_DOCUMENT_KEY, out var obj) && obj is DockDocument doc)
{
Root?.HandleDrop(doc, this, e.GetPosition(this));
}
}
private void OnDragLeave(object sender, DragEventArgs e)
{
Root?.HideHighlight();
}
private void OnTabCloseRequested(TabView sender, TabViewTabCloseRequestedEventArgs args)
{
if (args.Tab.Tag is DockDocument doc)
{
RemoveChild(doc);
}
}
protected override void OnChildrenUpdated()
{
UpdateTabs();
}
private void UpdateTabs()
{
if (_tabView == null || Root == null) return;
var selectedDoc = _tabView.SelectedItem is TabViewItem selectedItem ? selectedItem.Tag as DockDocument : null;
// Remove tabs that are no longer in Children
for (int i = _tabView.TabItems.Count - 1; i >= 0; i--)
{
if (_tabView.TabItems[i] is TabViewItem tabItem && tabItem.Tag is DockDocument doc)
{
if (!Children.Contains(doc))
{
tabItem.Content = null;
_tabView.TabItems.RemoveAt(i);
}
}
}
// Add new tabs that aren't in TabItems yet, and ensure correct order
for (int i = 0; i < Children.Count; i++)
{
if (Children[i] is DockDocument doc)
{
TabViewItem? existingTab = null;
for (int j = 0; j < _tabView.TabItems.Count; j++)
{
if (_tabView.TabItems[j] is TabViewItem tabItem && tabItem.Tag == doc)
{
existingTab = tabItem;
// Fix order if necessary
if (j != i)
{
_tabView.TabItems.RemoveAt(j);
_tabView.TabItems.Insert(i, existingTab);
}
break;
}
}
if (existingTab == null)
{
existingTab = new TabViewItem
{
Tag = doc,
Content = doc
};
existingTab.SetBinding(TabViewItem.HeaderProperty, new Binding
{
Source = doc,
Path = new PropertyPath(nameof(DockDocument.Title)),
Mode = Microsoft.UI.Xaml.Data.BindingMode.OneWay
});
_tabView.TabItems.Insert(i, existingTab);
}
}
}
// Restore selection
if (selectedDoc != null && _tabView.TabItems.FirstOrDefault(t => t is TabViewItem item && item.Tag == selectedDoc) is TabViewItem newSelected)
{
_tabView.SelectedItem = newSelected;
}
else if (_tabView.TabItems.Count > 0)
{
_tabView.SelectedItem = _tabView.TabItems[0];
}
}
}

View File

@@ -0,0 +1,23 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Ghost.Editor.Views.Controls.Docking">
<Style TargetType="local:DockGroup">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:DockGroup">
<TabView
x:Name="PART_TabView"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
AllowDrop="True"
CanDragTabs="True"
CanReorderTabs="True"
IsAddTabButtonVisible="True"
TabWidthMode="Compact" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,47 @@
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Views.Controls.Docking;
/// <summary>
/// Base class for all dockable modules in the docking system.
/// </summary>
public abstract class DockModule : Control
{
/// <summary>
/// Gets the container that owns this module.
/// </summary>
public DockContainer? Owner { get; internal set; }
/// <summary>
/// Gets or sets the proportional length (star weight) of this module within its parent panel.
/// </summary>
public double DockLength { get; set; } = 1.0;
private DockingLayout? _root;
/// <summary>
/// Gets or sets the root docking layout this module belongs to.
/// </summary>
public virtual DockingLayout? Root
{
get => _root;
internal set
{
if (_root != value)
{
_root = value;
OnRootChanged();
}
}
}
protected virtual void OnRootChanged() { }
/// <summary>
/// Detaches this module from its current owner.
/// </summary>
public void Detach()
{
Owner?.RemoveChild(this);
}
}

View File

@@ -0,0 +1,231 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using CommunityToolkit.WinUI.Controls;
using Windows.Foundation;
namespace Ghost.Editor.Views.Controls.Docking;
/// <summary>
/// A container that can host multiple dock modules with splitters.
/// </summary>
[TemplatePart(Name = PART_GRID, Type = typeof(Grid))]
public partial class DockPanel : DockContainer
{
private const string PART_GRID = "PART_Grid";
private const double SPLITTER_THICKNESS = 4;
public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register(
nameof(Orientation), typeof(Orientation), typeof(DockPanel), new PropertyMetadata(Orientation.Horizontal, OnOrientationChanged));
/// <summary>
/// Gets or sets the orientation of the panel.
/// </summary>
public Orientation Orientation
{
get => (Orientation)GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}
private Grid? _grid;
public DockPanel()
{
DefaultStyleKey = typeof(DockPanel);
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
_grid = GetTemplateChild(PART_GRID) as Grid;
UpdateLayoutStructure();
}
protected override void OnChildrenUpdated()
{
UpdateLayoutStructure();
}
internal void SyncLengths()
{
if (_grid == null) return;
if (Orientation == Orientation.Horizontal)
{
for (int i = 0; i < Children.Count; i++)
{
int col = i * 2;
if (col < _grid.ColumnDefinitions.Count)
{
Children[i].DockLength = _grid.ColumnDefinitions[col].Width.Value;
}
}
}
else
{
for (int i = 0; i < Children.Count; i++)
{
int row = i * 2;
if (row < _grid.RowDefinitions.Count)
{
Children[i].DockLength = _grid.RowDefinitions[row].Height.Value;
}
}
}
}
internal override void CheckCleanup()
{
base.CheckCleanup();
if (Children.Count == 1)
{
var child = Children[0];
var owner = Owner;
if (owner != null)
{
owner.ReplaceChild(this, child);
}
else if (Root != null && Root.RootModule == this)
{
RemoveChildInternal(child, false);
Root.RootModule = child;
}
}
}
private static void OnOrientationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((DockPanel)d).UpdateLayoutStructure();
}
private void UpdateLayoutStructure()
{
if (_grid == null) return;
// Remove splitters and children that are no longer in the collection
for (int i = _grid.Children.Count - 1; i >= 0; i--)
{
var child = _grid.Children[i];
if (child is GridSplitter)
{
_grid.Children.RemoveAt(i);
}
else if (child is DockModule module && !Children.Contains(module))
{
_grid.Children.RemoveAt(i);
}
}
if (Children.Count == 0)
{
_grid.RowDefinitions.Clear();
_grid.ColumnDefinitions.Clear();
return;
}
if (Orientation == Orientation.Horizontal)
{
_grid.RowDefinitions.Clear();
int requiredColumns = (Children.Count * 2) - 1;
while (_grid.ColumnDefinitions.Count > requiredColumns)
{
_grid.ColumnDefinitions.RemoveAt(_grid.ColumnDefinitions.Count - 1);
}
for (var i = 0; i < Children.Count; i++)
{
int columnIndex = i * 2;
if (columnIndex >= _grid.ColumnDefinitions.Count)
{
_grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(Children[i].DockLength, GridUnitType.Star), MinWidth = 250 });
}
else
{
_grid.ColumnDefinitions[columnIndex].Width = new GridLength(Children[i].DockLength, GridUnitType.Star);
_grid.ColumnDefinitions[columnIndex].MinWidth = 250;
}
var child = Children[i];
if (!_grid.Children.Contains(child))
{
_grid.Children.Add(child);
}
Grid.SetColumn(child, columnIndex);
Grid.SetRow(child, 0);
if (i < Children.Count - 1)
{
int splitterIndex = columnIndex + 1;
if (splitterIndex >= _grid.ColumnDefinitions.Count)
{
_grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
}
else
{
_grid.ColumnDefinitions[splitterIndex].Width = GridLength.Auto;
}
var splitter = new GridSplitter { ResizeDirection = GridSplitter.GridResizeDirection.Columns, Width = SPLITTER_THICKNESS };
Grid.SetColumn(splitter, splitterIndex);
Grid.SetRow(splitter, 0);
_grid.Children.Add(splitter);
}
}
}
else
{
_grid.ColumnDefinitions.Clear();
int requiredRows = (Children.Count * 2) - 1;
while (_grid.RowDefinitions.Count > requiredRows)
{
_grid.RowDefinitions.RemoveAt(_grid.RowDefinitions.Count - 1);
}
for (var i = 0; i < Children.Count; i++)
{
int rowIndex = i * 2;
if (rowIndex >= _grid.RowDefinitions.Count)
{
_grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(Children[i].DockLength, GridUnitType.Star), MinHeight = 250 });
}
else
{
_grid.RowDefinitions[rowIndex].Height = new GridLength(Children[i].DockLength, GridUnitType.Star);
_grid.RowDefinitions[rowIndex].MinHeight = 250;
}
var child = Children[i];
if (!_grid.Children.Contains(child))
{
_grid.Children.Add(child);
}
Grid.SetRow(child, rowIndex);
Grid.SetColumn(child, 0);
if (i < Children.Count - 1)
{
int splitterIndex = rowIndex + 1;
if (splitterIndex >= _grid.RowDefinitions.Count)
{
_grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
}
else
{
_grid.RowDefinitions[splitterIndex].Height = GridLength.Auto;
}
var splitter = new GridSplitter { ResizeDirection = GridSplitter.GridResizeDirection.Rows, Height = SPLITTER_THICKNESS };
Grid.SetRow(splitter, splitterIndex);
Grid.SetColumn(splitter, 0);
_grid.Children.Add(splitter);
}
}
}
UpdateLayout();
}
}

View File

@@ -0,0 +1,15 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Ghost.Editor.Views.Controls.Docking">
<Style TargetType="local:DockPanel">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:DockPanel">
<Grid x:Name="PART_Grid" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,15 @@
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Views.Controls.Docking;
/// <summary>
/// Represents a visual highlight for a docking region.
/// </summary>
public partial class DockRegionHighlight : Control
{
public DockRegionHighlight()
{
DefaultStyleKey = typeof(DockRegionHighlight);
IsHitTestVisible = false;
}
}

View File

@@ -0,0 +1,20 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Ghost.Editor.Views.Controls.Docking">
<Style TargetType="local:DockRegionHighlight">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:DockRegionHighlight">
<Border
Background="{ThemeResource SystemControlHighlightAccentBrush}"
BorderBrush="{ThemeResource SystemControlHighlightAccentBrush}"
BorderThickness="2"
CornerRadius="4"
Opacity="0.25" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,344 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Views.Controls.Docking;
/// <summary>
/// The root control for the docking system layout.
/// </summary>
[TemplatePart(Name = PART_OVERLAY_CANVAS, Type = typeof(Canvas))]
[TemplatePart(Name = PART_HIGHLIGHT, Type = typeof(DockRegionHighlight))]
public partial class DockingLayout : Control
{
private const string PART_OVERLAY_CANVAS = "PART_OverlayCanvas";
private const string PART_HIGHLIGHT = "PART_Highlight";
/// <summary>
/// Gets or sets the root module of the docking layout.
/// </summary>
public static readonly DependencyProperty RootModuleProperty = DependencyProperty.Register(
nameof(RootModule), typeof(DockModule), typeof(DockingLayout), new PropertyMetadata(null, OnRootModuleChanged));
/// <summary>
/// Gets or sets the root module of the docking layout.
/// </summary>
public DockModule? RootModule
{
get => (DockModule?)GetValue(RootModuleProperty);
set => SetValue(RootModuleProperty, value);
}
/// <summary>
/// Occurs when the layout becomes empty.
/// </summary>
public event EventHandler? LayoutEmpty;
internal void NotifyLayoutEmpty() => LayoutEmpty?.Invoke(this, EventArgs.Empty);
private Canvas? _overlayCanvas;
private DockRegionHighlight? _highlight;
private readonly List<FloatingWindow> _floatingWindows = new();
/// <summary>
/// Initializes a new instance of the <see cref="DockingLayout"/> class.
/// </summary>
public DockingLayout()
{
DefaultStyleKey = typeof(DockingLayout);
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
_overlayCanvas = GetTemplateChild(PART_OVERLAY_CANVAS) as Canvas;
_highlight = GetTemplateChild(PART_HIGHLIGHT) as DockRegionHighlight;
}
private static void OnRootModuleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is DockingLayout layout)
{
if (e.OldValue is DockModule oldModule)
{
oldModule.Root = null;
}
if (e.NewValue is DockModule newModule)
{
if (newModule.Root != null && newModule.Root != layout)
{
throw new InvalidOperationException("Module is already owned by another DockingLayout");
}
if (newModule.Owner != null)
{
newModule.Owner.RemoveChild(newModule);
}
newModule.Root = layout;
}
}
}
/// <summary>
/// Adds a document to the docking layout.
/// </summary>
/// <param name="document">The document to add.</param>
/// <param name="target">The docking target position.</param>
/// <param name="targetGroup">The target group to add the document to. If null, a suitable group will be found or created.</param>
public void AddDocument(DockDocument document, DockTarget target, DockGroup? targetGroup = null)
{
ArgumentNullException.ThrowIfNull(document);
if (targetGroup != null && targetGroup.Root != this)
{
throw new ArgumentException("targetGroup does not belong to this DockingLayout", nameof(targetGroup));
}
if (targetGroup == null)
{
if (RootModule != null)
{
targetGroup = FindFirstDockGroup(RootModule as DockContainer);
if (targetGroup == null)
{
// Root is not a container, or contains no pGroups. Wrap it.
var newGroup = new DockGroup();
newGroup.AddChild(document);
if (RootModule is DockDocument existingDoc)
{
RootModule = null;
newGroup.AddChild(existingDoc);
RootModule = newGroup;
}
else
{
var oldRoot = RootModule;
RootModule = null;
var panel = new DockPanel();
panel.AddChild(oldRoot);
panel.AddChild(newGroup);
RootModule = panel;
}
targetGroup = newGroup;
}
}
else
{
targetGroup = new DockGroup();
RootModule = targetGroup;
}
}
if (target == DockTarget.Center || targetGroup.Children.Count == 0)
{
targetGroup.AddChild(document);
}
else
{
SplitGroup(targetGroup, document, target);
}
}
private void SplitGroup(DockGroup targetGroup, DockDocument doc, DockTarget target)
{
var parentPanel = targetGroup.Owner as DockPanel;
parentPanel?.SyncLengths(); // Sync before modifying!
var newGroup = new DockGroup();
newGroup.AddChild(doc);
var orientation = (target == DockTarget.Left || target == DockTarget.Right) ? Orientation.Horizontal : Orientation.Vertical;
if (parentPanel == null)
{
// targetGroup is the RootModule
var newPanel = new DockPanel { Orientation = orientation };
RootModule = newPanel;
targetGroup.DockLength = 1.0;
newGroup.DockLength = 1.0;
if (target == DockTarget.Left || target == DockTarget.Top)
{
newPanel.AddChild(newGroup);
newPanel.AddChild(targetGroup);
}
else
{
newPanel.AddChild(targetGroup);
newPanel.AddChild(newGroup);
}
return;
}
int index = parentPanel.Children.IndexOf(targetGroup);
if (index < 0)
{
throw new InvalidOperationException("targetGroup not found in parentPanel");
}
if (parentPanel.Orientation == orientation)
{
// Splitting in the same orientation. Share the length.
double halfLength = targetGroup.DockLength / 2.0;
targetGroup.DockLength = halfLength;
newGroup.DockLength = halfLength;
if (target == DockTarget.Left || target == DockTarget.Top)
{
parentPanel.InsertChild(index, newGroup);
}
else
{
parentPanel.InsertChild(index + 1, newGroup);
}
}
else
{
// Splitting in opposite orientation. New panel takes the full length.
var newPanel = new DockPanel { Orientation = orientation };
newPanel.DockLength = targetGroup.DockLength;
targetGroup.DockLength = 1.0;
newGroup.DockLength = 1.0;
parentPanel.ReplaceChild(targetGroup, newPanel);
if (target == DockTarget.Left || target == DockTarget.Top)
{
newPanel.AddChild(newGroup);
newPanel.AddChild(targetGroup);
}
else
{
newPanel.AddChild(targetGroup);
newPanel.AddChild(newGroup);
}
}
}
private static DockGroup? FindFirstDockGroup(DockContainer? container)
{
if (container == null) return null;
if (container is DockGroup group)
{
return group;
}
foreach (var child in container.Children)
{
if (child is DockContainer childContainer)
{
var result = FindFirstDockGroup(childContainer);
if (result != null)
{
return result;
}
}
}
return null;
}
internal void ShowHighlight(DockGroup targetGroup, global::Windows.Foundation.Point position)
{
if (_highlight == null || _overlayCanvas == null) return;
_highlight.Visibility = Visibility.Visible;
var target = CalculateDockTarget(targetGroup, position);
// Calculate rect based on target
double width = targetGroup.ActualWidth;
double height = targetGroup.ActualHeight;
double x = 0, y = 0;
switch (target)
{
case DockTarget.Left: width /= 2; break;
case DockTarget.Right: width /= 2; x = width; break;
case DockTarget.Top: height /= 2; break;
case DockTarget.Bottom: height /= 2; y = height; break;
case DockTarget.Center: break;
}
var transform = targetGroup.TransformToVisual(_overlayCanvas);
var point = transform.TransformPoint(new global::Windows.Foundation.Point(x, y));
Microsoft.UI.Xaml.Controls.Canvas.SetLeft(_highlight, point.X);
Microsoft.UI.Xaml.Controls.Canvas.SetTop(_highlight, point.Y);
_highlight.Width = width;
_highlight.Height = height;
}
internal void HideHighlight()
{
if (_highlight != null) _highlight.Visibility = Visibility.Collapsed;
}
internal void HandleDrop(DockDocument doc, DockGroup targetGroup, global::Windows.Foundation.Point position)
{
HideHighlight();
var target = CalculateDockTarget(targetGroup, position);
var oldOwner = doc.Owner as DockContainer;
oldOwner?.RemoveChildInternal(doc, false);
if (target == DockTarget.Center)
{
if (doc.Owner == targetGroup) return;
targetGroup.AddChild(doc);
}
else
{
if (doc.Owner == targetGroup && targetGroup.Children.Count == 1) return;
SplitGroup(targetGroup, doc, target);
}
if (oldOwner != null)
{
DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, () =>
{
oldOwner.CheckCleanup();
});
}
}
private DockTarget CalculateDockTarget(DockGroup group, global::Windows.Foundation.Point position)
{
double w = group.ActualWidth;
double h = group.ActualHeight;
double x = position.X;
double y = position.Y;
if (x < w * 0.25) return DockTarget.Left;
if (x > w * 0.75) return DockTarget.Right;
if (y < h * 0.25) return DockTarget.Top;
if (y > h * 0.75) return DockTarget.Bottom;
return DockTarget.Center;
}
internal void CreateFloatingWindow(DockDocument doc)
{
ArgumentNullException.ThrowIfNull(doc);
var oldOwner = doc.Owner as DockContainer;
oldOwner?.RemoveChildInternal(doc, false);
var window = new FloatingWindow(doc);
_floatingWindows.Add(window);
window.Closed += (s, e) => _floatingWindows.Remove(window);
window.Activate();
if (oldOwner != null)
{
DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, () =>
{
oldOwner.CheckCleanup();
});
}
}
}

View File

@@ -0,0 +1,20 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Ghost.Editor.Views.Controls.Docking">
<Style TargetType="local:DockingLayout">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:DockingLayout">
<Grid>
<ContentPresenter x:Name="PART_Content" Content="{TemplateBinding RootModule}" />
<Canvas x:Name="PART_OverlayCanvas" IsHitTestVisible="False">
<local:DockRegionHighlight x:Name="PART_Highlight" Visibility="Collapsed" />
</Canvas>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,10 @@
namespace Ghost.Editor.Views.Controls.Docking;
public enum DockTarget
{
Center,
Left,
Right,
Top,
Bottom
}

View File

@@ -0,0 +1,42 @@
using Microsoft.UI.Xaml;
namespace Ghost.Editor.Views.Controls.Docking;
/// <summary>
/// A floating window that contains a docking layout.
/// </summary>
public class FloatingWindow : Window
{
private readonly DockingLayout _layout;
/// <summary>
/// Initializes a new instance of the <see cref="FloatingWindow"/> class with the specified document.
/// </summary>
/// <param name="document">The document to display in the floating window.</param>
public FloatingWindow(DockDocument document)
{
ArgumentNullException.ThrowIfNull(document);
_layout = new DockingLayout();
var group = new DockGroup();
group.AddChild(document);
_layout.RootModule = group;
_layout.LayoutEmpty += (s, e) => Close();
Content = _layout;
// Basic window setup
AppWindow.Resize(new global::Windows.Graphics.SizeInt32(800, 600));
// When the user manually closes the window, ensure we clean up the documents inside
this.Closed += FloatingWindow_Closed;
}
private void FloatingWindow_Closed(object sender, WindowEventArgs args)
{
// Force cleanup of the visual tree so we don't leak anything from this window
_layout.RootModule = null;
Content = null;
}
}

View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="Ghost.Editor.Views.Controls.Hierarchy"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Ghost.Editor.Views.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid
Grid.Row="0"
Height="40"
Padding="8,8,8,4"
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
BorderThickness="0,0,0,1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<AutoSuggestBox
Grid.Column="0"
PlaceholderText="Search"
QueryIcon="Find" />
<StackPanel Grid.Column="1" Orientation="Horizontal">
<AppBarSeparator />
<Button Style="{ThemeResource ToolbarButton}">
<FontIcon FontSize="{StaticResource ToolbarIconSize}" Glyph="&#xE8F4;" />
</Button>
</StackPanel>
</Grid>
<Border Grid.Row="1" Padding="4">
<ListView>
<StackPanel Orientation="Horizontal" Spacing="4">
<FontIcon FontSize="{StaticResource ToolbarIconSize}" Glyph="&#xF159;" />
<TextBlock Text="Test" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="4">
<FontIcon FontSize="{StaticResource ToolbarIconSize}" Glyph="&#xF159;" />
<TextBlock Text="Test" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="4">
<FontIcon FontSize="{StaticResource ToolbarIconSize}" Glyph="&#xF159;" />
<TextBlock Text="Test" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="4">
<FontIcon FontSize="{StaticResource ToolbarIconSize}" Glyph="&#xF159;" />
<TextBlock Text="Test" />
</StackPanel>
</ListView>
</Border>
</Grid>
</UserControl>

View File

@@ -0,0 +1,14 @@
using Microsoft.UI.Xaml.Controls;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace Ghost.Editor.Views.Controls;
public sealed partial class Hierarchy : UserControl
{
public Hierarchy()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,53 @@
using Ghost.Editor.Core;
namespace Ghost.Editor.Views.Controls;
internal partial class ProjectBrowser
{
[ContextMenuItem("project-browser", "Show in Explorer")]
private static void ShowInExplorer()
{
var path = LastFocused?.ViewModel.CurrentDirectoryPath;
if (!Directory.Exists(path))
{
return;
}
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo()
{
FileName = path,
UseShellExecute = true,
Verb = "open"
});
}
[ContextMenuItem("project-browser", "Create/Folder")]
private static void CreateFolder()
{
// TODO: Use AssetService
var viewModel = LastFocused?.ViewModel;
if (viewModel is null)
{
return;
}
var currentDir = viewModel.CurrentDirectoryPath;
if (!Directory.Exists(currentDir))
{
return;
}
var newFolderPath = Path.Combine(currentDir, "New Folder");
var folderIndex = 1;
while (Directory.Exists(newFolderPath))
{
newFolderPath = Path.Combine(currentDir, $"New Folder ({folderIndex})");
folderIndex++;
}
Directory.CreateDirectory(newFolderPath);
// Refresh the view model to show the new folder
viewModel.NavigateToDirectory(currentDir);
}
}

View File

@@ -0,0 +1,210 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="Ghost.Editor.Views.Controls.ProjectBrowser"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:community="using:CommunityToolkit.WinUI.Controls"
xmlns:converter="using:Ghost.Editor.Utilities.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ghost="using:Ghost.Editor.Core.Controls"
xmlns:local="using:Ghost.Editor.Views.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:model="using:Ghost.Editor.Models"
xmlns:sys="using:System"
mc:Ignorable="d">
<UserControl.Resources>
<converter:ExplorerItemToIconUriConverter x:Key="ExplorerItemToIconUriConverter" />
</UserControl.Resources>
<Grid MinHeight="50">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Toolbar -->
<Grid
Height="36"
Padding="4"
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
BorderThickness="0,0,0,1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Orientation="Horizontal">
<Button Style="{ThemeResource ToolbarButton}">
<Button.Content>
<FontIcon FontSize="{StaticResource ToolbarIconSize}" Glyph="&#xE710;" />
</Button.Content>
<Button.Flyout>
<MenuFlyout>
<MenuFlyoutItem Text="Folder" />
<MenuFlyoutItem Text="Script" />
<MenuFlyoutSubItem Text="Rendering">
<MenuFlyoutItem Text="Material" />
<MenuFlyoutItem Text="Volume Profile" />
</MenuFlyoutSubItem>
</MenuFlyout>
</Button.Flyout>
</Button>
</StackPanel>
<StackPanel Grid.Column="2" Orientation="Horizontal">
<AutoSuggestBox
Width="250"
PlaceholderText="Search"
QueryIcon="Find" />
<AppBarSeparator />
<Button Style="{ThemeResource ToolbarButton}">
<Button.Content>
<FontIcon FontSize="{StaticResource ToolbarIconSize}" Glyph="&#xE97C;" />
</Button.Content>
<Button.Flyout>
<MenuFlyout>
<ToggleMenuFlyoutItem Text="Animation" />
<ToggleMenuFlyoutItem Text="Audio" />
<ToggleMenuFlyoutItem Text="Material" />
<ToggleMenuFlyoutItem Text="Script" />
<ToggleMenuFlyoutItem Text="Texture" />
</MenuFlyout>
</Button.Flyout>
</Button>
<Button Style="{ThemeResource ToolbarButton}">
<Button.Content>
<FontIcon FontSize="{StaticResource ToolbarIconSize}" Glyph="&#xE8EC;" />
</Button.Content>
</Button>
</StackPanel>
</Grid>
<!-- Conent Viewer -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border
Grid.Column="0"
Width="200"
Padding="4,0,0,0"
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
BorderThickness="0,0,1,0">
<TreeView
x:Name="PART_DirectoriesView"
ItemsSource="{x:Bind ViewModel.Directories, Mode=OneWay}"
SelectionChanged="PART_DirectoriesView_SelectionChanged"
SelectionMode="Single">
<TreeView.ItemTemplate>
<DataTemplate x:DataType="model:ExplorerItem">
<TreeViewItem ItemsSource="{x:Bind Children}">
<StackPanel Orientation="Horizontal" Spacing="4">
<!-- TODO: Open/Close folder icon based on state -->
<FontIcon
VerticalAlignment="Center"
FontSize="12"
Glyph="&#xE8B7;" />
<TextBlock
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind Name}"
TextTrimming="CharacterEllipsis" />
</StackPanel>
</TreeViewItem>
</DataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Border>
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<!--<RowDefinition Height="Auto" />-->
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!--<Border
Grid.Row="0"
Height="24"
Background="{ThemeResource LayerFillColorDefaultBrush}"
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
BorderThickness="0,0,0,1">
<BreadcrumbBar />
</Border>-->
<GridView
x:Name="PART_FilesView"
Grid.Row="0"
Padding="8"
DoubleTapped="PART_FilesView_DoubleTapped"
IsDoubleTapEnabled="True"
ItemsSource="{x:Bind ViewModel.Files}"
SelectionChanged="PART_FilesView_SelectionChanged"
SelectionMode="Single">
<GridView.ItemTemplate>
<DataTemplate x:DataType="model:ExplorerItem">
<ItemContainer>
<Grid
Width="72"
Padding="8"
HorizontalAlignment="Center"
VerticalAlignment="Center"
RowSpacing="4"
ToolTipService.ToolTip="{x:Bind Name}">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ContextFlyout>
<MenuFlyout>
<MenuFlyoutItem Text="Open" />
<MenuFlyoutItem Text="Rename" />
<MenuFlyoutItem Text="Delete" />
<MenuFlyoutItem Text="Show in Explorer" />
</MenuFlyout>
</Grid.ContextFlyout>
<community:ConstrainedBox Grid.Row="0" AspectRatio="1:1">
<Image HorizontalAlignment="Center">
<Image.Source>
<BitmapImage DecodePixelWidth="48" UriSource="{x:Bind Converter={StaticResource ExplorerItemToIconUriConverter}}" />
</Image.Source>
</Image>
</community:ConstrainedBox>
<TextBlock
Grid.Row="1"
HorizontalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind Name}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" />
</Grid>
</ItemContainer>
</DataTemplate>
</GridView.ItemTemplate>
<GridView.ContextFlyout>
<ghost:ContextFlyout Tag="project-browser" />
</GridView.ContextFlyout>
</GridView>
<Border
Grid.Row="1"
Height="24"
Padding="8,2"
Background="{ThemeResource LayerFillColorDefaultBrush}"
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
BorderThickness="0,1,0,0">
<StackPanel>
<TextBlock Text="{x:Bind sys:String.Format('{0} items', ViewModel.Files.Count), Mode=OneWay}" />
</StackPanel>
</Border>
</Grid>
</Grid>
</Grid>
</UserControl>

View File

@@ -0,0 +1,147 @@
using CommunityToolkit.WinUI;
using Ghost.Editor.Core.Contracts;
using Ghost.Editor.Models;
using Ghost.Editor.ViewModels.Controls;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
namespace Ghost.Editor.Views.Controls;
internal sealed partial class ProjectBrowser : UserControl
{
public static ProjectBrowser? LastFocused
{
get;
private set;
}
private readonly IInspectorService _inspectorService;
private bool _isUpdatingSelection = false;
public ProjectBrowserViewModel ViewModel
{
get;
}
public ProjectBrowser()
{
_inspectorService = App.GetService<IInspectorService>();
ViewModel = App.GetService<ProjectBrowserViewModel>();
InitializeComponent();
Loaded += ProjectBrowser_Loaded;
Unloaded += ProjectBrowser_Unloaded;
GettingFocus += ProjectBrowser_GettingFocus;
}
private void ProjectBrowser_GettingFocus(UIElement sender, GettingFocusEventArgs args)
{
if (_isUpdatingSelection)
{
return;
}
LastFocused = this;
}
private void ProjectBrowser_Loaded(object sender, RoutedEventArgs e)
{
_inspectorService.OnSelectionChanged += _inspectorService_OnSelectionChanged;
}
private void ProjectBrowser_Unloaded(object sender, RoutedEventArgs e)
{
_inspectorService.OnSelectionChanged -= _inspectorService_OnSelectionChanged;
if (LastFocused == this)
{
LastFocused = null;
}
}
private void _inspectorService_OnSelectionChanged(object? sender, InspectorSelectionChangedEventArgs e)
{
if (e.Source is not ProjectBrowserViewModel)
{
PART_FilesView.DeselectAll();
PART_DirectoriesView.SelectedNodes.Clear();
}
}
private void PART_DirectoriesView_SelectionChanged(TreeView sender, TreeViewSelectionChangedEventArgs args)
{
if (_isUpdatingSelection)
{
return;
}
_isUpdatingSelection = true;
PART_FilesView.DeselectAll();
if (args.AddedItems.Count > 0 && args.AddedItems[0] is ExplorerItem selectedItem)
{
ViewModel.SelectedItem = selectedItem;
ViewModel.NavigateToDirectory(selectedItem.FullName);
}
_isUpdatingSelection = false;
}
private void PART_FilesView_SelectionChanged(object sender, SelectionChangedEventArgs args)
{
if (_isUpdatingSelection)
{
return;
}
_isUpdatingSelection = true;
PART_DirectoriesView.SelectedNodes.Clear();
if (PART_FilesView.SelectedItem is ExplorerItem selectedItem)
{
ViewModel.SelectedItem = selectedItem;
}
_isUpdatingSelection = false;
}
private async void PART_FilesView_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
{
if (PART_FilesView.SelectedItem is ExplorerItem selectedItem)
{
_isUpdatingSelection = true;
PART_FilesView.DeselectAll();
PART_DirectoriesView.SelectedNodes.Clear();
// NOTE: There is bug that the hover state of the item may remain when navigating to another folder.
// Which causes incorrect selection (double click a folder -> directories view select target folder -> files view select the item that has the same index as the double clicked one and the hover visual stay remain from last level)
// Not sure if this is a WinUI bug or something else. This may because of the virtualization of the ItemsView.
// The core issue is not sure why PART_FilesView_SelectionChanged been triggered after NavigateToDirectory is already finished. And this only happens after the first double click navigation (first time is always fine).
// HACK: Wait a moment to let the ui clear it's state, otherwise the bug above will happen because of the virtualization.
await Task.Delay(100);
ViewModel.SelectedItem = selectedItem;
var navigatedItem = ViewModel.OpenSelected();
if (navigatedItem.Item1 != null)
{
if (navigatedItem.Item2 == 0)
{
PART_DirectoriesView.SelectedItem = navigatedItem.Item1;
}
else if (navigatedItem.Item2 == 1)
{
var index = ViewModel.Files.IndexOf(navigatedItem.Item1);
PART_FilesView.SelectedIndex = index;
}
}
_isUpdatingSelection = false;
}
}
}