use std::{borrow::Cow, collections::HashMap};
use super::{
state::{CanvasItem, GraphView},
transform::{Transform, WorldPos, WorldRect},
viewer::{GraphViewer, NodeDesc, NodeId, PortDesc, PortId, PortSide, StackId},
};
pub const NODE_WIDTH: f64 = 204.0;
pub const HEADER_H: f64 = 26.0;
pub const PORT_ROW_H: f64 = 22.0;
pub const BODY_PAD_TOP: f64 = 6.0;
pub const BODY_PAD_BOTTOM: f64 = 8.0;
pub const PORT_RADIUS: f64 = 5.0;
pub const CLOSE_BTN_SIZE: f64 = 14.0;
pub const CLOSE_BTN_MARGIN: f64 = 6.0;
pub const TOGGLE_SIZE: f64 = 12.0;
pub const TOGGLE_MARGIN: f64 = 6.0;
pub const PORT_GRAB_RADIUS: f64 = PORT_RADIUS * 1.8;
pub const PORT_GRAB_MIN_PX: f32 = 4.0;
pub const PORT_GRAB_MAX_PX: f32 = 18.0;
pub fn port_grab_radius_screen(t: &Transform) -> f32 {
t.world_len_to_screen(PORT_GRAB_RADIUS)
.clamp(PORT_GRAB_MIN_PX, PORT_GRAB_MAX_PX)
}
pub fn port_grab_radius_world(t: &Transform) -> f64 {
t.screen_len_to_world(port_grab_radius_screen(t))
}
pub const STACK_HEADER_H: f64 = 24.0;
pub const STACK_PAD: f64 = 8.0;
pub const MEMBER_GAP: f64 = 6.0;
pub const STACK_FOOTER_H: f64 = 20.0;
pub const STACK_BTN_SIZE: f64 = 16.0;
pub const STACK_BTN_MARGIN: f64 = 6.0;
#[derive(Debug, Clone)]
pub struct PortLayout {
pub id: PortId,
pub center: WorldPos,
pub label: Cow<'static, str>,
pub color: Option<egui::Color32>,
pub arity: u8,
pub value: Option<Cow<'static, str>>,
pub connectable: bool,
pub row_height: f64,
pub collapsible: bool,
}
#[derive(Debug, Clone)]
pub struct NodeLayout {
pub id: NodeId,
pub rect: WorldRect,
pub title: Cow<'static, str>,
pub accent: Option<egui::Color32>,
pub inputs: Vec<PortLayout>,
pub outputs: Vec<PortLayout>,
pub stack: Option<StackId>,
pub warning: Option<Cow<'static, str>>,
pub close_button: Option<WorldRect>,
pub collapsed: bool,
pub collapse_toggle: Option<WorldRect>,
}
impl NodeLayout {
pub fn port_center(&self, port: PortId) -> Option<WorldPos> {
let list = match port.side {
PortSide::Input => &self.inputs,
PortSide::Output => &self.outputs,
};
list.iter().find(|p| p.id == port).map(|p| p.center)
}
pub fn port_color(&self, port: PortId) -> Option<egui::Color32> {
let list = match port.side {
PortSide::Input => &self.inputs,
PortSide::Output => &self.outputs,
};
list.iter().find(|p| p.id == port).and_then(|p| p.color)
}
}
#[derive(Debug, Clone)]
pub struct StackLayout {
#[allow(dead_code)]
pub id: StackId,
pub rect: WorldRect,
pub title: Cow<'static, str>,
pub accent: Option<egui::Color32>,
pub members: Vec<NodeId>,
pub add_button: WorldRect,
pub collapse_all_button: WorldRect,
pub all_collapsed: bool,
}
impl StackLayout {
pub fn top_pin(&self) -> WorldPos {
WorldPos::new(self.rect.center().x, self.rect.min.y)
}
pub fn bottom_pin(&self) -> WorldPos {
WorldPos::new(self.rect.center().x, self.rect.max().y)
}
}
#[derive(Debug, Clone, Default)]
pub struct GraphLayout {
pub nodes: Vec<NodeLayout>,
pub stacks: Vec<StackLayout>,
}
fn node_height(body: f64) -> f64 {
HEADER_H + BODY_PAD_TOP + body + BODY_PAD_BOTTOM
}
fn column_rows(ports: &[PortDesc], top: f64) -> (Vec<(f64, f64)>, f64) {
let mut rows = Vec::with_capacity(ports.len());
let mut y = top;
for p in ports {
let row_h = PORT_ROW_H + p.expand_height.unwrap_or(0.0);
rows.push((y + PORT_ROW_H * 0.5, row_h));
y += row_h;
}
(rows, y - top)
}
fn node_layout(desc: &NodeDesc, min: WorldPos, stack: Option<StackId>) -> NodeLayout {
let body_top = min.y + HEADER_H + BODY_PAD_TOP;
let (in_rows, in_total) = column_rows(&desc.inputs, body_top);
let (out_rows, out_total) = column_rows(&desc.outputs, body_top);
let body = in_total.max(out_total);
let rect = WorldRect::new(min, NODE_WIDTH, node_height(body));
let inputs = desc
.inputs
.iter()
.enumerate()
.map(|(i, p)| PortLayout {
id: PortId::input(i as u16),
center: WorldPos::new(min.x, in_rows[i].0),
label: p.label.clone(),
color: p.color,
arity: p.arity,
value: p.value.clone(),
connectable: p.connectable,
row_height: in_rows[i].1,
collapsible: p.collapsible,
})
.collect();
let outputs = desc
.outputs
.iter()
.enumerate()
.map(|(i, p)| PortLayout {
id: PortId::output(i as u16),
center: WorldPos::new(min.x + NODE_WIDTH, out_rows[i].0),
label: p.label.clone(),
color: p.color,
arity: p.arity,
value: p.value.clone(),
connectable: p.connectable,
row_height: out_rows[i].1,
collapsible: p.collapsible,
})
.collect();
NodeLayout {
id: NodeId::new(1).unwrap(), rect,
title: desc.title.clone(),
accent: desc.accent,
inputs,
outputs,
stack,
warning: desc.warning.clone(),
close_button: desc.closable.then(|| {
WorldRect::new(
WorldPos::new(
min.x + NODE_WIDTH - CLOSE_BTN_MARGIN - CLOSE_BTN_SIZE,
min.y + (HEADER_H - CLOSE_BTN_SIZE) * 0.5,
),
CLOSE_BTN_SIZE,
CLOSE_BTN_SIZE,
)
}),
collapsed: false,
collapse_toggle: None,
}
}
fn collapse_member(layout: &mut NodeLayout) {
let min = layout.rect.min;
let width = layout.rect.width;
layout.rect = WorldRect::new(min, width, HEADER_H);
layout.collapsed = true;
let mid_y = min.y + HEADER_H * 0.5;
let in_pin = WorldPos::new(min.x, mid_y);
for p in &mut layout.inputs {
p.center = in_pin;
}
let out_pin = WorldPos::new(min.x + width, mid_y);
for p in &mut layout.outputs {
p.center = out_pin;
}
}
pub fn compute(viewer: &dyn GraphViewer, view: &GraphView) -> GraphLayout {
let stacks_desc = viewer.stacks();
let mut membership: HashMap<NodeId, StackId> = HashMap::new();
for s in &stacks_desc {
for &m in &s.members {
membership.insert(m, s.id);
}
}
let mut nodes = Vec::new();
let mut stacks = Vec::new();
for s in &stacks_desc {
let origin = view.stack_position(s.id);
let member_x = origin.x;
let mut cursor_y = origin.y + STACK_HEADER_H + STACK_PAD;
let mut collapsible = 0usize;
let mut collapsed_count = 0usize;
for (i, &member) in s.members.iter().enumerate() {
if i > 0 {
cursor_y += MEMBER_GAP;
}
let desc = viewer.node(member);
let mut layout = node_layout(&desc, WorldPos::new(member_x, cursor_y), Some(s.id));
layout.id = member;
let has_body = !desc.inputs.is_empty() || !desc.outputs.is_empty();
if has_body {
collapsible += 1;
layout.collapse_toggle = Some(WorldRect::new(
WorldPos::new(
member_x + TOGGLE_MARGIN,
cursor_y + (HEADER_H - TOGGLE_SIZE) * 0.5,
),
TOGGLE_SIZE,
TOGGLE_SIZE,
));
if view.is_collapsed(member) {
collapsed_count += 1;
collapse_member(&mut layout);
}
}
cursor_y += layout.rect.height;
nodes.push(layout);
}
let content_h = (cursor_y - origin.y).max(STACK_HEADER_H);
let button_top = origin.y + content_h + STACK_PAD;
let add_button = WorldRect::new(
WorldPos::new(member_x + STACK_PAD, button_top),
NODE_WIDTH - STACK_PAD * 2.0,
STACK_FOOTER_H,
);
let total_h = (button_top + STACK_FOOTER_H + STACK_PAD) - origin.y;
let rect = WorldRect::new(origin, NODE_WIDTH, total_h);
let collapse_all_button = WorldRect::new(
WorldPos::new(
origin.x + NODE_WIDTH - STACK_BTN_MARGIN - STACK_BTN_SIZE,
origin.y + (STACK_HEADER_H - STACK_BTN_SIZE) * 0.5,
),
STACK_BTN_SIZE,
STACK_BTN_SIZE,
);
stacks.push(StackLayout {
id: s.id,
rect,
title: s.title.clone(),
accent: s.accent,
members: s.members.clone(),
add_button,
collapse_all_button,
all_collapsed: collapsible > 0 && collapsed_count == collapsible,
});
}
for id in viewer.node_ids() {
if membership.contains_key(&id) {
continue;
}
let desc = viewer.node(id);
let mut layout = node_layout(&desc, view.position(id), None);
layout.id = id;
nodes.push(layout);
}
nodes.sort_by_key(|n| view.z_key(node_item(n)));
stacks.sort_by_key(|s| view.z_key(CanvasItem::Stack(s.id)));
GraphLayout { nodes, stacks }
}
pub fn node_item(node: &NodeLayout) -> CanvasItem {
match node.stack {
Some(sid) => CanvasItem::Stack(sid),
None => CanvasItem::Node(node.id),
}
}