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()
}
}