refactor(core): asset pipeline overhaul & dock removal
- Introduced IAsset interface and refactored asset loading/saving. - Migrated TextureContentHeader to Ghost.Engine; updated usage. - Rewrote AssetRegistry, AssetCatalog, ImportCoordinator for new asset flow. - Added thread-safe ConcurrentHashSet utility. - Improved EditorApplication folder management/init. - Updated TextureAssetHandler/TextureProcessor for new import/export. - Added EditorContentProvider for asset access. - Updated AssetManager to use new AssetType enum; removed GCHandle. - Removed all custom docking controls and templates. - Deleted obsolete ViewModels/Pages (Console, Hierarchy, Inspector, Project). - Renamed ProjectBrowser to ContentBrowser; updated references. - Updated NuGet packages, Result conversions, and commit instructions. - General cleanup: namespaces, dead code, structure.
This commit is contained in:
7
.github/commit-instructions.md
vendored
Normal file
7
.github/commit-instructions.md
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
Use this instructions when writing a git commit message
|
||||
|
||||
The first line should be a single line with no more than 50 characters that summary the changes. The second line should be blank. Start at the third line for actual changes.
|
||||
|
||||
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.
|
||||
|
||||
Commits MUST be prefixed with a type, which consists of a noun, feat, fix, etc., followed by the OPTIONAL scope, OPTIONAL !, and REQUIRED terminal colon and space. The type feat MUST be used when a commit adds a new feature to your application or library. The type fix MUST be used when a commit represents a bug fix for your application. A scope MAY be provided after a type. A scope MUST consist of a noun describing a section of the codebase surrounded by parenthesis, e.g., fix(parser) A description MUST immediately follow the colon and space after the typescope prefix. The description is a short summary of the code changes, e.g., fix array parsing issue when multiple spaces were contained in string. A longer commit body MAY be provided after the short description, providing additional contextual information about the code changes. The body MUST begin one blank line after the description. A commit body is free-form and MAY consist of any number of newline separated paragraphs. One or more footers MAY be provided one blank line after the body. Each footer MUST consist of a word token, followed by either a or # separator, followed by a string value (this is inspired by the git trailer convention). A footer’s token MUST use - in place of whitespace characters, e.g., Acked-by (this helps differentiate the footer section from a multi-paragraph body). An exception is made for BREAKING CHANGE, which MAY also be used as a token. A footer’s value MAY contain spaces and newlines, and parsing MUST terminate when the next valid footer tokenseparator pair is observed. Breaking changes MUST be indicated in the typescope prefix of a commit, or as an entry in the footer. If included as a footer, a breaking change MUST consist of the uppercase text BREAKING CHANGE, followed by a colon, space, and description, e.g., BREAKING CHANGE environment variables now take precedence over config files. If included in the typescope prefix, breaking changes MUST be indicated by a ! immediately before the . If ! is used, BREAKING CHANGE MAY be omitted from the footer section, and the commit description SHALL be used to describe the breaking change. Types other than feat and fix MAY be used in your commit messages, e.g., docs update ref docs. The units of information that make up Conventional Commits MUST NOT be treated as case-sensitive by implementors, with the exception of BREAKING CHANGE which MUST be uppercase. BREAKING-CHANGE MUST be synonymous with BREAKING CHANGE, when used as a token in a footer.
|
||||
77
src/.github/agents/code-executor.agent.md
vendored
77
src/.github/agents/code-executor.agent.md
vendored
@@ -1,77 +0,0 @@
|
||||
---
|
||||
name: code-executor
|
||||
description: plan-executing coding agent. It heavily emphasizes strict adherence to the provided plan, rigorous Test-Driven Development (TDD) practices, and high-performance output.
|
||||
---
|
||||
|
||||
# code-executor
|
||||
|
||||
## 1. Agent Identity & Core Objective
|
||||
**Role:** Senior Plan Executor & Performance-Oriented Developer
|
||||
**Objective:** To meticulously execute predefined architectural and feature plans using a strict Test-Driven Development (TDD) workflow, ensuring all deliverables are highly optimized, performant, and perfectly aligned with the provided specifications.
|
||||
|
||||
You do not invent new features. You do not alter the architectural vision. You execute the plan with precision, speed, and uncompromising quality.
|
||||
|
||||
---
|
||||
|
||||
## 2. Core Directives
|
||||
|
||||
### I. Strict Plan Adherence
|
||||
* **Zero Deviation:** Implement strictly what is detailed in the provided plan. Do not add "nice-to-have" features, scope creep, or unauthorized structural changes.
|
||||
* **Clarification over Assumption:** If a step in the plan is ambiguous, incomplete, or technically unfeasible, halt execution and request clarification. Do not guess.
|
||||
* **Traceability:** Every piece of code written must directly map back to a specific requirement or step in the provided plan.
|
||||
|
||||
### II. Absolute TDD Workflow (Red-Green-Refactor)
|
||||
* You must follow strict TDD principles for every implementation. No production code is written without a failing test existing first.
|
||||
* **Red:** Write comprehensive, edge-case-aware unit and integration tests based *only* on the plan's requirements.
|
||||
* **Green:** Write the minimal necessary production code to make the tests pass.
|
||||
* **Refactor:** Optimize the code for readability, maintainability, and performance without changing its behavior (tests must remain green).
|
||||
|
||||
### III. Performance & Optimization Focus
|
||||
* **Algorithmic Efficiency:** Prioritize optimal Time (Big O) and Space complexity.
|
||||
* **Resource Management:** Ensure proper memory management, garbage collection awareness, and prevent memory leaks.
|
||||
* **Concurrency & Asynchrony:** Utilize non-blocking operations and efficient concurrency models where appropriate to maximize throughput.
|
||||
|
||||
---
|
||||
|
||||
## 3. Execution Protocol
|
||||
|
||||
When provided with a plan, you will execute the following phases sequentially:
|
||||
|
||||
### Phase 1: Plan Ingestion & Test Strategy
|
||||
1. **Analyze:** Read the provided plan thoroughly.
|
||||
2. **Deconstruct:** Break the plan down into testable units (functions, classes, API endpoints).
|
||||
3. **Report:** Output a brief summary acknowledging the exact scope of what will be built and the testing strategy.
|
||||
|
||||
### Phase 2: Test Generation (RED)
|
||||
1. Write the tests for the current step of the plan.
|
||||
2. Ensure tests cover standard use cases, boundary conditions, invalid inputs, and error handling.
|
||||
3. *Output the test files and confirm they will currently fail.*
|
||||
|
||||
### Phase 3: Implementation (GREEN)
|
||||
1. Write the exact production code required to satisfy the generated tests.
|
||||
2. Focus strictly on passing the tests. Do not prematurely optimize in this phase.
|
||||
3. *Output the production code and confirm tests are now passing.*
|
||||
|
||||
### Phase 4: Performance Refactoring (REFACTOR)
|
||||
1. Review the passing code for bottlenecks, redundant logic, or memory inefficiencies.
|
||||
2. Refactor the code applying performance best practices.
|
||||
3. Rerun tests to ensure functional equivalence.
|
||||
4. *Output the refactored code alongside a brief explanation of the performance improvements made.*
|
||||
|
||||
---
|
||||
|
||||
## 4. Constraints & Anti-Patterns
|
||||
|
||||
* **DO NOT** skip the testing phase under any circumstances, even for "simple" scripts.
|
||||
* **DO NOT** mock core logic that is meant to be implemented in the current step; only mock external dependencies (databases, network calls, file systems).
|
||||
* **DO NOT** modify the testing framework or configuration unless explicitly stated in the plan.
|
||||
* **DO NOT** return code with placeholder comments (e.g., `// TODO: Implement this`). Write complete, working code for the current step.
|
||||
|
||||
---
|
||||
|
||||
## 5. Output Format
|
||||
When delivering responses, structure your output clearly:
|
||||
1. **Context:** Which step of the plan is currently being addressed.
|
||||
2. **Tests:** Code blocks containing the test specifications.
|
||||
3. **Implementation:** Code blocks containing the production code.
|
||||
4. **Notes:** Any necessary instructions for running the code or specific performance characteristics achieved.
|
||||
@@ -1,22 +1,45 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Engine;
|
||||
|
||||
namespace Ghost.Editor.Core.AssetHandler;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class CustomAssetHandlerAttribute : Attribute
|
||||
{
|
||||
public CustomAssetHandlerAttribute(string id, string[] supportedExtensions, int version = 1)
|
||||
public CustomAssetHandlerAttribute(string TypeID, string[] supportedExtensions, int version = 1)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public interface IAsset : IDisposable
|
||||
{
|
||||
public Guid ID
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public Guid TypeID
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public IAssetSettings Settings
|
||||
{
|
||||
get;
|
||||
}
|
||||
}
|
||||
|
||||
public interface IAssetExportOptions;
|
||||
|
||||
public interface IAssetHandler
|
||||
{
|
||||
bool CanExport => false;
|
||||
AssetType TargetAssetType { get; }
|
||||
|
||||
IAssetSettings? CreateDefaultSettings();
|
||||
|
||||
ValueTask<Result<IAsset>> LoadAssetAsync(Stream assetStream, Guid id, IAssetSettings? settings, CancellationToken token = default);
|
||||
ValueTask<Result> SaveAssetAsync(Stream targetStream, IAsset asset, CancellationToken token = default);
|
||||
ValueTask<Result> ImportAsync(Stream sourceStream, Stream targetStream, Guid id, IAssetSettings? settings, CancellationToken token = default);
|
||||
ValueTask<Result> ExportAsync(Stream assetStream, Stream targetStream, IAssetExportOptions? options, CancellationToken token = default);
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using Ghost.Engine;
|
||||
|
||||
namespace Ghost.Editor.Core.AssetHandler;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Ghost.Engine;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
@@ -67,7 +68,8 @@ public sealed class AssetMeta
|
||||
|
||||
internal static class AssetMetaIO
|
||||
{
|
||||
private const string _META_EXTENSION = ".gmeta";
|
||||
public const string META_EXTENSION_NAME = "gmeta";
|
||||
public const string META_EXTENSION = ".gmeta";
|
||||
|
||||
private static readonly JsonSerializerOptions s_options = new()
|
||||
{
|
||||
@@ -107,10 +109,17 @@ internal static class AssetMetaIO
|
||||
{
|
||||
File.Delete(metaPath);
|
||||
}
|
||||
|
||||
File.Move(tempPath, metaPath);
|
||||
}
|
||||
|
||||
public static string GetMetaPath(string sourceFilePath) => sourceFilePath + _META_EXTENSION;
|
||||
|
||||
public static string GetSourcePath(string metaPath) => metaPath[..^_META_EXTENSION.Length];
|
||||
public static string GetMetaPath(string sourceFilePath)
|
||||
{
|
||||
return sourceFilePath + META_EXTENSION;
|
||||
}
|
||||
|
||||
public static string GetSourcePath(string metaPath)
|
||||
{
|
||||
return metaPath[..^META_EXTENSION.Length];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
using Ghost.Editor.Core.Contracts;
|
||||
using Ghost.Engine.AssetLoader;
|
||||
|
||||
namespace Ghost.Editor.Core.AssetHandler;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class CustomAssetProcesserAttribute<T> : Attribute
|
||||
{
|
||||
public Type Type => typeof(T);
|
||||
}
|
||||
|
||||
public readonly struct AssetProcesserContext
|
||||
{
|
||||
public IAssetRegistry Registry
|
||||
{
|
||||
get; init;
|
||||
}
|
||||
|
||||
public string AssetPath
|
||||
{
|
||||
get; init;
|
||||
}
|
||||
|
||||
public Asset Asset
|
||||
{
|
||||
get; init;
|
||||
}
|
||||
|
||||
public IAssetHandler Handler
|
||||
{
|
||||
get; init;
|
||||
}
|
||||
}
|
||||
|
||||
public interface IAssetProcesser
|
||||
{
|
||||
ValueTask ProcessAsync(AssetProcesserContext ctx);
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Engine;
|
||||
using Ghost.Engine.AssetLoader;
|
||||
using Ghost.Graphics.RHI;
|
||||
using ImageMagick;
|
||||
using Misaki.HighPerformance.LowLevel;
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ghost.Editor.Core.AssetHandler;
|
||||
@@ -46,6 +49,59 @@ public enum MipmapFilter : uint
|
||||
MitchellNetravali
|
||||
}
|
||||
|
||||
[Guid(GUID)]
|
||||
public class TextureAsset : IAsset
|
||||
{
|
||||
public const string GUID = "27965FFF-860C-40EF-9123-1874D7DE9CDC";
|
||||
|
||||
private static readonly Guid s_typeID = Guid.Parse(GUID);
|
||||
|
||||
private readonly Guid _id;
|
||||
private readonly IAssetSettings _settings;
|
||||
|
||||
private readonly MagickImage _textureData;
|
||||
private readonly uint _width;
|
||||
private readonly uint _height;
|
||||
private readonly uint _depth;
|
||||
private readonly uint _colorComponents;
|
||||
private readonly uint _dimension;
|
||||
|
||||
public Guid ID => _id;
|
||||
public Guid TypeID => s_typeID;
|
||||
public IAssetSettings Settings => _settings;
|
||||
|
||||
public MagickImage TextureData => _textureData;
|
||||
public uint Width => _width;
|
||||
public uint Height => _height;
|
||||
public uint Depth => _depth;
|
||||
public uint Dimension => _dimension;
|
||||
public uint ColorComponents => _colorComponents;
|
||||
|
||||
internal TextureAsset([OwnershipTransfer] MagickImage data, TextureContentHeader header, Guid id, IAssetSettings settings)
|
||||
{
|
||||
_id = id;
|
||||
_settings = settings;
|
||||
|
||||
_textureData = data;
|
||||
_width = header.width;
|
||||
_height = header.height;
|
||||
_depth = header.depth;
|
||||
_dimension = header.dimension;
|
||||
_colorComponents = header.colorComponents;
|
||||
}
|
||||
|
||||
~TextureAsset()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_textureData.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
public class TextureAssetSettings : IAssetSettings
|
||||
{
|
||||
public struct BasicSettings()
|
||||
@@ -70,6 +126,11 @@ public class TextureAssetSettings : IAssetSettings
|
||||
get; set;
|
||||
} = 1;
|
||||
|
||||
public int Depth
|
||||
{
|
||||
get; set;
|
||||
} = 1;
|
||||
|
||||
public bool IsSRGB
|
||||
{
|
||||
get; set;
|
||||
@@ -193,26 +254,99 @@ public class TextureAssetSettings : IAssetSettings
|
||||
} = new SamplerSettings();
|
||||
}
|
||||
|
||||
[CustomAssetHandler(_GUID, [".png", ".jpg", ".jpeg", ".tga", ".bmp", ".hdr"], 1)]
|
||||
[Guid(_GUID)]
|
||||
[CustomAssetHandler(TextureAsset.GUID, [".png", ".jpg", ".jpeg", ".tga", ".bmp", ".hdr"], 1)]
|
||||
internal class TextureAssetHandler : IAssetHandler
|
||||
{
|
||||
private const string _GUID = "27965FFF-860C-40EF-9123-1874D7DE9CDC";
|
||||
public AssetType TargetAssetType => AssetType.Texture;
|
||||
|
||||
public IAssetSettings? CreateDefaultSettings()
|
||||
{
|
||||
return new TextureAssetSettings();
|
||||
}
|
||||
|
||||
private static TextureDimension GetTextureDimension(TextureAssetSettings settings)
|
||||
{
|
||||
if (settings.Basic.Columns > 1 && settings.Basic.Rows > 1)
|
||||
{
|
||||
if (settings.Basic.Depth > 1)
|
||||
{
|
||||
return TextureDimension.Texture3D;
|
||||
}
|
||||
|
||||
return TextureDimension.Texture2DArray;
|
||||
}
|
||||
|
||||
if (settings.Basic.Columns == 1 && settings.Basic.Rows == 1)
|
||||
{
|
||||
if (settings.Basic.Depth == 6)
|
||||
{
|
||||
return TextureDimension.TextureCube;
|
||||
}
|
||||
else if (settings.Basic.Depth > 6 && settings.Basic.Depth % 6 == 0)
|
||||
{
|
||||
return TextureDimension.TextureCubeArray;
|
||||
}
|
||||
}
|
||||
|
||||
// If none of the above conditions are met, we will treat it as a regular 2D texture.
|
||||
return TextureDimension.Texture2D;
|
||||
}
|
||||
|
||||
public ValueTask<Result<IAsset>> LoadAssetAsync(Stream assetStream, Guid id, IAssetSettings? settings, CancellationToken token = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var image = new MagickImage(assetStream);
|
||||
|
||||
var textureSettings = settings as TextureAssetSettings ?? new TextureAssetSettings();
|
||||
var contentHeader = new TextureContentHeader
|
||||
{
|
||||
width = image.Width,
|
||||
height = image.Height,
|
||||
depth = image.Depth,
|
||||
colorComponents = image.ChannelCount,
|
||||
dimension = (uint)GetTextureDimension(textureSettings)
|
||||
};
|
||||
|
||||
return ValueTask.FromResult(Result.Success<IAsset>(new TextureAsset(image, contentHeader, id, textureSettings)));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ValueTask.FromResult(Result<IAsset>.Failure(ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result> SaveAssetAsync(Stream targetStream, IAsset asset, CancellationToken token = default)
|
||||
{
|
||||
if (asset is not TextureAsset textureAsset)
|
||||
{
|
||||
return Result.Failure("Asset type is not TextureAsset");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await textureAsset.TextureData.WriteAsync(targetStream, token);
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Failure(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result> ImportAsync(Stream sourceStream, Stream targetStream, Guid id, IAssetSettings? settings, CancellationToken token = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var textureSettings = settings as TextureAssetSettings ?? new TextureAssetSettings();
|
||||
using var image = new MagickImage(sourceStream);
|
||||
var bytes = image.ToByteArray();
|
||||
var pixels = image.GetPixels().GetValues();
|
||||
if (pixels == null)
|
||||
{
|
||||
return Result.Failure("Failed to retrieve pixel data from the source image.");
|
||||
}
|
||||
|
||||
var (path, mip) = await TextureProcessor.CompressToCacheAsync(EditorApplication.ImportsFolderPath, id, bytes, image.Width, image.Height, image.Depth, textureSettings, token)
|
||||
var textureSettings = settings as TextureAssetSettings ?? new TextureAssetSettings();
|
||||
var (path, mip) = await TextureProcessor.CompressToCacheAsync(EditorApplication.CacheFolderPath, id, pixels, image.Width, image.Height, image.Depth, textureSettings, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
targetStream.Seek(0, SeekOrigin.Begin);
|
||||
@@ -224,12 +358,13 @@ internal class TextureAssetHandler : IAssetHandler
|
||||
depth = image.Depth,
|
||||
colorComponents = image.ChannelCount,
|
||||
mipLevels = (uint)mip,
|
||||
dimension = (int)TextureDimension.Texture2D // TODO: Implement dimension calculation
|
||||
dimension = (uint)GetTextureDimension(textureSettings)
|
||||
};
|
||||
|
||||
targetStream.Write(MemoryMarshal.AsBytes(new Span<TextureContentHeader>(ref contentHeader)));
|
||||
|
||||
await targetStream.WriteAsync(bytes, token).ConfigureAwait(false);
|
||||
await using var ddsStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
await ddsStream.CopyToAsync(targetStream, token).ConfigureAwait(false);
|
||||
await targetStream.FlushAsync(token).ConfigureAwait(false);
|
||||
|
||||
return Result.Success();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Ghost.Graphics.RHI;
|
||||
using Ghost.Nvtt;
|
||||
using ImageMagick;
|
||||
using Misaki.HighPerformance.LowLevel;
|
||||
@@ -25,7 +26,7 @@ internal static class TextureProcessor
|
||||
{
|
||||
private readonly string _outputPath;
|
||||
|
||||
private readonly byte[] _image;
|
||||
private readonly float[] _image;
|
||||
private readonly uint _depth;
|
||||
private readonly uint _width;
|
||||
private readonly uint _height;
|
||||
@@ -37,7 +38,7 @@ internal static class TextureProcessor
|
||||
|
||||
public Task Task => _completionSource.Task;
|
||||
|
||||
public NvttPipelineTask(string outputPath, byte[] image, uint width, uint height, uint depth, TextureAssetSettings settings)
|
||||
public NvttPipelineTask(string outputPath, float[] image, uint width, uint height, uint depth, TextureAssetSettings settings)
|
||||
{
|
||||
_outputPath = outputPath;
|
||||
_image = image;
|
||||
@@ -163,11 +164,15 @@ internal static class TextureProcessor
|
||||
}
|
||||
}
|
||||
|
||||
public static async ValueTask<(string cachePath, int mipmapCount)> CompressToCacheAsync(string cachesFolderPath, Guid assetId, byte[] image, uint width, uint height, uint depth, TextureAssetSettings settings, CancellationToken cancellationToken)
|
||||
public static async ValueTask<(string cachePath, int mipmapCount)> CompressToCacheAsync(string cachesFolderPath, Guid assetId, float[] image, uint width, uint height, uint depth, TextureAssetSettings settings, CancellationToken cancellationToken)
|
||||
{
|
||||
var settingsHash = ComputeSettingsHash(settings);
|
||||
var cacheFileName = $"texturecache_{assetId:N}_{settingsHash:X16}.dds";
|
||||
var cachePath = Path.Combine(cachesFolderPath, cacheFileName);
|
||||
|
||||
var textureCachePath = Path.Combine(cachesFolderPath, "TextureCache");
|
||||
var cachePath = Path.Combine(textureCachePath, cacheFileName);
|
||||
|
||||
Directory.CreateDirectory(textureCachePath);
|
||||
|
||||
if (File.Exists(cachePath))
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Editor.Core.Services;
|
||||
using Ghost.Editor.Core.AssetHandler;
|
||||
using Ghost.Engine.AssetLoader;
|
||||
|
||||
namespace Ghost.Editor.Core.Contracts;
|
||||
@@ -38,7 +38,6 @@ public sealed class AssetChangedEventArgs : EventArgs
|
||||
}
|
||||
}
|
||||
|
||||
[EditorInjection(EditorInjectionAttribute.ServiceLifetime.Singleton, typeof(AssetRegistry))]
|
||||
public interface IAssetRegistry : IDisposable
|
||||
{
|
||||
string? GetAssetPath(Guid id);
|
||||
@@ -46,6 +45,12 @@ public interface IAssetRegistry : IDisposable
|
||||
|
||||
ValueTask<Result<Guid>> ImportAssetAsync(string sourceFilePath, string targetAssetPath, CancellationToken token = default);
|
||||
ValueTask<Result> ReimportAssetAsync(Guid assetId, string sourceFilePath, CancellationToken token = default);
|
||||
ValueTask<Result<Asset>> LoadAssetAsync(Guid id, CancellationToken token = default);
|
||||
ValueTask<Result> SaveAssetAsync(Asset asset, CancellationToken token = default);
|
||||
ValueTask<Result<IAsset>> LoadAssetAsync(Guid id, CancellationToken token = default);
|
||||
ValueTask<Result> SaveAssetAsync(IAsset asset, CancellationToken token = default);
|
||||
ValueTask<Result> SaveAssetAsync(Guid id, CancellationToken token = default);
|
||||
|
||||
void SetAssetDirty(Guid id);
|
||||
ValueTask<Result> SaveAssetIfDirtyAsync(IAsset asset, CancellationToken token = default);
|
||||
ValueTask<Result> SaveAssetIfDirtyAsync(Guid id, CancellationToken token = default);
|
||||
ValueTask<Result[]> SaveDirtyAssetsAsync();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using Ghost.Editor.Core.Utilities;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
@@ -7,17 +6,26 @@ namespace Ghost.Editor.Core;
|
||||
public static class EditorApplication
|
||||
{
|
||||
public const string ASSETS_FOLDER_NAME = "Assets";
|
||||
public const string SOURCES_FOLDER_NAME = "Sources";
|
||||
public const string PACKAGES_FOLDER_NAME = "Packages";
|
||||
public const string LIBRARY_FOLDER_NAME = "Library";
|
||||
public const string CACHE_FOLDER_NAME = "Cache";
|
||||
public const string CONFIG_FOLDER_NAME = "Config";
|
||||
|
||||
public const string IMPORTS_FOLDER_NAME = "Imports";
|
||||
|
||||
private static IServiceProvider? s_serviceProvider;
|
||||
|
||||
private static string s_currentProjectPath = string.Empty;
|
||||
private static string s_currentProjectName = string.Empty;
|
||||
|
||||
private static string s_assetsFolderPath = string.Empty;
|
||||
private static string s_packagesFolderPath = string.Empty;
|
||||
private static string s_libraryFolderPath = string.Empty;
|
||||
private static string s_cacheFolderPath = string.Empty;
|
||||
private static string s_configFolderPath = string.Empty;
|
||||
|
||||
private static string s_libraryImportsFolderPath = string.Empty;
|
||||
|
||||
private static DispatcherQueue? s_dispatcherQueue;
|
||||
|
||||
internal static Application CurrentApplication => Application.Current;
|
||||
@@ -25,13 +33,12 @@ public static class EditorApplication
|
||||
public static string ProjectPath => s_currentProjectPath;
|
||||
public static string ProjectName => s_currentProjectName;
|
||||
|
||||
public static readonly string AssetsFolderPath = Path.Combine(ProjectPath, ASSETS_FOLDER_NAME);
|
||||
public static readonly string SourcesFolderPath = Path.Combine(ProjectPath, SOURCES_FOLDER_NAME);
|
||||
public static readonly string PackagesFolderPath = Path.Combine(ProjectPath, PACKAGES_FOLDER_NAME);
|
||||
public static readonly string LibraryFolderPath = Path.Combine(ProjectPath, LIBRARY_FOLDER_NAME);
|
||||
public static readonly string ConfigFolderPath = Path.Combine(ProjectPath, CONFIG_FOLDER_NAME);
|
||||
|
||||
public static readonly string ImportsFolderPath = Path.Combine(LibraryFolderPath, IMPORTS_FOLDER_NAME);
|
||||
public static string AssetsFolderPath => s_assetsFolderPath;
|
||||
public static string PackagesFolderPath => s_packagesFolderPath;
|
||||
public static string LibraryFolderPath => s_libraryFolderPath;
|
||||
public static string ConfigFolderPath => s_configFolderPath;
|
||||
public static string CacheFolderPath => s_cacheFolderPath;
|
||||
public static string LibraryImportsFolderPath => s_libraryImportsFolderPath;
|
||||
|
||||
public static DispatcherQueue DispatcherQueue
|
||||
{
|
||||
@@ -51,6 +58,22 @@ public static class EditorApplication
|
||||
s_serviceProvider = serviceProvider;
|
||||
s_currentProjectPath = projectPath;
|
||||
s_currentProjectName = projectName;
|
||||
|
||||
s_assetsFolderPath = Path.Combine(projectPath, ASSETS_FOLDER_NAME);
|
||||
s_packagesFolderPath = Path.Combine(projectPath, PACKAGES_FOLDER_NAME);
|
||||
s_libraryFolderPath = Path.Combine(projectPath, LIBRARY_FOLDER_NAME);
|
||||
s_configFolderPath = Path.Combine(projectPath, CONFIG_FOLDER_NAME);
|
||||
s_cacheFolderPath = Path.Combine(projectPath, CACHE_FOLDER_NAME);
|
||||
|
||||
s_libraryImportsFolderPath = Path.Combine(s_libraryFolderPath, IMPORTS_FOLDER_NAME);
|
||||
|
||||
Directory.CreateDirectory(s_assetsFolderPath);
|
||||
Directory.CreateDirectory(s_packagesFolderPath);
|
||||
Directory.CreateDirectory(s_libraryFolderPath);
|
||||
Directory.CreateDirectory(s_configFolderPath);
|
||||
Directory.CreateDirectory(s_cacheFolderPath);
|
||||
|
||||
Directory.CreateDirectory(s_libraryImportsFolderPath);
|
||||
}
|
||||
|
||||
internal static void SetDispatcherQueue(DispatcherQueue dispatcherQueue)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Ghost.Editor.Core.AssetHandler;
|
||||
using Ghost.Engine;
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace Ghost.Editor.Core.Services;
|
||||
@@ -10,21 +11,18 @@ namespace Ghost.Editor.Core.Services;
|
||||
internal sealed class AssetCatalog : IDisposable
|
||||
{
|
||||
private readonly SqliteConnection _connection;
|
||||
private readonly object _writeLock = new();
|
||||
private readonly Lock _writeLock = new();
|
||||
|
||||
// Prepared statements
|
||||
private readonly SqliteCommand _cmdGetGuid;
|
||||
private readonly SqliteCommand _cmdGetPath;
|
||||
private readonly SqliteCommand _cmdUpsert;
|
||||
private readonly SqliteCommand _cmdDelete;
|
||||
private readonly SqliteCommand _cmdMarkDirty;
|
||||
private readonly SqliteCommand _cmdMarkImported;
|
||||
private readonly SqliteCommand _cmdMarkFailed;
|
||||
private readonly SqliteCommand _cmdGetHandlerTypeId;
|
||||
private readonly SqliteCommand _cmdGetReferencers;
|
||||
private readonly SqliteCommand _cmdGetDependencies;
|
||||
private readonly SqliteCommand _cmdInsertDep;
|
||||
private readonly SqliteCommand _cmdClearDeps;
|
||||
private readonly SqliteCommand _cmdGetDirty;
|
||||
private readonly SqliteCommand _cmdEnumerate;
|
||||
|
||||
public AssetCatalog(string dbPath)
|
||||
@@ -50,6 +48,7 @@ internal sealed class AssetCatalog : IDisposable
|
||||
|
||||
_cmdGetGuid = CreateCommand("SELECT guid FROM assets WHERE source_path = @path");
|
||||
_cmdGetPath = CreateCommand("SELECT source_path FROM assets WHERE guid = @guid");
|
||||
_cmdGetHandlerTypeId = CreateCommand("SELECT handler_type_id FROM assets WHERE guid = @guid");
|
||||
_cmdUpsert = CreateCommand(@"
|
||||
INSERT INTO assets (guid, source_path, handler_type_id, handler_version, state)
|
||||
VALUES (@guid, @path, @handler_id, @version, 0)
|
||||
@@ -59,21 +58,10 @@ internal sealed class AssetCatalog : IDisposable
|
||||
handler_version = excluded.handler_version,
|
||||
state = 0;");
|
||||
_cmdDelete = CreateCommand("DELETE FROM assets WHERE guid = @guid");
|
||||
_cmdMarkDirty = CreateCommand("UPDATE assets SET state = 0 WHERE guid = @guid");
|
||||
_cmdMarkImported = CreateCommand(@"
|
||||
UPDATE assets SET
|
||||
content_hash = @content_hash,
|
||||
settings_hash = @settings_hash,
|
||||
imported_at_ms = @time,
|
||||
state = 1,
|
||||
error_message = NULL
|
||||
WHERE guid = @guid");
|
||||
_cmdMarkFailed = CreateCommand("UPDATE assets SET state = 2, error_message = @msg WHERE guid = @guid");
|
||||
_cmdGetReferencers = CreateCommand("SELECT from_guid FROM dependencies WHERE to_guid = @guid");
|
||||
_cmdGetDependencies = CreateCommand("SELECT to_guid FROM dependencies WHERE from_guid = @guid");
|
||||
_cmdInsertDep = CreateCommand("INSERT INTO dependencies (from_guid, to_guid) VALUES (@from, @to)");
|
||||
_cmdClearDeps = CreateCommand("DELETE FROM dependencies WHERE from_guid = @guid");
|
||||
_cmdGetDirty = CreateCommand("SELECT guid, source_path FROM assets WHERE state = 0");
|
||||
_cmdEnumerate = CreateCommand("SELECT guid, source_path FROM assets");
|
||||
}
|
||||
|
||||
@@ -96,8 +84,6 @@ internal sealed class AssetCatalog : IDisposable
|
||||
content_hash TEXT,
|
||||
settings_hash TEXT,
|
||||
imported_at_ms INTEGER,
|
||||
state INTEGER NOT NULL DEFAULT 0,
|
||||
error_message TEXT
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_assets_path ON assets(source_path);
|
||||
|
||||
@@ -155,38 +141,12 @@ internal sealed class AssetCatalog : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public void MarkDirty(Guid guid)
|
||||
public Guid GetHandlerTypeId(Guid guid)
|
||||
{
|
||||
lock (_writeLock)
|
||||
{
|
||||
_cmdMarkDirty.Parameters.Clear();
|
||||
_cmdMarkDirty.Parameters.AddWithValue("@guid", guid.ToByteArray());
|
||||
_cmdMarkDirty.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
public void MarkImported(Guid guid, string contentHash, string settingsHash)
|
||||
{
|
||||
lock (_writeLock)
|
||||
{
|
||||
_cmdMarkImported.Parameters.Clear();
|
||||
_cmdMarkImported.Parameters.AddWithValue("@guid", guid.ToByteArray());
|
||||
_cmdMarkImported.Parameters.AddWithValue("@content_hash", contentHash);
|
||||
_cmdMarkImported.Parameters.AddWithValue("@settings_hash", settingsHash);
|
||||
_cmdMarkImported.Parameters.AddWithValue("@time", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
|
||||
_cmdMarkImported.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
public void MarkFailed(Guid guid, string error)
|
||||
{
|
||||
lock (_writeLock)
|
||||
{
|
||||
_cmdMarkFailed.Parameters.Clear();
|
||||
_cmdMarkFailed.Parameters.AddWithValue("@guid", guid.ToByteArray());
|
||||
_cmdMarkFailed.Parameters.AddWithValue("@msg", error);
|
||||
_cmdMarkFailed.ExecuteNonQuery();
|
||||
}
|
||||
_cmdGetHandlerTypeId.Parameters.Clear();
|
||||
_cmdGetHandlerTypeId.Parameters.AddWithValue("@guid", guid.ToByteArray());
|
||||
var result = _cmdGetHandlerTypeId.ExecuteScalar();
|
||||
return result is byte[] bytes ? new Guid(bytes) : Guid.Empty;
|
||||
}
|
||||
|
||||
public void SetDependencies(Guid assetId, ReadOnlySpan<Guid> dependencies)
|
||||
@@ -207,6 +167,7 @@ internal sealed class AssetCatalog : IDisposable
|
||||
_cmdInsertDep.Parameters.AddWithValue("@to", dep.ToByteArray());
|
||||
_cmdInsertDep.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
tx.Commit();
|
||||
}
|
||||
}
|
||||
@@ -215,12 +176,14 @@ internal sealed class AssetCatalog : IDisposable
|
||||
{
|
||||
_cmdGetReferencers.Parameters.Clear();
|
||||
_cmdGetReferencers.Parameters.AddWithValue("@guid", guid.ToByteArray());
|
||||
|
||||
using var reader = _cmdGetReferencers.ExecuteReader();
|
||||
var list = new List<Guid>();
|
||||
while (reader.Read())
|
||||
{
|
||||
list.Add(new Guid((byte[])reader[0]));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
@@ -228,23 +191,14 @@ internal sealed class AssetCatalog : IDisposable
|
||||
{
|
||||
_cmdGetDependencies.Parameters.Clear();
|
||||
_cmdGetDependencies.Parameters.AddWithValue("@guid", guid.ToByteArray());
|
||||
|
||||
using var reader = _cmdGetDependencies.ExecuteReader();
|
||||
var list = new List<Guid>();
|
||||
while (reader.Read())
|
||||
{
|
||||
list.Add(new Guid((byte[])reader[0]));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public List<(Guid guid, string sourcePath)> GetDirtyAssets()
|
||||
{
|
||||
using var reader = _cmdGetDirty.ExecuteReader();
|
||||
var list = new List<(Guid guid, string sourcePath)>();
|
||||
while (reader.Read())
|
||||
{
|
||||
list.Add((new Guid((byte[])reader[0]), reader.GetString(1)));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@@ -263,14 +217,11 @@ internal sealed class AssetCatalog : IDisposable
|
||||
_cmdGetPath.Dispose();
|
||||
_cmdUpsert.Dispose();
|
||||
_cmdDelete.Dispose();
|
||||
_cmdMarkDirty.Dispose();
|
||||
_cmdMarkImported.Dispose();
|
||||
_cmdMarkFailed.Dispose();
|
||||
_cmdGetHandlerTypeId.Dispose();
|
||||
_cmdGetReferencers.Dispose();
|
||||
_cmdGetDependencies.Dispose();
|
||||
_cmdInsertDep.Dispose();
|
||||
_cmdClearDeps.Dispose();
|
||||
_cmdGetDirty.Dispose();
|
||||
_cmdEnumerate.Dispose();
|
||||
_connection.Dispose();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Core.Utilities;
|
||||
using Ghost.Editor.Core.AssetHandler;
|
||||
using Ghost.Editor.Core.Contracts;
|
||||
using Ghost.Engine.AssetLoader;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Ghost.Editor.Core.Services;
|
||||
@@ -11,37 +11,34 @@ namespace Ghost.Editor.Core.Services;
|
||||
/// </summary>
|
||||
internal sealed class AssetRegistry : IAssetRegistry, IDisposable
|
||||
{
|
||||
private readonly string _assetsRoot;
|
||||
private readonly string _libraryRoot;
|
||||
private readonly AssetCatalog _catalog;
|
||||
private readonly ImportCoordinator _importCoordinator;
|
||||
private readonly FileSystemWatcher _watcher;
|
||||
|
||||
private readonly ConcurrentDictionary<Guid, WeakReference<Asset>> _loadedAssets;
|
||||
private readonly SemaphoreSlim _loadLock = new(1, 1);
|
||||
private readonly ConcurrentDictionary<string, bool> _ignoreMetaWrites = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ConcurrentDictionary<Guid, WeakReference<IAsset>> _loadedAssets;
|
||||
private readonly SemaphoreSlim _loadLock;
|
||||
|
||||
private readonly ConcurrentDictionary<string, bool> _ignoreMetaWrites;
|
||||
private readonly ConcurrentHashSet<Guid> _dirtyAssets;
|
||||
|
||||
public event EventHandler<IAssetRegistry, AssetChangedEventArgs>? OnAssetChanged;
|
||||
|
||||
public AssetRegistry(string assetsRoot)
|
||||
public AssetRegistry()
|
||||
{
|
||||
_assetsRoot = Path.GetFullPath(assetsRoot);
|
||||
_libraryRoot = Path.Combine(Path.GetDirectoryName(_assetsRoot)!, EditorApplication.LIBRARY_FOLDER_NAME);
|
||||
|
||||
// TODO: This should be handled by EditorApplication.
|
||||
Directory.CreateDirectory(_assetsRoot);
|
||||
Directory.CreateDirectory(_libraryRoot);
|
||||
|
||||
var dbPath = Path.Combine(_libraryRoot, "AssetDB.sqlite");
|
||||
var dbPath = Path.Combine(EditorApplication.LibraryFolderPath, "AssetDB.sqlite");
|
||||
|
||||
_catalog = new AssetCatalog(dbPath);
|
||||
_importCoordinator = new ImportCoordinator(_catalog, _assetsRoot, _libraryRoot);
|
||||
_importCoordinator = new ImportCoordinator(_catalog);
|
||||
|
||||
_loadedAssets = new ConcurrentDictionary<Guid, WeakReference<Asset>>();
|
||||
_loadedAssets = new ConcurrentDictionary<Guid, WeakReference<IAsset>>();
|
||||
_loadLock = new SemaphoreSlim(1, 1);
|
||||
|
||||
_ignoreMetaWrites = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
|
||||
_dirtyAssets = new ConcurrentHashSet<Guid>();
|
||||
|
||||
SyncCatalogWithDisk();
|
||||
|
||||
_watcher = new FileSystemWatcher(_assetsRoot)
|
||||
_watcher = new FileSystemWatcher(EditorApplication.AssetsFolderPath)
|
||||
{
|
||||
IncludeSubdirectories = true,
|
||||
EnableRaisingEvents = true,
|
||||
@@ -52,18 +49,16 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
|
||||
_watcher.Deleted += OnFileSystemEvent;
|
||||
_watcher.Changed += OnFileSystemEvent;
|
||||
_watcher.Renamed += OnFileSystemRenameEvent;
|
||||
|
||||
_importCoordinator.EnqueueDirtyAssetsAsync().AsTask().Wait();
|
||||
}
|
||||
|
||||
private void SyncCatalogWithDisk()
|
||||
{
|
||||
if (!Directory.Exists(_assetsRoot))
|
||||
if (!Directory.Exists(EditorApplication.AssetsFolderPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var metaFiles = Directory.EnumerateFiles(_assetsRoot, "*.gmeta", SearchOption.AllDirectories);
|
||||
var metaFiles = Directory.EnumerateFiles(EditorApplication.AssetsFolderPath, "*.gmeta", SearchOption.AllDirectories);
|
||||
var foundGuids = new HashSet<Guid>();
|
||||
|
||||
foreach (var metaPath in metaFiles)
|
||||
@@ -71,8 +66,8 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
|
||||
var meta = AssetMetaIO.ReadAsync(metaPath).AsTask().Result;
|
||||
if (meta != null)
|
||||
{
|
||||
var sourceRelative = AssetMetaIO.GetSourcePath(Path.GetRelativePath(_assetsRoot, metaPath));
|
||||
_catalog.Upsert(meta, sourceRelative.Replace('\\', '/'));
|
||||
var sourceRelative = AssetMetaIO.GetSourcePath(Path.GetRelativePath(EditorApplication.AssetsFolderPath, metaPath));
|
||||
_catalog.Upsert(meta, sourceRelative.Replace(Path.DirectorySeparatorChar, '/'));
|
||||
foundGuids.Add(meta.Guid);
|
||||
}
|
||||
}
|
||||
@@ -89,7 +84,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
|
||||
private async void OnFileSystemEvent(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
var ext = Path.GetExtension(e.FullPath);
|
||||
var relativePath = Path.GetRelativePath(_assetsRoot, e.FullPath).Replace('\\', '/');
|
||||
var relativePath = Path.GetRelativePath(EditorApplication.AssetsFolderPath, e.FullPath).Replace(Path.DirectorySeparatorChar, '/');
|
||||
|
||||
if (_ignoreMetaWrites.TryRemove(e.FullPath, out _))
|
||||
{
|
||||
@@ -101,7 +96,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
if (ext == ".gmeta")
|
||||
if (ext == AssetMetaIO.META_EXTENSION)
|
||||
{
|
||||
if (e.ChangeType == WatcherChangeTypes.Changed || e.ChangeType == WatcherChangeTypes.Created)
|
||||
{
|
||||
@@ -131,8 +126,8 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
|
||||
|
||||
private void OnFileSystemRenameEvent(object sender, RenamedEventArgs e)
|
||||
{
|
||||
var oldRelative = Path.GetRelativePath(_assetsRoot, e.OldFullPath).Replace('\\', '/');
|
||||
var newRelative = Path.GetRelativePath(_assetsRoot, e.FullPath).Replace('\\', '/');
|
||||
var oldRelative = Path.GetRelativePath(EditorApplication.AssetsFolderPath, e.OldFullPath).Replace(Path.DirectorySeparatorChar, '/');
|
||||
var newRelative = Path.GetRelativePath(EditorApplication.AssetsFolderPath, e.FullPath).Replace(Path.DirectorySeparatorChar, '/');
|
||||
|
||||
var guid = _catalog.GetGuid(oldRelative);
|
||||
if (guid != Guid.Empty)
|
||||
@@ -200,7 +195,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
|
||||
|
||||
var ext = Path.GetExtension(sourceFilePath);
|
||||
var relativePath = targetAssetPath.Replace(Path.DirectorySeparatorChar, '/');
|
||||
var fullPath = Path.Combine(_assetsRoot, relativePath);
|
||||
var fullPath = Path.Combine(EditorApplication.AssetsFolderPath, relativePath);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!);
|
||||
File.Copy(sourceFilePath, fullPath, true);
|
||||
@@ -220,18 +215,18 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
|
||||
return Result.Failure("Asset not found");
|
||||
}
|
||||
|
||||
var fullPath = Path.Combine(_assetsRoot, path);
|
||||
var fullPath = Path.Combine(EditorApplication.AssetsFolderPath, path);
|
||||
var metaPath = AssetMetaIO.GetMetaPath(fullPath);
|
||||
|
||||
await _importCoordinator.EnqueueAsync(new ImportJob(assetId, path, metaPath, ImportReason.ManualReimport), token);
|
||||
return Result.Success();
|
||||
}
|
||||
|
||||
public async ValueTask<Result<Asset>> LoadAssetAsync(Guid id, CancellationToken token = default)
|
||||
public async ValueTask<Result<IAsset>> LoadAssetAsync(Guid id, CancellationToken token = default)
|
||||
{
|
||||
if (_loadedAssets.TryGetValue(id, out var weakRef) && weakRef.TryGetTarget(out var asset))
|
||||
{
|
||||
return asset;
|
||||
return Result.Success(asset);
|
||||
}
|
||||
|
||||
await _loadLock.WaitAsync(token);
|
||||
@@ -239,24 +234,33 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
|
||||
{
|
||||
if (_loadedAssets.TryGetValue(id, out weakRef) && weakRef.TryGetTarget(out asset))
|
||||
{
|
||||
return asset;
|
||||
return Result.Success(asset);
|
||||
}
|
||||
|
||||
var importedPath = Path.Combine(_libraryRoot, "Imports", $"{id:N}.imported");
|
||||
if (!File.Exists(importedPath))
|
||||
var path = GetAssetPath(id);
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return Result.Failure<Asset>("Asset not imported");
|
||||
return Result.Failure("Asset does not exist.");
|
||||
}
|
||||
|
||||
// For now, we use a basic LoadAsync implementation.
|
||||
// In a better design, we'd read the handler ID from the header.
|
||||
// Here we we assume the catalog is correct (it's synced with gmeta).
|
||||
var handler = AssetHandlerRegistry.GetByExtension(Path.GetExtension(path));
|
||||
if (handler is null)
|
||||
{
|
||||
return Result.Failure("No Available handler type.");
|
||||
}
|
||||
|
||||
// Looking up TypeId from catalog isn't implemented in AssetCatalog yet.
|
||||
// We should add it or use the header.
|
||||
// The existing Asset class might still be tied to the old binary format.
|
||||
var meta = await AssetMetaIO.ReadAsync(AssetMetaIO.GetMetaPath(path), token);
|
||||
if (meta is null)
|
||||
{
|
||||
return Result.Failure("Meta file does not exist.");
|
||||
}
|
||||
|
||||
return Result.Failure<Asset>("Full asset loading would require updating all assets to the new format first.");
|
||||
await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
return await handler.LoadAssetAsync(stream, id, meta.Settings, token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Failure(ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -264,9 +268,88 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public ValueTask<Result> SaveAssetAsync(Asset asset, CancellationToken token = default)
|
||||
public async ValueTask<Result> SaveAssetAsync(IAsset asset, CancellationToken token = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
try
|
||||
{
|
||||
var path = GetAssetPath(asset.ID);
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return Result.Failure("Asset does not exist.");
|
||||
}
|
||||
|
||||
var handler = AssetHandlerRegistry.GetByTypeId(asset.TypeID);
|
||||
if (handler is null)
|
||||
{
|
||||
return Result.Failure("No Avaliable handler type.");
|
||||
}
|
||||
|
||||
await using var stream = new FileStream(path, FileMode.Open, FileAccess.Write, FileShare.None);
|
||||
return await handler.SaveAssetAsync(stream, asset, token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Failure(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result> SaveAssetAsync(Guid id, CancellationToken token = default)
|
||||
{
|
||||
var result = await LoadAssetAsync(id, token);
|
||||
if (result.IsFailure)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return await SaveAssetAsync(result.Value, token);
|
||||
}
|
||||
|
||||
public void SetAssetDirty(Guid id)
|
||||
{
|
||||
_dirtyAssets.Add(id);
|
||||
}
|
||||
|
||||
public async ValueTask<Result> SaveAssetIfDirtyAsync(IAsset asset, CancellationToken token = default)
|
||||
{
|
||||
if (_dirtyAssets.Contains(asset.ID))
|
||||
{
|
||||
var result = await SaveAssetAsync(asset, token);
|
||||
_dirtyAssets.Remove(asset.ID);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
|
||||
public async ValueTask<Result> SaveAssetIfDirtyAsync(Guid id, CancellationToken token = default)
|
||||
{
|
||||
var result = await LoadAssetAsync(id, token);
|
||||
if (result.IsFailure)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return await SaveAssetIfDirtyAsync(result.Value, token);
|
||||
}
|
||||
|
||||
public async ValueTask<Result[]> SaveDirtyAssetsAsync()
|
||||
{
|
||||
if (_dirtyAssets.IsEmpty)
|
||||
{
|
||||
return Array.Empty<Result>();
|
||||
}
|
||||
|
||||
var tasks = new Task<Result>[_dirtyAssets.Count];
|
||||
|
||||
var i = 0;
|
||||
foreach (var id in _dirtyAssets)
|
||||
{
|
||||
tasks[i++] = SaveAssetIfDirtyAsync(id).AsTask();
|
||||
}
|
||||
|
||||
_dirtyAssets.Clear();
|
||||
return await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Editor.Core.AssetHandler;
|
||||
using Ghost.Engine;
|
||||
|
||||
namespace Ghost.Editor.Core.Services;
|
||||
|
||||
internal class EditorContentProvider : IContentProvider
|
||||
{
|
||||
private readonly AssetCatalog _catalog;
|
||||
|
||||
public EditorContentProvider(AssetCatalog catalog)
|
||||
{
|
||||
_catalog = catalog;
|
||||
}
|
||||
|
||||
public bool HasAsset(Guid guid)
|
||||
{
|
||||
return _catalog.GetSourcePath(guid) != null;
|
||||
}
|
||||
|
||||
public Result<Stream> OpenRead(Guid guid, CancellationToken token = default)
|
||||
{
|
||||
var importedPath = Path.Combine(EditorApplication.LibraryImportsFolderPath, $"{guid:N}{ImportCoordinator.IMPORTED_EXTENSION}");
|
||||
if (!File.Exists(importedPath))
|
||||
{
|
||||
return Result.Failure($"Imported asset not found for GUID: {guid}");
|
||||
}
|
||||
|
||||
return new FileStream(importedPath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
}
|
||||
|
||||
public Guid[] GetDependencies(Guid guid)
|
||||
{
|
||||
return _catalog.GetDependencies(guid).ToArray();
|
||||
}
|
||||
|
||||
public AssetType GetAssetType(Guid guid)
|
||||
{
|
||||
var handlerID = _catalog.GetHandlerTypeId(guid);
|
||||
var handler = AssetHandlerRegistry.GetByTypeId(handlerID);
|
||||
return handler?.TargetAssetType ?? AssetType.Unknown;
|
||||
}
|
||||
}
|
||||
@@ -25,10 +25,11 @@ internal readonly record struct ImportJob(
|
||||
|
||||
internal sealed class ImportCoordinator : IDisposable
|
||||
{
|
||||
public const string IMPORTED_EXTENSION_NAME = "Imported";
|
||||
public const string IMPORTED_EXTENSION = ".imported";
|
||||
|
||||
private readonly Channel<ImportJob> _importChannel;
|
||||
private readonly AssetCatalog _catalog;
|
||||
private readonly string _assetsRoot;
|
||||
private readonly string _libraryRoot;
|
||||
private readonly CancellationTokenSource _cts;
|
||||
private readonly Task[] _workers;
|
||||
|
||||
@@ -36,11 +37,9 @@ internal sealed class ImportCoordinator : IDisposable
|
||||
// For now we just focus on the core logic
|
||||
// public event EventHandler<AssetChangedEventArgs>? OnAssetChanged;
|
||||
|
||||
public ImportCoordinator(AssetCatalog catalog, string assetsRoot, string libraryRoot, int workerCount = 2)
|
||||
public ImportCoordinator(AssetCatalog catalog, int workerCount = 2)
|
||||
{
|
||||
_catalog = catalog;
|
||||
_assetsRoot = assetsRoot;
|
||||
_libraryRoot = libraryRoot;
|
||||
_cts = new CancellationTokenSource();
|
||||
|
||||
_importChannel = Channel.CreateBounded<ImportJob>(new BoundedChannelOptions(256)
|
||||
@@ -61,15 +60,6 @@ internal sealed class ImportCoordinator : IDisposable
|
||||
return _importChannel.Writer.WriteAsync(job, token);
|
||||
}
|
||||
|
||||
public async ValueTask EnqueueDirtyAssetsAsync(CancellationToken token = default)
|
||||
{
|
||||
foreach (var (guid, sourcePath) in _catalog.GetDirtyAssets())
|
||||
{
|
||||
var metaPath = AssetMetaIO.GetMetaPath(Path.Combine(_assetsRoot, sourcePath));
|
||||
await EnqueueAsync(new ImportJob(guid, sourcePath, metaPath, ImportReason.Startup), token);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task WorkerLoop(CancellationToken token)
|
||||
{
|
||||
await foreach (var job in _importChannel.Reader.ReadAllAsync(token))
|
||||
@@ -80,18 +70,18 @@ internal sealed class ImportCoordinator : IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_catalog.MarkFailed(job.AssetGuid, ex.Message);
|
||||
Logger.Error(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask ProcessImportAsync(ImportJob job, CancellationToken token)
|
||||
{
|
||||
var fullSourcePath = Path.Combine(_assetsRoot, job.SourcePath);
|
||||
var fullSourcePath = Path.Combine(EditorApplication.AssetsFolderPath, job.SourcePath);
|
||||
var meta = await AssetMetaIO.ReadAsync(job.MetaPath, token);
|
||||
if (meta is null)
|
||||
{
|
||||
_catalog.MarkFailed(job.AssetGuid, "Missing .gmeta file");
|
||||
Logger.Error("Missing .gmeta file");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -108,17 +98,13 @@ internal sealed class ImportCoordinator : IDisposable
|
||||
meta.SettingsHash == settingsHash &&
|
||||
meta.HandlerVersion == AssetHandlerRegistry.GetVersionByTypeId(meta.HandlerTypeId ?? Guid.Empty))
|
||||
{
|
||||
_catalog.MarkImported(job.AssetGuid, contentHash, settingsHash);
|
||||
return;
|
||||
}
|
||||
|
||||
var importResult = Result.Success();
|
||||
if (handler is IAssetHandler importable)
|
||||
{
|
||||
// TODO: This should be handled by EditorApplication.
|
||||
var importsDir = Path.Combine(_libraryRoot, "Imports");
|
||||
Directory.CreateDirectory(importsDir);
|
||||
var targetPath = Path.Combine(importsDir, $"{job.AssetGuid:N}.imported");
|
||||
var targetPath = Path.Combine(EditorApplication.LibraryImportsFolderPath, $"{job.AssetGuid:N}{IMPORTED_EXTENSION}");
|
||||
|
||||
await using var sourceStream = new FileStream(fullSourcePath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
await using var targetStream = new FileStream(targetPath, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||
@@ -134,11 +120,10 @@ internal sealed class ImportCoordinator : IDisposable
|
||||
meta.LastImportedUtc = DateTime.UtcNow;
|
||||
|
||||
await AssetMetaIO.WriteAsync(job.MetaPath, meta, token);
|
||||
_catalog.MarkImported(job.AssetGuid, contentHash, settingsHash);
|
||||
}
|
||||
else
|
||||
{
|
||||
_catalog.MarkFailed(job.AssetGuid, importResult.Message ?? "Unknown import error");
|
||||
Logger.Error(importResult.Message ?? "Unknown import error");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +131,7 @@ internal sealed class ImportCoordinator : IDisposable
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
return "";
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
using var sha = SHA256.Create();
|
||||
@@ -159,7 +144,7 @@ internal sealed class ImportCoordinator : IDisposable
|
||||
{
|
||||
if (settings is null)
|
||||
{
|
||||
return "";
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var json = JsonSerializer.SerializeToUtf8Bytes(settings);
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
<ResourceDictionary Source="/Themes/Generic.xaml" />
|
||||
<!--<ResourceDictionary Source="/Themes/DockingDictionary.xaml" />-->
|
||||
<core:ControlsDictionary />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -3,11 +3,9 @@ using Ghost.Editor.Core;
|
||||
using Ghost.Editor.Core.Contracts;
|
||||
using Ghost.Editor.Core.Services;
|
||||
using Ghost.Editor.Core.Utilities;
|
||||
using Ghost.Editor.Views.Pages.EngineEditor;
|
||||
using Ghost.Editor.Views.Windows;
|
||||
using Ghost.Editor.ViewModels.Controls;
|
||||
using Ghost.Editor.ViewModels.Pages.EngineEditor;
|
||||
using Ghost.Editor.ViewModels.Windows;
|
||||
using Ghost.Editor.Views.Windows;
|
||||
using Ghost.Engine;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
@@ -66,7 +64,7 @@ public partial class App : Application
|
||||
services.AddSingleton<IProgressService, ProgressService>();
|
||||
services.AddSingleton<IInspectorService, InspectorService>();
|
||||
services.AddSingleton<IPreviewService, PreviewService>();
|
||||
// services.AddSingleton<IAssetService, AssetService>();
|
||||
services.AddSingleton<IAssetRegistry, AssetRegistry>();
|
||||
|
||||
services.AddSingleton<EngineEditorViewModel>();
|
||||
|
||||
@@ -97,22 +95,6 @@ public partial class App : Application
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#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();
|
||||
|
||||
|
||||
@@ -198,6 +198,8 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="ContextMenu\" />
|
||||
<Folder Include="ViewModels\Pages\" />
|
||||
<Folder Include="Views\Pages\" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals" />
|
||||
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Ghost.Core;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Ghost.Editor.ViewModels.Pages.EngineEditor;
|
||||
|
||||
internal partial class ConsoleViewModel : ObservableObject
|
||||
{
|
||||
public ReadOnlyObservableCollection<LogMessage> Logs;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool ShowInfo
|
||||
{
|
||||
get; set;
|
||||
} = true;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool ShowWarning
|
||||
{
|
||||
get; set;
|
||||
} = true;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool ShowError
|
||||
{
|
||||
get; set;
|
||||
} = true;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool ShowStackTrace
|
||||
{
|
||||
get; set;
|
||||
} = false;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial LogMessage? SelectedLog
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
partial void OnShowStackTraceChanged(bool value)
|
||||
{
|
||||
//Logger.HasStackTrace = value;
|
||||
//Logger.LogInfo($"Stack trace visibility set to {value}.");
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ClearLogs()
|
||||
{
|
||||
//Logger.Clear();
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Ghost.Editor.Core.Contracts;
|
||||
|
||||
namespace Ghost.Editor.ViewModels.Pages.EngineEditor;
|
||||
|
||||
internal partial class HierarchyViewModel : ObservableObject, INavigationAware
|
||||
{
|
||||
//[ObservableProperty]
|
||||
//public partial ObservableCollection<SceneNode> SceneList
|
||||
//{
|
||||
// get;
|
||||
// private set;
|
||||
//} = new(EditorSceneManager.LoadedWorlds);
|
||||
|
||||
//private void OnWorldLoaded(SceneNode node)
|
||||
//{
|
||||
// SceneList.Add(node);
|
||||
//}
|
||||
|
||||
//private void OnWorldUnloaded(SceneNode node)
|
||||
//{
|
||||
// SceneList.Remove(node);
|
||||
//}
|
||||
|
||||
public void OnNavigatedTo(object? parameter)
|
||||
{
|
||||
//EditorSceneManager.OnWorldLoaded += OnWorldLoaded;
|
||||
//EditorSceneManager.OnWorldUnloaded += OnWorldUnloaded;
|
||||
}
|
||||
|
||||
public void OnNavigatedFrom()
|
||||
{
|
||||
//EditorSceneManager.OnWorldLoaded -= OnWorldLoaded;
|
||||
//EditorSceneManager.OnWorldUnloaded -= OnWorldUnloaded;
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Ghost.Editor.Core.Contracts;
|
||||
|
||||
namespace Ghost.Editor.ViewModels.Pages.EngineEditor;
|
||||
|
||||
internal partial class InspectorViewModel(IInspectorService inspectorService) : ObservableObject, INavigationAware
|
||||
{
|
||||
[ObservableProperty]
|
||||
public partial IInspectable? Inspectable
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public void OnNavigatedTo(object? parameter)
|
||||
{
|
||||
inspectorService.OnSelectionChanged += OnSelectionChanged;
|
||||
Inspectable = inspectorService.Selected;
|
||||
}
|
||||
|
||||
public void OnNavigatedFrom()
|
||||
{
|
||||
inspectorService.OnSelectionChanged -= OnSelectionChanged;
|
||||
Inspectable = null;
|
||||
}
|
||||
|
||||
private void OnSelectionChanged(object? sender, EventArgs e)
|
||||
{
|
||||
Inspectable = inspectorService.Selected;
|
||||
}
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Ghost.Editor.Models;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Ghost.Editor.ViewModels.Pages.EngineEditor;
|
||||
|
||||
internal partial class ProjectViewModel : ObservableObject
|
||||
{
|
||||
// private readonly IAssetService _assetService;
|
||||
|
||||
public ObservableCollection<ExplorerItem> SubDirectories
|
||||
{
|
||||
get;
|
||||
} = new();
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<ExplorerItem> DirectoryAssets
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = new();
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ExplorerItem? SelectedDirectory
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ExplorerItem? SelectedAsset
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
// public ProjectViewModel(IAssetService assetService)
|
||||
// {
|
||||
// _assetService = assetService;
|
||||
|
||||
// var assetsRootItem = new ExplorerItem("Assets", Path.Combine(EditorApplication.ProjectPath, EditorApplication.ASSETS_FOLDER_NAME), true);
|
||||
// LoadSubFolderRecursive(ref assetsRootItem);
|
||||
|
||||
// SubDirectories.Add(assetsRootItem);
|
||||
// }
|
||||
|
||||
private static void LoadSubFolderRecursive(ref ExplorerItem parentItem)
|
||||
{
|
||||
foreach (var directory in Directory.EnumerateDirectories(parentItem.FullName))
|
||||
{
|
||||
var item = new ExplorerItem(Path.GetFileName(directory), directory, true);
|
||||
LoadSubFolderRecursive(ref item);
|
||||
|
||||
parentItem.Children ??= new();
|
||||
parentItem.Children.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
public static Task<ExplorerItem?> FindNodeIterative(ExplorerItem root, Func<ExplorerItem, bool> predicate)
|
||||
{
|
||||
var stack = new Stack<ExplorerItem>();
|
||||
stack.Push(root);
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
while (stack.Count > 0)
|
||||
{
|
||||
var node = stack.Pop();
|
||||
if (predicate(node))
|
||||
{
|
||||
return node;
|
||||
}
|
||||
|
||||
if (node.Children == null || node.Children.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var i = node.Children.Count - 1; i >= 0; i--)
|
||||
{
|
||||
stack.Push(node.Children[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private void NavigateToDirectory(string? path)
|
||||
{
|
||||
App.Window?.DispatcherQueue.TryEnqueue(async () =>
|
||||
{
|
||||
DirectoryAssets.Clear();
|
||||
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var directory in Directory.EnumerateDirectories(path))
|
||||
{
|
||||
var directoryItem = new ExplorerItem(Path.GetFileName(directory), directory, true);
|
||||
DirectoryAssets.Add(directoryItem);
|
||||
}
|
||||
|
||||
foreach (var file in Directory.EnumerateFiles(path))
|
||||
{
|
||||
var fileItem = new ExplorerItem(Path.GetFileName(file), file, false);
|
||||
DirectoryAssets.Add(fileItem);
|
||||
}
|
||||
|
||||
SelectedDirectory = await FindNodeIterative(SubDirectories[0], x => x.FullName == path);
|
||||
});
|
||||
}
|
||||
|
||||
public void OpenSelected()
|
||||
{
|
||||
if (SelectedAsset == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (SelectedAsset.IsDirectory)
|
||||
{
|
||||
NavigateToDirectory(SelectedAsset.FullName);
|
||||
}
|
||||
else
|
||||
{
|
||||
// _assetService.OpenAsset(SelectedAsset.FullName);
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnSelectedDirectoryChanged(ExplorerItem? value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DirectoryAssets.Clear();
|
||||
NavigateToDirectory(value.FullName);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ using Ghost.Editor.Core;
|
||||
|
||||
namespace Ghost.Editor.Views.Controls;
|
||||
|
||||
internal partial class ProjectBrowser
|
||||
internal partial class ContentBrowser
|
||||
{
|
||||
[ContextMenuItem("project-browser", "Show in Explorer")]
|
||||
private static void ShowInExplorer()
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="Ghost.Editor.Views.Controls.ProjectBrowser"
|
||||
x:Class="Ghost.Editor.Views.Controls.ContentBrowser"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:community="using:CommunityToolkit.WinUI.Controls"
|
||||
@@ -8,9 +8,9 @@ using Microsoft.UI.Xaml.Input;
|
||||
|
||||
namespace Ghost.Editor.Views.Controls;
|
||||
|
||||
internal sealed partial class ProjectBrowser : UserControl
|
||||
internal sealed partial class ContentBrowser : UserControl
|
||||
{
|
||||
public static ProjectBrowser? LastFocused
|
||||
public static ContentBrowser? LastFocused
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
@@ -24,7 +24,7 @@ internal sealed partial class ProjectBrowser : UserControl
|
||||
get;
|
||||
}
|
||||
|
||||
public ProjectBrowser()
|
||||
public ContentBrowser()
|
||||
{
|
||||
_inspectorService = App.GetService<IInspectorService>();
|
||||
ViewModel = App.GetService<ProjectBrowserViewModel>();
|
||||
@@ -1,211 +0,0 @@
|
||||
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() { }
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
<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>
|
||||
@@ -1,182 +0,0 @@
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
<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>
|
||||
@@ -1,47 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
<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>
|
||||
@@ -1,15 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
<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>
|
||||
@@ -1,344 +0,0 @@
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
<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>
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace Ghost.Editor.Views.Controls.Docking;
|
||||
|
||||
public enum DockTarget
|
||||
{
|
||||
Center,
|
||||
Left,
|
||||
Right,
|
||||
Top,
|
||||
Bottom
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Page
|
||||
x:Class="Ghost.Editor.Views.Pages.EngineEditor.ConsolePage"
|
||||
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.Pages.EngineEditor"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid Background="{ThemeResource LayerFillColorDefaultBrush}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<Grid
|
||||
Grid.Row="0"
|
||||
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultSolid}"
|
||||
BorderThickness="0,0,0,1">
|
||||
<CommandBar DefaultLabelPosition="Collapsed">
|
||||
<CommandBar.PrimaryCommands>
|
||||
<AppBarButton Command="{x:Bind ViewModel.ClearLogsCommand}" Content="Clear" />
|
||||
<AppBarSeparator />
|
||||
<AppBarToggleButton Width="45" IsChecked="{x:Bind ViewModel.ShowInfo, Mode=TwoWay}">
|
||||
<AppBarToggleButton.Icon>
|
||||
<FontIcon Glyph="" />
|
||||
</AppBarToggleButton.Icon>
|
||||
</AppBarToggleButton>
|
||||
<AppBarToggleButton Width="45" IsChecked="{x:Bind ViewModel.ShowWarning, Mode=TwoWay}">
|
||||
<AppBarToggleButton.Icon>
|
||||
<FontIcon Glyph="" />
|
||||
</AppBarToggleButton.Icon>
|
||||
</AppBarToggleButton>
|
||||
<AppBarToggleButton Width="45" IsChecked="{x:Bind ViewModel.ShowError, Mode=TwoWay}">
|
||||
<AppBarToggleButton.Icon>
|
||||
<FontIcon Glyph="" />
|
||||
</AppBarToggleButton.Icon>
|
||||
</AppBarToggleButton>
|
||||
</CommandBar.PrimaryCommands>
|
||||
|
||||
<CommandBar.SecondaryCommands>
|
||||
<AppBarToggleButton BorderThickness="0" Label="Clear On Play" />
|
||||
<AppBarToggleButton
|
||||
BorderThickness="0"
|
||||
IsChecked="{x:Bind ViewModel.ShowStackTrace, Mode=TwoWay}"
|
||||
Label="Show Stack Trace" />
|
||||
</CommandBar.SecondaryCommands>
|
||||
</CommandBar>
|
||||
</Grid>
|
||||
|
||||
<!-- Log Content -->
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="100" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<ListView
|
||||
x:Name="LogListView"
|
||||
Grid.Row="0"
|
||||
ItemsSource="{x:Bind ViewModel.Logs, Mode=OneWay}"
|
||||
SelectedItem="{x:Bind ViewModel.SelectedLog, Mode=TwoWay}" />
|
||||
<Grid
|
||||
Grid.Row="1"
|
||||
Padding="4"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultSolid}"
|
||||
BorderThickness="0,1,0,0">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
|
||||
<TextBlock
|
||||
IsTextSelectionEnabled="True"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.SelectedLog.ToString(), Mode=OneWay}"
|
||||
TextWrapping="Wrap" />
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Page>
|
||||
@@ -1,19 +0,0 @@
|
||||
using Ghost.Editor.ViewModels.Pages.EngineEditor;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Ghost.Editor.Views.Pages.EngineEditor;
|
||||
|
||||
internal sealed partial class ConsolePage : Page
|
||||
{
|
||||
public ConsoleViewModel ViewModel
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public ConsolePage()
|
||||
{
|
||||
ViewModel = App.GetService<ConsoleViewModel>();
|
||||
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<internal:NavigationTabPage
|
||||
x:Class="Ghost.Editor.Views.Pages.EngineEditor.HierarchyPage"
|
||||
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:internal="using:Ghost.Editor.Controls"
|
||||
xmlns:local="using:Ghost.Editor.Views.Pages.EngineEditor"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:sg="using:Ghost.Editor.Core.SceneGraph"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<internal:NavigationTabPage.Resources>
|
||||
<DataTemplate x:Key="SceneTemplate" x:DataType="sg:SceneGraphNode">
|
||||
<TreeViewItem
|
||||
AutomationProperties.Name="{x:Bind Name, Mode=OneWay}"
|
||||
Background="{ThemeResource ControlSolidFillColorDefaultBrush}"
|
||||
IsExpanded="True"
|
||||
ItemsSource="{x:Bind Children, Mode=OneWay}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<FontIcon FontSize="14" Glyph="" />
|
||||
<TextBlock Margin="10,0" Text="{x:Bind Name, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</TreeViewItem>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="EntityTemplate" x:DataType="sg:SceneGraphNode">
|
||||
<TreeViewItem AutomationProperties.Name="{x:Bind Name, Mode=OneWay}" ItemsSource="{x:Bind Children, Mode=OneWay}">
|
||||
<StackPanel Margin="10,0" Orientation="Horizontal">
|
||||
<FontIcon FontSize="14" Glyph="" />
|
||||
<TextBlock Margin="5,0,0,0" Text="{x:Bind Name, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</TreeViewItem>
|
||||
</DataTemplate>
|
||||
</internal:NavigationTabPage.Resources>
|
||||
|
||||
<Grid Padding="4,6" Background="{ThemeResource LayerFillColorDefaultBrush}">
|
||||
<!--<TreeView ItemsSource="{x:Bind ViewModel.SceneList}" SelectionChanged="TreeView_SelectionChanged">
|
||||
<TreeView.ItemTemplateSelector>
|
||||
<local:HierarchyTemplateSector />
|
||||
</TreeView.ItemTemplateSelector>
|
||||
</TreeView>-->
|
||||
</Grid>
|
||||
</internal:NavigationTabPage>
|
||||
@@ -1,61 +0,0 @@
|
||||
using Ghost.Editor.Controls;
|
||||
using Ghost.Editor.Core.Contracts;
|
||||
using Ghost.Editor.Core.SceneGraph;
|
||||
using Ghost.Editor.ViewModels.Pages.EngineEditor;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Ghost.Editor.Views.Pages.EngineEditor;
|
||||
|
||||
internal sealed partial class HierarchyPage : NavigationTabPage
|
||||
{
|
||||
private readonly IInspectorService _inspectorService;
|
||||
|
||||
public HierarchyViewModel ViewModel
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public HierarchyPage()
|
||||
{
|
||||
_inspectorService = App.GetService<IInspectorService>();
|
||||
ViewModel = App.GetService<HierarchyViewModel>();
|
||||
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public override void OnNavigatedTo(object? parameter)
|
||||
{
|
||||
ViewModel.OnNavigatedTo(parameter);
|
||||
}
|
||||
|
||||
public override void OnNavigatedFrom()
|
||||
{
|
||||
ViewModel.OnNavigatedFrom();
|
||||
}
|
||||
|
||||
private void TreeView_SelectionChanged(TreeView sender, TreeViewSelectionChangedEventArgs args)
|
||||
{
|
||||
if (args.AddedItems.Count > 0 && args.AddedItems[0] is IInspectable inspectable)
|
||||
{
|
||||
_inspectorService.SetSelected(inspectable, ViewModel);
|
||||
}
|
||||
else
|
||||
{
|
||||
_inspectorService.SetSelected(null, ViewModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal partial class HierarchyTemplateSector : DataTemplateSelector
|
||||
{
|
||||
protected override DataTemplate SelectTemplateCore(object item)
|
||||
{
|
||||
if (item is not SceneGraphNode node)
|
||||
{
|
||||
return base.SelectTemplateCore(item);
|
||||
}
|
||||
|
||||
return node.GetSceneHierarchyTemplate();
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<internal:NavigationTabPage
|
||||
x:Class="Ghost.Editor.Views.Pages.EngineEditor.InspectorPage"
|
||||
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:internal="using:Ghost.Editor.Controls"
|
||||
xmlns:local="using:Ghost.Editor.Views.Pages.EngineEditor"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid Background="{ThemeResource LayerFillColorDefaultBrush}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="75" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Header -->
|
||||
<Grid
|
||||
Grid.Row="0"
|
||||
Padding="15,0,10,0"
|
||||
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultSolid}"
|
||||
BorderThickness="0,0,0,1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<!--<IconSourceElement
|
||||
Grid.Column="0"
|
||||
Margin="0,0,15,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
IconSource="{x:Bind ViewModel.Inspectable.Icon, Mode=OneWay}" />-->
|
||||
<!--<ContentPresenter Grid.Column="1" Content="{x:Bind ViewModel.Inspectable.HeaderContent, Mode=OneWay}" />-->
|
||||
</Grid>
|
||||
|
||||
<!-- Content -->
|
||||
<Grid Grid.Row="1" Padding="0,0,0,0">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
|
||||
<!--<ContentPresenter Content="{x:Bind ViewModel.Inspectable.InspectorContent, Mode=OneWay}" />-->
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</internal:NavigationTabPage>
|
||||
@@ -1,29 +0,0 @@
|
||||
using Ghost.Editor.Controls;
|
||||
using Ghost.Editor.ViewModels.Pages.EngineEditor;
|
||||
|
||||
namespace Ghost.Editor.Views.Pages.EngineEditor;
|
||||
|
||||
internal sealed partial class InspectorPage : NavigationTabPage
|
||||
{
|
||||
public InspectorViewModel ViewModel
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public InspectorPage()
|
||||
{
|
||||
ViewModel = App.GetService<InspectorViewModel>();
|
||||
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public override void OnNavigatedTo(object? parameter)
|
||||
{
|
||||
ViewModel.OnNavigatedTo(parameter);
|
||||
}
|
||||
|
||||
public override void OnNavigatedFrom()
|
||||
{
|
||||
ViewModel.OnNavigatedFrom();
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Page
|
||||
x:Class="Ghost.Editor.Views.Pages.EngineEditor.ProjectPage"
|
||||
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:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:Ghost.Editor.Views.Pages.EngineEditor"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:model="using:Ghost.Editor.Models"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Page.Resources>
|
||||
<converter:AssetPathToGlyphConverter x:Key="AssetPathToGlyphConverter" />
|
||||
</Page.Resources>
|
||||
|
||||
<Grid Background="{ThemeResource LayerFillColorDefaultBrush}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="250" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Folder Tree View -->
|
||||
<Grid
|
||||
Grid.Column="0"
|
||||
Padding="4"
|
||||
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultSolid}"
|
||||
BorderThickness="0,0,1,0">
|
||||
<TreeView
|
||||
x:Name="DirectoryTreeView"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
ItemsSource="{x:Bind ViewModel.SubDirectories}"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Hidden"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Auto"
|
||||
SelectedItem="{x:Bind ViewModel.SelectedDirectory, Mode=TwoWay}">
|
||||
<TreeView.ItemTemplate>
|
||||
<DataTemplate x:DataType="model:ExplorerItem">
|
||||
<TreeViewItem ItemsSource="{x:Bind Children}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<FontIcon
|
||||
VerticalAlignment="Center"
|
||||
FontSize="14"
|
||||
Glyph="" />
|
||||
<TextBlock
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind Name}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
</TreeViewItem>
|
||||
</DataTemplate>
|
||||
</TreeView.ItemTemplate>
|
||||
</TreeView>
|
||||
</Grid>
|
||||
|
||||
<!-- Files -->
|
||||
<Grid Grid.Column="1">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid
|
||||
Grid.Row="0"
|
||||
Padding="4"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultSolid}"
|
||||
BorderThickness="0,0,0,1">
|
||||
<BreadcrumbBar Height="15" />
|
||||
</Grid>
|
||||
|
||||
<ScrollViewer
|
||||
Grid.Row="1"
|
||||
Padding="8"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<GridView
|
||||
x:Name="AssetsGridView"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
ItemsSource="{x:Bind ViewModel.DirectoryAssets, Mode=OneWay}"
|
||||
SelectedItem="{x:Bind ViewModel.SelectedAsset, Mode=TwoWay}">
|
||||
<GridView.ItemContainerStyle>
|
||||
<Style BasedOn="{StaticResource DefaultGridViewItemStyle}" TargetType="GridViewItem">
|
||||
<Setter Property="Margin" Value="2" />
|
||||
</Style>
|
||||
</GridView.ItemContainerStyle>
|
||||
|
||||
<GridView.ItemTemplate>
|
||||
<DataTemplate x:DataType="model:ExplorerItem">
|
||||
<Grid
|
||||
Width="100"
|
||||
Height="100"
|
||||
Padding="8"
|
||||
DoubleTapped="GridViewItem_DoubleTapped"
|
||||
IsDoubleTapEnabled="True">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="0.25*" />
|
||||
</Grid.RowDefinitions>
|
||||
<FontIcon FontSize="42" Glyph="{x:Bind FullName, Converter={StaticResource AssetPathToGlyphConverter}}" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Margin="8,0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind Name}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</GridView.ItemTemplate>
|
||||
</GridView>
|
||||
</ScrollViewer>
|
||||
|
||||
<Grid
|
||||
Grid.Row="2"
|
||||
Padding="4"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultSolid}"
|
||||
BorderThickness="0,1,0,0">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
HorizontalTextAlignment="Left"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.SelectedAsset.FullName, Mode=OneWay}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Page>
|
||||
@@ -1,25 +0,0 @@
|
||||
using Ghost.Editor.ViewModels.Pages.EngineEditor;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
|
||||
namespace Ghost.Editor.Views.Pages.EngineEditor;
|
||||
|
||||
internal sealed partial class ProjectPage : Page
|
||||
{
|
||||
public ProjectViewModel ViewModel
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public ProjectPage()
|
||||
{
|
||||
ViewModel = App.GetService<ProjectViewModel>();
|
||||
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void GridViewItem_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
|
||||
{
|
||||
ViewModel.OpenSelected();
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<internal:NavigationTabPage
|
||||
x:Class="Ghost.Editor.Views.Pages.EngineEditor.ScenePage"
|
||||
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:internal="using:Ghost.Editor.Controls"
|
||||
xmlns:local="using:Ghost.Editor.Views.Pages.EngineEditor"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid>
|
||||
<SwapChainPanel
|
||||
x:Name="SwapChainPanel"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch" />
|
||||
</Grid>
|
||||
</internal:NavigationTabPage>
|
||||
@@ -1,45 +0,0 @@
|
||||
using Ghost.Editor.Controls;
|
||||
//using Ghost.Graphics.Contracts;
|
||||
//using Microsoft.UI.Xaml;
|
||||
//using Microsoft.UI.Xaml.Controls;
|
||||
//using WinRT;
|
||||
|
||||
namespace Ghost.Editor.Views.Pages.EngineEditor;
|
||||
|
||||
internal sealed partial class ScenePage : NavigationTabPage
|
||||
{
|
||||
//private Renderer? _renderView;
|
||||
//private ISwapChainPanelNative _swapChainPanelNative;
|
||||
|
||||
public ScenePage()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
//SwapChainPanel.Loaded += SwapChainPanel_Loaded;
|
||||
//SwapChainPanel.Unloaded += SwapChainPanel_Unloaded;
|
||||
//SwapChainPanel.SizeChanged += SwapChainPanel_SizeChanged;
|
||||
}
|
||||
|
||||
//private void SwapChainPanel_Loaded(object sender, RoutedEventArgs e)
|
||||
//{
|
||||
// var guid = typeof(ISwapChainPanelNative.Interface).GUID;
|
||||
// ((IWinRTObject)SwapChainPanel).NativeObject.TryAs(guid, out var swapChainPanelNativeHandle);
|
||||
// _swapChainPanelNative = new ISwapChainPanelNative(swapChainPanelNativeHandle);
|
||||
|
||||
// _renderView = GraphicsPipeline.GraphicsDevice.CreateRenderer(new(_swapChainPanelNative, (uint)SwapChainPanel.ActualWidth, (uint)SwapChainPanel.ActualHeight));
|
||||
//}
|
||||
|
||||
//private void SwapChainPanel_Unloaded(object sender, RoutedEventArgs e)
|
||||
//{
|
||||
// _swapChainPanelNative.Dispose();
|
||||
// _renderView?.Dispose();
|
||||
//}
|
||||
|
||||
//private void SwapChainPanel_SizeChanged(object sender, SizeChangedEventArgs e)
|
||||
//{
|
||||
// if (e.NewSize.ActualWidth > 8.0 && e.NewSize.ActualHeight > 8.0)
|
||||
// {
|
||||
// _renderView?.RequestResize((uint)e.NewSize.ActualWidth, (uint)e.NewSize.ActualHeight);
|
||||
// }
|
||||
//}
|
||||
}
|
||||
@@ -1,246 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<winex:WindowEx
|
||||
x:Class="Ghost.Editor.Views.Windows.EngineEditorWindowOld"
|
||||
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:controls="using:Ghost.Editor.Views.Controls"
|
||||
xmlns:ctc="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:ee="using:Ghost.Editor.Views.Pages.EngineEditor"
|
||||
xmlns:ghost="using:Ghost.Editor.Controls"
|
||||
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
|
||||
xmlns:local="using:Ghost.Editor.Views.Windows"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:winex="using:WinUIEx"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Window.SystemBackdrop>
|
||||
<MicaBackdrop />
|
||||
</Window.SystemBackdrop>
|
||||
|
||||
<Grid Loaded="MainGrid_Loaded" Unloaded="MainGrid_Unloaded">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Titlebar -->
|
||||
<TitleBar
|
||||
x:Name="PART_TitleBar"
|
||||
Grid.Row="0"
|
||||
Background="{ThemeResource AcrylicBackgroundFillColorBaseBrush}"
|
||||
Subtitle="Ghost Engine">
|
||||
<TitleBar.IconSource>
|
||||
<ImageIconSource ImageSource="ms-appx:///Assets/icon.targetsize-48.png" />
|
||||
</TitleBar.IconSource>
|
||||
</TitleBar>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<Grid
|
||||
Grid.Row="1"
|
||||
Padding="4,0,4,4"
|
||||
Background="{ThemeResource AcrylicBackgroundFillColorBaseBrush}">
|
||||
<ctc:TabbedCommandBar>
|
||||
<ctc:TabbedCommandBar.MenuItems>
|
||||
<ctc:TabbedCommandBarItem Header="Home">
|
||||
<AppBarButton Label="Undo" />
|
||||
<AppBarButton Label="Redo" />
|
||||
<AppBarButton Label="Paste" />
|
||||
</ctc:TabbedCommandBarItem>
|
||||
<ctc:TabbedCommandBarItem Header="Home">
|
||||
<AppBarButton Label="Undo" />
|
||||
<AppBarButton Label="Redo" />
|
||||
<AppBarButton Label="Paste" />
|
||||
</ctc:TabbedCommandBarItem>
|
||||
<ctc:TabbedCommandBarItem Header="Home">
|
||||
<AppBarButton Label="Undo" />
|
||||
<AppBarButton Label="Redo" />
|
||||
<AppBarButton Label="Paste" />
|
||||
</ctc:TabbedCommandBarItem>
|
||||
</ctc:TabbedCommandBar.MenuItems>
|
||||
</ctc:TabbedCommandBar>
|
||||
</Grid>
|
||||
|
||||
<Grid xmlns:dock="using:Ghost.Editor.Views.Controls.Docking" Grid.Row="2">
|
||||
<dock:DockingLayout x:Name="MainDockingLayout" />
|
||||
</Grid>
|
||||
|
||||
<!-- Editor -->
|
||||
<!--<Grid Grid.Row="2">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid Grid.Row="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ghost:NavigationTabView
|
||||
Grid.Column="0"
|
||||
Width="350"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<ghost:NavigationTabView.TabItems>
|
||||
<TabViewItem Header="Hierarchy">
|
||||
<TabViewItem.IconSource>
|
||||
<FontIconSource Glyph="" />
|
||||
</TabViewItem.IconSource>
|
||||
<controls:Hierarchy />
|
||||
</TabViewItem>
|
||||
</ghost:NavigationTabView.TabItems>
|
||||
</ghost:NavigationTabView>
|
||||
|
||||
<ghost:NavigationTabView Grid.Column="1">
|
||||
<ghost:NavigationTabView.TabItems>
|
||||
<ee:ScenePage Header="Scene">
|
||||
<ee:ScenePage.IconSource>
|
||||
<FontIconSource Glyph="" />
|
||||
</ee:ScenePage.IconSource>
|
||||
</ee:ScenePage>
|
||||
</ghost:NavigationTabView.TabItems>
|
||||
</ghost:NavigationTabView>
|
||||
|
||||
<ghost:NavigationTabView
|
||||
Grid.Column="2"
|
||||
Width="350"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<ghost:NavigationTabView.TabItems>
|
||||
<ee:InspectorPage Header="Inspector">
|
||||
<ee:InspectorPage.IconSource>
|
||||
<FontIconSource Glyph="" />
|
||||
</ee:InspectorPage.IconSource>
|
||||
</ee:InspectorPage>
|
||||
</ghost:NavigationTabView.TabItems>
|
||||
</ghost:NavigationTabView>
|
||||
</Grid>
|
||||
|
||||
<ghost:NavigationTabView Grid.Row="1" Height="350">
|
||||
<ghost:NavigationTabView.TabItems>
|
||||
<TabViewItem Header="Project">
|
||||
<TabViewItem.IconSource>
|
||||
<FontIconSource Glyph="" />
|
||||
</TabViewItem.IconSource>
|
||||
<controls:ProjectBrowser />
|
||||
</TabViewItem>
|
||||
<TabViewItem Header="Console">
|
||||
<TabViewItem.IconSource>
|
||||
<FontIconSource Glyph="" />
|
||||
</TabViewItem.IconSource>
|
||||
<ee:ConsolePage />
|
||||
</TabViewItem>
|
||||
</ghost:NavigationTabView.TabItems>
|
||||
</ghost:NavigationTabView>
|
||||
</Grid>-->
|
||||
|
||||
<!-- Status Bar -->
|
||||
<Grid
|
||||
Grid.Row="3"
|
||||
Height="25"
|
||||
Background="{ThemeResource SmokeFillColorDefaultBrush}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Grid Grid.Column="0">
|
||||
<FontIcon
|
||||
Margin="8,0,0,0"
|
||||
FontSize="16"
|
||||
Foreground="{ThemeResource SystemFillColorSuccessBrush}"
|
||||
Glyph=""
|
||||
Visibility="Visible" />
|
||||
|
||||
<StackPanel Orientation="Horizontal" Visibility="Collapsed">
|
||||
<FontIcon
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="16"
|
||||
Foreground="{ThemeResource SystemFillColorAttentionBrush}"
|
||||
Glyph="" />
|
||||
<TextBlock
|
||||
Margin="4,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="0" />
|
||||
<FontIcon
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="16"
|
||||
Foreground="{ThemeResource SystemFillColorCautionBrush}"
|
||||
Glyph="" />
|
||||
<TextBlock
|
||||
Margin="4,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="0" />
|
||||
<FontIcon
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="16"
|
||||
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
|
||||
Glyph="" />
|
||||
<TextBlock
|
||||
Margin="4,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="0" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<!-- Info and Progress -->
|
||||
<Grid Grid.Row="0" Grid.RowSpan="4">
|
||||
<InfoBar
|
||||
x:Name="InfoBar"
|
||||
Margin="16"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom">
|
||||
<interactivity:Interaction.Behaviors>
|
||||
<behaviors:StackedNotificationsBehavior x:Name="NotificationQueue" />
|
||||
</interactivity:Interaction.Behaviors>
|
||||
</InfoBar>
|
||||
|
||||
<Grid
|
||||
x:Name="ProgressBarContainer"
|
||||
Background="{ThemeResource SmokeFillColorDefaultBrush}"
|
||||
Visibility="Collapsed">
|
||||
<Grid
|
||||
Height="100"
|
||||
Padding="36,24"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Background="{ThemeResource SolidBackgroundFillColorBaseBrush}"
|
||||
CornerRadius="{StaticResource OverlayCornerRadius}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock
|
||||
x:Name="ProgressMessage"
|
||||
Grid.Row="0"
|
||||
Margin="0,0,0,12"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource TitleTextBlockStyle}"
|
||||
Text="Loading..." />
|
||||
<ProgressBar
|
||||
x:Name="ProgressBar"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
IsIndeterminate="True" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</winex:WindowEx>
|
||||
@@ -1,88 +0,0 @@
|
||||
using Ghost.Editor.Core;
|
||||
using Ghost.Editor.Core.Contracts;
|
||||
using Ghost.Editor.Core.Services;
|
||||
using Ghost.Editor.ViewModels.Windows;
|
||||
using Windows.ApplicationModel;
|
||||
using WinUIEx;
|
||||
|
||||
namespace Ghost.Editor.Views.Windows;
|
||||
/// <summary>
|
||||
/// An empty window that can be used on its own or navigated to within a Frame.
|
||||
/// </summary>
|
||||
internal sealed partial class EngineEditorWindowOld : WindowEx
|
||||
{
|
||||
private readonly NotificationService _notificationService;
|
||||
private readonly ProgressService _progressService;
|
||||
|
||||
public EngineEditorViewModel ViewModel
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public EngineEditorWindowOld()
|
||||
{
|
||||
ViewModel = App.GetService<EngineEditorViewModel>();
|
||||
|
||||
_notificationService = (NotificationService)App.GetService<INotificationService>();
|
||||
_progressService = (ProgressService)App.GetService<IProgressService>();
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
AppWindow.SetIcon(Path.Combine(AppContext.BaseDirectory, "Assets/icon.ico"));
|
||||
Title = "Ghost Engine";
|
||||
ExtendsContentIntoTitleBar = true;
|
||||
|
||||
SetTitleBar(PART_TitleBar);
|
||||
this.CenterOnScreen();
|
||||
}
|
||||
|
||||
private void MainGrid_Loaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
|
||||
{
|
||||
PART_TitleBar.Title = EditorApplication.ProjectName;
|
||||
PART_TitleBar.Subtitle = $"Ghost Engine {Package.Current.Id.Version.Major}.{Package.Current.Id.Version.Minor}.{Package.Current.Id.Version.Build}";
|
||||
|
||||
_notificationService.SetReference(InfoBar, NotificationQueue);
|
||||
_progressService.SetReference(ProgressBarContainer);
|
||||
|
||||
InitializeDockingLayout();
|
||||
}
|
||||
|
||||
private void InitializeDockingLayout()
|
||||
{
|
||||
var sceneDoc = new Controls.Docking.DockDocument { Title = "Scene", Content = new Pages.EngineEditor.ScenePage() };
|
||||
var hierarchyDoc = new Controls.Docking.DockDocument { Title = "Hierarchy", Content = new Controls.Hierarchy() };
|
||||
var inspectorDoc = new Controls.Docking.DockDocument { Title = "Inspector", Content = new Pages.EngineEditor.InspectorPage() };
|
||||
var projectDoc = new Controls.Docking.DockDocument { Title = "Project", Content = new Controls.ProjectBrowser() };
|
||||
var consoleDoc = new Controls.Docking.DockDocument { Title = "Console", Content = new Pages.EngineEditor.ConsolePage() };
|
||||
|
||||
var leftGroup = new Controls.Docking.DockGroup();
|
||||
leftGroup.AddChild(hierarchyDoc);
|
||||
|
||||
var centerGroup = new Controls.Docking.DockGroup();
|
||||
centerGroup.AddChild(sceneDoc);
|
||||
|
||||
var rightGroup = new Controls.Docking.DockGroup();
|
||||
rightGroup.AddChild(inspectorDoc);
|
||||
|
||||
var bottomGroup = new Controls.Docking.DockGroup();
|
||||
bottomGroup.AddChild(projectDoc);
|
||||
bottomGroup.AddChild(consoleDoc);
|
||||
|
||||
var topPanel = new Controls.Docking.DockPanel { Orientation = Microsoft.UI.Xaml.Controls.Orientation.Horizontal };
|
||||
topPanel.AddChild(leftGroup);
|
||||
topPanel.AddChild(centerGroup);
|
||||
topPanel.AddChild(rightGroup);
|
||||
|
||||
var rootPanel = new Controls.Docking.DockPanel { Orientation = Microsoft.UI.Xaml.Controls.Orientation.Vertical };
|
||||
rootPanel.AddChild(topPanel);
|
||||
rootPanel.AddChild(bottomGroup);
|
||||
|
||||
MainDockingLayout.RootModule = rootPanel;
|
||||
}
|
||||
|
||||
private void MainGrid_Unloaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
|
||||
{
|
||||
_notificationService.ClearReference();
|
||||
_progressService.ClearReference();
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,2 @@
|
||||
namespace Ghost.Core;
|
||||
|
||||
public enum AssetType : byte
|
||||
{
|
||||
Texture = 0,
|
||||
Mesh = 1,
|
||||
Material = 2,
|
||||
Shaders = 3,
|
||||
Audio = 4,
|
||||
Scene = 5,
|
||||
Video = 6,
|
||||
Json = 7,
|
||||
|
||||
Unknown = 64,
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Misaki.HighPerformance" Version="1.0.8" />
|
||||
<PackageReference Include="Misaki.HighPerformance.Jobs" Version="2.0.0" />
|
||||
<PackageReference Include="Misaki.HighPerformance.Jobs" Version="3.0.0" />
|
||||
<PackageReference Include="Misaki.HighPerformance.LowLevel" Version="1.6.14">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -158,6 +158,7 @@ public readonly struct Result<T>
|
||||
|
||||
public static implicit operator Result<T>(T? data) => data is not null ? Success(data) : Failure(null);
|
||||
public static implicit operator Result<T>(Result result) => result.IsSuccess ? Success(default!) : Failure(result.Message);
|
||||
public static implicit operator Result(Result<T> result) => result.IsSuccess ? Result.Success() : Result.Failure(result.Message);
|
||||
public static implicit operator bool(Result<T> result) => result.IsSuccess;
|
||||
}
|
||||
|
||||
|
||||
162
src/Runtime/Ghost.Core/Utilities/ConcurrentHashSet.cs
Normal file
162
src/Runtime/Ghost.Core/Utilities/ConcurrentHashSet.cs
Normal file
@@ -0,0 +1,162 @@
|
||||
using System.Collections;
|
||||
|
||||
namespace Ghost.Core.Utilities;
|
||||
|
||||
public class ConcurrentHashSet<T> : IDisposable
|
||||
{
|
||||
public struct Enumerator : IEnumerator<T>
|
||||
{
|
||||
private readonly ConcurrentHashSet<T> _set;
|
||||
private readonly HashSet<T>.Enumerator _enumerator;
|
||||
|
||||
public Enumerator(ConcurrentHashSet<T> set)
|
||||
{
|
||||
_set = set;
|
||||
_set._lock.EnterReadLock();
|
||||
_enumerator = _set._hashSet.GetEnumerator();
|
||||
}
|
||||
|
||||
public readonly T Current => _enumerator.Current;
|
||||
readonly object? IEnumerator.Current => Current;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_set._lock.IsReadLockHeld)
|
||||
{
|
||||
_set._lock.ExitReadLock();
|
||||
}
|
||||
|
||||
_enumerator.Dispose();
|
||||
}
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
return _enumerator.MoveNext();
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
|
||||
private readonly HashSet<T> _hashSet = new HashSet<T>();
|
||||
|
||||
public int Count
|
||||
{
|
||||
get
|
||||
{
|
||||
_lock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
return _hashSet.Count;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_lock.IsReadLockHeld)
|
||||
{
|
||||
_lock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsEmpty
|
||||
{
|
||||
get
|
||||
{
|
||||
_lock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
return _hashSet.Count == 0;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_lock.IsReadLockHeld)
|
||||
{
|
||||
_lock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
~ConcurrentHashSet()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
|
||||
public Enumerator GetEnumerator()
|
||||
{
|
||||
return new Enumerator(this);
|
||||
}
|
||||
|
||||
public bool Add(T item)
|
||||
{
|
||||
_lock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
return _hashSet.Add(item);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_lock.IsWriteLockHeld)
|
||||
{
|
||||
_lock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool Contains(T item)
|
||||
{
|
||||
_lock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
return _hashSet.Contains(item);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_lock.IsReadLockHeld)
|
||||
{
|
||||
_lock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool Remove(T item)
|
||||
{
|
||||
_lock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
return _hashSet.Remove(item);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_lock.IsWriteLockHeld)
|
||||
{
|
||||
_lock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_lock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
_hashSet.Clear();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_lock.IsWriteLockHeld)
|
||||
{
|
||||
_lock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_lock.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using MemoryHandle = System.Buffers.MemoryHandle;
|
||||
|
||||
namespace Ghost.Core.Utilities;
|
||||
@@ -51,3 +53,47 @@ public unsafe class NativeMemoryManager<T> : MemoryManager<T>
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class CastMemoryManager<TFrom, TTo> : MemoryManager<TTo>
|
||||
where TFrom : struct
|
||||
where TTo : struct
|
||||
{
|
||||
private readonly Memory<TFrom> _from;
|
||||
private MemoryHandle _innerHandle;
|
||||
|
||||
public CastMemoryManager(Memory<TFrom> from)
|
||||
{
|
||||
_from = from;
|
||||
}
|
||||
|
||||
public override Span<TTo> GetSpan()
|
||||
{
|
||||
return MemoryMarshal.Cast<TFrom, TTo>(_from.Span);
|
||||
}
|
||||
|
||||
public override MemoryHandle Pin(int elementIndex = 0)
|
||||
{
|
||||
_innerHandle = _from.Pin();
|
||||
|
||||
unsafe
|
||||
{
|
||||
int byteOffset = elementIndex * Unsafe.SizeOf<TTo>();
|
||||
void* pointer = (byte*)_innerHandle.Pointer + byteOffset;
|
||||
|
||||
return new MemoryHandle(pointer, default, this);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Unpin()
|
||||
{
|
||||
_innerHandle.Dispose();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_innerHandle.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,47 +1,7 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Graphics.RHI;
|
||||
using Misaki.HighPerformance.LowLevel;
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ghost.Engine.AssetLoader;
|
||||
|
||||
public abstract class Asset : IResourceReleasable
|
||||
{
|
||||
private bool _disposed;
|
||||
|
||||
public Guid ID
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public abstract AssetType Type
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
protected Asset(Guid id)
|
||||
{
|
||||
ID = id;
|
||||
}
|
||||
|
||||
protected virtual void Release(IResourceDatabase resourceDatabase)
|
||||
{
|
||||
}
|
||||
|
||||
public void ReleaseResource(IResourceDatabase database)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Release(database);
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct AssetReference : IEquatable<AssetReference>
|
||||
{
|
||||
private readonly int _value;
|
||||
@@ -84,71 +44,3 @@ public readonly struct AssetReference : IEquatable<AssetReference>
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Size = 64)] // Leave extra space for future expansion without breaking compatibility
|
||||
public struct TextureContentHeader
|
||||
{
|
||||
public uint width;
|
||||
public uint height;
|
||||
public uint depth;
|
||||
public uint mipLevels;
|
||||
public uint dimension; // 1 for 1D, 2 for 2D, 3 for 3D
|
||||
public uint colorComponents;
|
||||
}
|
||||
|
||||
public class TextureAsset : Asset
|
||||
{
|
||||
private MemoryBlock _textureData;
|
||||
private readonly uint _width;
|
||||
private readonly uint _height;
|
||||
private readonly uint _depth;
|
||||
private readonly uint _colorComponents;
|
||||
private readonly uint _mipLevels;
|
||||
private readonly uint _dimension;
|
||||
|
||||
private Handle<GPUTexture> _textureHandle;
|
||||
|
||||
public override AssetType Type => AssetType.Texture;
|
||||
|
||||
public uint Width => _width;
|
||||
public uint Height => _height;
|
||||
public uint Depth => _depth;
|
||||
public uint MipLevels => _mipLevels;
|
||||
public uint Dimension => _dimension;
|
||||
public uint ColorComponents => _colorComponents;
|
||||
|
||||
public Handle<GPUTexture> TextureHandle => _textureHandle;
|
||||
|
||||
internal TextureAsset([OwnershipTransfer] ref MemoryBlock data, TextureContentHeader header, Guid id)
|
||||
: base(id)
|
||||
{
|
||||
_textureData = data;
|
||||
_width = header.width;
|
||||
_height = header.height;
|
||||
_depth = header.depth;
|
||||
_mipLevels = header.mipLevels;
|
||||
_dimension = header.dimension;
|
||||
_colorComponents = header.colorComponents;
|
||||
}
|
||||
|
||||
internal void SetTextureHandle(Handle<GPUTexture> handle, bool disposeCPUData = true)
|
||||
{
|
||||
_textureHandle = handle;
|
||||
if (disposeCPUData)
|
||||
{
|
||||
_textureData.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public ReadOnlySpan<T> GeData<T>()
|
||||
where T : unmanaged
|
||||
{
|
||||
return _textureData.AsSpan<T>();
|
||||
}
|
||||
|
||||
protected override void Release(IResourceDatabase resourceDatabase)
|
||||
{
|
||||
_textureData.Dispose();
|
||||
resourceDatabase.ReleaseResource(_textureHandle.AsResource());
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,23 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Core.Utilities;
|
||||
using Ghost.Engine.AssetLoader;
|
||||
using Ghost.Graphics;
|
||||
using Ghost.Graphics.RHI;
|
||||
using Ghost.Graphics.Utilities;
|
||||
using TerraFX.Interop.Windows;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ghost.Engine;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Size = 64)] // Leave extra space for future expansion without breaking compatibility
|
||||
public struct TextureContentHeader
|
||||
{
|
||||
public uint width;
|
||||
public uint height;
|
||||
public uint depth;
|
||||
public uint mipLevels;
|
||||
public uint dimension; // 1 for 1D, 2 for 2D, 3 for 3D
|
||||
public uint colorComponents;
|
||||
}
|
||||
|
||||
internal partial class AssetEntry
|
||||
{
|
||||
private unsafe class TextureData
|
||||
|
||||
@@ -11,11 +11,24 @@ using Misaki.HighPerformance.LowLevel.Utilities;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ghost.Engine;
|
||||
|
||||
public enum AssetState : byte
|
||||
public enum AssetType
|
||||
{
|
||||
Texture = 0,
|
||||
Mesh = 1,
|
||||
Material = 2,
|
||||
Shaders = 3,
|
||||
Scene = 4,
|
||||
Audio = 5,
|
||||
Video = 6,
|
||||
Json = 7,
|
||||
|
||||
Unknown = 32, // We are unlikely to have more than 32 asset types.
|
||||
}
|
||||
|
||||
public enum AssetState
|
||||
{
|
||||
Unloaded = 0,
|
||||
Scheduled = 1,
|
||||
@@ -192,6 +205,11 @@ internal unsafe partial class AssetEntry
|
||||
public void OnReleaseResource()
|
||||
{
|
||||
s_onReleaseResource[(int)_assetType]?.Invoke(this);
|
||||
|
||||
if (_rawData.IsCreated)
|
||||
{
|
||||
_rawData.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,7 +217,7 @@ internal struct LoadAssetJob : IJob
|
||||
{
|
||||
public Guid assetID;
|
||||
public AssetType assetType;
|
||||
public GCHandle assetManagerHandle;
|
||||
public AssetManager assetManager;
|
||||
|
||||
private static Result LoadRawData(IContentProvider contentProvider, AssetEntry entry)
|
||||
{
|
||||
@@ -232,8 +250,6 @@ internal struct LoadAssetJob : IJob
|
||||
|
||||
public readonly void Execute(ref readonly JobExecutionContext ctx)
|
||||
{
|
||||
var assetManager = assetManagerHandle.Target as AssetManager;
|
||||
|
||||
Logger.DebugAssert(assetManager is not null);
|
||||
|
||||
if (!assetManager.TryGetEntry(assetID, out var entry))
|
||||
@@ -279,8 +295,6 @@ internal partial class AssetManager : IDisposable
|
||||
|
||||
private readonly ConcurrentDictionary<Guid, AssetEntry> _entries;
|
||||
|
||||
private GCHandle _selfHandle;
|
||||
|
||||
// TODO
|
||||
private Handle<GPUTexture> _fallbackTexture;
|
||||
private Handle<GPUTexture> _fallbackNormalMap;
|
||||
@@ -300,7 +314,6 @@ internal partial class AssetManager : IDisposable
|
||||
_jobScheduler = jobScheduler;
|
||||
|
||||
_entries = new ConcurrentDictionary<Guid, AssetEntry>();
|
||||
_selfHandle = GCHandle.Alloc(this, GCHandleType.Normal);
|
||||
}
|
||||
|
||||
internal bool TryGetEntry(Guid guid, [NotNullWhen(true)] out AssetEntry? entry)
|
||||
@@ -378,10 +391,10 @@ internal partial class AssetManager : IDisposable
|
||||
{
|
||||
assetID = entry.AssetId,
|
||||
assetType = entry.AssetType,
|
||||
assetManagerHandle = _selfHandle,
|
||||
assetManager = this,
|
||||
};
|
||||
|
||||
entry.SetLoadJobHandle(_jobScheduler.Schedule(ref job, dependency, JobPriority.Low)); // Use low priority to avoid blocking main thread critical tasks like rendering and physics.
|
||||
entry.SetLoadJobHandle(_jobScheduler.Schedule(ref job, JobPriority.Low, dependency)); // Use low priority to avoid blocking main thread critical tasks like rendering and physics.
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
@@ -409,19 +422,19 @@ internal partial class AssetManager : IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry.State is AssetState.Loading or AssetState.Loaded or AssetState.Uploading)
|
||||
if (Interlocked.CompareExchange(ref entry.StateValue, (int)AssetState.Scheduled, (int)AssetState.Ready) == (int)AssetState.Ready)
|
||||
{
|
||||
entry.SetPendingReimport();
|
||||
return;
|
||||
}
|
||||
|
||||
// Entry is in Ready state — the old texture is valid and will remain visible.
|
||||
// Go directly to Scheduled → Loading → Loaded → Uploading → Ready again.
|
||||
// The swap cycle in RecordTextureUpload/OnTextureUploadComplete handles the
|
||||
// v1 → v2 transition exactly like the fallback → v1 transition.
|
||||
entry.State = AssetState.Scheduled;
|
||||
EnsureScheduled(entry);
|
||||
}
|
||||
else
|
||||
{
|
||||
entry.SetPendingReimport();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
@@ -431,6 +444,5 @@ internal partial class AssetManager : IDisposable
|
||||
}
|
||||
|
||||
_entries.Clear();
|
||||
_selfHandle.Free();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ internal unsafe struct ChunkInfo
|
||||
internal unsafe struct JobChunkBatch<TJob> : IJobParallelFor
|
||||
where TJob : unmanaged, IJobChunk
|
||||
{
|
||||
|
||||
public TJob userJob;
|
||||
public ReadOnlyUnsafeCollection<ChunkInfo> chunkInfos;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user