Skip to main content

khora_core/ui/editor/
state.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Editor state shared between the application logic and UI panels.
16//!
17//! The [`EditorState`] is populated by `Application::update()` every frame
18//! with a lightweight snapshot of the ECS world. Editor panels read this
19//! snapshot through a shared `Arc<Mutex<EditorState>>` retrieved from the
20//! `ServiceRegistry`.
21
22use crate::ecs::entity::EntityId;
23use std::collections::HashSet;
24
25/// The active gizmo tool in the viewport toolbar.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
27pub enum GizmoMode {
28    /// Selection / pointer tool.
29    #[default]
30    Select,
31    /// Translation gizmo.
32    Move,
33    /// Rotation gizmo.
34    Rotate,
35    /// Scale gizmo.
36    Scale,
37}
38
39/// The play-mode state of the editor.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
41pub enum PlayMode {
42    /// Normal editing mode — scene is static.
43    #[default]
44    Editing,
45    /// Game simulation is running.
46    Playing,
47    /// Game simulation is paused (can resume or stop).
48    Paused,
49}
50
51/// The active editing workspace, switched via the left "spine" mode bar.
52///
53/// Two workspaces ship today: `Scene` (the default 3D dock) and `ControlPlane`
54/// (the DCC / agents inspector). Future authoring workspaces (2D canvas, node
55/// graph, animation, shader graph) will be added as they are built.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
57pub enum EditorMode {
58    /// Default 3D viewport workspace.
59    #[default]
60    Scene,
61    /// Dynamic Context Core / agents control workspace.
62    ControlPlane,
63}
64
65/// A lightweight description of an entity in the scene tree.
66///
67/// This is a UI-oriented DTO extracted from the ECS world each frame.
68/// It does not borrow any ECS data and can be freely shared between threads.
69#[derive(Debug, Clone)]
70pub struct SceneNode {
71    /// The entity identifier.
72    pub entity: EntityId,
73    /// Human-readable name (`Name` component, or fallback like "Entity 42").
74    pub name: String,
75    /// Visual hint about what kind of entity this is.
76    pub icon: EntityIcon,
77    /// Direct children in the scene hierarchy (from the `Children` component).
78    pub children: Vec<SceneNode>,
79    /// Number of tags carried by the entity (`Tag` component). The scene
80    /// tree paints a small tag glyph next to the row when this is non-zero.
81    pub tag_count: usize,
82}
83
84/// Icon hint for the scene tree.
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum EntityIcon {
87    /// Generic / unknown entity.
88    Empty,
89    /// Entity has a `Camera` component.
90    Camera,
91    /// Entity has a `Light` component.
92    Light,
93    /// Entity has a mesh handle.
94    Mesh,
95    /// Entity has an `AudioSource` component.
96    Audio,
97}
98
99/// Shared editor state populated by `Application::update()` each frame.
100///
101/// Panels read this to display the scene tree, selection highlights, etc.
102/// All data is owned (snapshot) — no ECS borrows.
103#[derive(Debug, Clone, Default)]
104pub struct EditorState {
105    /// Root-level scene nodes (entities without a `Parent` component).
106    pub scene_roots: Vec<SceneNode>,
107    /// Currently selected entity IDs.
108    pub selection: HashSet<EntityId>,
109    /// Total entity count in the world.
110    pub entity_count: usize,
111    /// Scene tree search / filter text.
112    pub search_filter: String,
113    /// Whether the Ctrl key is currently held (for multi-select).
114    pub ctrl_held: bool,
115    /// A pending spawn request tag (e.g. "Empty", "Cube", "Light", "Camera").
116    /// The application reads and clears this each frame.
117    pub pending_spawn: Option<String>,
118    /// Pending delete request for a specific entity (from context menu).
119    pub pending_delete: Option<EntityId>,
120    /// Pending duplicate request for a specific entity (from context menu).
121    pub pending_duplicate: Option<EntityId>,
122    /// Entity currently being renamed (inline text editing).
123    pub renaming_entity: Option<EntityId>,
124    /// Buffer for the rename text input.
125    pub rename_buffer: String,
126    /// Pending rename to apply: (entity, new_name).
127    pub pending_rename: Option<(EntityId, String)>,
128    /// Pending reparent to apply: (child, new_parent_or_None_for_root).
129    /// Set by the scene tree drag-and-drop handler, drained by
130    /// `process_reparents` in the editor's tick.
131    pub pending_reparent: Option<(EntityId, Option<EntityId>)>,
132
133    // ── Phase 4: Properties Inspector ──────────────────
134    /// Snapshot of the single-selected entity's components (if any).
135    pub inspected: Option<InspectedEntity>,
136    /// Pending property edits to apply back to the ECS world.
137    pub pending_edits: Vec<PropertyEdit>,
138
139    // ── Phase 5: Console / Status Bar ──────────────────
140    /// Log entries captured by the editor log sink.
141    pub log_entries: Vec<LogEntry>,
142    /// Status bar data (FPS, entity count, memory).
143    pub status: StatusBarData,
144    /// Asset entries for the asset browser (populated from the VFS).
145    pub asset_entries: Vec<AssetEntry>,
146
147    // ── Phase 6: Viewport interaction + Gizmo ──────────
148    /// Whether the 3D viewport is currently hovered (for camera controls).
149    /// Updated each frame from `is_last_item_hovered()` after the viewport
150    /// image is laid out — only meaningful for the current paint pass; UI
151    /// code that reads input handlers should prefer `viewport_screen_rect`
152    /// + the live cursor position.
153    pub viewport_hovered: bool,
154    /// Screen-space rect of the 3D viewport image (`[x, y, w, h]` in pixels)
155    /// for the current frame. `None` when the editor isn't in Scene mode or
156    /// the viewport hasn't been laid out yet. Used by the input pipeline to
157    /// decide if a mouse event lives over the viewport — checked against
158    /// the live cursor position so it doesn't suffer from the
159    /// frame-of-latency `viewport_hovered` had.
160    pub viewport_screen_rect: Option<[f32; 4]>,
161    /// The active gizmo tool.
162    pub gizmo_mode: GizmoMode,
163    /// Index of the currently selected asset in the asset browser (if any).
164    pub selected_asset: Option<usize>,
165    /// Forward-slash relative path (under `<project>/assets/`) of the
166    /// asset selected in the browser, or `None`. When set, the
167    /// Inspector switches to asset-metadata mode (Phase 5). Cleared
168    /// when an entity selection is made.
169    pub inspected_asset_path: Option<String>,
170    /// Pending menu action (e.g. "new_scene", "save", "quit").
171    pub pending_menu_action: Option<String>,
172    /// Currently set project folder (used for asset scanning).
173    pub project_folder: Option<String>,
174    /// Human-readable project name (read from `project.json`).
175    pub project_name: Option<String>,
176    /// Engine version string read from `project.json::engine_version` (set
177    /// by the hub at project creation time). The status bar and command
178    /// palette display this so users see the engine they targeted, not the
179    /// editor binary's own crate version.
180    pub project_engine_version: Option<String>,
181    /// Whether the asset browser should open a folder picker next frame.
182    pub pending_browse_project_folder: bool,
183
184    // ── Phase 7: Play Mode ─────────────────────────────
185    /// Current play mode (Editing / Playing / Paused).
186    pub play_mode: PlayMode,
187    /// Serialised snapshot of the scene taken when entering play mode.
188    /// Restored when the user presses Stop.
189    pub scene_snapshot: Option<Vec<u8>>,
190
191    // ── Scene file path ──────────────────────────────
192    /// Path to the currently open scene file (for Save).
193    pub current_scene_path: Option<String>,
194    /// Pending scene load path (set by asset browser double-click, consumed by update).
195    pub pending_scene_load: Option<String>,
196
197    // ── Component addition ─────────────────────────────
198    /// Pending component addition (set by properties panel or scene tree context menu, consumed by update).
199    /// The String is the component type name (e.g., "Camera", "RigidBody").
200    pub pending_add_component: Option<(EntityId, String)>,
201
202    /// Registry mapping `type_name` → domain tag for every component the
203    /// engine currently knows about. Populated by `extract_inspected` once
204    /// per frame from the live `World`. Used by the inspector to bucket
205    /// the "+ Add Component" menu by domain without re-querying the world.
206    pub component_domain_registry: std::collections::HashMap<String, u8>,
207
208    // ── Editor workspace ───────────────────────────────
209    /// Currently active editing workspace (scene / control plane / …).
210    pub active_mode: EditorMode,
211    /// Whether the command palette modal is open.
212    pub command_palette_open: bool,
213    /// Inspector card expand/collapse state, keyed by stable card id
214    /// (typically `"<entity_index>::<title>"` to avoid cross-entity bleed).
215    pub inspector_card_open: std::collections::HashMap<String, bool>,
216    /// Inspector card on/off toggle state. UI-only for now (no engine wiring).
217    pub inspector_card_enabled: std::collections::HashMap<String, bool>,
218    /// Current git branch name read from `.git/HEAD` of the project folder.
219    /// `None` if the project isn't a git repository or we couldn't read it.
220    pub current_git_branch: Option<String>,
221    /// Entities the user has hidden via the eye icon in the scene tree.
222    /// UI-only state today — see `pending_visibility_toggle` for the engine
223    /// hook the editor consumes each frame.
224    pub hidden_entities: HashSet<EntityId>,
225    /// Pending visibility toggle from the scene tree eye icon. The editor
226    /// pops this each frame and applies it to the engine (when an engine
227    /// `Visible` component lands; for now it just flips `hidden_entities`).
228    pub pending_visibility_toggle: Option<EntityId>,
229
230    // ── Prefab workflow ────────────────────────────────
231    /// Set when the user picks "Save as Prefab" in the scene tree
232    /// context menu. The editor consumes this next frame, opens a
233    /// file dialog, calls `serialize_subtree`, and writes the resulting
234    /// `.kprefab` file.
235    pub pending_save_as_prefab: Option<EntityId>,
236    /// Same payload as `pending_save_as_prefab`, but with a pre-chosen
237    /// destination forward-slash relative path under `<project>/assets/`
238    /// — set by drag-and-drop (e.g. dragging an entity onto the asset
239    /// browser's current folder). The dispatcher writes directly with
240    /// no file dialog and reuses the entity name as the file stem when
241    /// the path ends in `/`.
242    pub pending_save_as_prefab_at: Option<(EntityId, String)>,
243    /// Set when the user drops a `.kprefab` tile onto the viewport / hierarchy
244    /// (or activates one from the asset browser). Holds the forward-slash
245    /// relative path under `<project>/assets/` plus an optional parent entity
246    /// (the hierarchy row it was dropped on) so the instantiated root is
247    /// parented under it. Consumed next frame to load the recipe via the asset
248    /// service and call `instantiate_subtree`.
249    pub pending_prefab_spawn: Option<(String, Option<EntityId>)>,
250
251    // ── Material authoring workflow ─────────────────────
252    /// Set when the user picks "Save Material as .kmat" on an entity that
253    /// carries a `MaterialRef::Inline`. Holds the entity plus the chosen
254    /// material name (file stem). The editor consumes this next frame:
255    /// serializes the inline material to RON under
256    /// `assets/materials/<name>.kmat`, reindexes, and rewrites the entity's
257    /// component to `MaterialRef::Asset(uuid)` so it now references the
258    /// shared, reloadable asset.
259    pub pending_save_as_material: Option<(EntityId, String)>,
260    /// Set when the user assigns a `.kmat` from the asset browser to the
261    /// current selection. Holds the forward-slash relative path of the
262    /// `.kmat` under `<project>/assets/`. Consumed next frame: each
263    /// selected entity's `MaterialRef` is set to `Asset(uuid)`.
264    pub pending_assign_material: Option<String>,
265
266    // ── Asset explorer: file operations & scene drop ───
267    /// Every directory under `assets/` (forward-slash, relative), so the asset
268    /// browser can show empty folders the file-only VFS can't. Refreshed with
269    /// `asset_entries`.
270    pub asset_dirs: Vec<String>,
271    /// Bumped whenever `asset_entries`/`asset_dirs` change. The asset browser
272    /// rescans its flattened cache on epoch change instead of on entry-count
273    /// change (a modified-in-place file used to be missed).
274    pub asset_epoch: u64,
275    /// Create an empty folder at this forward-slash relative path under
276    /// `assets/`. Consumed next frame.
277    pub pending_create_folder: Option<String>,
278    /// Rename/move an asset: `(old_rel, new_rel)`, both forward-slash under
279    /// `assets/`. The identity registry freezes the UUID so references survive.
280    pub pending_rename_asset: Option<(String, String)>,
281    /// Move an asset into a folder: `(src_rel, dest_dir)` (dest_dir `""` = root).
282    pub pending_move_asset: Option<(String, String)>,
283    /// Send an asset to the OS recycle bin: forward-slash relative path.
284    pub pending_delete_asset: Option<String>,
285    /// Duplicate an asset next to itself: forward-slash relative path.
286    pub pending_duplicate_asset: Option<String>,
287    /// Spawn a mesh asset into the scene: `(rel_path, [x,y,z] world point,
288    /// optional parent entity)`. Set by dragging a mesh tile onto the viewport
289    /// (parent `None`) or a hierarchy row (parent = that entity); drained into a
290    /// `MeshRef::Asset` entity spawn, parented under the target when present.
291    pub pending_spawn_mesh_asset: Option<(String, [f32; 3], Option<EntityId>)>,
292    /// Assign a texture/material asset to a specific entity (drag onto an entity
293    /// in the viewport): `(rel_path, entity)`.
294    pub pending_assign_texture: Option<(String, EntityId)>,
295}
296
297impl EditorState {
298    /// Returns `true` if the entity is currently selected.
299    pub fn is_selected(&self, entity: EntityId) -> bool {
300        self.selection.contains(&entity)
301    }
302
303    /// Select a single entity (clears previous selection).
304    pub fn select(&mut self, entity: EntityId) {
305        self.selection.clear();
306        self.selection.insert(entity);
307    }
308
309    /// Toggle an entity in the selection (Ctrl+click behavior).
310    pub fn toggle_select(&mut self, entity: EntityId) {
311        if !self.selection.remove(&entity) {
312            self.selection.insert(entity);
313        }
314    }
315
316    /// Clear the selection entirely.
317    pub fn clear_selection(&mut self) {
318        self.selection.clear();
319    }
320
321    /// Drops every piece of state that names an [`EntityId`], for use whenever
322    /// the world is replaced wholesale — new scene, scene load, or `Stop`
323    /// restoring the pre-play snapshot.
324    ///
325    /// Those paths rebuild the world with **fresh** ids, so anything still
326    /// holding an old one is not merely stale but actively dangerous: entity
327    /// slots are recycled, and an index+generation pair can come back attached
328    /// to a different entity. A surviving selection then makes `Delete` act on
329    /// something the user never selected.
330    ///
331    /// Card state is keyed by entity too, so it is cleared here rather than
332    /// growing without bound across scene changes.
333    pub fn clear_entity_references(&mut self) {
334        self.selection.clear();
335        self.inspected = None;
336        self.hidden_entities.clear();
337        self.renaming_entity = None;
338        self.rename_buffer.clear();
339        self.inspector_card_open.clear();
340        self.inspector_card_enabled.clear();
341        self.scene_roots.clear();
342        self.entity_count = 0;
343    }
344
345    /// Returns the single selected entity, if exactly one is selected.
346    pub fn single_selected(&self) -> Option<EntityId> {
347        if self.selection.len() == 1 {
348            self.selection.iter().next().copied()
349        } else {
350            None
351        }
352    }
353
354    /// Push a property edit to be applied back to the ECS world next frame.
355    pub fn push_edit(&mut self, edit: PropertyEdit) {
356        self.pending_edits.push(edit);
357    }
358
359    /// Drain all pending edits (called by `Application::update()`).
360    pub fn drain_edits(&mut self) -> Vec<PropertyEdit> {
361        std::mem::take(&mut self.pending_edits)
362    }
363}
364
365// ════════════════════════════════════════════════════════
366//  Properties Inspector types
367// ════════════════════════════════════════════════════════
368
369/// Snapshot of one component on an inspected entity, captured generically
370/// as JSON via the macro-generated `to_json` on `ComponentRegistration`.
371///
372/// This is the path adding a new component costs **zero** editor code: any
373/// type that derives `Component` is auto-registered and shows up here.
374/// Components with a hard-coded inspector card (Transform, Camera, etc.)
375/// are skipped from the generic list to avoid double-rendering.
376#[derive(Debug, Clone)]
377pub struct ComponentJson {
378    /// `ComponentRegistration::type_name` — card title and lookup key.
379    pub type_name: String,
380    /// Domain bucket, mirrored from `SemanticDomain` (Spatial=0, Render=1,
381    /// Audio=2, Physics=3, Ui=4). `None` means the type isn't registered
382    /// on a CRPECS page (e.g. types registered via inventory only).
383    pub domain: Option<u8>,
384    /// Live JSON value. The shape mirrors the component's
385    /// `Serializable<Type>` form.
386    pub value: serde_json::Value,
387}
388
389/// Snapshot of an inspected entity.
390///
391/// Components are captured generically as JSON via the macro-generated
392/// `ComponentRegistration::to_json`. The inspector iterates `components_json`
393/// and renders every entry through a single field-typed walker — there is
394/// no per-component code path. The entity's `Name` lives in the inspector
395/// header rather than as a component card.
396#[derive(Debug, Clone)]
397#[allow(missing_docs)]
398pub struct InspectedEntity {
399    pub entity: EntityId,
400    pub name: String,
401    /// Every component on this entity, captured as JSON. The inspector
402    /// renders this list directly — no per-component hard-coding.
403    pub components_json: Vec<ComponentJson>,
404}
405
406/// A property edit to apply back to the ECS world.
407///
408/// Components are mutated through a single generic round-trip via
409/// `ComponentRegistration::from_json`: the inspector walks each field of
410/// the component's JSON shape, lets the user edit it, and ships the whole
411/// patched `serde_json::Value` back through `SetComponentJson`. There is
412/// no per-component editor variant — adding a new component costs zero
413/// editor code.
414#[derive(Debug, Clone)]
415#[allow(missing_docs)]
416pub enum PropertyEdit {
417    /// Rename the entity (`Name` component is special — it shows up in the
418    /// inspector header rather than as a component card).
419    SetName(EntityId, String),
420    /// Replace the entire JSON value of one component on `entity`. The
421    /// editor looks up the registration by `type_name` and calls
422    /// `from_json` to commit.
423    SetComponentJson {
424        entity: EntityId,
425        type_name: String,
426        value: serde_json::Value,
427    },
428    /// Remove a component from `entity`. The editor looks up the
429    /// registration by `type_name` and calls `remove` to commit.
430    RemoveComponent { entity: EntityId, type_name: String },
431}
432
433// ════════════════════════════════════════════════════════
434//  Console / Status Bar types
435// ════════════════════════════════════════════════════════
436
437/// A captured log entry for the console panel.
438#[derive(Debug, Clone)]
439#[allow(missing_docs)]
440pub struct LogEntry {
441    pub level: LogLevel,
442    pub message: String,
443    pub target: String,
444    /// Wall-clock time the entry was captured, as `HH:MM:SS`.
445    ///
446    /// Formatted at capture rather than stored as an instant: the console is
447    /// the only consumer, it always renders it, and doing it once at capture
448    /// keeps the paint path free of formatting work.
449    pub time: String,
450}
451
452/// Log severity matching `log::Level`.
453#[derive(Debug, Clone, Copy, PartialEq, Eq)]
454#[allow(missing_docs)]
455pub enum LogLevel {
456    Error,
457    Warn,
458    Info,
459    Debug,
460    Trace,
461}
462
463/// Status bar data displayed at the bottom of the editor.
464///
465/// Populated each frame by `EditorApp::update`; the GPU-related fields are
466/// pulled from `TelemetryService` when available, otherwise stay at 0.
467#[derive(Debug, Clone)]
468#[allow(missing_docs)]
469pub struct StatusBarData {
470    pub fps: f32,
471    pub frame_time_ms: f32,
472    pub entity_count: usize,
473    pub memory_used_mb: f32,
474    /// GPU draw calls last frame (from `GpuReport`). 0 when telemetry has no
475    /// reading yet.
476    pub draw_calls: u32,
477    /// GPU triangles rendered last frame.
478    pub triangles: u64,
479    /// Approximate VRAM use in MB. 0 if not reported.
480    pub vram_mb: f32,
481    /// CPU load (0.0–1.0) as reported by `HardwareReport`.
482    pub cpu_load: f32,
483    /// GPU load (0.0–1.0) as reported by `HardwareReport`.
484    pub gpu_load: f32,
485}
486
487impl Default for StatusBarData {
488    fn default() -> Self {
489        Self {
490            fps: 0.0,
491            frame_time_ms: 0.0,
492            entity_count: 0,
493            memory_used_mb: 0.0,
494            draw_calls: 0,
495            triangles: 0,
496            vram_mb: 0.0,
497            cpu_load: 0.0,
498            gpu_load: 0.0,
499        }
500    }
501}
502
503// ════════════════════════════════════════════════════════
504//  Asset Browser types
505// ════════════════════════════════════════════════════════
506
507/// A lightweight description of an asset for the asset browser panel.
508#[derive(Debug, Clone)]
509pub struct AssetEntry {
510    /// Human-readable file/asset name.
511    pub name: String,
512    /// Asset type string (e.g. "Mesh", "Texture", "Shader", "Audio").
513    pub asset_type: String,
514    /// Source path (for display purposes).
515    pub source_path: String,
516}