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 std::sync::Arc;

use crate::widgets::common::diagram_text::wrap_label;
use crate::widgets::common::simple_diagram::{
    EndpointGlyph, SimpleDiagramBox, SimpleDiagramBoxShape, SimpleDiagramEdge, SimpleDiagramOutput,
    build_simple_diagram_output,
};

use super::{ClassDiagram, ClassMember, ClassRelationKind, ClassVisibility};

pub fn measure_class_diagram(diagram: &ClassDiagram) -> (u16, u16) {
    let output = build_class_diagram_output(diagram);
    (
        output.width.saturating_add(diagram.padding.horizontal()),
        output.height.saturating_add(diagram.padding.vertical()),
    )
}

pub(crate) fn build_class_diagram_output(diagram: &ClassDiagram) -> SimpleDiagramOutput {
    let boxes = class_boxes(diagram);
    let edges = diagram
        .relations
        .iter()
        .map(|relation| {
            let (from_glyph, to_glyph, dashed) = relation_glyphs(relation.kind);
            SimpleDiagramEdge {
                from: relation.from.clone(),
                to: relation.to.clone(),
                label: relation_label(relation),
                from_label: relation.multiplicity_from.clone(),
                to_label: relation.multiplicity_to.clone(),
                line_style: diagram.edge_style,
                label_style: diagram.label_style,
                dashed,
                from_glyph,
                to_glyph,
                prefer_vertical_backedge_labels: false,
            }
        })
        .collect::<Vec<_>>();
    build_simple_diagram_output(
        &boxes,
        &edges,
        diagram.node_padding,
        diagram.layer_gap,
        diagram.node_gap,
    )
}

pub(crate) fn class_boxes(diagram: &ClassDiagram) -> Vec<SimpleDiagramBox> {
    diagram
        .classes
        .iter()
        .map(|class| {
            let mut rows = Vec::new();
            push_wrapped(&mut rows, &class.name, diagram.max_node_width);
            let mut divider_after = rows
                .len()
                .checked_sub(1)
                .map(|title_last| vec![title_last])
                .unwrap_or_default();

            let attr_start = rows.len();
            for member in &class.attributes {
                let formatted = format_member(member, &diagram.theme);
                push_wrapped(&mut rows, &formatted, diagram.max_node_width);
            }
            if rows.len() > attr_start {
                divider_after.push(rows.len() - 1);
            }

            for member in &class.methods {
                let formatted = format_member(member, &diagram.theme);
                push_wrapped(&mut rows, &formatted, diagram.max_node_width);
            }
            SimpleDiagramBox {
                id: class.name.clone(),
                rows,
                divider_after,
                fill_style: diagram.class_style,
                border_style_fg: diagram.class_style,
                label_style: diagram.class_style,
                border_style: diagram.border_style,
                shape: SimpleDiagramBoxShape::Rect,
            }
        })
        .collect()
}

fn push_wrapped(rows: &mut Vec<Arc<str>>, text: &str, max_width: u16) {
    rows.extend(wrap_label(text, max_width).iter().cloned());
}

fn format_member(member: &ClassMember, theme: &super::ClassDiagramTheme) -> Arc<str> {
    let prefix = match member.visibility {
        ClassVisibility::Public => theme.public,
        ClassVisibility::Private => theme.private,
        ClassVisibility::Protected => theme.protected,
        ClassVisibility::Package => theme.package,
    };
    match &member.ty {
        Some(ty) => format!("{prefix}{}: {ty}", member.name).into(),
        None => format!("{prefix}{}", member.name).into(),
    }
}

fn relation_glyphs(kind: ClassRelationKind) -> (EndpointGlyph, EndpointGlyph, bool) {
    match kind {
        ClassRelationKind::Inheritance => (EndpointGlyph::None, EndpointGlyph::Triangle, false),
        ClassRelationKind::Realization => (EndpointGlyph::None, EndpointGlyph::Triangle, true),
        ClassRelationKind::Composition => (EndpointGlyph::Diamond, EndpointGlyph::None, false),
        ClassRelationKind::Aggregation => (EndpointGlyph::Circle, EndpointGlyph::None, false),
        ClassRelationKind::Dependency => (EndpointGlyph::None, EndpointGlyph::Arrow, true),
        ClassRelationKind::Association => (EndpointGlyph::None, EndpointGlyph::Arrow, false),
    }
}

fn relation_label(relation: &super::ClassRelation) -> Option<Arc<str>> {
    relation.label.clone()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::style::Padding;

    #[test]
    fn class_member_compartments_add_divider_rows() {
        let diagram = ClassDiagram::new()
            .attribute("User", ClassVisibility::Private, "id", "u64")
            .method("User", ClassVisibility::Public, "save", "() -> bool");

        let boxes = class_boxes(&diagram);
        let user = &boxes[0];

        assert_eq!(user.rows.len(), 3);
        assert_eq!(user.divider_after, vec![0, 1]);
        assert_eq!(user.min_size(Padding::default()).h, 7);
    }

    #[test]
    fn long_member_rows_wrap_before_measurement() {
        let diagram = ClassDiagram::new().max_node_width(8).attribute(
            "User",
            ClassVisibility::Private,
            "very_long_identifier",
            "String",
        );

        let boxes = class_boxes(&diagram);
        let user = &boxes[0];

        assert!(user.rows.len() > 2);
        assert!(user.rows.iter().all(|row| row.chars().count() <= 8));
    }
}