Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

facett-app/src/editor.rs

//! **`editor` — Blender's EDITOR TYPE, for facett panes.**
//!
//! Blender does not have a privileged central editor with tool windows around it (the
//! Eclipse/IntelliJ *perspective* model). It tiles the window into **areas**, and **any
//! area can host any editor type**, swapped from a dropdown in that area's header. That
//! is strictly more powerful for a control centre: a set of "tabs" stops being a set of
//! *screens* and becomes a set of **editor types**, so an analyst can put Graph and
//! Tables side by side, or two Graphs at different queries. A tab model forbids both by
//! construction.
//!
//! This module is the registry half of that:
//! - [`Editor`] — one editor TYPE: a stable id, a display name, and a **factory** that
//!   mints a fresh [`Facet`] instance.
//! - [`EditorRegistry`] — the host-supplied set of types an area may host. It drives the
//!   per-area header dropdown and is what a swap resolves against.
//! - [`EditorAreas`] — the live instances, keyed by **pane name**. The pane name is the
//!   stable, robot-addressable identity; **which editor it hosts is *state***
//!   ([`NavState::area_editor`](crate::scene::NavState::area_editor)), never identity.
//!   Swapping an area's editor therefore keeps its name, its rect, and its chrome.
//!
//! Instances are **retained per (pane, editor)**, exactly as Blender retains an area's
//! visited space data: swap Graph → Tables → Graph and the graph is where you left it.

use facett_core::Facet;
use serde_json::{Value, json};
use std::collections::BTreeMap;

/// One **editor type** — the thing an area can be switched to.
///
/// The `id` is the stable, serialized handle (it is what lands in `state_json` and in
/// the persisted layout); `name` is what the header dropdown shows. `factory` mints a
/// fresh facet, so N areas can host N independent instances of the same type — that is
/// the whole point of "two Graphs at different queries".
pub struct Editor {
    id: String,
    name: String,
    factory: Box<dyn Fn() -> Box<dyn Facet>>,
}

impl Editor {
    /// Declare an editor type: stable `id`, display `name`, and the factory.
    pub fn new(id: impl Into<String>, name: impl Into<String>, factory: impl Fn() -> Box<dyn Facet> + 'static) -> Self {
        Self { id: id.into(), name: name.into(), factory: Box::new(factory) }
    }
    /// The stable id (serialized; what an area's binding stores).
    pub fn id(&self) -> &str {
        &self.id
    }
    /// The display name (what the header dropdown shows).
    pub fn name(&self) -> &str {
        &self.name
    }
    /// Mint a fresh instance of this editor type.
    pub fn create(&self) -> Box<dyn Facet> {
        (self.factory)()
    }
}

impl std::fmt::Debug for Editor {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Editor").field("id", &self.id).field("name", &self.name).finish_non_exhaustive()
    }
}

/// The **host-supplied registry** of editor types. Registration ORDER is the dropdown
/// order (a `BTreeMap` alone would silently re-sort the host's menu), while lookup is by
/// id.
#[derive(Default)]
pub struct EditorRegistry {
    order: Vec<String>,
    by_id: BTreeMap<String, Editor>,
}

impl EditorRegistry {
    /// An empty registry.
    pub fn new() -> Self {
        Self::default()
    }
    /// Register an editor type (builder form). Re-registering an id REPLACES the type
    /// and keeps its position, so a host can override one entry without reordering its
    /// menu.
    pub fn with(mut self, editor: Editor) -> Self {
        self.register(editor);
        self
    }
    /// Register an editor type.
    pub fn register(&mut self, editor: Editor) {
        if !self.by_id.contains_key(&editor.id) {
            self.order.push(editor.id.clone());
        }
        self.by_id.insert(editor.id.clone(), editor);
    }
    /// Look an editor type up by id.
    pub fn get(&self, id: &str) -> Option<&Editor> {
        self.by_id.get(id)
    }
    /// Is `id` a registered editor type?
    pub fn contains(&self, id: &str) -> bool {
        self.by_id.contains_key(id)
    }
    /// Every registered id, in registration (menu) order.
    pub fn ids(&self) -> Vec<&str> {
        self.order.iter().map(String::as_str).collect()
    }
    /// The `(id, display name)` pairs the header dropdown offers, in menu order. This is
    /// exactly what a host returns from
    /// [`SceneHost::editors`](crate::scene::SceneHost::editors).
    pub fn options(&self) -> Vec<(String, String)> {
        self.order.iter().filter_map(|i| self.by_id.get(i)).map(|e| (e.id.clone(), e.name.clone())).collect()
    }
    /// How many editor types are registered.
    pub fn len(&self) -> usize {
        self.order.len()
    }
    /// Is the registry empty? (An empty registry means every area header draws a
    /// dropdown with nothing to swap TO — worth asserting in a host's own test.)
    pub fn is_empty(&self) -> bool {
        self.order.is_empty()
    }
    /// The registry as JSON (a host folds this into its `state_json` so a robot can read
    /// what an area MAY be switched to, not just what it currently is).
    pub fn state_json(&self) -> Value {
        json!({
            "count": self.order.len(),
            "editors": self.options().into_iter().map(|(id, name)| json!({"id": id, "name": name})).collect::<Vec<_>>(),
        })
    }
}

