Skip to content
54 changes: 54 additions & 0 deletions docs/editor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Editor Framework — Overview

Stride's editor is built on a ViewModel layer that sits above Quantum and the asset system. Every mutation goes through a transaction stack that feeds undo/redo. A separate selection history stack, sharing the same low-level infrastructure, powers back/forward navigation. Both systems use `ITransactionStack` and `Operation` from the `Stride.Core.Transactions` namespace (assembly: `Stride.Core.Design`).

## Shared Infrastructure

```mermaid
flowchart TD
A["ITransactionStack + Operation<br/>Stride.Core.Design · Stride.Core.Transactions<br/>sources/core/Stride.Core.Design/Transactions/<br/>Shared interface and base type — each consumer creates its own instance"]
B["IUndoRedoService<br/>Stride.Core.Presentation<br/>sources/presentation/Stride.Core.Presentation/Services/<br/>Wraps stack with naming, dirtiable sync, and save-point tracking"]
C["SelectionService<br/>Stride.Core.Assets.Editor<br/>sources/editor/Stride.Core.Assets.Editor/Services/<br/>Separate unbounded stack; records selection snapshots"]

A -. "uses" .-> B
A -. "uses" .-> C
```

## Projects

The editor codebase spans `sources/presentation/` (MVVM framework, Quantum-to-UI binding, shared controls) and `sources/editor/` (editor infrastructure and concrete asset editors). ViewModels are platform-agnostic; WPF coupling belongs in XAML files, code-behind, and WPF-specific service implementations. See [projects.md](projects.md) for the full project map and assembly reference.

## When You Need These Systems

