tui_nodes/
graph.rs

1use ratatui::layout::Position;
2
3use super::*;
4
5const MARGIN: u16 = 5;
6
7#[derive(Debug)]
8pub struct NodeGraph<'a> {
9	nodes: Vec<NodeLayout<'a>>,
10	connections: Vec<Connection>,
11	placements: Map<usize, Rect>,
12	pub conn_layout: ConnectionsLayout,
13	width: usize,
14}
15
16impl<'a> NodeGraph<'a> {
17	pub fn new(
18		nodes: Vec<NodeLayout<'a>>,
19		connections: Vec<Connection>,
20		width: usize,
21		height: usize,
22	) -> Self {
23		Self {
24			nodes,
25			connections,
26			conn_layout: ConnectionsLayout::new(width, height),
27			placements: Default::default(),
28			width,
29		}
30	}
31
32	pub fn calculate(&mut self) {
33		self.placements.clear();
34
35		// find root nodes
36		let mut roots: Set<_> = (0..self.nodes.len()).collect();
37		for ea_connection in self.connections.iter() {
38			roots.remove(&ea_connection.from_node);
39		}
40
41		// place them and their children (recursively)
42		let mut main_chain = Vec::new();
43		for ea_root in roots {
44			self.place_node(ea_root, 0, 0, &mut main_chain);
45			assert!(main_chain.is_empty());
46		}
47
48		// calculate connections (eventually, this should be done during node
49		// placement, but thats really complicated and i dont wanna deal with that
50		// right now. essentially, adding non-trivial connections nudges nodes,
51		// and nudging nodes nudges existing connections.)
52		let mut conn_map = Map::<(usize, usize), usize>::new();
53		let mut next_idx = 1;
54		for ea_conn in self.connections.iter() {
55			let a_pos = self.placements[&ea_conn.from_node];
56			let b_pos = self.placements[&ea_conn.to_node];
57			// NOTE: don't forget that left and right are swapped
58			let a_point = (
59				self.width.saturating_sub(a_pos.left().into()),
60				a_pos.top() as usize + ea_conn.from_port + 1,
61			);
62			let b_point = (
63				self.width.saturating_sub(b_pos.right() as usize + 1),
64				b_pos.top() as usize + ea_conn.to_port + 1,
65			);
66			self.conn_layout.insert_port(
67				false,
68				ea_conn.from_node,
69				ea_conn.from_port,
70				a_point,
71			);
72			self.conn_layout.insert_port(true, ea_conn.to_node, ea_conn.to_port, b_point);
73			let key = (ea_conn.from_node, ea_conn.from_port);
74			if !conn_map.contains_key(&key) {
75				conn_map.insert(key, next_idx);
76				next_idx += 1;
77			}
78			self.conn_layout.push_connection((*ea_conn, conn_map[&key]));
79			self.conn_layout.block_port(a_point);
80			self.conn_layout.block_port(b_point);
81		}
82		for mut ea_placement in self.placements.values().cloned() {
83			ea_placement.x =
84				(self.width as u16).saturating_sub(ea_placement.x + ea_placement.width);
85			self.conn_layout.block_zone(ea_placement);
86		}
87		self.conn_layout.calculate();
88	}
89
90	/// ATTENTION: x_offs works in the opposite direction (higher values are
91	/// further left) and y_offs is the same as tui (higher values are further
92	/// down)
93	fn place_node(
94		&mut self,
95		idx_node: usize,
96		x: u16,
97		y: u16,
98		main_chain: &mut Vec<usize>,
99	) {
100		// place the node
101		let size_me = self.nodes[idx_node].size;
102		let mut rect_me = Rect { x, y, width: size_me.0, height: size_me.1 };
103
104		// nudge placement. if a node intersects with another node, its entire
105		// main chain (largest subset of nodes including this one where every
106		// node is the first child of its parent) should be moved down to not
107		// intersect.
108		//
109		// Repeat the for loop until in runs all the way through without any
110		// intersections. Surely there's a more efficient way to do this.
111		'outer: loop {
112			for (_, ea_them) in self.placements.iter() {
113				if rect_me.intersects(*ea_them) {
114					rect_me.y = rect_me.y.max(ea_them.bottom());
115					continue 'outer;
116				}
117			}
118			break;
119		}
120		for ea_node in main_chain.iter() {
121			let y = self.placements[ea_node].y.max(rect_me.y);
122			self.placements.get_mut(ea_node).unwrap().y = y;
123		}
124		self.placements.insert(idx_node, rect_me);
125
126		// find children and order them
127		let mut y = y;
128		main_chain.push(idx_node);
129		for ea_child in get_upstream(&self.connections, idx_node) {
130			if self.placements.contains_key(&ea_child.from_node) {
131				// nudge it (if necessary)
132				self.nudge(ea_child.from_node, rect_me.x + rect_me.width + MARGIN);
133			} else {
134				// place it
135				self.place_node(
136					ea_child.from_node,
137					x + rect_me.width + MARGIN,
138					y,
139					main_chain,
140				);
141				main_chain.clear();
142				y += self.placements[&ea_child.from_node].height;
143			}
144		}
145		main_chain.pop();
146	}
147
148	fn nudge(&mut self, idx_node: usize, x: u16) {
149		let rect_me = self.placements[&idx_node];
150		if rect_me.x < x {
151			self.placements.get_mut(&idx_node).unwrap().x = x;
152			for ea_child in get_upstream(&self.connections, idx_node) {
153				assert!(self.placements.contains_key(&ea_child.from_node));
154				self.nudge(ea_child.from_node, x + rect_me.width + MARGIN);
155			}
156		}
157	}
158
159	pub fn split(&self, area: Rect) -> Vec<Rect> {
160		(0..self.nodes.len())
161			.map(|idx_node| {
162				self.placements
163					.get(&idx_node)
164					.map(|pos| {
165						if pos.right() > area.width || pos.bottom() > area.height {
166							return Rect { x: 0, y: 0, width: 0, height: 0 };
167						}
168						let mut pos = *pos;
169						pos.x = area.width - pos.right() + area.x;
170						pos.y += area.y;
171						pos.inner(Margin { horizontal: 1, vertical: 1 })
172					})
173					.unwrap_or_default()
174			})
175			.collect()
176	}
177}
178
179fn get_upstream(conns: &[Connection], idx_node: usize) -> Vec<Connection> {
180	// find children and order them
181	let mut upstream: Vec<_> =
182		conns.iter().filter(|ea| ea.to_node == idx_node).copied().collect();
183	upstream.sort_by(|a, b| a.to_port.cmp(&b.to_port));
184	upstream
185}
186
187fn get_downstream(conns: &[Connection], idx_node: usize) -> Vec<Connection> {
188	// find parents and order them
189	let mut downstream: Vec<_> =
190		conns.iter().filter(|ea| ea.from_node == idx_node).copied().collect();
191	downstream.sort_by(|a, b| a.from_port.cmp(&b.from_port));
192	downstream
193}
194
195impl<'a> ratatui::widgets::StatefulWidget for NodeGraph<'a> {
196	// eventually, this will contain stuff like view position
197	//	type State = NodeGraphState;
198	type State = ();
199
200	fn render(self, area: Rect, buf: &mut Buffer, _state: &mut Self::State) {
201		// draw connections
202		self.conn_layout.render(area, buf);
203
204		// draw nodes
205		'node: for (idx_node, ea_node) in self.nodes.into_iter().enumerate() {
206			if let Some(mut pos) = self.placements.get(&idx_node).copied() {
207				if pos.right() > area.width || pos.bottom() > area.height {
208					continue 'node;
209				}
210				// draw box
211				pos.x = area.left() + area.width - pos.right();
212				pos.y += area.top();
213				let block = ea_node.block();
214				block.render(pos, buf);
215				// draw connection ports
216				for ea_conn in get_upstream(&self.connections, idx_node) {
217					// draw connection alias
218					if let Some(alias_char) = self.conn_layout.alias_connections.get(&(
219						true,
220						idx_node,
221						ea_conn.to_port,
222					)) {
223						let y = pos.top() + ea_conn.to_port as u16 + 1;
224						if pos.left() > 0 && y < area.width {
225							buf.cell_mut(Position::new(pos.left() - 1, y))
226								.unwrap()
227								.set_symbol(alias_char)
228								.set_style(
229									Style::default()
230										.add_modifier(Modifier::BOLD)
231										.bg(Color::Red),
232								);
233						}
234					}
235
236					// draw port
237					buf.cell_mut(Position::new(
238						pos.left(),
239						pos.top() + ea_conn.to_port as u16 + 1,
240					))
241					.unwrap()
242					.set_symbol(conn_symbol(
243						true,
244						ea_node.border_type(),
245						ea_conn.line_type(),
246					));
247				}
248				for ea_conn in get_downstream(&self.connections, idx_node) {
249					// draw connection alias
250					if let Some(alias_char) = self.conn_layout.alias_connections.get(&(
251						false,
252						idx_node,
253						ea_conn.from_port,
254					)) {
255						buf.cell_mut(Position::new(
256							pos.right(),
257							pos.top() + ea_conn.from_port as u16 + 1,
258						))
259						.unwrap()
260						.set_symbol(alias_char)
261						.set_style(
262							Style::default().add_modifier(Modifier::BOLD).bg(Color::Red),
263						);
264					}
265
266					// draw port
267					buf.cell_mut(Position::new(
268						pos.right() - 1,
269						pos.top() + ea_conn.from_port as u16 + 1,
270					))
271					.unwrap()
272					.set_symbol(conn_symbol(
273						false,
274						ea_node.border_type(),
275						ea_conn.line_type(),
276					));
277				}
278			} else {
279				buf.set_string(
280					0,
281					idx_node as u16,
282					format!("{idx_node}"),
283					Style::default(),
284				);
285			}
286		}
287	}
288}