Update editor

This commit is contained in:
2026-02-03 21:49:14 +09:00
parent 9fcf06dbe4
commit 59991f47d5
88 changed files with 1157 additions and 1288 deletions

View File

@@ -1,6 +0,0 @@
namespace Ghost.Data.Repository;
internal class AssetsRepository
{
}

View File

@@ -1,97 +0,0 @@
using Ghost.Core;
namespace Ghost.Editor.Core.AppState;
internal partial class AppStateMachine : IDisposable, IAsyncDisposable
{
private Dictionary<StateKey, Lazy<IAppState>> _states = new();
private IAppState? _current;
private bool _disposed;
public void RegisterState(StateKey key, Func<IAppState> stateFactory)
{
_states[key] = new(stateFactory);
}
public async Task<Result> TransitionToAsync(StateKey stateKey, object? parameter = null)
{
var previous = _current;
if (!_states.TryGetValue(stateKey, out var next))
{
return Result.Failure($"State '{stateKey}' not found.");
}
Result result;
if (previous != null)
{
result = await previous.OnExitingAsync();
if (result.IsFailure)
{
return result;
}
}
result = await next.Value.OnEnteringAsync(parameter);
if (result.IsFailure)
{
if (previous != null)
{
await previous.OnEnteredAsync(parameter);
}
return result;
}
if (previous != null)
{
result = await previous.OnExitedAsync();
if (result.IsFailure)
{
await next.Value.OnExitedAsync();
await previous.OnEnteredAsync(parameter);
return result;
}
}
result = await next.Value.OnEnteredAsync(parameter);
if (result.IsFailure)
{
await next.Value.OnExitedAsync();
if (previous != null)
{
await previous.OnEnteredAsync(parameter);
}
return result;
}
_current = next.Value;
return Result.Success();
}
public void Dispose()
{
DisposeAsync().AsTask().Wait();
}
public async ValueTask DisposeAsync()
{
if (_disposed)
{
return;
}
_states.Clear();
if (_current != null)
{
await _current.OnExitingAsync();
await _current.OnExitedAsync();
}
_current = null;
_disposed = true;
}
}

View File

@@ -1,28 +0,0 @@
using Ghost.Core;
namespace Ghost.Editor.Core.AppState;
internal interface IAppState
{
/// <summary>
/// Called when exiting the state.
/// </summary>
public ValueTask<Result> OnExitingAsync();
/// <summary>
/// Called when entering the state, right after OnEnteringAsync.
/// <paramref name="parameter">can be used to pass data into the state, such as a project to load.</summary>
/// </summary>
public ValueTask<Result> OnEnteringAsync(object? parameter);
/// <summary>
/// Called when exiting the state, specifically for pose transitions.
/// </summary>
public ValueTask<Result> OnExitedAsync();
/// <summary>
/// Called when entered the state, specifically after the state has been fully initialized and is ready for interaction.
/// </summary>
/// <param name="parameter">can be used to pass data into the state, such as a project to load.</param>
public ValueTask<Result> OnEnteredAsync(object? parameter);
}

View File

@@ -1,8 +0,0 @@
namespace Ghost.Editor.Core.AppState;
internal enum StateKey
{
None,
Landing,
EngineEditor,
}

View File

@@ -208,7 +208,7 @@ public static partial class AssetDatabase
/// <summary> /// <summary>
/// Move an asset to a new location by path. /// Move an asset to a new location by path.
/// </summary> /// </summary>
/// <param name="oldPath">Current path of the asset.</param> /// <param name="oldPath">CurrentApplication path of the asset.</param>
/// <param name="newPath">New path for the asset (relative or absolute).</param> /// <param name="newPath">New path for the asset (relative or absolute).</param>
/// <returns>Result indicating success or failure.</returns> /// <returns>Result indicating success or failure.</returns>
public static ValueTask<Result> MoveAssetAsync(string oldPath, string newPath, CancellationToken token = default) public static ValueTask<Result> MoveAssetAsync(string oldPath, string newPath, CancellationToken token = default)

View File

