Skip to main content

tui_lipan/widgets/er_diagram/
mod.rs

1//! Static entity-relationship diagram widget.
2
3mod layout;
4mod node;
5mod reconcile;
6mod theme;
7
8pub use layout::measure_er_diagram;
9pub use node::ErDiagramNode;
10pub use reconcile::reconcile_er_diagram;
11pub use theme::ErDiagramTheme;
12
13use crate::core::element::{Element, ElementKind};
14use crate::style::{BorderStyle, Length, Padding, Style};
15use std::sync::Arc;
16
17/// Cardinality of one side of an [`ErRelation`], rendered as crow's-foot notation.
18#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
19pub enum ErCardinality {
20    /// Zero or one (`|o`).
21    ZeroOrOne,
22    /// Exactly one (`||`).
23    ExactlyOne,
24    /// Zero or more (`}o`).
25    ZeroOrMore,
26    /// One or more (`}|`).
27    #[default]
28    OneOrMore,
29}
30
31/// A single column of an [`ErEntity`].
32#[derive(Clone, Debug, PartialEq, Eq, Hash)]
33pub struct ErAttribute {
34    /// Attribute data type (e.g. `"int"`, `"varchar"`).
35    pub ty: Arc<str>,
36    /// Attribute name.
37    pub name: Arc<str>,
38    /// Whether this attribute is a primary key.
39    pub pk: bool,
40    /// Whether this attribute is a foreign key.
41    pub fk: bool,
42    /// Whether this attribute carries a unique key constraint.
43    pub uk: bool,
44}
45impl ErAttribute {
46    /// Creates an attribute with the given type and name (no key flags set).
47    pub fn new(ty: impl Into<Arc<str>>, name: impl Into<Arc<str>>) -> Self {
48        Self {
49            ty: ty.into(),
50            name: name.into(),
51            pk: false,
52            fk: false,
53            uk: false,
54        }
55    }
56    /// Marks this attribute as a primary key.
57    pub fn pk(mut self) -> Self {
58        self.pk = true;
59        self
60    }
61    /// Marks this attribute as a foreign key.
62    pub fn fk(mut self) -> Self {
63        self.fk = true;
64        self
65    }
66    /// Marks this attribute as a unique key.
67    pub fn uk(mut self) -> Self {
68        self.uk = true;
69        self
70    }
71}
72
73/// A table/entity in an [`ErDiagram`], with a name and ordered attributes.
74#[derive(Clone, Debug, PartialEq, Eq, Hash)]
75pub struct ErEntity {
76    /// Entity (table) name.
77    pub name: Arc<str>,
78    /// Ordered list of attributes (columns).
79    pub attributes: Vec<ErAttribute>,
80}
81impl ErEntity {
82    /// Creates an entity with the given name and no attributes.
83    pub fn new(name: impl Into<Arc<str>>) -> Self {
84        Self {
85            name: name.into(),
86            attributes: Vec::new(),
87        }
88    }
89    /// Appends an attribute and returns the entity for chaining.
90    pub fn attribute(mut self, attribute: ErAttribute) -> Self {
91        self.attributes.push(attribute);
92        self
93    }
94}
95
96/// A relationship between two entities, with crow's-foot cardinality on each end.
97#[derive(Clone, Debug, PartialEq, Eq, Hash)]
98pub struct ErRelation {
99    /// Name of the entity on the left side.
100    pub left: Arc<str>,
101    /// Name of the entity on the right side.
102    pub right: Arc<str>,
103    /// Cardinality at the left end.
104    pub left_cardinality: ErCardinality,
105    /// Cardinality at the right end.
106    pub right_cardinality: ErCardinality,
107    /// Optional label drawn on the relationship edge.
108    pub label: Option<Arc<str>>,
109}
110
111/// A static entity-relationship diagram laid out automatically from entities and
112/// relations. Build it with the chaining setters and convert into an [`Element`].
113#[derive(Clone)]
114pub struct ErDiagram {
115    pub(crate) entities: Arc<[ErEntity]>,
116    pub(crate) relations: Arc<[ErRelation]>,
117    pub(crate) style: Style,
118    pub(crate) entity_style: Style,
119    pub(crate) edge_style: Style,
120    pub(crate) label_style: Style,
121    pub(crate) border_style: BorderStyle,
122    pub(crate) padding: Padding,
123    pub(crate) node_padding: Padding,
124    pub(crate) layer_gap: u16,
125    pub(crate) node_gap: u16,
126    pub(crate) max_node_width: u16,
127    pub(crate) theme: ErDiagramTheme,
128    pub(crate) width: Length,
129    pub(crate) height: Length,
130}
131
132impl Default for ErDiagram {
133    fn default() -> Self {
134        Self {
135            entities: Arc::new([]),
136            relations: Arc::new([]),
137            style: Style::default(),
138            entity_style: Style::default(),
139            edge_style: Style::default(),
140            label_style: Style::default(),
141            border_style: BorderStyle::Plain,
142            padding: Padding::default(),
143            node_padding: (0, 1).into(),
144            layer_gap: 4,
145            node_gap: 4,
146            max_node_width: 32,
147            theme: ErDiagramTheme::default(),
148            width: Length::Auto,
149            height: Length::Auto,
150        }
151    }
152}
153
154impl ErDiagram {
155    /// Creates an empty diagram with default styling.
156    pub fn new() -> Self {
157        Self::default()
158    }
159    /// Replaces the entity set with `entities`.
160    pub fn entities(mut self, entities: impl IntoIterator<Item = ErEntity>) -> Self {
161        self.entities = entities.into_iter().collect::<Vec<_>>().into();
162        self
163    }
164    /// Replaces the relation set with `relations`.
165    pub fn relations(mut self, relations: impl IntoIterator<Item = ErRelation>) -> Self {
166        self.relations = relations.into_iter().collect::<Vec<_>>().into();
167        self
168    }
169    /// Appends an entity by name (no attributes). See [`attribute`](Self::attribute)
170    /// to add columns to it.
171    pub fn entity(mut self, name: impl Into<Arc<str>>) -> Self {
172        let mut v = self.entities.to_vec();
173        v.push(ErEntity::new(name));
174        self.entities = v.into();
175        self
176    }
177    /// Adds an attribute to the named entity, creating the entity if it does not
178    /// yet exist.
179    pub fn attribute(
180        mut self,
181        entity: impl AsRef<str>,
182        ty: impl Into<Arc<str>>,
183        name: impl Into<Arc<str>>,
184    ) -> Self {
185        self.update_entity(entity.as_ref(), |e| {
186            e.attributes.push(ErAttribute::new(ty, name))
187        });
188        self
189    }
190    /// Adds a relationship between two entities with the given cardinalities and
191    /// optional edge label.
192    pub fn relation(
193        mut self,
194        left: impl Into<Arc<str>>,
195        right: impl Into<Arc<str>>,
196        left_cardinality: ErCardinality,
197        right_cardinality: ErCardinality,
198        label: impl Into<Option<Arc<str>>>,
199    ) -> Self {
200        let mut v = self.relations.to_vec();
201        v.push(ErRelation {
202            left: left.into(),
203            right: right.into(),
204            left_cardinality,
205            right_cardinality,
206            label: label.into(),
207        });
208        self.relations = v.into();
209        self
210    }
211    /// Sets the base style of the diagram container.
212    pub fn style(mut self, style: Style) -> Self {
213        self.style = style;
214        self
215    }
216    /// Sets the style applied to entity (table) boxes.
217    pub fn entity_style(mut self, style: Style) -> Self {
218        self.entity_style = style;
219        self
220    }
221    /// Sets the style applied to relationship edges.
222    pub fn edge_style(mut self, style: Style) -> Self {
223        self.edge_style = style;
224        self
225    }
226    /// Sets the style applied to edge labels.
227    pub fn label_style(mut self, style: Style) -> Self {
228        self.label_style = style;
229        self
230    }
231    /// Sets the border line style for entity boxes.
232    pub fn border_style(mut self, style: BorderStyle) -> Self {
233        self.border_style = style;
234        self
235    }
236    /// Sets the outer padding of the diagram.
237    pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
238        self.padding = padding.into();
239        self
240    }
241    /// Sets the inner padding of each entity box.
242    pub fn node_padding(mut self, padding: impl Into<Padding>) -> Self {
243        self.node_padding = padding.into();
244        self
245    }
246    /// Caps the rendered width of an entity box (minimum 1); longer content wraps
247    /// or truncates.
248    pub fn max_node_width(mut self, width: u16) -> Self {
249        self.max_node_width = width.max(1);
250        self
251    }
252    /// Sets the width of the diagram container.
253    pub fn width(mut self, width: Length) -> Self {
254        self.width = width;
255        self
256    }
257    /// Sets the height of the diagram container.
258    pub fn height(mut self, height: Length) -> Self {
259        self.height = height;
260        self
261    }
262    fn update_entity(&mut self, name: &str, f: impl FnOnce(&mut ErEntity)) {
263        let mut entities = self.entities.to_vec();
264        let index = entities
265            .iter()
266            .position(|e| e.name.as_ref() == name)
267            .unwrap_or_else(|| {
268                entities.push(ErEntity::new(name.to_owned()));
269                entities.len() - 1
270            });
271        f(&mut entities[index]);
272        self.entities = entities.into();
273    }
274}
275impl From<ErDiagram> for Element {
276    fn from(value: ErDiagram) -> Self {
277        Element::new(ElementKind::ErDiagram(Box::new(value)))
278    }
279}