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;
pub const GUI_REF_EXT_KEY: &str = "gantz.gui";
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum GuiRole {
#[default]
Body,
View,
Inspector,
Compact,
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum GuiDisplay {
#[default]
Full,
Compact,
Label,
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, Deserialize, Serialize, NodeTag)]
pub struct Gui {
#[serde(default)]
pub role: GuiRole,
#[serde(default)]
pub display: GuiDisplay,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct GuiRefExt {
#[serde(default)]
pub display: GuiDisplay,
}
impl GuiRole {
pub const ALL: [Self; 4] = [Self::Body, Self::View, Self::Inspector, Self::Compact];
pub fn as_str(&self) -> &'static str {
match self {
Self::Body => "body",
Self::View => "view",
Self::Inspector => "inspector",
Self::Compact => "compact",
}
}
pub fn from_str(s: &str) -> Option<Self> {
Self::ALL.into_iter().find(|r| r.as_str() == s)
}
}
impl GuiDisplay {
pub const ALL: [Self; 3] = [Self::Full, Self::Compact, Self::Label];
pub fn as_str(&self) -> &'static str {
match self {
Self::Full => "full",
Self::Compact => "compact",
Self::Label => "label",
}
}
pub fn from_str(s: &str) -> Option<Self> {
Self::ALL.into_iter().find(|d| d.as_str() == s)
}
}
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()
}
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()
}
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
}
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;
fn no_lookup(_: &gantz_ca::ContentAddr) -> Option<&'static dyn Node> {
None
}
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
}
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();
}
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();
}
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:?}"),
}
}
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())
}
#[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);
}
#[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);
}
#[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);
}
#[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();
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);
}
}