gantz_egui 0.6.1

UI traits and widgets that make up the GUI for gantz, an environment for creative systems.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
//! The `Gui` marker node: declares a graph's GUI from within the graph.
//!
//! Just as [`Inlet`][gantz_core::node::graph::Inlet]/[`Outlet`][gantz_core::node::graph::Outlet]
//! markers declare a graph's sockets, a [`Gui`] marker declares its GUI: the
//! tree pull-evaluated into the marker is stored in the marker's state slot,
//! where the host reads it and renders it via the `ui_tree` interpreter. One
//! marker per [`GuiRole`] (a duplicate role is resolved as first-in-index-order
//! wins).

use crate::widget::node_inspector::{self, radio_option};
use crate::{Env, InspectorRowsResponse, NodeCtx, NodeUi, NodeUiResponse, SocketDoc, SocketKind};
use gantz_core::node::{self, EvalConf, ExprCtx, ExprResult, MetaCtx, RegCtx};
use gantz_nodetag::NodeTag;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;

/// The ext key under which a ref stores its per-instance GUI overrides.
pub const GUI_REF_EXT_KEY: &str = "gantz.gui";

/// The `NodeUi` surface a marker's tree presents.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum GuiRole {
    /// The in-graph node form (`NodeUi::ui`).
    #[default]
    Body,
    /// The detached pane (`NodeUi::view_ui`).
    View,
    /// Appended after the inspector's default table (`NodeUi::inspector_ui`).
    Inspector,
    /// Condensed body variant for dense patching.
    Compact,
}

/// How instances present the graph's body GUI by default.
///
/// Meaningful on the [`GuiRole::Body`] marker; instances may override it via
/// [`GuiRefExt`].
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum GuiDisplay {
    /// Render the full body marker tree.
    #[default]
    Full,
    /// Render the compact marker tree (falls back to a label).
    Compact,
    /// Render the name label only.
    Label,
}

/// A marker node declaring the tree wired into it as this graph's GUI for
/// `role`.
///
/// Stateful with a single pull-evaluated input: the pull stores the incoming
/// tree in the marker's state slot, where the host reads it each frame.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, Deserialize, Serialize, NodeTag)]
pub struct Gui {
    /// The surface this marker's tree presents.
    #[serde(default)]
    pub role: GuiRole,
    /// The default display mode instances use for the body GUI.
    #[serde(default)]
    pub display: GuiDisplay,
}

/// Per-instance GUI overrides, stored in a ref's ext map under
/// [`GUI_REF_EXT_KEY`].
///
/// Absent means "follow the definition default" (the body marker's
/// [`display`][Gui::display]); present means the user chose an explicit
/// display for this instance, even if it matches the definition default.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct GuiRefExt {
    /// The display mode this instance renders the referenced GUI with.
    #[serde(default)]
    pub display: GuiDisplay,
}

impl GuiRole {
    /// All roles, in inspector/palette order.
    pub const ALL: [Self; 4] = [Self::Body, Self::View, Self::Inspector, Self::Compact];

    /// The lowercase name used in labels, sugar and the wire format.
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::Body => "body",
            Self::View => "view",
            Self::Inspector => "inspector",
            Self::Compact => "compact",
        }
    }

    /// The role for its lowercase name.
    pub fn from_str(s: &str) -> Option<Self> {
        Self::ALL.into_iter().find(|r| r.as_str() == s)
    }
}

impl GuiDisplay {
    /// All display modes, in inspector/palette order.
    pub const ALL: [Self; 3] = [Self::Full, Self::Compact, Self::Label];

    /// The lowercase name used in labels, sugar and the wire format.
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::Full => "full",
            Self::Compact => "compact",
            Self::Label => "label",
        }
    }

    /// The display mode for its lowercase name.
    pub fn from_str(s: &str) -> Option<Self> {
        Self::ALL.into_iter().find(|d| d.as_str() == s)
    }
}

