Skip to main content

merman_render/text/
types.rs

1//! Shared text measurement types.
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
6pub enum WrapMode {
7    #[default]
8    SvgLike,
9    /// SVG `<text>` behaves as a single shaping run (no whitespace-to-`<tspan>` tokenization).
10    ///
11    /// Mermaid uses this behavior in some diagrams (e.g. sequence message labels), where the
12    /// resulting `getBBox()` width differs measurably from per-word `<tspan>` tokenization.
13    SvgLikeSingleRun,
14    HtmlLike,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct TextStyle {
19    pub font_family: Option<String>,
20    pub font_size: f64,
21    pub font_weight: Option<String>,
22    #[serde(default, skip_serializing_if = "Option::is_none")]
23    pub font_style: Option<String>,
24}
25
26impl Default for TextStyle {
27    fn default() -> Self {
28        Self {
29            font_family: None,
30            font_size: 16.0,
31            font_weight: None,
32            font_style: None,
33        }
34    }
35}
36
37#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
38pub struct TextMetrics {
39    pub width: f64,
40    pub height: f64,
41    pub line_count: usize,
42}
43
44impl TextMetrics {
45    /// Apply Mermaid's `labelHelper` minimum without changing the text's height or wrapping.
46    /// The caller selects shapes that use `labelHelper`; explicit shape widths remain authoritative.
47    pub(crate) fn with_label_min_width(
48        mut self,
49        label: &str,
50        min_width: f64,
51        explicit_width: Option<f64>,
52    ) -> Self {
53        if !label.is_empty() && !explicit_width.is_some_and(|width| width != 0.0) {
54            self.width = self.width.max(min_width);
55        }
56        self
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::TextMetrics;
63
64    #[test]
65    fn label_minimum_preserves_empty_and_explicit_width_contracts() {
66        let metrics = TextMetrics {
67            width: 20.0,
68            height: 16.0,
69            line_count: 2,
70        };
71        for (label, explicit_width, expected) in [
72            ("X", None, 120.0),
73            ("X", Some(90.0), 20.0),
74            ("X", Some(0.0), 120.0),
75            ("", None, 20.0),
76            (" ", None, 120.0),
77        ] {
78            let actual = metrics.with_label_min_width(label, 120.0, explicit_width);
79            assert_eq!(actual.width, expected);
80            assert_eq!(actual.height, metrics.height);
81            assert_eq!(actual.line_count, metrics.line_count);
82        }
83        assert_eq!(metrics.with_label_min_width("X", 10.0, None).width, 20.0);
84    }
85}