use std::collections::HashMap;
use std::sync::RwLockReadGuard;
use eframe::egui::{Color32, Pos2, Ui};
use egui_snarl::ui::{PinInfo, SnarlStyle, SnarlViewer};
use egui_snarl::{InPin, InPinId, NodeId, OutPin, OutPinId, Snarl};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::core::event_bus::BoxedEvent;
use crate::entities::{AttrValue, Comp, Project};
use crate::entities::node::Node;
use crate::entities::Attrs;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CompNode {
pub uuid: Uuid,
pub source_uuid: Uuid,
}
struct CompNodeViewer<'a> {
project: &'a Project,
output_uuid: Uuid, }
impl<'a> CompNodeViewer<'a> {
fn get_node(&self, source_uuid: Uuid) -> Option<std::sync::Arc<crate::entities::NodeKind>> {
self.project.media.read().ok()?.get(&source_uuid).cloned()
}
fn get_comp(&self, source_uuid: Uuid) -> Option<Comp> {
self.project.media.read().ok()?.get(&source_uuid).and_then(|n| n.as_comp()).cloned()
}
}
#[allow(refining_impl_trait)]
impl<'a> SnarlViewer<CompNode> for CompNodeViewer<'a> {
fn title(&mut self, node: &CompNode) -> String {
self.get_node(node.source_uuid)
.map(|n| n.name().to_string())
.unwrap_or_else(|| "Unknown".to_string())
}
fn outputs(&mut self, _node: &CompNode) -> usize {
1
}
fn inputs(&mut self, node: &CompNode) -> usize {
self.get_comp(node.source_uuid)
.map(|c| c.layers.len())
.unwrap_or(0)
}
fn show_input(
&mut self,
pin: &InPin,
ui: &mut Ui,
_snarl: &mut Snarl<CompNode>,
) -> PinInfo {
ui.label(format!("L{}", pin.id.input));
PinInfo::circle().with_fill(Color32::from_rgb(100, 180, 255))
}
fn show_output(
&mut self,
_pin: &OutPin,
ui: &mut Ui,
_snarl: &mut Snarl<CompNode>,
) -> PinInfo {
ui.label("Out");
PinInfo::circle().with_fill(Color32::from_rgb(180, 180, 180))
}
fn has_body(&mut self, _node: &CompNode) -> bool {
false
}
fn show_body(
&mut self,
_node: NodeId,
_inputs: &[InPin],
_outputs: &[OutPin],
_ui: &mut Ui,
_snarl: &mut Snarl<CompNode>,
) {
}
fn show_header(
&mut self,
node: NodeId,
_inputs: &[InPin],
_outputs: &[OutPin],
ui: &mut Ui,
snarl: &mut Snarl<CompNode>,
) {
let source_uuid = snarl[node].source_uuid;
let comp = self.get_comp(source_uuid);
let (icon, color, name) = match comp {
Some(c) => {
let is_output = source_uuid == self.output_uuid;
let is_file = c.is_file_mode();
let name = c.name().to_string();
if is_output {
("[OUT]", Color32::from_rgb(255, 100, 100), name)
} else if is_file {
("[F]", Color32::from_rgb(255, 180, 100), name)
} else {
("[C]", Color32::from_rgb(100, 255, 180), name)
}
}
None => ("[?]", Color32::GRAY, "Unknown".to_string()),
};
ui.horizontal(|ui| {
ui.colored_label(color, icon);
ui.label(name);
});
}
}
const HORIZONTAL_SPACING: f32 = 200.0;
const VERTICAL_SPACING: f32 = 70.0;
fn default_true() -> bool { true }
#[derive(Clone, Default, Serialize, Deserialize)]
pub struct NodeEditorState {
#[serde(skip)]
pub snarl: Snarl<CompNode>,
#[serde(skip)]
pub comp_uuid: Option<Uuid>,
#[serde(skip, default = "default_true")]
needs_rebuild: bool,
#[serde(skip)]
pub fit_all_requested: bool,
#[serde(skip)]
pub fit_selected_requested: bool,
#[serde(skip)]
pub layout_requested: bool,
#[serde(skip)]
viewport_reset_counter: u64,
}
impl NodeEditorState {
pub fn new() -> Self {
Self {
snarl: Snarl::new(),
comp_uuid: None,
needs_rebuild: true,
fit_all_requested: false,
fit_selected_requested: false,
layout_requested: false,
viewport_reset_counter: 0,
}
}
pub fn mark_dirty(&mut self) {
self.needs_rebuild = true;
}
pub fn set_comp(&mut self, comp_uuid: Uuid) {
if self.comp_uuid != Some(comp_uuid) {
self.comp_uuid = Some(comp_uuid);
self.needs_rebuild = true;
}
}
pub fn rebuild_from_comp(&mut self, comp_uuid: Uuid, project: &Project) {
if !self.needs_rebuild {
return;
}
self.needs_rebuild = false;
self.snarl = Snarl::new();
let root_uuid = comp_uuid;
let media = project.media.read().expect("media lock");
let comp_name = media.get(&root_uuid)
.map(|n| n.name().to_string())
.unwrap_or_else(|| "Unknown".to_string());
log::trace!(
"NodeEditor: rebuilding for comp '{}' ({}), media has {} items",
comp_name,
root_uuid,
media.len()
);
log::trace!("NodeEditor: comp is in media? {}", media.contains_key(&root_uuid));
let mut node_info: HashMap<Uuid, NodeInfo> = HashMap::new();
let mut ancestors: Vec<Uuid> = Vec::new();
let mut max_depth = 0;
collect_tree_recursive(
root_uuid,
root_uuid,
0,
&media,
&mut node_info,
&mut ancestors,
&mut max_depth,
);
log::trace!(
"NodeEditor rebuild: root={}, nodes={}, max_depth={}",
root_uuid,
node_info.len(),
max_depth
);
drop(media);
let mut depth_slots: HashMap<usize, usize> = HashMap::new();
let mut uuid_to_node: HashMap<Uuid, NodeId> = HashMap::new();
for info in node_info.values() {
let depth = info.depth;
let slot = *depth_slots.get(&depth).unwrap_or(&0);
depth_slots.insert(depth, slot + 1);
let default_x = (max_depth - depth) as f32 * HORIZONTAL_SPACING + 50.0;
let default_y = slot as f32 * VERTICAL_SPACING + 50.0;
let default_pos = Pos2::new(default_x, default_y);
let pos = load_node_pos(project, comp_uuid, info.instance_uuid, default_pos);
log::trace!(
"NodeEditor: creating node {} (src {}) at ({}, {})",
info.instance_uuid,
info.source_uuid,
pos.x,
pos.y
);
let node_id = self.snarl.insert_node(
pos,
CompNode {
uuid: info.instance_uuid,
source_uuid: info.source_uuid,
},
);
uuid_to_node.insert(info.instance_uuid, node_id);
}
for (&parent_uuid, info) in &node_info {
if let Some(&parent_id) = uuid_to_node.get(&parent_uuid) {
for (input_idx, &(child_instance, _)) in info.children.iter().enumerate() {
if let Some(&child_id) = uuid_to_node.get(&child_instance) {
let out_pin = OutPinId {
node: child_id,
output: 0,
};
let in_pin = InPinId {
node: parent_id,
input: input_idx,
};
let _ = self.snarl.connect(out_pin, in_pin);
}
}
}
}
self.fit_all_requested = true;
}
pub fn relayout(&mut self, project: &Project, dispatch: &mut impl FnMut(BoxedEvent)) {
if self.snarl.node_ids().next().is_none() {
return;
}
let mut uuid_to_node: HashMap<Uuid, NodeId> = HashMap::new();
for (node_id, node) in self.snarl.node_ids() {
uuid_to_node.insert(node.uuid, node_id);
}
let Some(comp_uuid) = self.comp_uuid else { return };
let media = project.media.read().expect("media lock");
let mut node_info: HashMap<Uuid, NodeInfo> = HashMap::new();
let mut ancestors: Vec<Uuid> = Vec::new();
let mut max_depth = 0;
collect_tree_recursive(
comp_uuid,
comp_uuid,
0,
&media,
&mut node_info,
&mut ancestors,
&mut max_depth,
);
drop(media);
let mut new_positions: HashMap<Uuid, Pos2> = HashMap::new();
let mut depth_slots: HashMap<usize, usize> = HashMap::new();
for (&instance_uuid, info) in &node_info {
let depth = info.depth;
let slot = *depth_slots.get(&depth).unwrap_or(&0);
depth_slots.insert(depth, slot + 1);
let x = (max_depth - depth) as f32 * HORIZONTAL_SPACING + 50.0;
let y = slot as f32 * VERTICAL_SPACING + 50.0;
new_positions.insert(instance_uuid, Pos2::new(x, y));
}
for node in self.snarl.nodes_info_mut() {
if let Some(&new_pos) = new_positions.get(&node.value.uuid) {
node.pos = new_pos;
}
}
if let Some(&pos) = new_positions.get(&comp_uuid) {
project.modify_comp(comp_uuid, |c| {
set_node_pos(&mut c.attrs, pos);
c.attrs.clear_dirty(); });
}
for (&instance_uuid, &pos) in &new_positions {
if instance_uuid != comp_uuid {
dispatch(Box::new(crate::entities::comp_events::SetLayerAttrsEvent {
comp_uuid,
layer_uuids: vec![instance_uuid],
attrs: vec![("node_pos".to_string(), AttrValue::Vec3([pos.x, pos.y, 0.0]))],
}));
}
}
self.fit_all_requested = true;
}
}
struct NodeInfo {
depth: usize,
instance_uuid: Uuid,
source_uuid: Uuid,
children: Vec<(Uuid, Uuid)>, }
fn load_node_pos(project: &Project, comp_uuid: Uuid, instance_uuid: Uuid, default: Pos2) -> Pos2 {
let maybe_pos = project.with_comp(comp_uuid, |comp| {
let maybe_attr = if instance_uuid == comp.uuid() {
comp.attrs.get("node_pos")
} else {
comp.layers_attrs_get(&instance_uuid)
.and_then(|a| a.get("node_pos"))
};
if let Some(AttrValue::Vec3([x, y, _])) = maybe_attr {
Some(Pos2::new(*x, *y))
} else {
None
}
}).flatten();
maybe_pos.unwrap_or(default)
}
fn set_node_pos(attrs: &mut Attrs, pos: Pos2) -> bool {
let new_val = [pos.x, pos.y, 0.0];
let mut changed = true;
if let Some(AttrValue::Vec3(current)) = attrs.get("node_pos") {
let dx = (current[0] - new_val[0]).abs();
let dy = (current[1] - new_val[1]).abs();
changed = dx > 0.001 || dy > 0.001;
}
if changed {
attrs.set("node_pos", AttrValue::Vec3(new_val));
attrs.clear_dirty(); }
changed
}
fn collect_tree_recursive(
instance_uuid: Uuid,
source_uuid: Uuid,
depth: usize,
media: &RwLockReadGuard<'_, HashMap<Uuid, std::sync::Arc<crate::entities::NodeKind>>>,
node_info: &mut HashMap<Uuid, NodeInfo>,
ancestors: &mut Vec<Uuid>,
max_depth: &mut usize,
) {
if ancestors.contains(&source_uuid) {
node_info.insert(
instance_uuid,
NodeInfo {
depth,
instance_uuid,
source_uuid,
children: vec![],
},
);
return;
}
ancestors.push(source_uuid);
*max_depth = (*max_depth).max(depth);
let Some(comp) = media.get(&source_uuid).and_then(|n| n.as_comp()) else {
node_info.insert(
instance_uuid,
NodeInfo {
depth,
instance_uuid,
source_uuid,
children: vec![],
},
);
ancestors.pop();
return;
};
let children: Vec<(Uuid, Uuid)> = comp.get_children_sources();
node_info.insert(
instance_uuid,
NodeInfo {
depth,
instance_uuid,
source_uuid,
children: children.clone(),
},
);
for (child_instance, child_source) in children {
collect_tree_recursive(
child_instance,
child_source,
depth + 1,
media,
node_info,
ancestors,
max_depth,
);
}
ancestors.pop();
}
pub fn render_node_editor(
ui: &mut Ui,
state: &mut NodeEditorState,
project: &Project,
comp_uuid: Uuid,
mut dispatch: impl FnMut(BoxedEvent),
) -> bool {
let _widget_id = ui.make_persistent_id("comp_node_editor");
state.set_comp(comp_uuid);
if state.needs_rebuild {
state.rebuild_from_comp(comp_uuid, project);
}
if state.fit_all_requested || state.fit_selected_requested {
state.fit_all_requested = false;
state.fit_selected_requested = false;
state.viewport_reset_counter += 1;
log::trace!("[NODE_EDITOR] fit viewport requested, reset_counter={}", state.viewport_reset_counter);
}
if state.layout_requested {
state.layout_requested = false;
state.relayout(project, &mut dispatch);
}
ui.horizontal(|ui| {
ui.add_space(4.0);
if ui
.button("A")
.on_hover_text("Fit All - zoom to see all nodes")
.clicked()
{
state.fit_all_requested = true;
}
if ui
.button("F")
.on_hover_text("Fit Selected - zoom to selected nodes (or all)")
.clicked()
{
state.fit_selected_requested = true;
}
if ui
.button("L")
.on_hover_text("Layout - arrange nodes in tree")
.clicked()
{
state.layout_requested = true;
}
ui.separator();
let node_count = state.snarl.node_ids().count();
ui.label(format!("{} nodes", node_count));
});
ui.separator();
let mut viewer = CompNodeViewer {
project,
output_uuid: comp_uuid,
};
let style = SnarlStyle {
centering: Some(true),
..Default::default()
};
let before_positions: HashMap<NodeId, Pos2> = state
.snarl
.nodes_pos_ids()
.map(|(id, pos, _)| (id, pos))
.collect();
let snarl_id = format!("comp_node_editor_{}", state.viewport_reset_counter);
state
.snarl
.show(&mut viewer, &style, &snarl_id, ui);
if let Some(comp_uuid) = state.comp_uuid {
let mut moved_layers = Vec::new();
let mut moved_root = None;
for (node_id, pos, node) in state.snarl.nodes_pos_ids() {
let was = before_positions.get(&node_id).copied();
if was.map(|p| p.distance(pos)).unwrap_or(f32::INFINITY) > 0.01 {
if node.uuid == comp_uuid {
moved_root = Some(pos);
} else {
moved_layers.push((node.uuid, pos));
}
}
}
if let Some(pos) = moved_root {
project.modify_comp(comp_uuid, |c| {
set_node_pos(&mut c.attrs, pos);
c.attrs.clear_dirty();
});
}
if !moved_layers.is_empty() {
for (layer_uuid, pos) in moved_layers {
dispatch(Box::new(crate::entities::comp_events::SetLayerAttrsEvent {
comp_uuid,
layer_uuids: vec![layer_uuid],
attrs: vec![("node_pos".to_string(), AttrValue::Vec3([pos.x, pos.y, 0.0]))],
}));
}
}
}
ui.rect_contains_pointer(ui.max_rect())
}