/// The `gui` markers among a graph's stored nodes, in index order.
///
/// A pure data walk: a marker is a node whose tag is [`Gui`]'s, with its
/// role/display read via the type's own serde (no codec, no reification).
/// Call sites resolve a duplicate role as first-in-index-order.
pub fn markers(g: &gantz_ca::DataGraph) -> Vec<(node::Id, Gui)> {
    use petgraph::visit::{IntoNodeReferences, NodeRef};
    g.node_references()
        .filter_map(|n| {
            let nd: &gantz_ca::NodeData = n.weight();
            (nd.tag == Gui::TAG)
                .then(|| gantz_core::data::reify_node_concrete::<Gui>(nd).ok())
                .flatten()
                .map(|gui| (n.id().index(), gui))
        })
        .collect()
}

/// The graph address a stored node pins, if it is a reference *stand-in* (a
/// `NamedRef` or bare `Ref`).
///
/// `Fn`-wrapped refs deliberately do not match: a function value references
/// a graph without standing in for it. This mirrors the typed `AsRefNode`
/// rule as a pure data check, the same tag rule the plyphon DSP-graph walk
/// uses.
pub fn ref_target_of(nd: &gantz_ca::NodeData) -> Option<gantz_ca::ContentAddr> {
    let is_ref = nd.tag == crate::node::NamedRef::TAG || nd.tag == gantz_core::node::Ref::TAG;
    is_ref.then(|| nd.refs.first().copied()).flatten()
}

/// The path and input count of every `gui` marker in the graph tree rooted
/// at `g`, recursing through reference stand-ins via the registry.
///
/// Backs the hosts' eager marker refresh: each `(path, n_inputs)` names the
/// singleton pull entrypoint compiled for that marker instance. A pure data
/// walk over stored graphs - no codec and no reified cache involved.
pub fn marker_paths(
    reg: &gantz_ca::Registry,
    g: &gantz_ca::DataGraph,
) -> Vec<(Vec<node::Id>, usize)> {
    let mut out = Vec::new();
    let mut path = Vec::new();
    let mut descent = Vec::new();
    collect_marker_paths(reg, g, &mut path, &mut descent, &mut out);
    out
}

/// The recursive body of [`marker_paths`]. `descent` guards against cyclic
/// reference data (the registry prevents true cycles; corrupt data must not
/// hang the walk).
fn collect_marker_paths(
    reg: &gantz_ca::Registry,
    g: &gantz_ca::DataGraph,
    path: &mut Vec<node::Id>,
    descent: &mut Vec<gantz_ca::GraphAddr>,
    out: &mut Vec<(Vec<node::Id>, usize)>,
) {
    use petgraph::visit::{IntoNodeReferences, NodeRef};
    for n in g.node_references() {
        let nd: &gantz_ca::NodeData = n.weight();
        path.push(n.id().index());
        if nd.tag == Gui::TAG {
            if let Ok(gui) = gantz_core::data::reify_node_concrete::<Gui>(nd) {
                let get_node = |_: &gantz_ca::ContentAddr| None;
                let n_inputs = gantz_core::Node::n_inputs(&gui, MetaCtx::new(&get_node));
                out.push((path.clone(), n_inputs));
            }
        } else if let Some(target) = ref_target_of(nd) {
            let ga = gantz_ca::GraphAddr::from(target);
            if !descent.contains(&ga) {
                if let Some(child) = reg.graph(&ga) {
                    descent.push(ga);
                    collect_marker_paths(reg, child, path, descent, out);
                    descent.pop();
                }
            }
        }
        path.pop();
    }
}

impl gantz_core::Node for Gui {
    fn n_inputs(&self, _ctx: MetaCtx) -> usize {
        1
    }

    fn stateful(&self, _ctx: MetaCtx) -> bool {
        true
    }

    fn pull_eval(&self, _ctx: MetaCtx) -> Vec<EvalConf> {
        vec![EvalConf::All]
    }

    fn expr(&self, ctx: ExprCtx<'_, '_>) -> ExprResult {
        let expr = match ctx.inputs().get(0) {
            Some(Some(val)) => format!("(begin (set! state {val}) state)"),
            _ => "(begin state)".to_string(),
        };
        node::parse_expr(&expr)
    }

    fn register(&self, mut ctx: RegCtx<'_, '_>) {
        let path = ctx.path();
        node::state::init_value_if_absent(ctx.vm(), path, || steel::SteelVal::Void).unwrap()
    }
}

impl NodeUi for Gui {
    fn name(&self, _: &Env<'_>) -> Cow<'_, str> {
        "gui".into()
    }

    fn description(&self) -> Option<&'static str> {
        Some("Declares the wired tree as this graph's GUI for a role")
    }

