tui-lipan 0.1.0

Opinionated, component-based TUI framework for Rust - declarative components, reconciliation, layout engine, focus, overlays, and rich widgets on top of ratatui.
Documentation
use crate::style::Rect;

use super::{DagInput, PositionedNode, RoutedEdge, WorkingNode};

pub(super) fn assign_coordinates(input: &DagInput, nodes: &[WorkingNode]) -> Vec<PositionedNode> {
    let max_layer = nodes.iter().map(|node| node.layer).max().unwrap_or(0);
    let mut layer_heights = vec![0u16; max_layer.saturating_add(1)];
    let mut layer_widths = vec![0u16; max_layer.saturating_add(1)];

    for node in nodes {
        layer_heights[node.layer] = layer_heights[node.layer].max(node.height);
        if layer_widths[node.layer] > 0 {
            layer_widths[node.layer] =
                layer_widths[node.layer].saturating_add(input.options.node_gap);
        }
        layer_widths[node.layer] = layer_widths[node.layer].saturating_add(node.width);
    }

    let widest_layer = layer_widths.iter().copied().max().unwrap_or(0);
    let mut layer_y = vec![input.options.margin_y as i16; max_layer.saturating_add(1)];
    for layer in 1..=max_layer {
        layer_y[layer] = layer_y[layer - 1]
            .saturating_add(layer_heights[layer - 1] as i16)
            .saturating_add(input.options.layer_gap as i16);
    }

    let mut positioned = Vec::with_capacity(nodes.len());
    for layer in 0..=max_layer {
        let mut layer_nodes: Vec<_> = nodes.iter().filter(|node| node.layer == layer).collect();
        layer_nodes.sort_by_key(|node| (node.order, node.spec_index));
        let mut x = input.options.margin_x as i16
            + ((widest_layer.saturating_sub(layer_widths[layer])) / 2) as i16;
        for node in layer_nodes {
            let spec = &input.nodes[node.spec_index];
            let y =
                layer_y[layer] + ((layer_heights[layer].saturating_sub(node.height)) / 2) as i16;
            positioned.push(PositionedNode {
                id: spec.id.clone(),
                rect: Rect {
                    x,
                    y,
                    w: node.width,
                    h: node.height,
                },
                layer: node.layer,
                order: node.order,
                group: spec.group.clone(),
            });
            x = x
                .saturating_add(node.width as i16)
                .saturating_add(input.options.node_gap as i16);
        }
    }
    positioned.sort_by_key(|node| (node.layer, node.order, node.id.clone()));
    positioned
}

pub(super) fn bounds(nodes: &[PositionedNode], edges: &[RoutedEdge]) -> Rect {
    let mut min_x = 0i16;
    let mut min_y = 0i16;
    let mut max_x = 0i16;
    let mut max_y = 0i16;

    for node in nodes {
        min_x = min_x.min(node.rect.x);
        min_y = min_y.min(node.rect.y);
        max_x = max_x.max(node.rect.x.saturating_add(node.rect.w as i16));
        max_y = max_y.max(node.rect.y.saturating_add(node.rect.h as i16));
    }
    for edge in edges {
        for point in &edge.points {
            min_x = min_x.min(point.x);
            min_y = min_y.min(point.y);
            max_x = max_x.max(point.x.saturating_add(1));
            max_y = max_y.max(point.y.saturating_add(1));
        }
    }

    Rect {
        x: min_x,
        y: min_y,
        w: (max_x - min_x).max(0) as u16,
        h: (max_y - min_y).max(0) as u16,
    }
}