impl std::fmt::Debug for EditorRegistry {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("EditorRegistry").field("ids", &self.order).finish()
    }
}

/// The **live editor instances**, keyed by pane name then editor id.
///
/// Two-level on purpose: an area retains every editor it has visited, so swapping away
/// and back returns the instance you left (Blender's per-area space-data list). The
/// outer key is the pane NAME — the stable identity a robot addresses — and it never
/// changes when the editor does.
#[derive(Default)]
pub struct EditorAreas {
    live: BTreeMap<String, BTreeMap<String, Box<dyn Facet>>>,
    current: BTreeMap<String, String>,
}

impl EditorAreas {
    /// No areas mounted yet.
    pub fn new() -> Self {
        Self::default()
    }

    /// The facet `pane` should draw for `editor_id`, minting it from the registry on
    /// first use. `None` when the id is not registered (the host draws a "no such
    /// editor" note rather than panicking — a persisted layout naming an editor a later
    /// build removed must degrade, never crash).
    pub fn facet<'a>(
        &'a mut self,
        registry: &EditorRegistry,
        pane: &str,
        editor_id: &str,
    ) -> Option<&'a mut (dyn Facet + 'a)> {
        let ed = registry.get(editor_id)?;
        let slot = self.live.entry(pane.to_string()).or_default();
        if !slot.contains_key(editor_id) {
            slot.insert(editor_id.to_string(), ed.create());
        }
        self.current.insert(pane.to_string(), editor_id.to_string());
        match slot.get_mut(editor_id) {
            Some(b) => Some(&mut **b),
            None => None,
        }
    }

    /// **The one call a [`SceneHost::area`](crate::scene::SceneHost::area) needs**: draw
    /// the area's editor and return its `state_json`, tagged with the editor that
    /// produced it. An unregistered id draws a visible note and returns an `error` blob
    /// (never `Null`, so the miss is observable).
    pub fn draw(&mut self, registry: &EditorRegistry, pane: &str, editor_id: &str, ui: &mut egui::Ui) -> Value {
        match self.facet(registry, pane, editor_id) {
            Some(f) => {
                let title = f.title().to_string();
                f.ui(ui);
                json!({ "editor": editor_id, "title": title, "state": f.state_json() })
            }
            None => {
                ui.label(format!("no such editor: {editor_id}"));
                json!({ "editor": editor_id, "error": "unregistered" })
            }
        }
    }

    /// Which editor `pane` last drew.
    pub fn editor_of(&self, pane: &str) -> Option<&str> {
        self.current.get(pane).map(String::as_str)
    }
    /// Every editor id `pane` has ever hosted (its retained instances), sorted.
    pub fn retained(&self, pane: &str) -> Vec<&str> {
        self.live.get(pane).map(|m| m.keys().map(String::as_str).collect()).unwrap_or_default()
    }
    /// Drop every retained instance for `pane` (a host's "reset this area").
    pub fn forget(&mut self, pane: &str) {
        self.live.remove(pane);
        self.current.remove(pane);
    }
    /// The mounted areas as JSON: pane → { current, retained }.
    pub fn state_json(&self) -> Value {
        let mut out = serde_json::Map::new();
        for (pane, insts) in &self.live {
            out.insert(
                pane.clone(),
                json!({
                    "current": self.current.get(pane),
                    "retained": insts.keys().collect::<Vec<_>>(),
                }),
            );
        }
        Value::Object(out)
    }
}

impl std::fmt::Debug for EditorAreas {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("EditorAreas").field("current", &self.current).finish()
    }
}