    fn ui(&mut self, _ctx: NodeCtx, uictx: egui_graph::NodeCtx) -> NodeUiResponse {
        let framed = uictx.framed(|ui, _sockets| {
            let text = format!("gui[{}]", self.role.as_str());
            ui.add(egui::Label::new(text).selectable(false))
        });
        NodeUiResponse::new(framed)
    }

    fn inspector_rows(
        &mut self,
        _ctx: &mut NodeCtx,
        body: &mut egui_extras::TableBody,
    ) -> InspectorRowsResponse {
        let mut changed = false;
        let row_h = node_inspector::table_row_h(body.ui_mut());

        body.row(row_h, |mut row| {
            row.col(|ui| {
                ui.label("role");
            });
            row.col(|ui| {
                ui.horizontal(|ui| {
                    changed |= radio_option(
                        ui,
                        &mut self.role,
                        GuiRole::Body,
                        "body",
                        "the in-graph node form",
                    );
                    changed |= radio_option(
                        ui,
                        &mut self.role,
                        GuiRole::View,
                        "view",
                        "the detached view pane",
                    );
                    changed |= radio_option(
                        ui,
                        &mut self.role,
                        GuiRole::Inspector,
                        "inspector",
                        "appended after the inspector table",
                    );
                    changed |= radio_option(
                        ui,
                        &mut self.role,
                        GuiRole::Compact,
                        "compact",
                        "condensed body for dense patching",
                    );
                });
            });
        });

        if self.role == GuiRole::Body {
            body.row(row_h, |mut row| {
                row.col(|ui| {
                    ui.label("display");
                });
                row.col(|ui| {
                    ui.horizontal(|ui| {
                        changed |= radio_option(
                            ui,
                            &mut self.display,
                            GuiDisplay::Full,
                            "full",
                            "instances render the full body tree",
                        );
                        changed |= radio_option(
                            ui,
                            &mut self.display,
                            GuiDisplay::Compact,
                            "compact",
                            "instances render the compact tree",
                        );
                        changed |= radio_option(
                            ui,
                            &mut self.display,
                            GuiDisplay::Label,
                            "label",
                            "instances render the name label",
                        );
                    });
                });
            });
        }

        let mut resp = InspectorRowsResponse::default();
        resp.set_changed(changed);
        resp
    }

