use petgraph::visit::{IntoNodeReferences, NodeRef};
use std::borrow::Cow;
use steel::{
SteelErr, SteelVal,
rvals::{FromSteelVal, IntoSteelVal},
steel_vm::engine::Engine,
};
pub mod action;
pub mod collab;
pub mod cycle;
pub mod export;
pub mod format;
mod impls;
pub mod keybind;
pub mod merge;
pub mod node;
pub mod ops;
pub mod reg;
pub mod response;
pub mod section;
pub mod sugar;
pub mod sync;
#[cfg(test)]
mod test_node;
pub mod ui_tree;
pub mod view;
pub mod widget;
#[doc(hidden)]
pub use gantz_ca;
#[doc(hidden)]
pub use gantz_core;
#[doc(hidden)]
pub use gantz_format;
#[doc(hidden)]
pub use gantz_nodetag;
pub use action::StateWritten;
pub use egui_graph::SocketKind;
pub use keybind::{Action, Keymap};
pub use node::builtins;
pub use reg::Env;
pub use response::{
ContextMenuResponse, DynResponse, InspectorRowsResponse, InspectorUiResponse, NodeUiResponse,
NodeViewResponse, ResponseData, Responses,
};
pub use sugar::EguiSugar;
pub use view::{Camera, SceneView};
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct SocketDoc {
pub ty: Cow<'static, str>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<Cow<'static, str>>,
}
impl SocketDoc {
pub fn ty(ty: impl Into<Cow<'static, str>>) -> Self {
SocketDoc {
ty: ty.into(),
description: None,
}
}
pub fn with_description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
self.description = Some(description.into());
self
}
pub fn is_empty(&self) -> bool {
self.ty.is_empty() && self.description.is_none()
}
}
#[derive(Clone, Debug, Default)]
pub struct CommandInfo {
pub name: String,
pub description: Option<Cow<'static, str>>,
pub inputs: Vec<SocketDoc>,
pub outputs: Vec<SocketDoc>,
}
pub fn node_info_ui(info: &CommandInfo, ui: &mut egui::Ui) {
if !info.name.is_empty() {
ui.strong(&info.name);
}
if let Some(desc) = &info.description {
ui.label(desc.as_ref());
}
socket_doc_list(ui, "Inputs", &info.inputs);
socket_doc_list(ui, "Outputs", &info.outputs);
}
fn socket_doc_list(ui: &mut egui::Ui, heading: &str, docs: &[SocketDoc]) {
if docs.is_empty() {
return;
}
ui.add_space(4.0);
ui.weak(heading);
for (ix, doc) in docs.iter().enumerate() {
ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing.x = 4.0;
ui.weak(format!("{ix}"));
if !doc.ty.is_empty() {
ui.strong(doc.ty.as_ref());
}
if let Some(desc) = &doc.description {
ui.label(format!("- {desc}"));
}
});
}
}
pub trait HeadAccess {
fn heads(&self) -> &[gantz_ca::Head];
fn with_head_mut<R>(
&mut self,
head: &gantz_ca::Head,
f: impl FnOnce(HeadDataMut<'_>) -> R,
) -> Option<R>;
fn module(&self, _head: &gantz_ca::Head) -> Option<&gantz_core::vm::Compiled> {
None
}
fn compile_error(&self, _head: &gantz_ca::Head) -> Option<&str> {
None
}
fn diagnostics(&self, _head: &gantz_ca::Head) -> &[gantz_core::Diagnostic] {
&[]
}
}
pub struct HeadDataMut<'a> {
pub graph: &'a mut gantz_ca::DataGraph,
pub view: &'a mut crate::SceneView,
pub vm: &'a mut Engine,
pub instances: &'a mut node::NodeInstances,
}
pub trait NodeUi: gantz_core::Node + Send + Sync {
fn name(&self, _env: &Env<'_>) -> Cow<'_, str>;
fn ui(&mut self, ctx: NodeCtx, uictx: egui_graph::NodeCtx) -> NodeUiResponse;
fn inspector_rows(
&mut self,
_ctx: &mut NodeCtx,
_body: &mut egui_extras::TableBody,
) -> InspectorRowsResponse {
InspectorRowsResponse::default()
}
fn inspector_ui(&mut self, _ctx: NodeCtx, _ui: &mut egui::Ui) -> InspectorUiResponse {
InspectorUiResponse::default()
}
fn view_ui(&mut self, ctx: NodeCtx, ui: &mut egui::Ui) -> NodeViewResponse {
default_view_ui(&ctx, ui)
}
fn view_no_margin(&self) -> bool {
false
}
fn context_menu(&mut self, _ctx: &mut NodeCtx, _ui: &mut egui::Ui) -> ContextMenuResponse {
ContextMenuResponse::default()
}
fn flow(&self, _env: &Env<'_>) -> egui::Direction {
egui::Direction::TopDown
}
fn demo_graph(&self, _env: &Env<'_>) -> Option<String> {
None
}
fn nav_head(&self, _env: &Env<'_>) -> Option<gantz_ca::Head> {
None
}
fn description(&self) -> Option<&'static str> {
None
}
fn socket_doc(&self, _env: &Env<'_>, _kind: SocketKind, _ix: usize) -> Option<SocketDoc> {
None
}
fn show_state(&self) -> bool {
true
}
}
pub(crate) fn default_view_ui(ctx: &NodeCtx, ui: &mut egui::Ui) -> NodeViewResponse {
let mut resp = NodeViewResponse::default();
let text = match ctx.extract_value() {
Ok(Some(val)) => format!("{val:?}"),
Ok(None) => "∅".to_string(),
Err(_) => "ERR".to_string(),
};
let inner = egui::ScrollArea::both()
.auto_shrink(false)
.show(ui, |ui| ui.add(egui::Label::new(text).selectable(true)))
.inner;
resp.inner = Some(inner);
resp
}
pub struct NodeCtx<'a> {
env: &'a Env<'a>,
path: &'a [node::Id],
inlets: &'a [node::Id],
outlets: &'a [node::Id],
ref_ext_uis: &'a [&'a dyn node::RefExtUi],
vm: &'a mut Engine,
writes: &'a mut Vec<action::StateWrite>,
}
#[derive(Clone, Debug)]
pub enum PastePos {
Offset(egui::Vec2),
GraphPos(egui::Pos2),
}
pub fn resolve_paste_offset(pos: &PastePos, copied_positions: &egui_graph::Layout) -> egui::Vec2 {
match pos {
PastePos::Offset(v) => *v,
PastePos::GraphPos(target) => {
if copied_positions.is_empty() {
target.to_vec2()
} else {
let center = copied_positions
.values()
.fold(egui::Vec2::ZERO, |acc, p| acc + p.to_vec2())
/ copied_positions.len() as f32;
target.to_vec2() - center
}
}
}
}
#[derive(Clone, Debug)]
pub struct BranchNode {
pub new_name: String,
pub ca: gantz_ca::ContentAddr,
pub path: Vec<node::Id>,
}
#[derive(Clone, Debug)]
pub struct CopyNodes(pub std::collections::HashSet<widget::graph_scene::NodeIndex>);
#[derive(Clone, Debug)]
pub struct CutNodes(pub std::collections::HashSet<widget::graph_scene::NodeIndex>);
#[derive(Clone, Debug)]
pub struct DuplicateNodes(pub std::collections::HashSet<widget::graph_scene::NodeIndex>);
#[derive(Clone, Debug)]
pub struct NestNodes(pub std::collections::HashSet<widget::graph_scene::NodeIndex>);
#[derive(Clone, Debug)]
pub struct CreateNode {
pub node_type: String,
pub pos: Option<egui::Pos2>,
}
#[derive(Clone, Copy, Debug)]
pub struct CreateNestedGraph {
pub pos: Option<egui::Pos2>,
}
#[derive(Clone, Debug)]
pub struct EvalEntry(pub gantz_core::compile::Entrypoint);
#[derive(Clone, Copy, Debug)]
pub struct ExportAllNamed;
#[derive(Clone, Copy, Debug)]
pub struct ExportHead;
#[derive(Clone, Debug)]
pub struct InspectEdge {
pub edge: petgraph::graph::EdgeIndex<usize>,
pub pos: egui::Pos2,
}
#[derive(Clone, Copy, Debug)]
pub struct OpenNodePalette;
#[derive(Clone, Copy, Debug)]
pub struct ResetTilesLayout;
#[derive(Clone, Copy, Debug)]
pub struct OpenLogs;
#[derive(Clone, Debug)]
pub struct OpenNodeView {
pub path: Vec<node::Id>,
pub ty_name: String,
}
#[derive(Clone, Debug)]
pub struct OpenHead(pub gantz_ca::Head);
#[derive(Clone, Debug)]
pub struct ReplaceHead(pub gantz_ca::Head);
#[derive(Clone, Debug)]
pub struct Paste {
pub text: Option<String>,
pub pos: PastePos,
}
#[derive(Clone, Debug)]
pub struct MergeHead {
pub source: String,
pub resolutions: gantz_ca::Resolutions,
pub auto_resolve: bool,
}
#[derive(Clone, Copy, Debug)]
pub struct ShareHead {
pub public: bool,
}
#[derive(Clone, Copy, Debug)]
pub struct StopSharing;
#[derive(Clone, Debug)]
pub struct JoinSession {
pub ticket: String,
}
#[derive(Clone, Copy, Debug)]
pub struct Redo;
#[derive(Clone, Copy, Debug)]
pub struct Undo;
macro_rules! impl_node_ui_for_ptr {
($($Ty:ident)::*) => {
impl<T> NodeUi for $($Ty)::*<T>
where
T: ?Sized + NodeUi,
{
fn name(&self, env: &Env<'_>) -> Cow<'_, str> {
(**self).name(env)
}
fn description(&self) -> Option<&'static str> {
(**self).description()
}
fn ui(&mut self, ctx: NodeCtx, uictx: egui_graph::NodeCtx) -> NodeUiResponse {
(**self).ui(ctx, uictx)
}
fn inspector_rows(&mut self, ctx: &mut NodeCtx, body: &mut egui_extras::TableBody) -> InspectorRowsResponse {
(**self).inspector_rows(ctx, body)
}
fn inspector_ui(&mut self, ctx: NodeCtx, ui: &mut egui::Ui) -> InspectorUiResponse {
(**self).inspector_ui(ctx, ui)
}
fn view_ui(&mut self, ctx: NodeCtx, ui: &mut egui::Ui) -> NodeViewResponse {
(**self).view_ui(ctx, ui)
}
fn view_no_margin(&self) -> bool {
(**self).view_no_margin()
}
fn flow(&self, env: &Env<'_>) -> egui::Direction {
(**self).flow(env)
}
fn demo_graph(&self, env: &Env<'_>) -> Option<String> {
(**self).demo_graph(env)
}
fn nav_head(&self, env: &Env<'_>) -> Option<gantz_ca::Head> {
(**self).nav_head(env)
}
fn socket_doc(&self, env: &Env<'_>, kind: SocketKind, ix: usize) -> Option<SocketDoc> {
(**self).socket_doc(env, kind, ix)
}
fn context_menu(&mut self, ctx: &mut NodeCtx, ui: &mut egui::Ui) -> ContextMenuResponse {
(**self).context_menu(ctx, ui)
}
fn show_state(&self) -> bool {
(**self).show_state()
}
}
};
}
impl_node_ui_for_ptr!(Box);
impl<'a> NodeCtx<'a> {
pub fn new(
env: &'a Env<'a>,
path: &'a [node::Id],
inlets: &'a [node::Id],
outlets: &'a [node::Id],
ref_ext_uis: &'a [&'a dyn node::RefExtUi],
vm: &'a mut Engine,
writes: &'a mut Vec<action::StateWrite>,
) -> Self {
Self {
env,
path,
inlets,
outlets,
ref_ext_uis,
vm,
writes,
}
}
pub fn env(&self) -> &'a Env<'a> {
self.env
}
pub fn path(&self) -> &'a [node::Id] {
self.path
}
pub fn vm(&self) -> &Engine {
&*self.vm
}
pub fn extract_value(&self) -> Result<Option<SteelVal>, SteelErr> {
node::state::extract_value(self.vm, self.path)
}
pub fn extract<T: FromSteelVal>(&self) -> Result<Option<T>, SteelErr> {
node::state::extract(self.vm, self.path)
}
pub fn update_value(&mut self, val: SteelVal) -> Result<(), SteelErr> {
let recorded = action::Value::try_from(&val).ok();
node::state::update_value(self.vm, self.path, val)?;
match recorded {
Some(value) => self.writes.push(action::StateWrite {
path: self.path.to_vec(),
value,
}),
None => log::debug!(
"state write at {:?} not recorded: no wire-encodable representation",
self.path
),
}
Ok(())
}
pub fn update<T: IntoSteelVal>(&mut self, val: T) -> Result<(), SteelErr> {
let val = val.into_steelval()?;
self.update_value(val)
}
pub fn update_value_local(&mut self, val: SteelVal) -> Result<(), SteelErr> {
node::state::update_value(self.vm, self.path, val)
}
pub fn extract_value_at(&self, path: &[node::Id]) -> Result<Option<SteelVal>, SteelErr> {
node::state::extract_value(self.vm, path)
}
pub fn update_value_at(&mut self, path: &[node::Id], val: SteelVal) -> Result<(), SteelErr> {
let recorded = action::Value::try_from(&val).ok();
node::state::update_value(self.vm, path, val)?;
match recorded {
Some(value) => self.writes.push(action::StateWrite {
path: path.to_vec(),
value,
}),
None => log::debug!(
"state write at {path:?} not recorded: no wire-encodable representation"
),
}
Ok(())
}
pub fn inlets(&self) -> &[node::Id] {
self.inlets
}
pub fn outlets(&self) -> &[node::Id] {
self.outlets
}
pub fn ref_ext_uis(&self) -> &'a [&'a dyn node::RefExtUi] {
self.ref_ext_uis
}
}
pub(crate) fn inlet_outlet_ids(
env: &Env<'_>,
g: &gantz_ca::DataGraph,
) -> (Vec<node::Id>, Vec<node::Id>) {
let get_node = |ca: &gantz_ca::ContentAddr| env.node(ca);
let ctx = gantz_core::node::MetaCtx::new(&get_node);
let mut inlets = vec![];
let mut outlets = vec![];
for n_ref in g.node_references() {
let Ok(inst) = env.codec.reify_ui(n_ref.weight()) else {
continue;
};
if inst.node.inlet(ctx) {
inlets.push(n_ref.id().index());
}
if inst.node.outlet(ctx) {
outlets.push(n_ref.id().index());
}
}
(inlets, outlets)
}
fn system_time_from_web(t: web_time::SystemTime) -> Option<std::time::SystemTime> {
let duration = t.duration_since(web_time::UNIX_EPOCH).ok()?;
std::time::UNIX_EPOCH.checked_add(duration)
}
pub fn head_is_focused<'a>(
heads: impl IntoIterator<Item = &'a gantz_ca::Head>,
focused_head: usize,
head: &gantz_ca::Head,
) -> bool {
heads
.into_iter()
.position(|h| h == head)
.map(|ix| ix == focused_head)
.unwrap_or(false)
}