@@ -1,5 +1,4 @@
using Ghost.Core; using Ghost.Core;
using Ghost.Data.Services;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Text.Json; using System.Text.Json;
@@ -26,7 +25,7 @@ public static partial class AssetDatabase
return Result<string>.Failure("AssetsDirectory not initialized"); return Result<string>.Failure("AssetsDirectory not initialized");
} }
var cacheDir = Path.Combine(AssetsDirectory.Parent!.FullName, ProjectService.CACHE_FOLDER, "ImportedAssets"); var cacheDir = Path.Combine(AssetsDirectory.Parent!.FullName, EditorApplication.CACHES_FOLDER_NAME, "ImportedAssets");
if (!Directory.Exists(cacheDir)) if (!Directory.Exists(cacheDir))
{ {
Directory.CreateDirectory(cacheDir); Directory.CreateDirectory(cacheDir);

View File

@@ -1,5 +1,4 @@
using Ghost.Core; using Ghost.Core;
using Ghost.Data.Services;
using Microsoft.Data.Sqlite; using Microsoft.Data.Sqlite;
using System.Text.Json; using System.Text.Json;
@@ -19,7 +18,7 @@ public static partial class AssetDatabase
throw new InvalidOperationException("AssetsDirectory is not set. Initialize() must be called first."); throw new InvalidOperationException("AssetsDirectory is not set. Initialize() must be called first.");
} }
var dbPath = Path.Combine(AssetsDirectory.Parent!.FullName, ProjectService.CACHE_FOLDER, "AssetDatabase.db"); var dbPath = Path.Combine(AssetsDirectory.Parent!.FullName, EditorApplication.CACHES_FOLDER_NAME, "AssetDatabase.db");
var cacheDir = Path.GetDirectoryName(dbPath); var cacheDir = Path.GetDirectoryName(dbPath);
if (!Directory.Exists(cacheDir)) if (!Directory.Exists(cacheDir))
{ {
@@ -301,7 +300,7 @@ public static partial class AssetDatabase
var sqlPattern = namePattern.Replace('*', '%').Replace('?', '_'); var sqlPattern = namePattern.Replace('*', '%').Replace('?', '_');
await using var cmd = s_dbConnection.CreateCommand(); await using var cmd = s_dbConnection.CreateCommand();
// Extract just the filename from the path for matching // Extract just the filename from the path for matching
// SQLite doesn't have a built-in path manipulation, so we search in the full path // SQLite doesn't have a built-in path manipulation, so we search in the full path
// and filter by checking if the pattern matches the filename part // and filter by checking if the pattern matches the filename part
@@ -319,12 +318,12 @@ public static partial class AssetDatabase
// Extract filename and check if it matches the pattern // Extract filename and check if it matches the pattern
var fileName = Path.GetFileName(path); var fileName = Path.GetFileName(path);
// Convert pattern to regex for proper matching // Convert pattern to regex for proper matching
var regexPattern = "^" + System.Text.RegularExpressions.Regex.Escape(namePattern) var regexPattern = "^" + System.Text.RegularExpressions.Regex.Escape(namePattern)
.Replace("\\*", ".*") .Replace("\\*", ".*")
.Replace("\\?", ".") + "$"; .Replace("\\?", ".") + "$";
if (System.Text.RegularExpressions.Regex.IsMatch(fileName, regexPattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase)) if (System.Text.RegularExpressions.Regex.IsMatch(fileName, regexPattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase))
{ {
if (Guid.TryParse(guidStr, out var guid)) if (Guid.TryParse(guidStr, out var guid))
@@ -377,10 +376,10 @@ public static partial class AssetDatabase
} }
// Remove orphaned entries // Remove orphaned entries
foreach (var guid in orphanedGuids) foreach (var guid in orphanedGuids)
{ {
await RemoveAssetFromDatabaseAsync(guid, token); await RemoveAssetFromDatabaseAsync(guid, token);
} }
} }
catch catch
{ {

View File

@@ -1,5 +1,4 @@
using Ghost.Core; using Ghost.Core;
using Ghost.Data.Services;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
@@ -92,12 +91,7 @@ public static partial class AssetDatabase
s_initialized = true; s_initialized = true;
} }
if (ProjectService.CurrentProject.Metadata == null) AssetsDirectory = new DirectoryInfo(Path.Combine(EditorApplication.CurrentProjectPath, EditorApplication.ASSETS_FOLDER_NAME));
{
throw new InvalidOperationException("Project metadata is not initialized. Ensure that the project is loaded before accessing the AssetDatabase.");
}
AssetsDirectory = new DirectoryInfo(Path.Combine(Path.GetDirectoryName(ProjectService.CurrentProject.Path)!, ProjectService.ASSETS_FOLDER));
s_commandChannel = Channel.CreateUnbounded<AssetCommand>(new UnboundedChannelOptions s_commandChannel = Channel.CreateUnbounded<AssetCommand>(new UnboundedChannelOptions
{ {

View File

@@ -1,7 +1,7 @@
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Core.Inspector; namespace Ghost.Editor.Core.Contracts;
public interface IInspectable public interface IInspectable
{ {

View File

@@ -0,0 +1,32 @@
namespace Ghost.Editor.Core.Contracts;
public class InspectorSelectionChangedEventArgs : EventArgs
{
public object? Source
{
get;
}
public IInspectable? Selected
{
get;
}
public InspectorSelectionChangedEventArgs(object? source, IInspectable? selected)
{
Source = source;
Selected = selected;
}
}
public interface IInspectorService
{
IInspectable? Selected
{
get;
}
event EventHandler<InspectorSelectionChangedEventArgs> OnSelectionChanged;
void SetSelected(IInspectable? inspectable, object? source);
}

View File

@@ -1,6 +1,7 @@
using CommunityToolkit.WinUI.Behaviors; using CommunityToolkit.WinUI.Behaviors;
using Ghost.Editor.Core.Notifications;
namespace Ghost.Editor.Core.Notifications; namespace Ghost.Editor.Core.Contracts;
public interface INotificationService public interface INotificationService
{ {

View File

@@ -0,0 +1,12 @@
namespace Ghost.Editor.Core.Contracts;
public enum IconSize
{
Small,
Large
}
public interface IPreviewService
{
string GetIconPath(string path, bool isDirectory, IconSize size);
}

View File

@@ -1,4 +1,4 @@
namespace Ghost.Editor.Core.Progress; namespace Ghost.Editor.Core.Contracts;
public interface IProgressService public interface IProgressService
{ {

View File

@@ -0,0 +1,39 @@
using Microsoft.UI.Xaml;
namespace Ghost.Editor.Core;
public static class EditorApplication
{
public const string ASSETS_FOLDER_NAME = "Assets";
public const string CACHES_FOLDER_NAME = "Caches";
private static IServiceProvider? s_serviceProvider;
private static string s_currentProjectPath = string.Empty;
private static string s_currentProjectName = string.Empty;
internal static Application CurrentApplication => Application.Current;
internal static string CurrentProjectPath => s_currentProjectPath;
internal static string CurrentProjectName => s_currentProjectName;
internal static void Initialize(IServiceProvider serviceProvider, string projectPath, string projectName)
{
s_serviceProvider = serviceProvider;
s_currentProjectPath = projectPath;
s_currentProjectName = projectName;
}
public static T GetService<T>()
where T : class
{
if (s_serviceProvider?.GetService(typeof(T)) is not T service)
{
throw new ArgumentException($"{typeof(T)} needs to be registered in ConfigureServices.");
}
return service;
}
internal static void Shutdown()
{
}
}

View File

@@ -0,0 +1,28 @@
namespace Ghost.Editor.Core;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = false, Inherited = false)]
public class EditorInjectionAttribute : Attribute
{
public enum ServiceLifetime
{
Singleton,
Transient,
Scoped
}
public ServiceLifetime Lifetime
{
get;
}
public Type? ImplementationType
{
get;
}
public EditorInjectionAttribute(ServiceLifetime lifetime, Type? implementationType = null)
{
Lifetime = lifetime;
ImplementationType = implementationType;
}
}

View File

@@ -21,7 +21,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Ghost.Data\Ghost.Data.csproj" />
<ProjectReference Include="..\Ghost.Core\Ghost.Core.csproj" /> <ProjectReference Include="..\Ghost.Core\Ghost.Core.csproj" />
<ProjectReference Include="..\Ghost.Engine\Ghost.Engine.csproj" /> <ProjectReference Include="..\Ghost.Engine\Ghost.Engine.csproj" />
</ItemGroup> </ItemGroup>

View File

@@ -1,12 +0,0 @@
namespace Ghost.Editor.Core.Inspector;
internal interface IInspectorService
{
public IInspectable? SelectedInspectable
{
get;
set;
}
public event Action? OnSelectionChanged;
}

View File

@@ -1,19 +0,0 @@
namespace Ghost.Editor.Core.Inspector;
public class InspectorService : IInspectorService
{
public IInspectable? SelectedInspectable
{
get => field;
set
{
if (field != value)
{
field = value;
OnSelectionChanged?.Invoke();
}
}
}
public event Action? OnSelectionChanged;
}

View File

@@ -1,5 +1,5 @@
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using Ghost.Editor.Core.Inspector; using Ghost.Editor.Core.Contracts;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;

View File

@@ -0,0 +1,21 @@
using Ghost.Editor.Core.Contracts;
namespace Ghost.Editor.Core.Services;
public class InspectorService : IInspectorService
{
private IInspectable? _selected;
public IInspectable? Selected => _selected;
public event EventHandler<InspectorSelectionChangedEventArgs>? OnSelectionChanged;
public void SetSelected(IInspectable? inspectable, object? source)
{
if (_selected != inspectable)
{
_selected = inspectable;
OnSelectionChanged?.Invoke(this, new InspectorSelectionChangedEventArgs(source, inspectable));
}
}
}

View File

@@ -1,7 +1,9 @@
using CommunityToolkit.WinUI.Behaviors; using CommunityToolkit.WinUI.Behaviors;
using Ghost.Editor.Core.Contracts;
using Ghost.Editor.Core.Notifications;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Core.Notifications; namespace Ghost.Editor.Core.Services;
public class NotificationService : INotificationService public class NotificationService : INotificationService
{ {

View File

@@ -0,0 +1,35 @@
using Ghost.Editor.Core.Contracts;
namespace Ghost.Editor.Core.Services;
internal class PreviewService : IPreviewService
{
public string GetIconPath(string path, bool isDirectory, IconSize size)
{
string iconPath;
if (isDirectory)
{
iconPath = "ms-appx:///Assets/EditorIcons/folder-{0}.png";
}
else
{
// TODO: Generate preview icons dynamically for known file types like images, meshes, materials, etc.
var ext = Path.GetExtension(path);
iconPath = ext switch
{
".png" or ".jpg" or ".jpeg" or ".gif" or ".bmp" or ".tiff" or ".svg" => "ms-appx:///Assets/EditorIcons/image-{0}.png",
_ => "ms-appx:///Assets/EditorIcons/document-{0}.png",
};
}
var sizeIndex = size switch
{
IconSize.Small => "0",
IconSize.Large => "1",
_ => "0"
};
iconPath = string.Format(iconPath, sizeIndex);
return iconPath;
}
}

View File

@@ -1,9 +1,10 @@
using CommunityToolkit.WinUI; using CommunityToolkit.WinUI;
using Ghost.Editor.Core.Contracts;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
namespace Ghost.Editor.Core.Progress; namespace Ghost.Editor.Core.Services;
public class ProgressService : IProgressService public class ProgressService : IProgressService
{ {

View File

@@ -1,26 +0,0 @@
using Microsoft.UI.Xaml;
namespace Ghost.Editor.Core.Utilities;
public static class EditorApplication
{
private static IServiceProvider? _serviceProvider;
public static Application Current => Application.Current;
internal static void Initialize(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public static T GetService<T>()
where T : class
{
if (_serviceProvider?.GetService(typeof(T)) is not T service)
{
throw new ArgumentException($"{typeof(T)} needs to be registered in ConfigureServices within App.xaml.cs.");
}
return service;
}
}

View File

@@ -1,12 +1,10 @@
using Ghost.Data.Models;
namespace Ghost.Editor.Core.Utilities; namespace Ghost.Editor.Core.Utilities;
internal static class FileExtensions internal static class FileExtensions
{ {
public const string META_FILE_EXTENSION = ".gmeta"; public const string META_FILE_EXTENSION = ".gmeta";
public const string PROJECT_FILE_EXTENSION = "." + ProjectMetadata.PROJECT_FILE_EXTENSION_NAME; public const string PROJECT_FILE_EXTENSION = ".gproj";
public const string TEMPLATE_FILE_EXTENSION = ".gtmpl"; public const string TEMPLATE_FILE_EXTENSION = ".gtmpl";
public const string SCENE_FILE_EXTENSION = ".gscene"; public const string SCENE_FILE_EXTENSION = ".gscene";
public const string ASSET_FILE_EXTENSION = ".gasset"; public const string ASSET_FILE_EXTENSION = ".gasset";

View File

@@ -29,6 +29,12 @@ public static class TypeCache
s_types = loadableTypes.Select(t => t.GetTypeInfo()).ToArray(); s_types = loadableTypes.Select(t => t.GetTypeInfo()).ToArray();
} }
internal static void Init()
{
// Intentionally left blank.
// This method exists to force the static constructor to run.
}
public static Type[] GetTypes() public static Type[] GetTypes()
{ {
return s_types; return s_types;

View File

@@ -1,30 +1,67 @@
using Ghost.Data.Resources; using Ghost.Editor.Core;
using Ghost.Data.Services;
using Ghost.Editor.Core.Utilities; using Ghost.Editor.Core.Utilities;
using Ghost.Editor.Models;
using Ghost.Engine;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using System.Reflection;
namespace Ghost.Editor; namespace Ghost.Editor;
internal static class ActivationHandler internal static class ActivationHandler
{ {
private static void FolderInitialization() public static LaunchArguments ParseArguments(ReadOnlySpan<char> args)
{ {
if (!Directory.Exists(DataPath.s_applicationDataFolder)) var arguments = new LaunchArguments();
var properties = typeof(LaunchArguments).GetProperties();
var split = args.Split(' ');
while (split.MoveNext())
{ {
Directory.CreateDirectory(DataPath.s_applicationDataFolder); var range = split.Current;
var arg = args[range.Start..range.End];
if (arg.Length > 2)
{
if (arg[0] == '-' && arg[1] == '-')
{
var argName = arg[2..];
foreach (var property in properties)
{
var propName = property.Name;
var attr = property.GetCustomAttributes<ArgumentNameAttribute>(false).FirstOrDefault();
if (attr != null)
{
propName = attr.Name;
}
if (argName.Equals(propName, StringComparison.OrdinalIgnoreCase))
{
if (split.MoveNext())
{
var valueRange = split.Current;
var value = args[valueRange.Start..valueRange.End];
var convertedValue = Convert.ChangeType(value.ToString(), property.PropertyType);
property.SetValue(arguments, convertedValue);
break;
}
}
}
}
}
} }
if (!Directory.Exists(DataPath.s_projectTemplateFolder)) return arguments;
{
Directory.CreateDirectory(DataPath.s_projectTemplateFolder);
}
} }
public static void Handle(LaunchActivatedEventArgs args) public static async Task HandleAsync(LaunchArguments args)
{ {
FolderInitialization(); await Task.Run(() =>
ProjectService.EnsureDefaultTemplate(); {
TypeCache.Init();
((EngineCore)App.GetService<IEngineContext>()).Init();
});
EditorApplication.Initialize(((App)(Application.Current)).Host.Services); // TODO: Initialize other subsystems here.
// await Task.Delay(10000); // Wait 10 seconds to simulate work.
} }
} }

View File

@@ -9,9 +9,8 @@
<ResourceDictionary> <ResourceDictionary>
<ResourceDictionary.MergedDictionaries> <ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" /> <XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<ResourceDictionary Source="ms-appx:///Microsoft.UI.Xaml/DensityStyles/Compact.xaml" /> <ResourceDictionary Source="/Themes/Generic.xaml" />
<core:ControlsDictionary /> <core:ControlsDictionary />
<ResourceDictionary Source="/Themes/Override.xaml" />
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
</ResourceDictionary> </ResourceDictionary>
</Application.Resources> </Application.Resources>

View File

@@ -1,15 +1,18 @@
using Ghost.Core; using Ghost.Core;
using Ghost.Editor.Core.AppState; using Ghost.Editor.Core;
using Ghost.Editor.Core.Inspector; using Ghost.Editor.Core.Contracts;
using Ghost.Editor.Core.Notifications; using Ghost.Editor.Core.Services;
using Ghost.Editor.Core.Progress; using Ghost.Editor.View.Pages.EngineEditor;
using Ghost.Editor.Utilities; using Ghost.Editor.View.Windows;
using Ghost.Editor.ViewModels.Controls;
using Ghost.Editor.ViewModels.Pages.EngineEditor;
using Ghost.Editor.ViewModels.Windows;
using Ghost.Engine;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using System.Diagnostics;
// To learn more about WinUI, the WinUI project structure, using WinUIEx;
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace Ghost.Editor; namespace Ghost.Editor;
@@ -53,13 +56,32 @@ public partial class App : Application
UseContentRoot(AppContext.BaseDirectory). UseContentRoot(AppContext.BaseDirectory).
ConfigureServices((context, services) => ConfigureServices((context, services) =>
{ {
HostHelper.AddLandingScope(context, services); services.AddSingleton<IEngineContext, EngineCore>();
HostHelper.AddEngineScope(context, services);
services.AddSingleton<AppStateMachine>();
services.AddSingleton<INotificationService, NotificationService>(); services.AddSingleton<INotificationService, NotificationService>();
services.AddSingleton<IProgressService, ProgressService>(); services.AddSingleton<IProgressService, ProgressService>();
services.AddSingleton<IInspectorService, InspectorService>(); services.AddSingleton<IInspectorService, InspectorService>();
services.AddSingleton<IPreviewService, PreviewService>();
services.AddSingleton<EngineEditorViewModel>();
services.AddTransient<ProjectBrowserViewModel>();
#region Should be deleted
services.AddTransient<ScenePage>();
services.AddTransient<HierarchyPage>();
services.AddTransient<HierarchyViewModel>();
services.AddTransient<ProjectPage>();
services.AddTransient<ProjectViewModel>();
services.AddTransient<ConsolePage>();
services.AddTransient<ConsoleViewModel>();
services.AddTransient<InspectorPage>();
services.AddTransient<InspectorViewModel>();
#endregion
}) })
.Build(); .Build();
@@ -81,32 +103,55 @@ public partial class App : Application
return service; return service;
} }
/// <summary>
/// Invoked when the application is launched.
/// </summary>
/// <param name="args">Details about the launch request and process.</param>
protected override async void OnLaunched(LaunchActivatedEventArgs args) protected override async void OnLaunched(LaunchActivatedEventArgs args)
{ {
base.OnLaunched(args); base.OnLaunched(args);
var arguments = ActivationHandler.ParseArguments("--project-path F:/GhostProject/Test2 --project-name Test2"); // args.Arguments
if (!arguments.IsValid())
{
Exit();
return;
}
EditorApplication.Initialize(Host.Services, arguments.ProjectPath, arguments.ProjectName);
var splashWindow = new SplashWindow();
splashWindow.Activate();
Window = splashWindow;
await Host.StartAsync(); await Host.StartAsync();
ActivationHandler.Handle(args); await ActivationHandler.HandleAsync(arguments);
var stateMachine = GetService<AppStateMachine>(); splashWindow.Hide();
stateMachine.RegisterState(StateKey.Landing, () => new LandingState());
stateMachine.RegisterState(StateKey.EngineEditor, () => new EditorState());
await stateMachine.TransitionToAsync(StateKey.Landing); var editorWindow = new EngineEditorWindow();
editorWindow.Activate();
Window = editorWindow;
splashWindow.Close();
} }
private void OnClosed(object? sender, WindowEventArgs args) private void OnClosed(object? sender, WindowEventArgs args)
{ {
Host.StopAsync().GetAwaiter().GetResult(); try
Host.Dispose(); {
Host.StopAsync().GetAwaiter().GetResult();
Host.Dispose();
EditorApplication.Shutdown();
}
catch (Exception ex)
{
Debugger.BreakForUserUnhandledException(ex);
}
} }
private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e) private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{ {
Logger.LogError(e.Exception); Logger.LogError(e.Exception);
#if DEBUG
Debugger.BreakForUserUnhandledException(e.Exception);
#endif
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 453 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 869 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 884 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 727 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 433 B

After

Width:  |  Height:  |  Size: 580 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 583 B

After

Width:  |  Height:  |  Size: 825 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 852 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

View File

@@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 48 48" width="48px" height="48px">
<defs>
<style>
.cls-1 {
fill: url(#Безымянный_градиент_199);
}
.cls-2 {
fill: #f7c13a;
}
</style>
<linearGradient id=езымянный_градиент_199" data-name="Безымянный градиент 199" x1="11.80415" y1="-22.237" x2="29.6085" y2="45.263" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#6d6d6d"/>
<stop offset="0.12552" stop-color="#626262"/>
<stop offset="0.987" stop-color="#464646"/>
<stop offset="0.998" stop-color="#454545"/>
</linearGradient>
</defs>
<rect class="cls-1" x="6" y="6" width="36" height="36" rx="2"/>
<path class="cls-2" d="M22,27v1.5a.5.5,0,0,1-.5.5H17a8,8,0,0,1-8-8V19.5a.5.5,0,0,1,.5-.5h4.00612a.501.501,0,0,1,.49758.49688C14.05815,23.13441,14.70582,26,15.5,26c.73344,0,1.33761-2.43083,1.46835-5.65979a.50624.50624,0,0,1,.74437-.42711A7.99072,7.99072,0,0,1,22,27Z"/>
<path class="cls-2" d="M39,19.5V21a8,8,0,0,1-8,8H26.5a.5.5,0,0,1-.5-.5V27a7.99072,7.99072,0,0,1,4.28728-7.0869.50624.50624,0,0,1,.74437.42711C31.16239,23.56917,31.76656,26,32.5,26c.79418,0,1.44185-2.86559,1.4963-6.50312A.501.501,0,0,1,34.49388,19H38.5A.5.5,0,0,1,39,19.5Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 580 B

After

Width:  |  Height:  |  Size: 580 B

View File

Before

Width:  |  Height:  |  Size: 580 B

After

Width:  |  Height:  |  Size: 580 B

View File

Before

Width:  |  Height:  |  Size: 825 B

After

Width:  |  Height:  |  Size: 825 B

View File

Before

Width:  |  Height:  |  Size: 825 B

After

Width:  |  Height:  |  Size: 825 B

View File

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,38 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Ghost.Editor.Core.Contracts;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Navigation;
namespace Ghost.Editor.Controls;
public abstract partial class ViewModelPage<VM> : Page
where VM : ObservableObject
{
public VM ViewModel
{
get;
}
protected ViewModelPage(VM viewModel)
{
ViewModel = viewModel;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if (ViewModel is INavigationAware navigationAware)
{
navigationAware.OnNavigatedTo(e.Parameter);
}
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
if (ViewModel is INavigationAware navigationAware)
{
navigationAware.OnNavigatedFrom();
}
}
}

View File

@@ -1,60 +0,0 @@
using Ghost.Core;
using Ghost.Data.Models;
using Ghost.Data.Services;
using Ghost.Editor.Core.AssetHandle;
using Ghost.Editor.View.Windows;
using Ghost.Engine;
namespace Ghost.Editor.Core.AppState;
internal class EditorState : IAppState
{
private EngineEditorWindow? _window;
private EngineCore? _engineCore;
public ValueTask<Result> OnExitingAsync()
{
if (App.Window == _window)
{
App.Window = null;
}
_engineCore?.Dispose();
return ValueTask.FromResult(Result.Success());
}
public ValueTask<Result> OnEnteringAsync(object? parameter)
{
if (parameter is not ProjectMetadataInfo metadataInfo)
{
return ValueTask.FromResult(Result.Failure("Invalid parameter for entering EditorState."));
}
ProjectService.CurrentProject = metadataInfo;
_engineCore = App.GetService<EngineCore>();
_engineCore.Init();
_window = App.GetService<EngineEditorWindow>();
_window.Activate();
App.Window = _window;
return ValueTask.FromResult(Result.Success());
}
public ValueTask<Result> OnExitedAsync()
{
_window?.Close();
_window = null;
return ValueTask.FromResult(Result.Success());
}
public async ValueTask<Result> OnEnteredAsync(object? parameter)
{
await AssetDatabase.Initialize();
return Result.Success();
}
}

View File

@@ -1,42 +0,0 @@
using Ghost.Core;
using Ghost.Editor.View.Windows;
namespace Ghost.Editor.Core.AppState;
internal class LandingState : IAppState
{
private LandingWindow? _window;
public ValueTask<Result> OnExitingAsync()
{
if (App.Window == _window)
{
App.Window = null;
}
return ValueTask.FromResult(Result.Success());
}
public ValueTask<Result> OnEnteringAsync(object? parameter)
{
_window = App.GetService<LandingWindow>();
_window.Activate();
App.Window = _window;
return ValueTask.FromResult(Result.Success());
}
public ValueTask<Result> OnExitedAsync()
{
_window?.Close();
_window = null;
return ValueTask.FromResult(Result.Success());
}
public ValueTask<Result> OnEnteredAsync(object? parameter)
{
return ValueTask.FromResult(Result.Success());
}
}

View File

@@ -45,7 +45,106 @@
<ProjectReference Include="..\Ghost.Entities\Ghost.Entities.csproj" /> <ProjectReference Include="..\Ghost.Entities\Ghost.Entities.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="View\Pages\Landing\CreateProjectPage.xaml"> <Content Update="Assets\EditorIcons\document-0.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\EditorIcons\document-1.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\EditorIcons\image-0.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\EditorIcons\image-1.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\icon.ico">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\icon.scale-100.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\icon.scale-125.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\icon.scale-150.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\icon.scale-200.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\icon.scale-400.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\icon.svg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\icon.targetsize-16.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\icon.targetsize-16_altform-lightunplated.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\icon.targetsize-16_altform-unplated.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\icon.targetsize-24.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\icon.targetsize-24_altform-lightunplated.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\icon.targetsize-24_altform-unplated.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\icon.targetsize-256.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\icon.targetsize-256_altform-lightunplated.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\icon.targetsize-256_altform-unplated.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\icon.targetsize-32.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\icon.targetsize-32_altform-lightunplated.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\icon.targetsize-32_altform-unplated.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\icon.targetsize-48.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\icon.targetsize-48_altform-lightunplated.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\icon.targetsize-48_altform-unplated.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<None Update="Assets\icon.svg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<None Update="Assets\EditorIcons\folder-0.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Assets\EditorIcons\folder-1.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Assets\icon.ico">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<Page Update="Themes\Generic.xaml">
<SubType>Designer</SubType>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Windows\SplashWindow.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
@@ -54,11 +153,6 @@
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Page Update="View\Pages\Landing\OpenProjectPage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="View\Pages\EngineEditor\InspectorPage.xaml"> <Page Update="View\Pages\EngineEditor\InspectorPage.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
@@ -79,11 +173,6 @@
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Page Update="Themes\Override.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="View\Windows\EngineEditorWindow.xaml"> <Page Update="View\Windows\EngineEditorWindow.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>

View File

@@ -0,0 +1,35 @@
namespace Ghost.Editor.Models;
[AttributeUsage(AttributeTargets.Property)]
internal sealed class ArgumentNameAttribute : Attribute
{
public string Name
{
get;
}
public ArgumentNameAttribute(string name)
{
Name = name;
}
}
internal class LaunchArguments
{
[ArgumentName("project-path")]
public string ProjectPath
{
get; set;
} = string.Empty;
[ArgumentName("project-name")]
public string ProjectName
{
get; set;
} = string.Empty;
public bool IsValid()
{
return Directory.Exists(ProjectPath) && !string.IsNullOrWhiteSpace(ProjectName);
}
}

View File

@@ -36,7 +36,7 @@
<uap:VisualElements <uap:VisualElements
DisplayName="GhostEngine" DisplayName="GhostEngine"
Description="GhostEngine" Description="GhostEngine"
Square150x150Logo="Assets\Square150x150Logo.png" Square44x44Logo="Assets\Icon.png" BackgroundColor="transparent"> Square150x150Logo="Assets\Square150x150Logo.png" Square44x44Logo="Assets\icon.png" BackgroundColor="transparent">
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" > <uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" >
</uap:DefaultTile > </uap:DefaultTile >
<uap:SplashScreen Image="Assets\SplashScreen.png" /> <uap:SplashScreen Image="Assets\SplashScreen.png" />

View File

@@ -0,0 +1,54 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.UI.Xaml.Controls"
xmlns:internal="using:Ghost.Editor.Controls.Internal"
xmlns:local="using:Ghost.Editor.Core">
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Dark">
<StaticResource x:Key="TabViewItemHeaderBackgroundSelected" ResourceKey="ControlFillColorSecondaryBrush" />
<StaticResource x:Key="TabViewBackground" ResourceKey="AcrylicBackgroundFillColorBaseBrush" />
</ResourceDictionary>
<ResourceDictionary x:Key="Light">
<StaticResource x:Key="TabViewItemHeaderBackgroundSelected" ResourceKey="ControlFillColorSecondaryBrush" />
<StaticResource x:Key="TabViewBackground" ResourceKey="AcrylicBackgroundFillColorBaseBrush" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<!-- Compact sizing -->
<Style TargetType="TextBlock">
<Setter Property="FontSize" Value="12" />
</Style>
<x:Double x:Key="ControlContentThemeFontSize">12</x:Double>
<x:Double x:Key="ContentControlFontSize">12</x:Double>
<x:Double x:Key="TextControlThemeMinHeight">24</x:Double>
<Thickness x:Key="TextControlThemePadding">2,2,6,1</Thickness>
<x:Double x:Key="ListViewItemMinHeight">32</x:Double>
<x:Double x:Key="TreeViewItemMinHeight">24</x:Double>
<x:Double x:Key="TreeViewItemMultiSelectCheckBoxMinHeight">24</x:Double>
<x:Double x:Key="TreeViewItemPresenterMargin">0</x:Double>
<x:Double x:Key="TreeViewItemPresenterPadding">0</x:Double>
<Thickness x:Key="TimePickerHostPadding">0,1,0,2</Thickness>
<Thickness x:Key="DatePickerHostPadding">0,1,0,2</Thickness>
<Thickness x:Key="DatePickerHostMonthPadding">9,0,0,1</Thickness>
<Thickness x:Key="ComboBoxEditableTextPadding">10,0,30,0</Thickness>
<x:Double x:Key="ComboBoxMinHeight">24</x:Double>
<Thickness x:Key="ComboBoxPadding">12,1,0,3</Thickness>
<x:Double x:Key="NavigationViewItemOnLeftMinHeight">32</x:Double>
<!-- Control override -->
<Style TargetType="internal:NavigationTabView">
<Setter Property="TabWidthMode" Value="Compact" />
</Style>
<Style TargetType="NumberBox" />
<!-- Named Style -->
<Style
x:Key="ToolbarButton"
BasedOn="{StaticResource SubtleButtonStyle}"
TargetType="Button" />
<!-- Named Resource -->
<x:Double x:Key="ToolbarIconSize">12</x:Double>
</ResourceDictionary>

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.UI.Xaml.Controls"
xmlns:internal="using:Ghost.Editor.Controls.Internal">
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Dark">
<StaticResource x:Key="TabViewItemHeaderBackgroundSelected" ResourceKey="ControlFillColorSecondaryBrush" />
</ResourceDictionary>
<ResourceDictionary x:Key="Light">
<StaticResource x:Key="TabViewItemHeaderBackgroundSelected" ResourceKey="ControlFillColorSecondaryBrush" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<Style TargetType="internal:NavigationTabView">
<Setter Property="TabWidthMode" Value="Compact" />
</Style>
<Style TargetType="NumberBox" />
</ResourceDictionary>

View File

@@ -0,0 +1,26 @@
using Ghost.Editor.Core.Contracts;
using Ghost.Editor.Models;
using Microsoft.UI.Xaml.Data;
namespace Ghost.Editor.Utilities.Converters;
public partial class ExplorerItemToIconUriConverter : IValueConverter
{
private readonly IPreviewService _previewService = App.GetService<IPreviewService>();
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is ExplorerItem item)
{
var path = _previewService.GetIconPath(item.FullName, item.IsDirectory, IconSize.Small);
return new Uri(path);
}
throw new NotSupportedException();
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}

View File

@@ -1,50 +0,0 @@
using Ghost.Data.Services;
using Ghost.Editor.View.Pages.EngineEditor;
using Ghost.Editor.View.Pages.Landing;
using Ghost.Editor.View.Windows;
using Ghost.Editor.ViewModels.Pages.EngineEditor;
using Ghost.Editor.ViewModels.Pages.Landing;
using Ghost.Editor.ViewModels.Windows;
using Ghost.Engine;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Ghost.Editor.Utilities;
internal static partial class HostHelper
{
public static void AddLandingScope(HostBuilderContext context, IServiceCollection services)
{
services.AddSingleton<LandingWindow>();
services.AddTransient<CreateProjectPage>();
services.AddTransient<CreateProjectViewModel>();
services.AddTransient<OpenProjectPage>();
services.AddTransient<OpenProjectViewModel>();
services.AddTransient<ProjectService>();
}
public static void AddEngineScope(HostBuilderContext context, IServiceCollection services)
{
services.AddSingleton<EngineCore>();
services.AddSingleton<EngineEditorWindow>();
services.AddSingleton<EngineEditorViewModel>();
services.AddTransient<ScenePage>();
services.AddTransient<HierarchyPage>();
services.AddTransient<HierarchyViewModel>();
services.AddTransient<ProjectPage>();
services.AddTransient<ProjectViewModel>();
services.AddTransient<ConsolePage>();
services.AddTransient<ConsoleViewModel>();
services.AddTransient<InspectorPage>();
services.AddTransient<InspectorViewModel>();
}
}

View File

@@ -0,0 +1,224 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="Ghost.Editor.Controls.ProjectBrowser"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converter="using:Ghost.Editor.Utilities.Converters"
xmlns:ctc="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Ghost.Editor.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>
<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="{StaticResource 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" QueryIcon="Find" />
<AppBarSeparator />
<Button Style="{StaticResource 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="{StaticResource 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>-->
<ItemsView
x:Name="PART_FilesView"
Grid.Row="0"
Padding="8"
DoubleTapped="PART_FilesView_DoubleTapped"
IsDoubleTapEnabled="True"
ItemsSource="{x:Bind ViewModel.Files, Mode=OneWay}"
SelectionChanged="PART_FilesView_SelectionChanged"
SelectionMode="Single">
<ItemsView.ItemTemplate>
<DataTemplate x:DataType="model:ExplorerItem">
<ItemContainer>
<Grid
Padding="8"
HorizontalAlignment="Center"
VerticalAlignment="Center"
RowSpacing="4">
<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>
<ctc:ConstrainedBox Grid.Row="0" AspectRatio="1:1">
<Image HorizontalAlignment="Center">
<Image.Source>
<BitmapImage DecodePixelWidth="48" UriSource="{x:Bind Converter={StaticResource ExplorerItemToIconUriConverter}}" />
</Image.Source>
</Image>
</ctc:ConstrainedBox>
<TextBlock
Grid.Row="1"
HorizontalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind Name}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" />
</Grid>
</ItemContainer>
</DataTemplate>
</ItemsView.ItemTemplate>
<ItemsView.Layout>
<UniformGridLayout
ItemsStretch="Fill"
MinColumnSpacing="4"
MinItemWidth="72"
MinRowSpacing="4" />
</ItemsView.Layout>
<ItemsView.ContextFlyout>
<MenuFlyout>
<MenuFlyoutSubItem Text="Create">
<MenuFlyoutItem Text="Folder" />
<MenuFlyoutItem Text="Script" />
<MenuFlyoutSubItem Text="Rendering">
<MenuFlyoutItem Text="Material" />
<MenuFlyoutItem Text="Volume Profile" />
</MenuFlyoutSubItem>
</MenuFlyoutSubItem>
<MenuFlyoutItem Text="Show in Explorer" />
<MenuFlyoutSeparator />
<MenuFlyoutItem Text="Refresh" />
<MenuFlyoutItem Text="Reimport All" />
</MenuFlyout>
</ItemsView.ContextFlyout>
</ItemsView>
<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,122 @@
using Ghost.Editor.Core.Contracts;
using Ghost.Editor.Models;
using Ghost.Editor.ViewModels.Controls;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Controls;
internal sealed partial class ProjectBrowser : UserControl
{
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;
}
private void ProjectBrowser_Loaded(object sender, RoutedEventArgs e)
{
_inspectorService.OnSelectionChanged += _inspectorService_OnSelectionChanged;
}
private void ProjectBrowser_Unloaded(object sender, RoutedEventArgs e)
{
_inspectorService.OnSelectionChanged -= _inspectorService_OnSelectionChanged;
}
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(ItemsView sender, ItemsViewSelectionChangedEventArgs 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, Microsoft.UI.Xaml.Input.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.Select(index);
}
}
_isUpdatingSelection = false;
}
}
}

View File

@@ -1,5 +1,5 @@
using Ghost.Editor.Controls.Internal; using Ghost.Editor.Controls.Internal;
using Ghost.Editor.Core.Inspector; using Ghost.Editor.Core.Contracts;
using Ghost.Editor.Core.SceneGraph; using Ghost.Editor.Core.SceneGraph;
using Ghost.Editor.ViewModels.Pages.EngineEditor; using Ghost.Editor.ViewModels.Pages.EngineEditor;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
@@ -38,11 +38,11 @@ internal sealed partial class HierarchyPage : NavigationTabPage
{ {
if (args.AddedItems.Count > 0 && args.AddedItems[0] is IInspectable inspectable) if (args.AddedItems.Count > 0 && args.AddedItems[0] is IInspectable inspectable)
{ {
_inspectorService.SelectedInspectable = inspectable; _inspectorService.SetSelected(inspectable, ViewModel);
} }
else else
{ {
_inspectorService.SelectedInspectable = null; _inspectorService.SetSelected(null, ViewModel);
} }
} }
} }

View File

@@ -1,142 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<Page
x:Class="Ghost.Editor.View.Pages.Landing.CreateProjectPage"
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:data="using:Ghost.Data.Models"
xmlns:editor="using:Ghost.Editor.Core.Controls"
xmlns:local="using:Ghost.Editor.View.Pages.Landing"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
NavigationCacheMode="Enabled"
mc:Ignorable="d">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Template Info -->
<Grid Grid.Column="0" Width="300">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock
Grid.Row="0"
Margin="0,0,0,24"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="Template" />
<ListView
Grid.Row="1"
ItemsSource="{x:Bind ViewModel.templates}"
SelectedItem="{x:Bind ViewModel.SelectedTemplate, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="data:TemplateData">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ImageIcon
Grid.Column="0"
Width="24"
Height="24">
<ImageIcon.Source>
<BitmapImage UriSource="{x:Bind GetIconURI()}" />
</ImageIcon.Source>
</ImageIcon>
<TextBlock
Grid.Column="1"
Margin="8,0"
VerticalAlignment="Center"
Text="{x:Bind Info.Name}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
<!-- Project Info -->
<Grid
Grid.Column="1"
Margin="16,0,0,0"
Padding="16"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="{StaticResource OverlayCornerRadius}">
<Grid.RowDefinitions>
<RowDefinition Height="300" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" CornerRadius="4">
<Image VerticalAlignment="Center" Stretch="UniformToFill">
<Image.Source>
<BitmapImage UriSource="{x:Bind ViewModel.SelectedTemplate.Value.GetPreviewURI(), Mode=OneWay}" />
</Image.Source>
</Image>
<Grid
MaxHeight="100"
VerticalAlignment="Bottom"
Background="{ThemeResource ControlOnImageFillColorDefaultBrush}">
<TextBlock
Margin="16"
VerticalAlignment="Bottom"
Foreground="{ThemeResource TextFillColorTertiaryBrush}"
Text="{x:Bind ViewModel.SelectedTemplate.Value.Info.Description, Mode=OneWay}" />
</Grid>
</Grid>
<StackPanel Grid.Row="1" Margin="8,0">
<TextBlock
Margin="0,16,0,8"
Style="{StaticResource TitleTextBlockStyle}"
Text="{x:Bind ViewModel.SelectedTemplate.Value.Info.Name, Mode=OneWay}" />
<TextBlock
Margin="0,8,0,16"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="Project Settings" />
<editor:PropertyField Label="Name">
<TextBox Text="{x:Bind ViewModel.ProjectName, Mode=TwoWay}" />
</editor:PropertyField>
<editor:PropertyField Label="Location">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox
Grid.Column="0"
IsReadOnly="True"
Text="{x:Bind ViewModel.ProjectLocation, Mode=TwoWay}" />
<Button
Grid.Column="1"
Margin="4,0,0,0"
VerticalAlignment="Stretch"
Command="{x:Bind ViewModel.SelectionProjectLocationCommand}">
<FontIcon FontSize="16" Glyph="&#xE8DA;" />
</Button>
</Grid>
</editor:PropertyField>
</StackPanel>
<Grid Grid.Row="2">
<Button
Width="150"
HorizontalAlignment="Right"
Command="{x:Bind ViewModel.CreateProjectCommand}"
Content="Create"
Style="{ThemeResource AccentButtonStyle}" />
</Grid>
</Grid>
</Grid>
</Page>

View File

@@ -1,32 +0,0 @@
using Ghost.Editor.ViewModels.Pages.Landing;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Navigation;
namespace Ghost.Editor.View.Pages.Landing;
internal sealed partial class CreateProjectPage : Page
{
public CreateProjectViewModel ViewModel
{
get;
}
public CreateProjectPage()
{
ViewModel = App.GetService<CreateProjectViewModel>();
InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
ViewModel.OnNavigatedTo(e.Parameter);
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
ViewModel.OnNavigatedFrom();
}
}

View File

@@ -1,165 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<Page
x:Class="Ghost.Editor.View.Pages.Landing.OpenProjectPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="using:Ghost.Editor.Utilities.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:data="using:Ghost.Data.Models"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:local="using:Ghost.Editor.View.Pages.Landing"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
NavigationCacheMode="Enabled"
mc:Ignorable="d">
<Page.Resources>
<converters:GetDirectoryNameConverter x:Key="DirNameConverter" />
</Page.Resources>
<Grid x:Name="MainContainer">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="16,4">
<TextBlock
VerticalAlignment="Center"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="Projects" />
<AutoSuggestBox
Width="300"
HorizontalAlignment="Right"
PlaceholderText="Search project by name"
QueryIcon="Find" />
</Grid>
<!-- Header for the ListView -->
<Grid Grid.Row="1" Margin="28,16,45,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="200" />
<ColumnDefinition Width="165" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Style="{StaticResource CaptionTextBlockStyle}"
Text="NAME" />
<TextBlock
Grid.Column="1"
HorizontalAlignment="Right"
Style="{StaticResource CaptionTextBlockStyle}"
Text="LAST OPEN" />
<TextBlock
Grid.Column="2"
HorizontalAlignment="Right"
Style="{StaticResource CaptionTextBlockStyle}"
Text="ENGINE VERSION" />
</Grid>
<!-- Project ListView -->
<Grid
Grid.Row="2"
Padding="8"
AllowDrop="True"
DragEnter="ProjectContainer_DragEnter"
DragLeave="ProjectContainer_DragLeave"
DragOver="ProjectContainer_DragOver"
Drop="ProjectContainer_Drop">
<ListView
Padding="4,8"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="{StaticResource OverlayCornerRadius}"
IsItemClickEnabled="True"
ItemClick="ListView_ItemClick"
ItemsSource="{x:Bind ViewModel.projects}"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="data:ProjectMetadataInfo">
<Grid Height="64" Padding="4,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="200" />
<ColumnDefinition Width="100" />
<ColumnDefinition Width="65" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" VerticalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock
Grid.Row="0"
VerticalAlignment="Center"
FontSize="16"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="{x:Bind Metadata.Name}" />
<TextBlock
Grid.Row="1"
Margin="0,4,0,0"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind Path, Converter={StaticResource DirNameConverter}}" />
</Grid>
<TextBlock
Grid.Column="1"
Margin="16,4"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Bind Metadata.LastOpened}" />
<TextBlock
Grid.Column="2"
Margin="16,4"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Bind Metadata.EngineVersion}" />
<Button
Grid.Column="3"
HorizontalAlignment="Right"
Background="Transparent"
BorderThickness="0">
<FontIcon Glyph="&#xE712;" />
</Button>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<!-- Drag Visual -->
<Grid
x:Name="DragVisual"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="{ThemeResource CardStrokeColorDefaultBrush}"
BorderBrush="{ThemeResource ControlStrongStrokeColorDefaultBrush}"
BorderThickness="2"
CornerRadius="{StaticResource OverlayCornerRadius}"
Visibility="{x:Bind ViewModel.DragVisibility, Mode=OneWay}">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource TitleTextBlockStyle}"
Text="Drage Project Folder Here" />
</Grid>
</Grid>
<!-- Empty Place Holder -->
<Grid
x:Name="EmptyPlaceHolder"
Grid.Row="2"
Visibility="{x:Bind ViewModel.EmptyVisibility, Mode=OneWay}">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
Style="{StaticResource TitleTextBlockStyle}"
Text="No projects found" />
</Grid>
</Grid>
</Page>

View File

@@ -1,72 +0,0 @@
using Ghost.Data.Models;
using Ghost.Editor.ViewModels.Pages.Landing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Navigation;
using Windows.ApplicationModel.DataTransfer;
namespace Ghost.Editor.View.Pages.Landing;
internal sealed partial class OpenProjectPage : Page
{
public OpenProjectViewModel ViewModel
{
get;
}
public OpenProjectPage()
{
ViewModel = App.GetService<OpenProjectViewModel>();
InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
ViewModel.OnNavigatedTo(e.Parameter);
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
ViewModel.OnNavigatedFrom();
}
private void ProjectContainer_DragEnter(object sender, DragEventArgs e)
{
ViewModel.DragVisibility = Visibility.Visible;
ViewModel.EmptyVisibility = Visibility.Collapsed;
}
private void ProjectContainer_DragLeave(object sender, DragEventArgs e)
{
ViewModel.DragVisibility = Visibility.Collapsed;
ViewModel.UpdateEmptyPlaceHolderVisibility();
}
private void ProjectContainer_DragOver(object sender, DragEventArgs e)
{
if (e.DataView.Contains(StandardDataFormats.StorageItems))
{
e.AcceptedOperation = DataPackageOperation.Link;
}
else
{
e.AcceptedOperation = DataPackageOperation.None;
}
}
private async void ProjectContainer_Drop(object sender, DragEventArgs e)
{
await ViewModel.ContentDrop(e.DataView);
}
private async void ListView_ItemClick(object sender, ItemClickEventArgs e)
{
if (e.ClickedItem is ProjectMetadataInfo project)
{
await Task.Yield();
await ViewModel.OpenProjectAsync(project);
}
}
}

View File

@@ -5,6 +5,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:behaviors="using:CommunityToolkit.WinUI.Behaviors" xmlns:behaviors="using:CommunityToolkit.WinUI.Behaviors"
xmlns:controls="using:CommunityToolkit.WinUI.Controls" xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:controls1="using:Ghost.Editor.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ee="using:Ghost.Editor.View.Pages.EngineEditor" xmlns:ee="using:Ghost.Editor.View.Pages.EngineEditor"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity" xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
@@ -12,15 +13,13 @@
xmlns:local="using:Ghost.Editor.View.Windows" xmlns:local="using:Ghost.Editor.View.Windows"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:winex="using:WinUIEx" xmlns:winex="using:WinUIEx"
Activated="WindowEx_Activated"
Closed="WindowEx_Closed"
mc:Ignorable="d"> mc:Ignorable="d">
<Window.SystemBackdrop> <Window.SystemBackdrop>
<MicaBackdrop /> <MicaBackdrop />
</Window.SystemBackdrop> </Window.SystemBackdrop>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <Grid Loaded="MainGrid_Loaded" Unloaded="MainGrid_Unloaded">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
@@ -29,29 +28,21 @@
</Grid.RowDefinitions> </Grid.RowDefinitions>
<!-- Titlebar --> <!-- Titlebar -->
<StackPanel <TitleBar
x:Name="PART_TitleBar"
Grid.Row="0" Grid.Row="0"
Padding="8" Background="{ThemeResource AcrylicBackgroundFillColorBaseBrush}"
Orientation="Horizontal"> Subtitle="Ghost Engine">
<ImageIcon <TitleBar.IconSource>
Width="24" <ImageIconSource ImageSource="ms-appx:///Assets/icon.targetsize-48.png" />
Height="24" </TitleBar.IconSource>
VerticalAlignment="Center" </TitleBar>
Source="ms-appx:///Assets/Icon.targetsize-32.png" />
<TextBlock
Margin="8,0,0,0"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind ViewModel.engineVersionDescriptor}" />
<TextBlock
Margin="8,0,0,0"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind ViewModel.CurrentProject.Metadata.Name, Mode=OneWay}" />
</StackPanel>
<!-- Toolbar --> <!-- Toolbar -->
<Grid Grid.Row="1" Margin="4,4"> <Grid
Grid.Row="1"
Padding="4,0,4,4"
Background="{ThemeResource AcrylicBackgroundFillColorBaseBrush}">
<controls:TabbedCommandBar> <controls:TabbedCommandBar>
<controls:TabbedCommandBar.MenuItems> <controls:TabbedCommandBar.MenuItems>
<controls:TabbedCommandBarItem Header="Home"> <controls:TabbedCommandBarItem Header="Home">
@@ -132,7 +123,7 @@
<TabViewItem.IconSource> <TabViewItem.IconSource>
<FontIconSource Glyph="&#xEC50;" /> <FontIconSource Glyph="&#xEC50;" />
</TabViewItem.IconSource> </TabViewItem.IconSource>
<ee:ProjectPage /> <controls1:ProjectBrowser />
</TabViewItem> </TabViewItem>
<TabViewItem Header="Console"> <TabViewItem Header="Console">
<TabViewItem.IconSource> <TabViewItem.IconSource>
@@ -148,7 +139,7 @@
<Grid <Grid
Grid.Row="3" Grid.Row="3"
Height="25" Height="25"
Background="{ThemeResource SolidBackgroundFillColorBaseAltBrush}"> Background="{ThemeResource SmokeFillColorDefaultBrush}">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
@@ -201,6 +192,7 @@
</Grid> </Grid>
</Grid> </Grid>
<!-- Info and Progress -->
<Grid Grid.Row="0" Grid.RowSpan="4"> <Grid Grid.Row="0" Grid.RowSpan="4">
<InfoBar <InfoBar
x:Name="InfoBar" x:Name="InfoBar"

View File

@@ -1,13 +1,11 @@
using Ghost.Data.Resources; using Ghost.Editor.Core;
using Ghost.Editor.Core.Notifications; using Ghost.Editor.Core.Contracts;
using Ghost.Editor.Core.Progress; using Ghost.Editor.Core.Services;
using Ghost.Editor.ViewModels.Windows; using Ghost.Editor.ViewModels.Windows;
using Ghost.Engine.Resources; using System.Diagnostics;
using Windows.ApplicationModel;
using WinUIEx; using WinUIEx;
// 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.View.Windows; namespace Ghost.Editor.View.Windows;
/// <summary> /// <summary>
/// An empty window that can be used on its own or navigated to within a Frame. /// An empty window that can be used on its own or navigated to within a Frame.
@@ -29,24 +27,26 @@ internal sealed partial class EngineEditorWindow : WindowEx
_notificationService = (NotificationService)App.GetService<INotificationService>(); _notificationService = (NotificationService)App.GetService<INotificationService>();
_progressService = (ProgressService)App.GetService<IProgressService>(); _progressService = (ProgressService)App.GetService<IProgressService>();
AppWindow.SetIcon(AssetsPath.s_appIconPath);
Title = EngineData.ENGINE_NAME;
ExtendsContentIntoTitleBar = true;
InitializeComponent(); InitializeComponent();
AppWindow.SetIcon(Path.Combine(AppContext.BaseDirectory, "Assets/icon.ico"));
Title = "Ghost Engine";
ExtendsContentIntoTitleBar = true;
SetTitleBar(PART_TitleBar);
this.CenterOnScreen(); this.CenterOnScreen();
} }
private void WindowEx_Activated(object sender, Microsoft.UI.Xaml.WindowActivatedEventArgs args) private void MainGrid_Loaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{ {
Bindings.Update(); PART_TitleBar.Title = EditorApplication.CurrentProjectName;
PART_TitleBar.Subtitle = $"Ghost Engine {Package.Current.Id.Version.Major}.{Package.Current.Id.Version.Minor}.{Package.Current.Id.Version.Build}";
_notificationService.SetReference(InfoBar, NotificationQueue); _notificationService.SetReference(InfoBar, NotificationQueue);
_progressService.SetReference(ProgressBarContainer); _progressService.SetReference(ProgressBarContainer);
} }
private void WindowEx_Closed(object sender, Microsoft.UI.Xaml.WindowEventArgs args) private void MainGrid_Unloaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{ {
_notificationService.ClearReference(); _notificationService.ClearReference();
_progressService.ClearReference(); _progressService.ClearReference();

View File

@@ -1,76 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<winex:WindowEx
x:Class="Ghost.Editor.View.Windows.LandingWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:behaviors="using:CommunityToolkit.WinUI.Behaviors"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:local="using:Ghost.Editor.View.Windows"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:winex="using:WinUIEx"
Activated="WindowEx_Activated"
Closed="WindowEx_Closed"
IsResizable="False"
mc:Ignorable="d">
<Window.SystemBackdrop>
<MicaBackdrop />
</Window.SystemBackdrop>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<TextBlock
Margin="24,0,0,0"
VerticalAlignment="Bottom"
Style="{StaticResource BodyTextBlockStyle}"
Text="Ghost Engine" />
</Grid>
<Grid Grid.Row="1" Padding="24,0,24,18">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<SelectorBar
Grid.Row="0"
HorizontalAlignment="Right"
SelectionChanged="SelectorBar_SelectionChanged">
<SelectorBarItem IsSelected="True" Text="Open">
<SelectorBarItem.Icon>
<FontIcon Glyph="&#xE838;" />
</SelectorBarItem.Icon>
</SelectorBarItem>
<SelectorBarItem Text="Create">
<SelectorBarItem.Icon>
<FontIcon Glyph="&#xE8F4;" />
</SelectorBarItem.Icon>
</SelectorBarItem>
</SelectorBar>
<Frame
x:Name="ContentFrame"
Grid.Row="1"
Padding="8"
CacheMode="BitmapCache"
CacheSize="10" />
</Grid>
<Grid Grid.Row="1" Padding="16">
<InfoBar
x:Name="InfoBar"
HorizontalAlignment="Right"
VerticalAlignment="Bottom">
<interactivity:Interaction.Behaviors>
<behaviors:StackedNotificationsBehavior x:Name="NotificationQueue" />
</interactivity:Interaction.Behaviors>
</InfoBar>
</Grid>
</Grid>
</winex:WindowEx>

View File

@@ -1,59 +0,0 @@
using Ghost.Data.Resources;
using Ghost.Editor.Core.Notifications;
using Ghost.Editor.View.Pages.Landing;
using Ghost.Engine.Resources;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Animation;
using WinUIEx;
namespace Ghost.Editor.View.Windows;
internal sealed partial class LandingWindow : WindowEx
{
private readonly NotificationService _notificationService;
private int _previousSelectedIndex;
public LandingWindow()
{
_notificationService = (NotificationService)App.GetService<INotificationService>();
AppWindow.SetIcon(AssetsPath.s_appIconPath);
Title = EngineData.ENGINE_NAME;
InitializeComponent();
this.SetWindowSize(1000, 750);
this.CenterOnScreen();
ExtendsContentIntoTitleBar = true;
}
private void WindowEx_Activated(object sender, Microsoft.UI.Xaml.WindowActivatedEventArgs args)
{
_notificationService.SetReference(InfoBar, NotificationQueue);
}
private void WindowEx_Closed(object sender, Microsoft.UI.Xaml.WindowEventArgs args)
{
_notificationService.ClearReference();
}
private void SelectorBar_SelectionChanged(SelectorBar sender, SelectorBarSelectionChangedEventArgs e)
{
var selectedItem = sender.SelectedItem;
var currentSelectedIndex = sender.Items.IndexOf(selectedItem);
var pageType = currentSelectedIndex switch
{
1 => typeof(CreateProjectPage),
_ => typeof(OpenProjectPage),
};
var slideNavigationTransitionEffect = currentSelectedIndex - _previousSelectedIndex > 0 ?
SlideNavigationTransitionEffect.FromRight : SlideNavigationTransitionEffect.FromLeft;
ContentFrame.Navigate(pageType, null, new SlideNavigationTransitionInfo() { Effect = slideNavigationTransitionEffect });
_previousSelectedIndex = currentSelectedIndex;
}
}

View File

@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8" ?>
<winex:WindowEx
x:Class="Ghost.Editor.View.Windows.SplashWindow"
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.View.Windows"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:winex="using:WinUIEx"
Title="SplashWindow"
mc:Ignorable="d">
<winex:WindowEx.SystemBackdrop>
<MicaBackdrop />
</winex:WindowEx.SystemBackdrop>
<Grid Margin="32,32,32,16" Loaded="MainGrid_Loaded">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" ColumnSpacing="24">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="1.2*" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" CornerRadius="8">
<Image
HorizontalAlignment="Center"
VerticalAlignment="Center"
Source="C:\Users\Misaki\Downloads\Screenshot 2024-07-20 035047.png"
Stretch="UniformToFill" />
</Grid>
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock
Grid.Row="0"
Style="{StaticResource TitleLargeTextBlockStyle}"
Text="Ghost Engine" />
<TextBlock
x:Name="VersionTextBlock"
Grid.Row="1"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource BodyLargeStrongTextBlockStyle}" />
<TextBlock
Grid.Row="2"
Margin="0,32,0,0"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="An open-source, modern game engine designed for flexibility and performance."
TextWrapping="WrapWholeWords" />
</Grid>
</Grid>
<ProgressBar
Grid.Row="1"
Margin="0,24,0,0"
IsIndeterminate="True" />
<TextBlock
x:Name="LoadingTextBlock"
Grid.Row="2"
Margin="0,4,0,0"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<TextBlock
x:Name="CopyrightTextBlock"
Grid.Row="3"
Margin="0,24,0,0"
HorizontalAlignment="Center"
Foreground="{ThemeResource TextFillColorTertiaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}" />
</Grid>
</winex:WindowEx>

View File

@@ -0,0 +1,33 @@
using Ghost.Editor.Core;
using Microsoft.UI.Xaml;
using Windows.ApplicationModel;
using Windows.Storage;
using WinUIEx;
namespace Ghost.Editor.View.Windows;
/// <summary>
/// An empty window that can be used on its own or navigated to within a Frame.
/// </summary>
internal sealed partial class SplashWindow : WindowEx
{
public SplashWindow()
{
InitializeComponent();
IsResizable = false;
IsMaximizable = false;
IsMinimizable = false;
ExtendsContentIntoTitleBar = true;
//this.CenterOnScreen(750, 400);
}
private void MainGrid_Loaded(object sender, RoutedEventArgs e)
{
var version = Package.Current.Id.Version;
VersionTextBlock.Text = $"Version {version.Major}.{version.Minor}.{version.Build}";
LoadingTextBlock.Text = $"Loading {EditorApplication.CurrentProjectName}...";
CopyrightTextBlock.Text = $"Copyright © {DateTime.Now.Year} Ghost Engine. All rights reserved.";
}
}

View File

@@ -0,0 +1,108 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Ghost.Editor.Core;
using Ghost.Editor.Core.AssetHandle;
using Ghost.Editor.Core.Contracts;
using Ghost.Editor.Core.Utilities;
using Ghost.Editor.Models;
using System.Collections.ObjectModel;
namespace Ghost.Editor.ViewModels.Controls;
internal partial class ProjectBrowserViewModel : ObservableObject
{
private readonly IInspectorService _inspectorService;
private readonly Dictionary<string, ExplorerItem> _pathToDirectoryItemMap = new();
private ExplorerItem? _selectedItem;
public ObservableCollection<ExplorerItem> Directories
{
get;
} = new();
public ObservableCollection<ExplorerItem> Files
{
get;
} = new();
public ExplorerItem? SelectedItem
{
get => _selectedItem;
set
{
// TODO: Resolve inspector by reading metadata from selected asset
_selectedItem = value;
}
}
public ProjectBrowserViewModel(IInspectorService inspectorService)
{
_inspectorService = inspectorService;
var assetsRootItem = new ExplorerItem(EditorApplication.ASSETS_FOLDER_NAME, Path.Combine(EditorApplication.CurrentProjectPath, EditorApplication.ASSETS_FOLDER_NAME), true);
LoadSubFolderRecursive(assetsRootItem);
Directories.Add(assetsRootItem);
}
private void LoadSubFolderRecursive(ExplorerItem parentItem)
{
foreach (var directory in Directory.EnumerateDirectories(parentItem.FullName))
{
var item = new ExplorerItem(Path.GetFileName(directory), directory, true);
LoadSubFolderRecursive(item);
_pathToDirectoryItemMap[directory] = item;
parentItem.Children ??= new();
parentItem.Children.Add(item);
}
}
internal void NavigateToDirectory(string? path)
{
Files.Clear();
if (!Directory.Exists(path))
{
return;
}
foreach (var directory in Directory.EnumerateDirectories(path))
{
var directoryItem = new ExplorerItem(Path.GetFileName(directory), directory, true);
Files.Add(directoryItem);
}
foreach (var file in Directory.EnumerateFiles(path))
{
if (Path.GetExtension(file) == FileExtensions.META_FILE_EXTENSION)
{
continue;
}
var fileItem = new ExplorerItem(Path.GetFileName(file), file, false);
Files.Add(fileItem);
}
}
internal (ExplorerItem?, int) OpenSelected()
{
if (SelectedItem == null)
{
return (null, 0);
}
if (SelectedItem.IsDirectory)
{
NavigateToDirectory(SelectedItem.FullName);
SelectedItem = _pathToDirectoryItemMap[SelectedItem.FullName];
return (SelectedItem, 0);
}
else
{
AssetDatabase.OpenAsset(SelectedItem.FullName);
return (null, 1);
}
}
}

View File

@@ -1,6 +1,5 @@
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using Ghost.Editor.Core.Contracts; using Ghost.Editor.Core.Contracts;
using Ghost.Editor.Core.Inspector;
namespace Ghost.Editor.ViewModels.Pages.EngineEditor; namespace Ghost.Editor.ViewModels.Pages.EngineEditor;
@@ -16,7 +15,7 @@ internal partial class InspectorViewModel(IInspectorService inspectorService) :
public void OnNavigatedTo(object? parameter) public void OnNavigatedTo(object? parameter)
{ {
inspectorService.OnSelectionChanged += OnSelectionChanged; inspectorService.OnSelectionChanged += OnSelectionChanged;
Inspectable = inspectorService.SelectedInspectable; Inspectable = inspectorService.Selected;
} }
public void OnNavigatedFrom() public void OnNavigatedFrom()
@@ -25,8 +24,8 @@ internal partial class InspectorViewModel(IInspectorService inspectorService) :
Inspectable = null; Inspectable = null;
} }
private void OnSelectionChanged() private void OnSelectionChanged(object? sender, EventArgs e)
{ {
Inspectable = inspectorService.SelectedInspectable; Inspectable = inspectorService.Selected;
} }
} }