> **Decision tree:**
>
> - Wrapping a property or collection mutation so it is undoable?
> → **`IUndoRedoService.CreateTransaction()` + `AnonymousDirtyingOperation`.** See [undo-redo.md](undo-redo.md).
>
> - Writing a reusable operation that merges consecutive edits on the same target?
> → **`DirtyingOperation` subclass + `IMergeableOperation`.** See [undo-redo.md](undo-redo.md).
>
> - Tracking which objects are "dirty" (unsaved) after changes?
> → **`IDirtiable` / `DirtiableManager`.** See [undo-redo.md](undo-redo.md).
>
> - Mutating a value through a Quantum node presenter (property grid edit)?
> → **No `PushOperation` needed** — `ContentValueChangeOperation` is pushed automatically by the Quantum infrastructure. See [undo-redo.md](undo-redo.md#how-quantum-feeds-the-stack-automatically).
>
> - Understanding how back/forward selection history works?
> → **`SelectionService`.** See [navigation.md](navigation.md).
>
> - Creating a dedicated editing surface for a new asset type?
> → **Write a custom editor.** See [custom-editor.md](custom-editor.md).
>
> - Understanding or modifying an existing editor?
> → **Existing editors catalogue.** See [editors.md](editors.md).

## Spoke Files

| File | Covers |
|---|---|
| [undo-redo.md](undo-redo.md) | `ITransactionStack`, `IUndoRedoService`, `DirtyingOperation`, `IMergeableOperation`, `IDirtiable`, dirty-flag synchronisation |
| [navigation.md](navigation.md) | `SelectionService`, selection history snapshots, back/forward navigation |
| [projects.md](projects.md) | Project inventory, WPF boundary rule, assembly map |
| [custom-editor.md](custom-editor.md) | Custom asset editor: base class, registration, lifecycle, services, MVVM patterns |
| [editors.md](editors.md) | Existing editors catalogue: SpriteSheet, Scene, Prefab, UIPage, UILibrary, GraphicsCompositor, Script, VisualScript |
182 changes: 182 additions & 0 deletions docs/editor/custom-editor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# Writing a Custom Asset Editor

## Role

A custom editor is a ViewModel + View pair registered against an `AssetViewModel` type. When the user double-clicks the asset in GameStudio, the framework instantiates the registered view, binds it to the registered ViewModel, and calls `InitializeEditor`. The ViewModel drives all logic; the View is pure WPF XAML bound to it.

## Choosing a Base Class

| Base class | Use when | What it adds |
|---|---|---|
| `AssetEditorViewModel` | Simple editor with no game viewport and no hierarchical parts (e.g. sprite sheet, graphics compositor, script) | Asset ownership, `Initialize`/`Destroy` lifecycle, `IUndoRedoService`, `SessionViewModel` |
| `GameEditorViewModel` | Editor that needs a live game instance for rendering (rarely subclassed directly — prefer the composite variant below) | `IEditorGameController` integration, game startup/shutdown, error recovery |
| `AssetCompositeHierarchyEditorViewModel<TAssetPartDesign, TAssetPart, TItemViewModel>` | Asset that contains a tree of selectable parts (scenes, prefabs, UI pages) | Selection tracking, copy/cut/paste/delete/duplicate for hierarchy parts, part ViewModel factory |

## Registration

Two attributes are required. Both are discovered automatically by `AssetsEditorPlugin` via reflection at startup — no manual registration needed.

```csharp
// On the editor ViewModel class — maps AssetViewModel subtype → editor ViewModel type.
[AssetEditorViewModel<%%AssetName%%ViewModel>]
public sealed class %%AssetName%%EditorViewModel : AssetEditorViewModel
{
public %%AssetName%%EditorViewModel([NotNull] %%AssetName%%ViewModel asset)
: base(asset) { }
}

// On the view code-behind — maps editor ViewModel type → view type.
[AssetEditorView<%%AssetName%%EditorViewModel>]
public partial class %%AssetName%%EditorView : UserControl, IEditorView { ... }
```

Both classes must live in `Stride.Assets.Presentation` (or an assembly loaded as a plugin via `AssetsEditorPlugin`).

## Lifecycle

**1. Construction** — synchronous; `base(asset)` is the only required call; do not perform async work here.

**2. `Initialize()`**

```csharp
public override async Task<bool> Initialize()
{
// Load resources, set up bindings, register selection scope.
// Return false to abort — the editor will not open and Destroy() will be called.
return true;
}
```

**3. Active editing** — user interacts; ViewModel handles commands; all mutations go through `UndoRedoService.CreateTransaction()` (see [undo-redo.md](undo-redo.md)).

**4. `PreviewClose(bool? save)`**

```csharp
public override bool PreviewClose(bool? save)
{
if (save == null)
{
// Ask user — show a dialog via ServiceProvider.Get<IEditorDialogService>().
// Return false to cancel close.
}
// save == true → force-save; save == false → discard.
return true;
}
```

**5. `Destroy()`** — inherited from the MVVM base infrastructure (`DispatcherViewModel`/`ViewModelBase`), not declared on `AssetEditorViewModel` itself; synchronous; unhook all events, stop game instance if any, release resources; must not throw; always call `base.Destroy()`.

## The View

Implement `IEditorView` in the code-behind. The XAML file contains only layout and data bindings — no business logic.

```csharp
[AssetEditorView<%%AssetName%%EditorViewModel>]
public partial class %%AssetName%%EditorView : UserControl, IEditorView
{
private readonly TaskCompletionSource editorInitializationTcs = new();

public object DataContext
{
get => base.DataContext;
set => base.DataContext = value;
}

public Task EditorInitialization => editorInitializationTcs.Task;

public async Task<bool> InitializeEditor(IAssetEditorViewModel editor)
{
if (!await editor.Initialize())
{
editor.Destroy();
return false;
}
// Wire up anything that requires the initialized ViewModel here
// (e.g. inject the game viewport: somePanel.Content = myEditor.Controller.EditorHost).
editorInitializationTcs.SetResult();
return true;
}
}
```

## Services

Access services via `ServiceProvider` (available on `AssetEditorViewModel`):

| Service | Access | Purpose |
|---|---|---|
| `IUndoRedoService` | `ServiceProvider.Get<IUndoRedoService>()` | Wrap mutations in transactions — see [undo-redo.md](undo-redo.md) |
| `IDispatcherService` | `ServiceProvider.Get<IDispatcherService>()` | Invoke code on the UI thread from a background thread |
| `IEditorDialogService` | `ServiceProvider.Get<IEditorDialogService>()` | Show dialogs, message boxes, and file pickers |
| `SelectionService` | `ServiceProvider.Get<SelectionService>()` | Register selection scope for back/forward navigation — see [navigation.md](navigation.md) |
| `IAssetEditorsManager` | `ServiceProvider.TryGet<IAssetEditorsManager>()` | Open or close other asset editors programmatically |

Use `TryGet<T>()` for optional services; `Get<T>()` throws if the service is not registered.

`UndoRedoService` is also available as a shorthand property on `AssetEditorViewModel` (equivalent to `ServiceProvider.Get<IUndoRedoService>()`).

## MVVM Patterns

### Binding a property with automatic undo/redo

`MemberGraphNodeBinding<T>` wraps a Quantum `IMemberNode`; get/set route through the binding and undo/redo is handled automatically. Obtain the root `IObjectNode` via `Session.AssetNodeContainer` (see [quantum/asset-graph.md](../quantum/asset-graph.md)):

```csharp
private readonly MemberGraphNodeBinding<Color> colorBinding;

public %%AssetName%%EditorViewModel([NotNull] %%AssetName%%ViewModel asset)
: base(asset)
{
// rootNode is an IObjectNode obtained via Session.AssetNodeContainer.
// See docs/quantum/asset-graph.md for how to retrieve it.
colorBinding = new MemberGraphNodeBinding<Color>(
rootNode[nameof(%%AssetName%%.Color)], // IMemberNode
nameof(%%AssetName%%EditorViewModel.Color), // ViewModel property name
OnPropertyChanging,
OnPropertyChanged,
UndoRedoService);
}

public Color Color { get => colorBinding.Value; set => colorBinding.Value = value; }
```

### Manual transaction wrapping

For mutations that bypass the node graph (direct collection changes, renaming, structural operations):

```csharp
using (var transaction = UndoRedoService.CreateTransaction())
{
// perform mutations here
UndoRedoService.SetName(transaction, "Descriptive operation name");
}
```

See [undo-redo.md](undo-redo.md#wrapping-a-mutation) for the full pattern including `AnonymousDirtyingOperation`.

### Commands

```csharp
public ICommandBase DoSomethingCommand { get; }

public %%AssetName%%EditorViewModel([NotNull] %%AssetName%%ViewModel asset)
: base(asset)
{
DoSomethingCommand = new AnonymousTaskCommand(ServiceProvider, DoSomethingAsync);
}

private async Task DoSomethingAsync()
{
using var transaction = UndoRedoService.CreateTransaction();
// ...
UndoRedoService.SetName(transaction, "Do something");
}
```

## Assembly Placement

| File | Path |
|---|---|
| `%%AssetName%%EditorViewModel.cs` | `sources/editor/Stride.Assets.Presentation/AssetEditors/%%EditorName%%/ViewModels/` |
| `%%AssetName%%EditorView.xaml` | `sources/editor/Stride.Assets.Presentation/AssetEditors/%%EditorName%%/Views/` |
| `%%AssetName%%EditorView.xaml.cs` | `sources/editor/Stride.Assets.Presentation/AssetEditors/%%EditorName%%/Views/` |
137 changes: 137 additions & 0 deletions docs/editor/editors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# Existing Asset Editors

## Overview

All concrete asset editors live in `Stride.Assets.Presentation` under `sources/editor/Stride.Assets.Presentation/AssetEditors/`. Most editor folders contain a `ViewModels/` subdirectory and a `Views/` subdirectory; ScriptEditor and VisualScriptEditor are exceptions where files sit flat at the folder root. The table below is the entry point for locating any existing editor.

## Editors

| Editor | Asset type | Base class | Game viewport | Folder |
|---|---|---|---|---|
| `SpriteSheetEditorViewModel` | `SpriteSheetAsset` | `AssetEditorViewModel` | No | `SpriteEditor/` |
| `SceneEditorViewModel` | `SceneAsset` | `EntityHierarchyEditorViewModel` | Yes | `EntityHierarchyEditor/` |
| `PrefabEditorViewModel` | `PrefabAsset` | `EntityHierarchyEditorViewModel` | Yes | `EntityHierarchyEditor/` |
| `UIPageEditorViewModel` | `UIPageAsset` | `AssetCompositeHierarchyEditorViewModel` | Yes | `UIPageEditor/` |
| `UILibraryEditorViewModel` | `UILibraryAsset` | `AssetCompositeHierarchyEditorViewModel` | Yes | `UILibraryEditor/` |
| `GraphicsCompositorEditorViewModel` | `GraphicsCompositorAsset` | `AssetEditorViewModel` | No | `GraphicsCompositorEditor/` |
| `ScriptEditorViewModel` | Script assets | `AssetEditorViewModel` | No | `ScriptEditor/` |
| `VisualScriptEditorViewModel` | `VisualScriptAsset` | `AssetEditorViewModel` | No | `VisualScriptEditor/` |

### SpriteSheetEditorViewModel

**What it does:** Lets the user define sprites within a texture — regions, pivot points, borders, and animation frames. Renders a preview using its own lightweight `ViewportViewModel` rather than a full game instance.

**Key types:**

| Class | Role |
|---|---|
| `SpriteSheetEditorViewModel` | Editor ViewModel |
| `SpriteEditorView` | XAML view |
| `ViewportViewModel` | Lightweight render preview (no full game loop) |

**Notable:**
- Uses `ViewportViewModel` for rendering instead of `IEditorGameController` — lighter than a full game editor.
- Implements `IAddChildViewModel` to support dragging textures onto the editor to add new sprites.

### SceneEditorViewModel / PrefabEditorViewModel

**What it does:** Full 3D scene and prefab editing with an entity hierarchy tree, transform gizmos, camera controls, and a live game viewport. Scene and prefab share the same base infrastructure with thin concrete subclasses.

**Key types:**

| Class | Role |
|---|---|
| `EntityHierarchyEditorViewModel` | Shared base ViewModel (`EntityHierarchyEditor/ViewModels/`) |
| `SceneEditorViewModel` | Thin subclass for scenes |
| `PrefabEditorViewModel` | Thin subclass for prefabs |
| `EntityViewModel` | Part ViewModel for each entity in the hierarchy |
| `EditorCameraViewModel` | Camera movement and controls (lives in `GameEditor/ViewModels/`, shared infrastructure) |
| `EntityGizmosViewModel` | Gizmo overlay (translate/rotate/scale handles) |
| `EntityHierarchyEditorView` | Abstract base view |
| `SceneEditorView` / `PrefabEditorView` | Concrete views |

**Notable:**
- Most logic is in `EntityHierarchyEditorViewModel`; the concrete subclasses are thin.
- Scene and prefab differ primarily in which hierarchy root they load and whether archetype (prefab base) linking is active.
- The view code-behind injects the game host into the XAML panel: `SceneView.Content = hierarchyEditor.Controller.EditorHost`.

### UIPageEditorViewModel / UILibraryEditorViewModel

**What it does:** WYSIWYG editing of UI hierarchies — pages (full-screen layouts) and libraries (reusable component collections). Renders elements in a live game viewport with selection adorners, resize handles, and snap guidelines.

**Key types:**

| Class | Role |
|---|---|
| `UIEditorBaseViewModel` | Shared base ViewModel (`UIEditor/ViewModels/`) |
| `UIPageEditorViewModel` | Subclass for UI pages |
| `UILibraryEditorViewModel` | Subclass for UI libraries |
| `UIElementViewModel` | Part ViewModel for each UI element |
| `UIEditorView` | Abstract base view (`UIEditor/Views/`) |
| `UIPageEditorView` / `UILibraryEditorView` | Concrete views |

**Notable:**
- The adorner overlay (guidelines, resize handles) is rendered as a WPF layer on top of the game viewport — one of the few places where WPF and game rendering are explicitly composited.
- `UIEditorBaseViewModel` handles element factories, zoom/pan state, and selection; subclasses add only asset-type-specific root handling.

### GraphicsCompositorEditorViewModel

**What it does:** Visual node-graph editor for the render pipeline. Nodes represent render features and render stages; edges represent data flow between them.

**Key types:**

| Class | Role |
|---|---|
| `GraphicsCompositorEditorViewModel` | Editor ViewModel (`GraphicsCompositorEditor/ViewModels/`) |
| `GraphicsCompositorEditorView` | XAML view |
| `SharedRendererFactoryViewModel` | Factory/list for shared renderer blocks |
| `RenderStageViewModel` | Node ViewModel for render stages |

**Notable:**
- Uses `Stride.Core.Presentation.Graph` for the WPF node-graph canvas.
- Does **not** use `IEditorGameController` — no live game instance; the compositor is a pure data-structure editor.

### ScriptEditorViewModel

**What it does:** Opens a script asset in an embedded code editor. Provides compilation feedback and basic IDE integration within GameStudio.

**Key types:**

| Class | Role |
|---|---|
| `ScriptEditorViewModel` | Editor ViewModel (`ScriptEditor/`) |
| `ScriptEditorView` | XAML view |

**Notable:**
- The lightest editor — mostly a shell around the embedded code editor control.
- Undo/redo is delegated to the code editor itself rather than `IUndoRedoService`; the standard transaction infrastructure does not apply here.

### VisualScriptEditorViewModel

**What it does:** Node-graph editor for visual scripting. Blocks represent operations; edges represent data and control flow between them.

**Key types:**

| Class | Role |
|---|---|
| `VisualScriptEditorViewModel` | Editor ViewModel (`VisualScriptEditor/`) |
| `VisualScriptMethodEditorViewModel` | Per-method graph editing |
| `VisualScriptBlockViewModel` | Node ViewModel for each block |
| `VisualScriptLinkViewModel` | Edge ViewModel for each connection |

**Notable:**
- Similar to `GraphicsCompositorEditorViewModel` in structure: a pure data-structure editor with no game instance.
- ViewModel files sit at the folder root (no `ViewModels/` subdir); views are in `Views/` as usual.

## Shared Game Editor Infrastructure

All game-viewport editors (Scene, Prefab, UIPage, UILibrary) inherit from `GameEditorViewModel` and run a game instance via `IEditorGameController`. The controller manages the game loop on a background thread; results are marshalled back to the ViewModel via `IDispatcherService`.

**Key implication for contributors:** property changes in these editors can originate from either the UI thread (user interaction) or the game thread (simulation update). Code that modifies ViewModel state from the game thread must dispatch to the UI thread via `Asset.Dispatcher` (`AssetEditorViewModel.Asset` inherits `Dispatcher` from `DispatcherViewModel`):

```csharp
Asset.Dispatcher.InvokeAsync(() =>
{
// safe to update ViewModel properties here
});
```
Loading