    fn socket_doc(&self, _: &Env<'_>, kind: SocketKind, _ix: usize) -> Option<SocketDoc> {
        match kind {
            SocketKind::Input => Some(
                SocketDoc::ty("ui tree")
                    .with_description("stored and presented as this graph's GUI for the role"),
            ),
            SocketKind::Output => None,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use gantz_core::node::{Node, WithPushEval};
    use gantz_core::{
        Edge, ROOT_STATE,
        compile::{EvalKind, entry_fn_name, entrypoint, push_pull_entrypoints},
    };
    use steel::SteelVal;
    use steel::steel_vm::engine::Engine;

    // A node lookup is unnecessary for these self-contained graphs.
    fn no_lookup(_: &gantz_ca::ContentAddr) -> Option<&'static dyn Node> {
        None
    }

    // Compile `g`, init a base VM with node state, and load the module.
    fn vm_for(g: &petgraph::graph::DiGraph<Box<dyn Node>, Edge>) -> Engine {
        let eps = push_pull_entrypoints(&no_lookup, g);
        let module = gantz_core::compile::module(&no_lookup, g, &eps, &Default::default()).unwrap();
        let mut vm = Engine::new_base();
        vm.register_value(ROOT_STATE, SteelVal::empty_hashmap());
        gantz_core::graph::register(&no_lookup, g, &[], &mut vm);
        for f in module {
            vm.run(format!("{f}")).unwrap();
        }
        vm
    }

    // Fire the pull entrypoint of the marker at `path`.
    fn fire_pull(vm: &mut Engine, path: Vec<usize>) {
        let ep = entrypoint::pull(path, 1);
        let fn_name = entry_fn_name(&ep.id());
        vm.call_function_by_name_with_args(&fn_name, vec![])
            .unwrap();
    }

    // Fire the push entrypoint of node `ix`.
    fn fire_push(vm: &mut Engine, g: &petgraph::graph::DiGraph<Box<dyn Node>, Edge>, ix: usize) {
        let ctx = node::MetaCtx::new(&no_lookup);
        let outs = g[petgraph::graph::NodeIndex::new(ix)].n_outputs(ctx) as u8;
        let ep = entrypoint::push(vec![ix], outs);
        let fn_name = entry_fn_name(&ep.id());
        vm.call_function_by_name_with_args(&fn_name, vec![])
            .unwrap();
    }

    // The marker's stored state as a list, panicking on any other shape.
    fn list_state(vm: &Engine, path: &[usize]) -> Vec<SteelVal> {
        match node::state::extract_value(vm, path).unwrap().unwrap() {
            SteelVal::ListV(list) => list.iter().cloned().collect(),
            other => panic!("expected list state, got {other:?}"),
        }
    }

    // Build `expr -> gui`, returning the graph and the two node indices.
    fn graph_with(
        src: Box<dyn Node>,
        gui: Gui,
    ) -> (petgraph::graph::DiGraph<Box<dyn Node>, Edge>, usize, usize) {
        let mut g = petgraph::graph::DiGraph::new();
        let s = g.add_node(src);
        let m = g.add_node(Box::new(gui) as Box<dyn Node>);
        g.add_edge(s, m, Edge::from((0, 0)));
        (g, s.index(), m.index())
    }

    // A pull at the marker evaluates the upstream tree and stores it.
    #[test]
    fn pull_stores_connected_tree() {
        let src = gantz_core::node::expr("'(col)").unwrap();
        let (g, _s, m) = graph_with(Box::new(src) as Box<dyn Node>, Gui::default());
        let mut vm = vm_for(&g);
        fire_pull(&mut vm, vec![m]);
        assert_eq!(list_state(&vm, &[m]).len(), 1);
    }

    // A push through the marker stores the pushed tree.
    #[test]
    fn push_through_stores_tree() {
        let src = gantz_core::node::expr("'(row (sep))")
            .unwrap()
            .with_push_eval();
        let (g, s, m) = graph_with(Box::new(src) as Box<dyn Node>, Gui::default());
        let mut vm = vm_for(&g);
        fire_push(&mut vm, &g, s);
        assert_eq!(list_state(&vm, &[m]).len(), 2);
    }

    // An unconnected marker's pull leaves its registered Void state.
    #[test]
    fn unconnected_pull_keeps_void() {
        let mut g = petgraph::graph::DiGraph::<Box<dyn Node>, Edge>::new();
        let m = g
            .add_node(Box::new(Gui::default()) as Box<dyn Node>)
            .index();
        let mut vm = vm_for(&g);
        fire_pull(&mut vm, vec![m]);
        let val = node::state::extract_value(&vm, &[m]).unwrap().unwrap();
        assert_eq!(val, SteelVal::Void);
    }

    // A marker nested in an inner graph gets its own per-instance pull
    // entrypoint (collected through the nesting), and firing it stores the
    // inner tree at the nested path. Gui is the first 0-output stateful node,
    // so this also pins the compiled form.
    #[test]
    fn nested_marker_pull_entrypoint_fires() {
        let mut inner = gantz_core::node::graph::Graph::<Box<dyn Node>>::default();
        let src = gantz_core::node::expr("'(col)").unwrap();
        let s = inner.add_node(Box::new(src) as Box<dyn Node>);
        let gui = inner.add_node(Box::new(Gui::default()) as Box<dyn Node>);
        let m = gui.index();
        inner.add_edge(s, gui, Edge::from((0, 0)));

        let mut outer = petgraph::graph::DiGraph::<Box<dyn Node>, Edge>::new();
        let n = outer.add_node(Box::new(inner) as Box<dyn Node>).index();

        // The collector emits the nested marker's singleton pull entrypoint.
        let eps = push_pull_entrypoints(&no_lookup, &outer);
        let expected = entrypoint::pull(vec![n, m], 1);
        assert!(
            eps.iter()
                .any(|ep| { ep == &expected && ep.0.iter().all(|s| s.kind == EvalKind::Pull) }),
            "expected a singleton pull entrypoint at [{n}, {m}], got {eps:?}"
        );

        let mut vm = vm_for(&outer);
        fire_pull(&mut vm, vec![n, m]);
        assert_eq!(list_state(&vm, &[n, m]).len(), 1);
    }
}