Skip to main content

lightweight_pdf_core/
style.rs

1//! Flat style properties shared across elements (no cascading, see
2//! `plan/03-builder-api-design.md`).
3
4/// Opaque handle for a font. `lightweight-pdf-core` never sees font bytes, only this
5/// key (see `plan/00a-contracts-and-artifacts.md`, point 3).
6///
7/// `serde` (issue #17): deserializes from a plain JSON string. Since the
8/// wrapped `&'static str` can't borrow from a transient JSON buffer, each
9/// distinct name deserialized is leaked (`Box::leak`) to get a `'static`
10/// reference — one small, permanent allocation per distinct font-key name
11/// ever seen in a document, acceptable for "parse a document, render it,
12/// done" but not for a long-running process parsing unbounded distinct
13/// names in a hot loop.
14#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
15pub struct FontKey(pub &'static str);
16
17impl FontKey {
18    pub const SANS_REGULAR: FontKey = FontKey("sans-regular");
19    pub const SANS_BOLD: FontKey = FontKey("sans-bold");
20    pub const SANS_ITALIC: FontKey = FontKey("sans-italic");
21    pub const SANS_BOLD_ITALIC: FontKey = FontKey("sans-bold-italic");
22
23    pub const fn custom(name: &'static str) -> Self {
24        FontKey(name)
25    }
26}
27
28#[cfg(feature = "serde")]
29impl serde::Serialize for FontKey {
30    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
31        serializer.serialize_str(self.0)
32    }
33}
34
35#[cfg(feature = "serde")]
36impl<'de> serde::Deserialize<'de> for FontKey {
37    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
38    where
39        D: serde::Deserializer<'de>,
40    {
41        let name = String::deserialize(deserializer)?;
42        Ok(FontKey(Box::leak(name.into_boxed_str())))
43    }
44}
45
46/// Hand-written to match the custom `Serialize`/`Deserialize` above
47/// (`#[derive(JsonSchema)]` only works from a real derive, not a custom
48/// serde impl).
49#[cfg(feature = "schemars")]
50impl schemars::JsonSchema for FontKey {
51    fn schema_name() -> std::borrow::Cow<'static, str> {
52        "FontKey".into()
53    }
54
55    fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
56        schemars::json_schema!({ "type": "string" })
57    }
58}
59
60#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "snake_case"))]
61#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
62#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
63pub enum Align {
64    #[default]
65    Start,
66    Center,
67    End,
68    /// Text only (`TextStyle::align`): word gaps stretch so every line but
69    /// the last of a paragraph is flush with both edges. Meaningless for
70    /// block/container alignment (`Row`/`Column`/`TableColumn`), which
71    /// treats it the same as `Start`.
72    Justify,
73}
74
75/// Overflow policy for explicitly, fixed-size elements. See
76/// `plan/05-overflow-and-robustness.md`, Grundprinzip 3. `Visible` is
77/// intentionally not part of V1 (ADR-011).
78#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "snake_case"))]
79#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
80#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
81pub enum Overflow {
82    /// Default: clip hard at the element's box.
83    #[default]
84    Clip,
85    /// Single-line text only: truncate with a trailing "…".
86    Ellipsis,
87}
88
89#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
90#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
91#[derive(Clone, Copy, PartialEq, Eq, Debug)]
92pub struct Color(pub u8, pub u8, pub u8);
93
94impl Color {
95    pub const BLACK: Color = Color(0, 0, 0);
96    pub const WHITE: Color = Color(255, 255, 255);
97
98    pub fn rgb(r: u8, g: u8, b: u8) -> Self {
99        Color(r, g, b)
100    }
101}
102
103impl Default for Color {
104    fn default() -> Self {
105        Color::BLACK
106    }
107}
108
109#[cfg_attr(
110    feature = "serde",
111    derive(serde::Serialize, serde::Deserialize),
112    serde(deny_unknown_fields, rename_all = "snake_case")
113)]
114#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
115#[derive(Clone, Copy, PartialEq, Debug, Default)]
116pub enum BorderStyle {
117    #[default]
118    Solid,
119    Dashed {
120        dash: f32,
121        gap: f32,
122    },
123}
124
125#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(deny_unknown_fields))]
126#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
127#[derive(Clone, Copy, PartialEq, Debug)]
128pub struct Border {
129    pub width: f32,
130    pub color: Color,
131    pub style: BorderStyle,
132}
133
134impl Border {
135    pub fn solid(width: f32, color: Color) -> Self {
136        Border {
137            width,
138            color,
139            style: BorderStyle::Solid,
140        }
141    }
142
143    pub fn dashed(width: f32, color: Color, dash: f32, gap: f32) -> Self {
144        Border {
145            width,
146            color,
147            style: BorderStyle::Dashed { dash, gap },
148        }
149    }
150}
151
152#[cfg_attr(
153    feature = "serde",
154    derive(serde::Serialize, serde::Deserialize),
155    serde(deny_unknown_fields, default)
156)]
157#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
158#[derive(Clone, Copy, PartialEq, Debug)]
159pub struct TextStyle {
160    pub font: FontKey,
161    pub size: f32,
162    pub color: Color,
163    pub align: Align,
164    /// Multiple of `size`, e.g. 1.2 for 20% leading.
165    pub line_height: f32,
166}
167
168impl Default for TextStyle {
169    fn default() -> Self {
170        TextStyle {
171            font: FontKey::SANS_REGULAR,
172            size: 12.0,
173            color: Color::BLACK,
174            align: Align::Start,
175            line_height: 1.2,
176        }
177    }
178}
179
180/// Properties shared by every container/block element: `padding, width,
181/// height, overflow, background, border`, plus
182/// `flex` (taffy-vocabulary, ADR-004) and `keep_with_next` (ADR-007 /
183/// Grundprinzip 9). Deliberately no `margin` on elements (ADR/03: only
184/// `padding` + `Row`/`Column` `gap`, margin is a `Document`-level property).
185#[cfg_attr(
186    feature = "serde",
187    derive(serde::Serialize, serde::Deserialize),
188    serde(deny_unknown_fields, default)
189)]
190#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
191#[derive(Clone, Copy, PartialEq, Debug, Default)]
192pub struct Common {
193    pub width: Option<f32>,
194    pub height: Option<f32>,
195    /// Growth factor along the parent's main axis; `None` means "auto",
196    /// i.e. sized from measured content. Only has an effect when the
197    /// parent's main-axis size is bounded (see lightweight-pdf-layout Row/Column).
198    pub flex: Option<f32>,
199    pub padding: f32,
200    pub corner_radius: f32,
201    pub overflow: Overflow,
202    pub background: Option<Color>,
203    pub border: Option<Border>,
204    /// See `plan/05-overflow-and-robustness.md` Grundprinzip 9 / ADR-007:
205    /// only placed if the immediately following sibling also still fits.
206    pub keep_with_next: bool,
207}