View File

@@ -1,5 +1,5 @@
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using Ghost.Data.Services; using Ghost.Editor.Core;
using Ghost.Editor.Core.AssetHandle; using Ghost.Editor.Core.AssetHandle;
using Ghost.Editor.Models; using Ghost.Editor.Models;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
@@ -36,12 +36,7 @@ internal partial class ProjectViewModel : ObservableObject
public ProjectViewModel() public ProjectViewModel()
{ {
if (ProjectService.CurrentProject.Metadata == null) var assetsRootItem = new ExplorerItem("Assets", Path.Combine(EditorApplication.CurrentProjectPath, EditorApplication.ASSETS_FOLDER_NAME), true);
{
throw new InvalidOperationException("Current project is not set.");
}
var assetsRootItem = new ExplorerItem("Assets", Path.Combine(Path.GetDirectoryName(ProjectService.CurrentProject.Path)!, ProjectService.ASSETS_FOLDER), true);
LoadSubFolderRecursive(ref assetsRootItem); LoadSubFolderRecursive(ref assetsRootItem);
SubDirectories.Add(assetsRootItem); SubDirectories.Add(assetsRootItem);

View File

@@ -1,91 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Ghost.Data.Models;
using Ghost.Data.Services;
using Ghost.Editor.Core.AppState;
using Ghost.Editor.Core.Contracts;
using Ghost.Editor.Core.Notifications;
using Ghost.Editor.Utilities;
using Ghost.Engine.Resources;
using System.Collections.ObjectModel;
namespace Ghost.Editor.ViewModels.Pages.Landing;
internal partial class CreateProjectViewModel(INotificationService notificationService, ProjectService projectService, AppStateMachine stateService) : ObservableObject, INavigationAware
{
public ObservableCollection<TemplateData> templates = new();
[ObservableProperty]
public partial TemplateData? SelectedTemplate
{
get;
set;
}
[ObservableProperty]
public partial string? ProjectName
{
get;
set;
}
[ObservableProperty]
public partial string? ProjectLocation
{
get;
set;
}
public async void OnNavigatedTo(object? parameter)
{
templates.Clear();
await foreach (var (path, info) in ProjectService.GetProjectTemplatesAsync())
{
templates.Add(new(path, info));
}
SelectedTemplate = templates.FirstOrDefault();
}
public void OnNavigatedFrom()
{
}
[RelayCommand]
private async Task SelectionProjectLocation()
{
var folder = await SystemUtilities.OpenFolderPickerAsync();
if (folder != null)
{
ProjectLocation = folder.Path;
}
}
[RelayCommand]
private async Task CreateProject()
{
if (string.IsNullOrWhiteSpace(ProjectName)
|| !Directory.Exists(ProjectLocation)
|| !SelectedTemplate.HasValue)
{
notificationService.ShowNotification("Incorrect project info", MessageType.Error);
return;
}
var result = await projectService.CreateProjectAsync(ProjectName, ProjectLocation, EngineData.EngineVersion, SelectedTemplate.Value.directory);
if (result.IsFailure)
{
notificationService.ShowNotification(result.Message, MessageType.Error);
return;
}
try
{
await stateService.TransitionToAsync(StateKey.EngineEditor, result.Value);
}
catch (Exception e)
{
notificationService.ShowNotification($"Failed to load project: {e.Message}", MessageType.Error);
}
}
}

View File

@@ -1,106 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Ghost.Data.Models;
using Ghost.Data.Services;
using Ghost.Editor.Core.AppState;
using Ghost.Editor.Core.Contracts;
using Ghost.Editor.Core.Notifications;
using Microsoft.UI.Xaml;
using System.Collections.ObjectModel;
using Windows.ApplicationModel.DataTransfer;
using Windows.Storage;
namespace Ghost.Editor.ViewModels.Pages.Landing;
internal partial class OpenProjectViewModel(ProjectService projectService, INotificationService _notificationService, AppStateMachine _stateService) : ObservableObject, INavigationAware
{
public readonly ObservableCollection<ProjectMetadataInfo> projects = new();
[ObservableProperty]
public partial Visibility EmptyVisibility
{
get;
set;
}
[ObservableProperty]
public partial Visibility DragVisibility
{
get;
set;
}
public void UpdateEmptyPlaceHolderVisibility()
{
EmptyVisibility = projects.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
}
public async void OnNavigatedTo(object? parameter)
{
await foreach (var projectInfo in projectService.GetAllProjectAsync())
{
var metadata = await ProjectService.LoadMetadataAsync(projectInfo.MetadataPath);
if (metadata == null)
{
continue;
}
projects.Add(new(projectInfo.MetadataPath, metadata));
}
UpdateEmptyPlaceHolderVisibility();
DragVisibility = Visibility.Collapsed;
}
public void OnNavigatedFrom()
{
projects.Clear();
}
public async Task ContentDrop(DataPackageView dataView)
{
var errorMessage = string.Empty;
if (dataView.Contains(StandardDataFormats.StorageItems))
{
var items = await dataView.GetStorageItemsAsync();
var rootFolder = items.OfType<StorageFolder>().FirstOrDefault();
if (rootFolder != null)
{
var result = await projectService.AddProjectFromDirectoryAsync(rootFolder.Path);
if (result.IsSuccess)
{
projects.Add(result.Value);
goto CloseDropPanel;
}
else
{
errorMessage = result.Message;
}
}
}
else
{
errorMessage = "Unsupported data format. Please drop a folder containing a project.";
}
_notificationService.ShowNotification(errorMessage, MessageType.Error);
CloseDropPanel:
DragVisibility = Visibility.Collapsed;
UpdateEmptyPlaceHolderVisibility();
}
public async Task OpenProjectAsync(ProjectMetadataInfo project)
{
try
{
project.Metadata.LastOpened = DateTime.Now;
await ProjectService.CreateMetadataFileAsync(project.Path, project.Metadata);
await _stateService.TransitionToAsync(StateKey.EngineEditor, project);
}
catch (Exception e)
{
_notificationService.ShowNotification($"Failed to load project: {e.Message}", MessageType.Error);
}
}
}

View File

@@ -1,13 +1,7 @@
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using Ghost.Data.Models;
using Ghost.Data.Services;
using Ghost.Engine.Resources;
namespace Ghost.Editor.ViewModels.Windows; namespace Ghost.Editor.ViewModels.Windows;
internal partial class EngineEditorViewModel : ObservableRecipient internal partial class EngineEditorViewModel : ObservableRecipient
{ {
public string engineVersionDescriptor = $"{EngineData.ENGINE_NAME} - {EngineData.EngineVersion}";
public ProjectMetadataInfo CurrentProject => ProjectService.CurrentProject;
} }

View File

@@ -3,7 +3,7 @@ using Misaki.HighPerformance.Jobs;
namespace Ghost.Engine; namespace Ghost.Engine;
public interface IEngineContext public interface IEngineContext : IDisposable
{ {
JobScheduler JobScheduler { get; } JobScheduler JobScheduler { get; }
} }

View File

@@ -5,7 +5,6 @@
<Platform Name="x86" /> <Platform Name="x86" />
</Configurations> </Configurations>
<Folder Name="/Editor/"> <Folder Name="/Editor/">
<Project Path="Ghost.Data/Ghost.Data.csproj" />
<Project Path="Ghost.Editor.Core/Ghost.Editor.Core.csproj" /> <Project Path="Ghost.Editor.Core/Ghost.Editor.Core.csproj" />
<Project Path="Ghost.Editor/Ghost.Editor.csproj"> <Project Path="Ghost.Editor/Ghost.Editor.csproj">
<Platform Solution="*|ARM64" Project="ARM64" /> <Platform Solution="*|ARM64" Project="ARM64" />