use ratatui::layout::{Position, Size};
use super::*;
const MARGIN: u16 = 12;
const LOOP_OFFSET: i32 = 3;
#[derive(Debug)]
pub struct NodeGraph<'a> {
nodes: Vec<NodeLayout<'a>>,
connections: Vec<Connection>,
placements: Map<usize, Rect>,
pub conn_layout: ConnectionsLayout,
width: usize,
loop_connections: Vec<Connection>,
}
impl<'a> NodeGraph<'a> {
pub fn new(
nodes: Vec<NodeLayout<'a>>,
connections: Vec<Connection>,
width: usize,
height: usize,
) -> Self {
Self {
nodes,
connections,
conn_layout: ConnectionsLayout::new(width, height),
placements: Default::default(),
width,
loop_connections: Vec::new(),
}
}
pub fn calculate(&mut self) {
self.placements.clear();
self.loop_connections.clear();
let mut roots: Set<_> = (0..self.nodes.len()).collect();
for ea_connection in self.connections.iter() {
if ea_connection.from_node != ea_connection.to_node {
roots.remove(&ea_connection.from_node);
}
}
if roots.is_empty() && !self.nodes.is_empty() {
roots.insert(0);
}
let mut main_chain = Vec::new();
for ea_root in roots {
self.place_node(ea_root, 0, 0, &mut main_chain);
assert!(main_chain.is_empty());
}
if self.placements.len() < self.nodes.len() {
let mut orphan_y = self
.placements
.values()
.map(|r| r.bottom())
.max()
.unwrap_or(0);
let mut orphan_chain = Vec::new();
for idx in 0..self.nodes.len() {
if self.placements.contains_key(&idx) {
continue;
}
self.place_node(idx, 0, orphan_y, &mut orphan_chain);
orphan_chain.clear();
orphan_y = self
.placements
.get(&idx)
.map_or(orphan_y.saturating_add(1), |r| r.bottom());
}
}
let mut conn_map = Map::<(usize, usize), usize>::new();
let mut next_idx = 1;
let mut skipped_oob = 0usize;
let mut skipped_missing_placement = 0usize;
for ea_conn in self.connections.iter() {
if ea_conn.from_node == ea_conn.to_node {
self.loop_connections.push(*ea_conn);
continue;
}
if ea_conn.from_node >= self.nodes.len() || ea_conn.to_node >= self.nodes.len() {
skipped_oob = skipped_oob.saturating_add(1);
continue;
}
let Some(a_pos) = self.placements.get(&ea_conn.from_node).copied() else {
skipped_missing_placement = skipped_missing_placement.saturating_add(1);
continue;
};
let Some(b_pos) = self.placements.get(&ea_conn.to_node).copied() else {
skipped_missing_placement = skipped_missing_placement.saturating_add(1);
continue;
};
let a_point = (
self.width.saturating_sub(a_pos.left().into()),
a_pos.top() as usize + ea_conn.from_port + 1,
);
let b_point = (
self.width.saturating_sub(b_pos.right() as usize + 1),
b_pos.top() as usize + ea_conn.to_port + 1,
);
self.conn_layout
.insert_port(false, ea_conn.from_node, ea_conn.from_port, a_point);
self.conn_layout
.insert_port(true, ea_conn.to_node, ea_conn.to_port, b_point);
let key = (ea_conn.from_node, ea_conn.from_port);
if let std::collections::hash_map::Entry::Vacant(entry) = conn_map.entry(key) {
entry.insert(next_idx);
next_idx += 1;
}
self.conn_layout.push_connection((*ea_conn, conn_map[&key]));
self.conn_layout.block_port(a_point, false);
self.conn_layout.block_port(b_point, true);
}
if skipped_oob > 0 || skipped_missing_placement > 0 {
eprintln!(
"CuConsoleMon warning: NodeGraph skipped {} out-of-bounds and {} unplaced connections",
skipped_oob, skipped_missing_placement
);
}
for mut ea_placement in self.placements.values().cloned() {
ea_placement.x =
(self.width as u16).saturating_sub(ea_placement.x + ea_placement.width);
self.conn_layout.block_zone(ea_placement);
}
self.conn_layout.calculate();
}
fn place_node(&mut self, idx_node: usize, x: u16, y: u16, main_chain: &mut Vec<usize>) {
let size_me = self.nodes[idx_node].size;
let mut rect_me = Rect {
x,
y,
width: size_me.0,
height: size_me.1,
};
'outer: loop {
for (_, ea_them) in self.placements.iter() {
if rect_me.intersects(*ea_them) {
rect_me.y = rect_me.y.max(ea_them.bottom());
continue 'outer;
}
}
break;
}
for ea_node in main_chain.iter() {
let y = self.placements[ea_node].y.max(rect_me.y);
self.placements.get_mut(ea_node).unwrap().y = y;
}
self.placements.insert(idx_node, rect_me);
let mut y = y;
main_chain.push(idx_node);
for ea_child in get_upstream(&self.connections, idx_node) {
if ea_child.from_node == idx_node {
continue;
}
if self.placements.contains_key(&ea_child.from_node) {
self.nudge(ea_child.from_node, rect_me.x + rect_me.width + MARGIN);
} else {
self.place_node(
ea_child.from_node,
x + rect_me.width + MARGIN,
y,
main_chain,
);
main_chain.clear();
y += self.placements[&ea_child.from_node].height;
}
}
main_chain.pop();
}
fn nudge(&mut self, idx_node: usize, x: u16) {
let mut active_path = Set::new();
let mut skipped_cycles = 0usize;
let mut skipped_missing = 0usize;
self.nudge_impl(
idx_node,
x,
&mut active_path,
&mut skipped_cycles,
&mut skipped_missing,
);
if skipped_cycles > 0 || skipped_missing > 0 {
eprintln!(
"CuConsoleMon warning: NodeGraph nudge skipped {} cyclic and {} missing-node edges",
skipped_cycles, skipped_missing
);
}
}
fn nudge_impl(
&mut self,
idx_node: usize,
x: u16,
active_path: &mut Set<usize>,
skipped_cycles: &mut usize,
skipped_missing: &mut usize,
) {
let Some(rect_me) = self.placements.get(&idx_node).copied() else {
*skipped_missing = skipped_missing.saturating_add(1);
return;
};
if rect_me.x >= x {
return;
}
if !active_path.insert(idx_node) {
*skipped_cycles = skipped_cycles.saturating_add(1);
return;
}
if let Some(rect) = self.placements.get_mut(&idx_node) {
rect.x = x;
} else {
*skipped_missing = skipped_missing.saturating_add(1);
active_path.remove(&idx_node);
return;
}
let child_x = x.saturating_add(rect_me.width).saturating_add(MARGIN);
for ea_child in get_upstream(&self.connections, idx_node) {
if ea_child.from_node == idx_node {
continue;
}
if active_path.contains(&ea_child.from_node) {
*skipped_cycles = skipped_cycles.saturating_add(1);
continue;
}
if !self.placements.contains_key(&ea_child.from_node) {
*skipped_missing = skipped_missing.saturating_add(1);
continue;
}
self.nudge_impl(
ea_child.from_node,
child_x,
active_path,
skipped_cycles,
skipped_missing,
);
}
active_path.remove(&idx_node);
}
pub fn content_bounds(&self) -> Size {
let mut width = 0;
let mut height = 0;
for placement in self.placements.values() {
width = width.max(placement.right());
height = height.max(placement.bottom());
}
Size::new(width, height)
}
pub fn split(&self, area: Rect) -> Vec<Rect> {
(0..self.nodes.len())
.map(|idx_node| {
self.placements
.get(&idx_node)
.map(|pos| {
if pos.right() > area.width || pos.bottom() > area.height {
return Rect {
x: 0,
y: 0,
width: 0,
height: 0,
};
}
let mut pos = *pos;
pos.x = area.width - pos.right() + area.x;
pos.y += area.y;
pos.inner(Margin {
horizontal: 1,
vertical: 1,
})
})
.unwrap_or_default()
})
.collect()
}
}
fn get_upstream(conns: &[Connection], idx_node: usize) -> Vec<Connection> {
let mut upstream: Vec<_> = conns
.iter()
.filter(|ea| ea.to_node == idx_node)
.copied()
.collect();
upstream.sort_by(|a, b| a.to_port.cmp(&b.to_port));
upstream
}
fn get_downstream(conns: &[Connection], idx_node: usize) -> Vec<Connection> {
let mut downstream: Vec<_> = conns
.iter()
.filter(|ea| ea.from_node == idx_node)
.copied()
.collect();
downstream.sort_by(|a, b| a.from_port.cmp(&b.from_port));
downstream
}
impl<'a> ratatui::widgets::StatefulWidget for NodeGraph<'a> {
type State = ();
fn render(self, area: Rect, buf: &mut Buffer, _state: &mut Self::State) {
self.render_into(area, buf);
}
}
impl<'a> NodeGraph<'a> {
fn render_into(&self, area: Rect, buf: &mut Buffer) {
self.conn_layout.render(area, buf);
self.draw_loop_connections(area, buf);
'node: for (idx_node, ea_node) in self.nodes.iter().enumerate() {
if let Some(mut pos) = self.placements.get(&idx_node).copied() {
if pos.right() > area.width || pos.bottom() > area.height {
continue 'node;
}
pos.x = area.left() + area.width - pos.right();
pos.y += area.top();
let block = ea_node.block();
block.render(pos, buf);
for ea_conn in get_upstream(&self.connections, idx_node) {
if let Some(alias_char) =
self.conn_layout
.alias_connections
.get(&(true, idx_node, ea_conn.to_port))
{
let y = pos.top() + ea_conn.to_port as u16 + 1;
if pos.left() > 0 && y < area.width {
buf.cell_mut(Position::new(pos.left() - 1, y))
.unwrap()
.set_symbol(alias_char)
.set_style(
Style::default().add_modifier(Modifier::BOLD).bg(Color::Red),
);
}
}
buf.cell_mut(Position::new(
pos.left(),
pos.top() + ea_conn.to_port as u16 + 1,
))
.unwrap()
.set_symbol(conn_symbol(
true,
ea_node.border_type(),
ea_conn.line_type(),
));
}
for ea_conn in get_downstream(&self.connections, idx_node) {
if let Some(alias_char) = self.conn_layout.alias_connections.get(&(
false,
idx_node,
ea_conn.from_port,
)) {
buf.cell_mut(Position::new(
pos.right(),
pos.top() + ea_conn.from_port as u16 + 1,
))
.unwrap()
.set_symbol(alias_char)
.set_style(Style::default().add_modifier(Modifier::BOLD).bg(Color::Red));
}
buf.cell_mut(Position::new(
pos.right() - 1,
pos.top() + ea_conn.from_port as u16 + 1,
))
.unwrap()
.set_symbol(conn_symbol(
false,
ea_node.border_type(),
ea_conn.line_type(),
));
}
} else {
buf.set_string(0, idx_node as u16, format!("{idx_node}"), Style::default());
}
}
}
fn draw_loop_connections(&self, area: Rect, buf: &mut Buffer) {
for conn in &self.loop_connections {
if let Some(rect) = self.placements.get(&conn.from_node).copied() {
if rect.right() > area.width || rect.bottom() > area.height {
continue;
}
let mut pos = rect;
pos.x = area.left() + area.width - pos.right();
pos.y += area.top();
self.draw_loop_path(pos, *conn, area, buf);
}
}
}
fn draw_loop_path(&self, rect: Rect, conn: Connection, area: Rect, buf: &mut Buffer) {
let start_y = rect.top().saturating_add(conn.from_port as u16 + 1);
let end_y = rect.top().saturating_add(conn.to_port as u16 + 1);
let start_x = rect.right().saturating_sub(1);
let end_x = rect.left();
let start_x_i = start_x as i32;
let end_x_i = end_x as i32;
let start_y_i = start_y as i32;
let end_y_i = end_y as i32;
let area_left = area.left() as i32;
let area_right = (area.left() + area.width).saturating_sub(1) as i32;
let area_bottom = (area.top() + area.height).saturating_sub(1) as i32;
let outer_x = (start_x_i + LOOP_OFFSET).min(area_right);
if outer_x <= start_x_i {
return;
}
let mut bottom_y = (rect.bottom() as i32 + LOOP_OFFSET).min(area_bottom);
let max_y = start_y_i.max(end_y_i);
if bottom_y <= max_y {
bottom_y = (max_y + 1).min(area_bottom);
}
if bottom_y <= max_y {
return;
}
let left_x = (end_x_i - 1).max(area_left);
if left_x >= end_x_i || left_x >= outer_x {
return;
}
let set = conn.line_type().to_line_set();
let style = conn.line_style();
draw_horizontal(
buf,
start_y_i,
start_x_i + 1,
outer_x,
set.horizontal,
style,
area,
);
draw_corner(buf, outer_x, start_y_i, set.top_right, style, area);
draw_vertical(
buf,
outer_x,
start_y_i + 1,
bottom_y - 1,
set.vertical,
style,
area,
);
draw_corner(buf, outer_x, bottom_y, set.bottom_right, style, area);
draw_horizontal(
buf,
bottom_y,
left_x + 1,
outer_x - 1,
set.horizontal,
style,
area,
);
draw_corner(buf, left_x, bottom_y, set.bottom_left, style, area);
draw_vertical(
buf,
left_x,
end_y_i + 1,
bottom_y - 1,
set.vertical,
style,
area,
);
draw_corner(buf, left_x, end_y_i, set.top_left, style, area);
}
}
impl<'a> Widget for &'a NodeGraph<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_into(area, buf);
}
}
fn draw_horizontal(
buf: &mut Buffer,
y: i32,
mut start: i32,
mut end: i32,
symbol: &str,
style: Style,
area: Rect,
) {
if start > end {
std::mem::swap(&mut start, &mut end);
}
for x in start..=end {
set_cell(buf, x, y, symbol, style, area);
}
}
fn draw_vertical(
buf: &mut Buffer,
x: i32,
mut start: i32,
mut end: i32,
symbol: &str,
style: Style,
area: Rect,
) {
if start > end {
std::mem::swap(&mut start, &mut end);
}
for y in start..=end {
set_cell(buf, x, y, symbol, style, area);
}
}
fn draw_corner(buf: &mut Buffer, x: i32, y: i32, symbol: &str, style: Style, area: Rect) {
set_cell(buf, x, y, symbol, style, area);
}
fn set_cell(buf: &mut Buffer, x: i32, y: i32, symbol: &str, style: Style, area: Rect) {
let min_x = area.left() as i32;
let max_x = (area.left() + area.width).saturating_sub(1) as i32;
let min_y = area.top() as i32;
let max_y = (area.top() + area.height).saturating_sub(1) as i32;
if x < min_x || x > max_x || y < min_y || y > max_y {
return;
}
if let Some(cell) = buf.cell_mut(Position::new(x as u16, y as u16)) {
cell.set_symbol(symbol).set_style(style);
}
}