egui_treeize/
layout.rs

1//! Layout algorithms for tree-like graphs.
2
3use std::collections::HashMap;
4
5use egui::{Context, Id, Pos2, Vec2, pos2};
6
7use crate::ui::TreeizeViewer;
8use crate::ui::state::NodeState;
9use crate::{NodeId, Treeize};
10
11/// Configuration for tree layout algorithm.
12#[derive(Clone, Copy, Debug, PartialEq)]
13pub struct LayoutConfig {
14  /// Horizontal spacing between nodes at the same level.
15  pub horizontal_spacing: f32,
16
17  /// Vertical spacing between levels.
18  pub vertical_spacing: f32,
19
20  /// Starting position for the layout.
21  pub start_pos: Pos2,
22}
23
24impl Default for LayoutConfig {
25  fn default() -> Self {
26    LayoutConfig { horizontal_spacing: 200.0, vertical_spacing: 150.0, start_pos: Pos2::ZERO }
27  }
28}
29
30/// Node dimension provider for the layout algorithm.
31/// This trait allows the layout algorithm to query node sizes and children.
32trait NodeDimensionProvider {
33  /// Returns `(width, height)` for a node.
34  fn get_size(&self, node_id: NodeId) -> (f32, f32);
35
36  /// Returns the list of child node IDs for a given node.
37  fn get_children(&self, node_id: NodeId) -> Vec<NodeId>;
38}
39
40/// Adapter that implements `NodeDimensionProvider` for `Treeize`.
41struct TreeizeAdapter<'a, T> {
42  node_sizes: Option<&'a HashMap<NodeId, Vec2>>,
43  children_map: HashMap<NodeId, Vec<NodeId>>,
44  config: LayoutConfig,
45  _phantom: std::marker::PhantomData<T>,
46}
47
48impl<'a, T> TreeizeAdapter<'a, T> {
49  fn new(
50    treeize: &Treeize<T>,
51    node_sizes: Option<&'a HashMap<NodeId, Vec2>>,
52    has_output: &mut impl FnMut(NodeId) -> bool,
53    has_input: &mut impl FnMut(NodeId) -> bool,
54    config: LayoutConfig,
55  ) -> Self {
56    // Build children map from wires
57    let mut children_map: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
58
59    for (out_pin, in_pin) in treeize.wires() {
60      let from_node = out_pin.node;
61      let to_node = in_pin.node;
62
63      if has_output(from_node) && has_input(to_node) {
64        children_map.entry(from_node).or_default().push(to_node);
65      }
66    }
67
68    TreeizeAdapter { node_sizes, children_map, config, _phantom: std::marker::PhantomData }
69  }
70}
71
72impl<T> NodeDimensionProvider for TreeizeAdapter<'_, T> {
73  fn get_size(&self, node_id: NodeId) -> (f32, f32) {
74    if let Some(sizes) = self.node_sizes
75      && let Some(size) = sizes.get(&node_id)
76    {
77      return (size.x, size.y);
78    }
79    // Default size if not provided
80    (self.config.horizontal_spacing, self.config.vertical_spacing)
81  }
82
83  fn get_children(&self, node_id: NodeId) -> Vec<NodeId> {
84    self.children_map.get(&node_id).cloned().unwrap_or_default()
85  }
86}
87
88/// Internal layout node structure.
89struct LayoutNode {
90  /// X offset relative to parent node's center.
91  offset_x: f32,
92  /// Contour: list of `(left_edge, right_edge)` for each depth level.
93  /// Depth 0 is the node itself, depth 1 is its children, etc.
94  contour: Vec<(f32, f32)>,
95  width: f32,
96  height: f32,
97}
98
99/// Layout state that stores intermediate calculations.
100struct LayoutState {
101  nodes: HashMap<NodeId, LayoutNode>,
102}
103
104impl LayoutState {
105  fn new() -> Self {
106    Self { nodes: HashMap::new() }
107  }
108}
109
110/// First walk: bottom-up traversal to calculate contours and offsets.
111fn first_walk<P: NodeDimensionProvider>(
112  node_id: NodeId,
113  provider: &P,
114  config: &LayoutConfig,
115  state: &mut LayoutState,
116) {
117  let (w, h) = provider.get_size(node_id);
118  let children = provider.get_children(node_id);
119
120  if children.is_empty() {
121    // Leaf node: contour is just the node itself
122    state.nodes.insert(
123      node_id,
124      LayoutNode { offset_x: 0.0, contour: vec![(-w / 2.0, w / 2.0)], width: w, height: h },
125    );
126    return;
127  }
128
129  // 1. Recursively process all children
130  for child_id in &children {
131    first_walk(*child_id, provider, config, state);
132  }
133
134  // 2. Merge children contours (Contour Merge - the core of Mr.Tree algorithm)
135  let mut merged_contour: Vec<(f32, f32)> = Vec::new();
136  let mut child_offsets: Vec<f32> = Vec::new();
137  let mut current_offset = 0.0;
138
139  for (i, child_id) in children.iter().enumerate() {
140    let child_node = state.nodes.get(child_id).unwrap();
141
142    if i == 0 {
143      // First subtree: use as-is
144      merged_contour.clone_from(&child_node.contour);
145      child_offsets.push(0.0);
146    } else {
147      // Subsequent subtrees: calculate minimum shift to avoid overlap
148      let mut shift = 0.0f32;
149
150      // Check all overlapping depth levels
151      for ((_l1, r1), (l2, _r2)) in merged_contour.iter().zip(child_node.contour.iter()) {
152        // Calculate required distance: r1 + gap - l2
153        let dist = (r1 + config.horizontal_spacing) - l2;
154        if dist > shift {
155          shift = dist;
156        }
157      }
158
159      child_offsets.push(shift);
160      current_offset = shift;
161
162      // Merge the new subtree's contour into the merged contour
163      for (depth, (l, r)) in child_node.contour.iter().enumerate() {
164        let shifted_l = l + shift;
165        let shifted_r = r + shift;
166
167        if depth < merged_contour.len() {
168          // Extend existing level
169          let (exist_l, exist_r) = merged_contour[depth];
170          merged_contour[depth] = (exist_l.min(shifted_l), exist_r.max(shifted_r));
171        } else {
172          // Add new level
173          merged_contour.push((shifted_l, shifted_r));
174        }
175      }
176    }
177  }
178
179  // 3. Calculate parent node position
180  // Parent should be centered above its children
181  // The center of children group is at current_offset / 2.0
182  let children_center = current_offset / 2.0;
183
184  // 4. Generate final contour for this node
185  // Layer 0: the node itself
186  // Layer 1+: children group, shifted so that children center aligns with parent center (0.0)
187  let shift_children_left = -children_center;
188  let mut final_contour = Vec::new();
189  final_contour.push((-w / 2.0, w / 2.0)); // Layer 0: parent node
190
191  // Layer 1+: children
192  for (l, r) in merged_contour {
193    final_contour.push((l + shift_children_left, r + shift_children_left));
194  }
195
196  // 5. Update child offsets in LayoutState
197  for (i, child_id) in children.iter().enumerate() {
198    if let Some(node) = state.nodes.get_mut(child_id) {
199      // Final X offset = offset within children group + group's offset relative to parent
200      node.offset_x = child_offsets[i] + shift_children_left;
201    }
202  }
203
204  // 6. Store this node's layout info
205  state.nodes.insert(
206    node_id,
207    LayoutNode {
208      offset_x: 0.0, // Root nodes have 0 offset (will be set in second_walk)
209      contour: final_contour,
210      width: w,
211      height: h,
212    },
213  );
214}
215
216/// Second walk: top-down traversal to calculate absolute positions.
217fn second_walk<P: NodeDimensionProvider>(
218  node_id: NodeId,
219  absolute_x: f32,
220  absolute_y: f32,
221  state: &LayoutState,
222  positions: &mut HashMap<NodeId, Pos2>,
223  provider: &P,
224  config: &LayoutConfig,
225) {
226  let layout_node = state.nodes.get(&node_id).unwrap();
227
228  // Calculate absolute position (top-left corner)
229  // absolute_x is the parent's center X
230  // layout_node.offset_x is the offset from parent's center
231  let center_x = absolute_x + layout_node.offset_x;
232  let top_left_x = center_x - layout_node.width / 2.0;
233
234  positions.insert(node_id, pos2(top_left_x, absolute_y));
235
236  let children = provider.get_children(node_id);
237  let next_y = absolute_y + layout_node.height + config.vertical_spacing;
238
239  for child_id in children {
240    second_walk(child_id, center_x, next_y, state, positions, provider, config);
241  }
242}
243
244/// Performs hierarchical layout on a tree-like graph.
245///
246/// This function arranges nodes in a top-to-bottom tree structure using
247/// a compact contour-based layout algorithm (similar to ELK Mr.Tree).
248/// Nodes are organized into levels based on their distance from root nodes.
249///
250/// # Arguments
251///
252/// * `treeize` - The tree graph to layout
253/// * `config` - Layout configuration parameters
254/// * `has_output` - Function to check if a node has output pins
255/// * `has_input` - Function to check if a node has input pins
256/// * `node_sizes` - Optional map from node ID to node size (width, height).
257///   If provided, the layout will consider actual node sizes to avoid overlaps.
258///
259/// # Returns
260///
261/// A map from node ID to its calculated position (top-left corner).
262///
263/// # Panics
264///
265/// This function does not panic, but may produce unexpected layouts if the graph
266/// contains cycles or if node sizes are invalid.
267#[allow(clippy::implicit_hasher)]
268pub fn layout_tree<T>(
269  treeize: &Treeize<T>,
270  config: LayoutConfig,
271  mut has_output: impl FnMut(NodeId) -> bool,
272  mut has_input: impl FnMut(NodeId) -> bool,
273  node_sizes: Option<&HashMap<NodeId, Vec2>>,
274) -> HashMap<NodeId, Pos2> {
275  let mut positions = HashMap::new();
276
277  // Build adapter
278  let adapter = TreeizeAdapter::new(treeize, node_sizes, &mut has_output, &mut has_input, config);
279
280  // Find root nodes (nodes with no incoming edges)
281  let mut has_incoming: HashMap<NodeId, bool> = HashMap::new();
282  for (out_pin, in_pin) in treeize.wires() {
283    let to_node = in_pin.node;
284    if has_output(out_pin.node) && has_input(to_node) {
285      has_incoming.insert(to_node, true);
286    }
287  }
288
289  let root_nodes: Vec<NodeId> = treeize
290    .node_ids()
291    .map(|(node_id, _)| node_id)
292    .filter(|node_id| !has_incoming.get(node_id).copied().unwrap_or(false))
293    .collect();
294
295  if root_nodes.is_empty() {
296    // No root nodes found, treat all nodes as disconnected
297    for (node_id, _) in treeize.node_ids() {
298      positions.insert(node_id, config.start_pos);
299    }
300    return positions;
301  }
302
303  // Calculate layout for each root node's tree
304  let mut layout_state = LayoutState::new();
305  let mut root_x_offset = 0.0;
306
307  for root_id in &root_nodes {
308    // First walk: calculate contours and offsets
309    first_walk(*root_id, &adapter, &config, &mut layout_state);
310
311    // Calculate the width of this root's tree
312    let root_node = layout_state.nodes.get(root_id).unwrap();
313    let tree_width = root_node.contour.iter().map(|(l, r)| r - l).fold(0.0f32, f32::max);
314
315    // Second walk: calculate absolute positions
316    // Start at config.start_pos.x + root_x_offset (centered)
317    let root_center_x = config.start_pos.x + root_x_offset + tree_width / 2.0;
318    second_walk(
319      *root_id,
320      root_center_x,
321      config.start_pos.y,
322      &layout_state,
323      &mut positions,
324      &adapter,
325      &config,
326    );
327
328    // Update offset for next root
329    root_x_offset += tree_width + config.horizontal_spacing;
330  }
331
332  // Handle disconnected nodes (nodes not reachable from any root)
333  for (node_id, _) in treeize.node_ids() {
334    positions.entry(node_id).or_insert(config.start_pos);
335  }
336
337  positions
338}
339
340/// Applies the calculated layout positions to the treeize.
341///
342/// # Arguments
343///
344/// * `treeize` - The tree graph to update
345/// * `positions` - Map from node ID to position
346pub fn apply_layout<T, H>(treeize: &mut Treeize<T>, positions: &HashMap<NodeId, Pos2, H>)
347where
348  H: std::hash::BuildHasher,
349{
350  for (node_id, pos) in positions {
351    if let Some(node) = treeize.get_node_info_mut(*node_id) {
352      node.pos = *pos;
353    }
354  }
355}
356
357/// Convenience function that performs layout and applies it in one step.
358///
359/// # Arguments
360///
361/// * `treeize` - The tree graph to layout and update
362/// * `config` - Layout configuration parameters
363/// * `has_output` - Function to check if a node has output pins
364/// * `has_input` - Function to check if a node has input pins
365/// * `node_sizes` - Optional map from node ID to node size (width, height).
366///   If provided, the layout will consider actual node sizes to avoid overlaps.
367///
368/// # Example
369///
370/// ```rs
371/// use egui_treeize::{Treeize, layout::{LayoutConfig, layout_and_apply}};
372/// use std::collections::HashMap;
373///
374/// struct MyNode;
375///
376/// let mut treeize = Treeize::<MyNode>::new();
377/// let config = LayoutConfig {
378///     horizontal_spacing: 200.0,
379///     vertical_spacing: 150.0,
380///     start_pos: egui::pos2(0.0, 0.0),
381/// };
382///
383/// // Optional: provide node sizes
384/// let mut node_sizes = HashMap::new();
385/// // node_sizes.insert(node_id, egui::vec2(100.0, 50.0));
386///
387/// layout_and_apply(
388///     &mut treeize,
389///     config,
390///     |node_id| {
391///         // Check if node has output pins
392///         let node = &treeize[node_id];
393///         // Your logic here
394///         true
395///     },
396///     |node_id| {
397///         // Check if node has input pins
398///         let node = &treeize[node_id];
399///         // Your logic here
400///         true
401///     },
402///     Some(&node_sizes),  // or None to ignore sizes
403/// );
404/// ```
405#[allow(clippy::implicit_hasher)]
406pub fn layout_and_apply<T>(
407  treeize: &mut Treeize<T>,
408  config: LayoutConfig,
409  has_output: impl FnMut(NodeId) -> bool,
410  has_input: impl FnMut(NodeId) -> bool,
411  node_sizes: Option<&HashMap<NodeId, Vec2>>,
412) {
413  let positions = layout_tree(treeize, config, has_output, has_input, node_sizes);
414  apply_layout(treeize, &positions);
415}
416
417/// Convenience function that performs layout using a `TreeizeViewer`.
418///
419/// This function automatically uses the viewer's `has_input` and `has_output` methods
420/// to determine node connectivity.
421///
422/// # Arguments
423///
424/// * `treeize` - The tree graph to layout and update
425/// * `viewer` - The [`TreeizeViewer`] implementation
426/// * `config` - Layout configuration parameters
427/// * `ctx` - The egui [`Context`] to read node sizes from [`NodeState`]
428/// * `treeize_id` - The ID of the treeize widget (used to construct node IDs)
429///
430/// # Example
431///
432/// ```rs
433/// use egui_treeize::{Treeize, layout::{LayoutConfig, layout_with_viewer}};
434/// use egui_treeize::ui::TreeizeViewer;
435///
436/// struct MyViewer;
437/// struct MyNode;
438///
439/// impl TreeizeViewer<MyNode> for MyViewer {
440///     // ... implement required methods
441/// }
442///
443/// let mut treeize = Treeize::<MyNode>::new();
444/// let mut viewer = MyViewer;
445/// let config = LayoutConfig::default();
446/// let ctx = ui.ctx();  // Get from your UI
447/// let treeize_id = treeize_widget.get_id(ui.id());  // Get from your TreeizeWidget
448///
449/// layout_with_viewer(&mut treeize, &mut viewer, config, ctx, treeize_id);
450/// ```
451pub fn layout_with_viewer<T, V>(
452  treeize: &mut Treeize<T>,
453  viewer: &mut V,
454  config: LayoutConfig,
455  ctx: &Context,
456  treeize_id: Id,
457) where
458  V: TreeizeViewer<T>,
459{
460  // Read node sizes from NodeState stored in Context
461  let mut node_sizes_map: HashMap<NodeId, Vec2> = HashMap::new();
462
463  for (node_id, _) in treeize.node_ids() {
464    let node_state_id = treeize_id.with(("treeize-node", node_id));
465    if let Some(node_data) = NodeState::pick_data(ctx, node_state_id) {
466      node_sizes_map.insert(node_id, node_data.size);
467    }
468  }
469
470  let node_sizes = if node_sizes_map.is_empty() { None } else { Some(&node_sizes_map) };
471
472  // First, collect all node data and compute has_input/has_output to avoid borrowing issues
473  let node_info: Vec<(NodeId, bool, bool)> = treeize
474    .node_ids()
475    .map(|(node_id, data)| {
476      let has_out = viewer.has_output(data);
477      let has_in = viewer.has_input(data);
478      (node_id, has_out, has_in)
479    })
480    .collect();
481
482  // Create a lookup map
483  let mut has_output_map: HashMap<NodeId, bool> = HashMap::new();
484  let mut has_input_map: HashMap<NodeId, bool> = HashMap::new();
485
486  for (node_id, has_out, has_in) in &node_info {
487    has_output_map.insert(*node_id, *has_out);
488    has_input_map.insert(*node_id, *has_in);
489  }
490
491  // Create closures that use the maps
492  let has_output_fn =
493    |node_id: NodeId| -> bool { has_output_map.get(&node_id).copied().unwrap_or(false) };
494
495  let has_input_fn =
496    |node_id: NodeId| -> bool { has_input_map.get(&node_id).copied().unwrap_or(false) };
497
498  layout_and_apply(treeize, config, has_output_fn, has_input_fn, node_sizes);
499}