Skip to main content

forme/style/
mod.rs

1//! # Style System
2//!
3//! A CSS-like style model for document nodes. This is intentionally a subset
4//! of CSS that covers the properties needed for document layout: flexbox,
5//! box model, typography, color, borders.
6//!
7//! We don't try to implement all of CSS. We implement the parts that matter
8//! for PDF documents, and we implement them correctly.
9
10use crate::model::{Edges, MarginEdges, Position};
11use serde::{Deserialize, Serialize};
12
13/// The complete set of style properties for a node.
14#[derive(Debug, Clone, Default, Serialize, Deserialize)]
15#[serde(rename_all = "camelCase")]
16pub struct Style {
17    // ── Box Model ──────────────────────────────────────────────
18    /// Explicit width in points.
19    pub width: Option<Dimension>,
20    /// Explicit height in points.
21    pub height: Option<Dimension>,
22    /// Minimum width.
23    pub min_width: Option<Dimension>,
24    /// Minimum height.
25    pub min_height: Option<Dimension>,
26    /// Maximum width.
27    pub max_width: Option<Dimension>,
28    /// Maximum height.
29    pub max_height: Option<Dimension>,
30
31    /// Padding inside the border.
32    #[serde(default)]
33    pub padding: Option<Edges>,
34    /// Margin outside the border. Supports auto values for centering.
35    #[serde(default)]
36    pub margin: Option<MarginEdges>,
37
38    // ── Display & Layout Mode ──────────────────────────────────
39    /// Display mode: flex (default) or grid.
40    pub display: Option<Display>,
41
42    // ── Flexbox Layout ─────────────────────────────────────────
43    /// Direction of the main axis.
44    #[serde(default)]
45    pub flex_direction: Option<FlexDirection>,
46    /// How to distribute space along the main axis.
47    #[serde(default)]
48    pub justify_content: Option<JustifyContent>,
49    /// How to align items along the cross axis.
50    #[serde(default)]
51    pub align_items: Option<AlignItems>,
52    /// Override align-items for this specific child.
53    #[serde(default)]
54    pub align_self: Option<AlignItems>,
55    /// Whether flex items wrap to new lines.
56    #[serde(default)]
57    pub flex_wrap: Option<FlexWrap>,
58    /// How to distribute space between flex lines on the cross axis.
59    pub align_content: Option<AlignContent>,
60    /// Flex grow factor.
61    pub flex_grow: Option<f64>,
62    /// Flex shrink factor.
63    pub flex_shrink: Option<f64>,
64    /// Flex basis (initial main size).
65    pub flex_basis: Option<Dimension>,
66    /// Gap between flex items.
67    pub gap: Option<f64>,
68    /// Row gap (overrides gap for rows).
69    pub row_gap: Option<f64>,
70    /// Column gap (overrides gap for columns).
71    pub column_gap: Option<f64>,
72
73    // ── CSS Grid Layout ──────────────────────────────────────────
74    /// Column track definitions (e.g., `[Pt(100), Fr(1), Fr(2)]`).
75    pub grid_template_columns: Option<Vec<GridTrackSize>>,
76    /// Row track definitions.
77    pub grid_template_rows: Option<Vec<GridTrackSize>>,
78    /// Auto-generated row size.
79    pub grid_auto_rows: Option<GridTrackSize>,
80    /// Auto-generated column size.
81    pub grid_auto_columns: Option<GridTrackSize>,
82    /// Grid placement for this child item.
83    pub grid_placement: Option<GridPlacement>,
84
85    // ── Typography ─────────────────────────────────────────────
86    /// Font family name.
87    pub font_family: Option<String>,
88    /// Font size in points.
89    pub font_size: Option<f64>,
90    /// Font weight (100-900).
91    pub font_weight: Option<u32>,
92    /// Font style.
93    pub font_style: Option<FontStyle>,
94    /// Line height as a multiplier of font size.
95    pub line_height: Option<f64>,
96    /// Text alignment within the text block.
97    pub text_align: Option<TextAlign>,
98    /// Letter spacing in points.
99    pub letter_spacing: Option<f64>,
100    /// Word spacing in points (extra width added to each ASCII space, PDF
101    /// `Tw` operator). Negative tightens word gaps. When the text is
102    /// justified (`text-align: justify`), the layout engine adds the
103    /// computed slack-per-space on top of this user value.
104    pub word_spacing: Option<f64>,
105    /// Text decoration.
106    pub text_decoration: Option<TextDecoration>,
107    /// Text transform.
108    pub text_transform: Option<TextTransform>,
109    /// Hyphenation mode (CSS `hyphens` property).
110    pub hyphens: Option<Hyphens>,
111    /// BCP 47 language tag for hyphenation and line breaking.
112    pub lang: Option<String>,
113    /// Text direction (ltr, rtl, or auto).
114    pub direction: Option<Direction>,
115    /// Text overflow behavior (wrap, ellipsis, clip).
116    pub text_overflow: Option<TextOverflow>,
117    /// Line breaking algorithm: optimal (Knuth-Plass, default) or greedy.
118    pub line_breaking: Option<LineBreaking>,
119
120    /// Overflow behavior for container elements.
121    pub overflow: Option<Overflow>,
122
123    // ── Color & Background ─────────────────────────────────────
124    /// Text color.
125    pub color: Option<Color>,
126    /// Background color.
127    pub background_color: Option<Color>,
128    /// Background paint that may be a solid color or a gradient. Wins
129    /// over `background_color` when set. Parsed from CSS strings on
130    /// the React side: `"#1e293b"`, `"linear-gradient(180deg, #fff
131    /// 0%, #000 100%)"`, `"radial-gradient(circle, #fff, #000)"`.
132    pub background: Option<Background>,
133    /// Opacity (0.0 - 1.0).
134    pub opacity: Option<f64>,
135    /// Drop shadow painted behind the element. v1 supports offset + color
136    /// (no blur — `blur` is parsed for forward-compat but ignored). When
137    /// the element has `border_radius`, the shadow path is rounded too.
138    pub box_shadow: Option<BoxShadow>,
139
140    // ── Border ─────────────────────────────────────────────────
141    /// Border width for all sides.
142    pub border_width: Option<EdgeValues<f64>>,
143    /// Border color for all sides.
144    pub border_color: Option<EdgeValues<Color>>,
145    /// Border radius (uniform or per-corner).
146    pub border_radius: Option<CornerValues>,
147
148    // ── Positioning ─────────────────────────────────────────────
149    /// Positioning mode (relative or absolute).
150    pub position: Option<Position>,
151    /// Top offset (for absolute positioning).
152    pub top: Option<f64>,
153    /// Right offset (for absolute positioning).
154    pub right: Option<f64>,
155    /// Bottom offset (for absolute positioning).
156    pub bottom: Option<f64>,
157    /// Left offset (for absolute positioning).
158    pub left: Option<f64>,
159
160    // ── Page Behavior ──────────────────────────────────────────
161    /// Whether this node can be broken across pages.
162    /// `true` = breakable (default for View, Text, Table).
163    /// `false` = keep on one page; if it doesn't fit, move to next page.
164    pub wrap: Option<bool>,
165
166    /// Force a page break before this node.
167    pub break_before: Option<bool>,
168
169    /// Minimum number of lines to keep at the bottom of a page before
170    /// breaking (widow control). Default: 2.
171    pub min_widow_lines: Option<u32>,
172
173    /// Minimum number of lines to keep at the top of a new page after
174    /// breaking (orphan control). Default: 2.
175    pub min_orphan_lines: Option<u32>,
176
177    // ── Transform ──────────────────────────────────────────────
178    /// Paint-only transform applied around the element's origin. Layout
179    /// flow is unaffected (matches CSS `transform` semantics) — the
180    /// element occupies the same axis-aligned box as if untransformed.
181    pub transform: Option<Vec<TransformOp>>,
182
183    /// Origin of the transform as fractions of the element box (0.0–1.0).
184    /// Defaults to (0.5, 0.5) — the element's center, matching CSS.
185    pub transform_origin: Option<(f64, f64)>,
186}
187
188/// A single transform operation. Applied in order — the first op listed
189/// is applied first (innermost), the last is applied last (outermost),
190/// matching CSS `transform` semantics.
191#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
192#[serde(tag = "type", rename_all = "camelCase")]
193pub enum TransformOp {
194    /// Rotate by the given angle in degrees, positive = clockwise (CSS).
195    Rotate { deg: f64 },
196    /// Uniform or non-uniform scale.
197    Scale { x: f64, y: f64 },
198    /// Translate in points.
199    Translate { x: f64, y: f64 },
200}
201
202/// A dimension that can be points, percentage, or auto.
203#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
204pub enum Dimension {
205    /// Fixed size in points (1/72 inch).
206    Pt(f64),
207    /// Percentage of parent's corresponding dimension.
208    Percent(f64),
209    /// Size determined by content.
210    Auto,
211}
212
213impl Dimension {
214    /// Resolve this dimension given a parent size.
215    /// Returns None for Auto.
216    pub fn resolve(&self, parent_size: f64) -> Option<f64> {
217        match self {
218            Dimension::Pt(v) => Some(*v),
219            Dimension::Percent(p) => Some(parent_size * p / 100.0),
220            Dimension::Auto => None,
221        }
222    }
223}
224
225/// Layout display mode.
226#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
227pub enum Display {
228    /// Flexbox layout (default).
229    #[default]
230    Flex,
231    /// CSS Grid layout.
232    Grid,
233}
234
235/// A single grid track size definition.
236#[derive(Debug, Clone, Serialize, Deserialize)]
237pub enum GridTrackSize {
238    /// Fixed size in points.
239    Pt(f64),
240    /// Fractional unit (distributes remaining space proportionally).
241    Fr(f64),
242    /// Size determined by content.
243    Auto,
244    /// Clamped between min and max.
245    MinMax(Box<GridTrackSize>, Box<GridTrackSize>),
246}
247
248/// Grid item placement.
249#[derive(Debug, Clone, Default, Serialize, Deserialize)]
250#[serde(rename_all = "camelCase")]
251pub struct GridPlacement {
252    /// Column start line (1-based).
253    pub column_start: Option<i32>,
254    /// Column end line (1-based).
255    pub column_end: Option<i32>,
256    /// Row start line (1-based).
257    pub row_start: Option<i32>,
258    /// Row end line (1-based).
259    pub row_end: Option<i32>,
260    /// Number of columns to span.
261    pub column_span: Option<u32>,
262    /// Number of rows to span.
263    pub row_span: Option<u32>,
264}
265
266#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
267pub enum FlexDirection {
268    #[default]
269    Column,
270    Row,
271    ColumnReverse,
272    RowReverse,
273}
274
275#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
276pub enum JustifyContent {
277    #[default]
278    FlexStart,
279    FlexEnd,
280    Center,
281    SpaceBetween,
282    SpaceAround,
283    SpaceEvenly,
284}
285
286#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
287pub enum AlignItems {
288    FlexStart,
289    FlexEnd,
290    Center,
291    #[default]
292    Stretch,
293    Baseline,
294}
295
296#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
297pub enum FlexWrap {
298    #[default]
299    NoWrap,
300    Wrap,
301    WrapReverse,
302}
303
304#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
305pub enum AlignContent {
306    #[default]
307    FlexStart,
308    FlexEnd,
309    Center,
310    SpaceBetween,
311    SpaceAround,
312    SpaceEvenly,
313    Stretch,
314}
315
316#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
317pub enum FontStyle {
318    #[default]
319    Normal,
320    Italic,
321    Oblique,
322}
323
324#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
325pub enum TextAlign {
326    #[default]
327    Left,
328    Right,
329    Center,
330    Justify,
331}
332
333#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
334pub enum TextDecoration {
335    #[default]
336    None,
337    Underline,
338    LineThrough,
339}
340
341#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
342pub enum TextTransform {
343    #[default]
344    None,
345    Uppercase,
346    Lowercase,
347    Capitalize,
348}
349
350/// Overflow behavior for container elements.
351#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
352pub enum Overflow {
353    /// Content can overflow the container bounds (default).
354    #[default]
355    Visible,
356    /// Content is clipped to the container bounds.
357    Hidden,
358}
359
360/// Text overflow behavior when text exceeds available width.
361#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
362pub enum TextOverflow {
363    /// Normal wrapping (default).
364    #[default]
365    Wrap,
366    /// Single-line truncation with "..." appended.
367    Ellipsis,
368    /// Single-line truncation without any indicator.
369    Clip,
370}
371
372/// Text direction for BiDi support.
373#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
374#[serde(rename_all = "lowercase")]
375pub enum Direction {
376    /// Left-to-right (default).
377    #[default]
378    Ltr,
379    /// Right-to-left (Arabic, Hebrew).
380    Rtl,
381    /// Auto-detect from first strong character.
382    Auto,
383}
384
385/// Line breaking algorithm.
386#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
387#[serde(rename_all = "lowercase")]
388pub enum LineBreaking {
389    /// Knuth-Plass optimal line breaking (default). Minimizes global raggedness.
390    #[default]
391    Optimal,
392    /// Simple greedy line breaking. Fills lines left-to-right, breaks at first overflow.
393    Greedy,
394}
395
396/// CSS `hyphens` property controlling word hyphenation.
397#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
398#[serde(rename_all = "lowercase")]
399pub enum Hyphens {
400    /// No hyphenation, not even at soft hyphens.
401    None,
402    /// Only break at soft hyphens (U+00AD) in the text.
403    #[default]
404    Manual,
405    /// Algorithmic hyphenation using language rules.
406    Auto,
407}
408
409/// An RGBA color.
410#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
411pub struct Color {
412    pub r: f64, // 0.0 - 1.0
413    pub g: f64,
414    pub b: f64,
415    pub a: f64,
416}
417
418/// CSS-style drop shadow. v1 paints a filled rectangle offset behind the
419/// element with the given color; the `blur` field is parsed for forward
420/// compatibility but ignored. Honors `border_radius` for rounded shadows.
421#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
422#[serde(rename_all = "camelCase")]
423pub struct BoxShadow {
424    pub offset_x: f64,
425    pub offset_y: f64,
426    /// Currently ignored (no blur in v1).
427    #[serde(default)]
428    pub blur: f64,
429    pub color: Color,
430}
431
432/// CSS-style background that may be a solid color or a gradient. The
433/// React layer parses CSS strings (`linear-gradient(180deg, #fff, #000)`,
434/// `radial-gradient(circle, #fff, #000)`, `#abcdef`) into this shape.
435///
436/// v1 supports 2-stop gradients via PDF Type 2 (axial) and Type 3
437/// (radial) shading dictionaries. Multi-stop gradients fall back to the
438/// first and last stop in v1; full stitching is a v2 task.
439#[derive(Debug, Clone, Serialize, Deserialize)]
440#[serde(tag = "type", rename_all = "camelCase")]
441pub enum Background {
442    Color(Color),
443    Linear(LinearGradient),
444    Radial(RadialGradient),
445}
446
447#[derive(Debug, Clone, Serialize, Deserialize)]
448#[serde(rename_all = "camelCase")]
449pub struct LinearGradient {
450    /// CSS angle in degrees. 0deg = bottom-to-top, 90deg = left-to-right,
451    /// 180deg = top-to-bottom (CSS clockwise-from-up convention).
452    pub angle_deg: f64,
453    pub stops: Vec<GradientStop>,
454}
455
456#[derive(Debug, Clone, Serialize, Deserialize)]
457#[serde(rename_all = "camelCase")]
458pub struct RadialGradient {
459    pub stops: Vec<GradientStop>,
460}
461
462#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
463#[serde(rename_all = "camelCase")]
464pub struct GradientStop {
465    /// Position along the gradient axis (0.0 to 1.0).
466    pub position: f64,
467    pub color: Color,
468}
469
470impl Color {
471    pub const BLACK: Color = Color {
472        r: 0.0,
473        g: 0.0,
474        b: 0.0,
475        a: 1.0,
476    };
477    pub const WHITE: Color = Color {
478        r: 1.0,
479        g: 1.0,
480        b: 1.0,
481        a: 1.0,
482    };
483    pub const TRANSPARENT: Color = Color {
484        r: 0.0,
485        g: 0.0,
486        b: 0.0,
487        a: 0.0,
488    };
489
490    pub fn rgb(r: f64, g: f64, b: f64) -> Self {
491        Self { r, g, b, a: 1.0 }
492    }
493
494    pub fn hex(hex: &str) -> Self {
495        let hex = hex.trim_start_matches('#');
496        let (r, g, b) = match hex.len() {
497            3 => {
498                let r = u8::from_str_radix(&hex[0..1].repeat(2), 16).unwrap_or(0);
499                let g = u8::from_str_radix(&hex[1..2].repeat(2), 16).unwrap_or(0);
500                let b = u8::from_str_radix(&hex[2..3].repeat(2), 16).unwrap_or(0);
501                (r, g, b)
502            }
503            6 => {
504                let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0);
505                let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0);
506                let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0);
507                (r, g, b)
508            }
509            _ => (0, 0, 0),
510        };
511        Self {
512            r: r as f64 / 255.0,
513            g: g as f64 / 255.0,
514            b: b as f64 / 255.0,
515            a: 1.0,
516        }
517    }
518}
519
520impl Default for Color {
521    fn default() -> Self {
522        Color::BLACK
523    }
524}
525
526/// Values for each edge (top, right, bottom, left).
527#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
528pub struct EdgeValues<T: Copy> {
529    pub top: T,
530    pub right: T,
531    pub bottom: T,
532    pub left: T,
533}
534
535impl<T: Copy> EdgeValues<T> {
536    pub fn uniform(v: T) -> Self {
537        Self {
538            top: v,
539            right: v,
540            bottom: v,
541            left: v,
542        }
543    }
544}
545
546/// Values for each corner (top-left, top-right, bottom-right, bottom-left).
547#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
548pub struct CornerValues {
549    pub top_left: f64,
550    pub top_right: f64,
551    pub bottom_right: f64,
552    pub bottom_left: f64,
553}
554
555impl CornerValues {
556    pub fn uniform(v: f64) -> Self {
557        Self {
558            top_left: v,
559            top_right: v,
560            bottom_right: v,
561            bottom_left: v,
562        }
563    }
564}
565
566/// Resolved style: all values are concrete (no Option, no Auto for computed values).
567/// This is what the layout engine works with after style resolution.
568#[derive(Debug, Clone)]
569pub struct ResolvedStyle {
570    // Box model
571    pub width: SizeConstraint,
572    pub height: SizeConstraint,
573    pub min_width: f64,
574    pub min_height: f64,
575    pub max_width: f64,
576    pub max_height: f64,
577    pub padding: Edges,
578    pub margin: MarginEdges,
579
580    // Display
581    pub display: Display,
582
583    // Flex
584    pub flex_direction: FlexDirection,
585    pub justify_content: JustifyContent,
586    pub align_items: AlignItems,
587    pub align_self: Option<AlignItems>,
588    pub flex_wrap: FlexWrap,
589    pub align_content: AlignContent,
590    pub flex_grow: f64,
591    pub flex_shrink: f64,
592    pub flex_basis: SizeConstraint,
593    pub gap: f64,
594    pub row_gap: f64,
595    pub column_gap: f64,
596
597    // Grid
598    pub grid_template_columns: Option<Vec<GridTrackSize>>,
599    pub grid_template_rows: Option<Vec<GridTrackSize>>,
600    pub grid_auto_rows: Option<GridTrackSize>,
601    pub grid_auto_columns: Option<GridTrackSize>,
602    pub grid_placement: Option<GridPlacement>,
603
604    // Text
605    pub font_family: String,
606    pub font_size: f64,
607    pub font_weight: u32,
608    pub font_style: FontStyle,
609    pub line_height: f64,
610    pub text_align: TextAlign,
611    pub letter_spacing: f64,
612    pub word_spacing: f64,
613    pub text_decoration: TextDecoration,
614    pub text_transform: TextTransform,
615    pub hyphens: Hyphens,
616    pub lang: Option<String>,
617    pub direction: Direction,
618    pub text_overflow: TextOverflow,
619    pub line_breaking: LineBreaking,
620
621    // Visual
622    pub color: Color,
623    pub background_color: Option<Color>,
624    pub opacity: f64,
625    pub overflow: Overflow,
626    pub border_width: Edges,
627    pub border_color: EdgeValues<Color>,
628    pub border_radius: CornerValues,
629    pub box_shadow: Option<BoxShadow>,
630    pub background: Option<Background>,
631
632    // Positioning
633    pub position: Position,
634    pub top: Option<f64>,
635    pub right: Option<f64>,
636    pub bottom: Option<f64>,
637    pub left: Option<f64>,
638
639    // Page behavior
640    pub breakable: bool,
641    pub break_before: bool,
642    pub min_widow_lines: u32,
643    pub min_orphan_lines: u32,
644
645    // Transform (paint-only — layout unaffected)
646    pub transform: Vec<TransformOp>,
647    /// Transform origin as fractions of the element box (0.0–1.0).
648    /// `(0.5, 0.5)` is the element center, matching CSS default.
649    pub transform_origin: (f64, f64),
650}
651
652#[derive(Debug, Clone, Copy)]
653pub enum SizeConstraint {
654    Fixed(f64),
655    Auto,
656}
657
658impl Style {
659    /// Resolve this style against a parent's resolved style and available dimensions.
660    pub fn resolve(&self, parent: Option<&ResolvedStyle>, available_width: f64) -> ResolvedStyle {
661        let parent_font_size = parent.map(|p| p.font_size).unwrap_or(12.0);
662        let parent_color = parent.map(|p| p.color).unwrap_or(Color::BLACK);
663        let parent_font_family = parent
664            .map(|p| p.font_family.clone())
665            .unwrap_or_else(|| "Helvetica".to_string());
666
667        let font_size = self.font_size.unwrap_or(parent_font_size);
668
669        ResolvedStyle {
670            width: self
671                .width
672                .map(|d| match d {
673                    Dimension::Pt(v) => SizeConstraint::Fixed(v),
674                    Dimension::Percent(p) => SizeConstraint::Fixed(available_width * p / 100.0),
675                    Dimension::Auto => SizeConstraint::Auto,
676                })
677                .unwrap_or(SizeConstraint::Auto),
678
679            height: self
680                .height
681                .map(|d| match d {
682                    Dimension::Pt(v) => SizeConstraint::Fixed(v),
683                    Dimension::Percent(p) => SizeConstraint::Fixed(p), // height % is complex, simplified
684                    Dimension::Auto => SizeConstraint::Auto,
685                })
686                .unwrap_or(SizeConstraint::Auto),
687
688            min_width: self
689                .min_width
690                .and_then(|d| d.resolve(available_width))
691                .unwrap_or(0.0),
692            min_height: self.min_height.and_then(|d| d.resolve(0.0)).unwrap_or(0.0),
693            max_width: self
694                .max_width
695                .and_then(|d| d.resolve(available_width))
696                .unwrap_or(f64::INFINITY),
697            max_height: self
698                .max_height
699                .and_then(|d| d.resolve(0.0))
700                .unwrap_or(f64::INFINITY),
701
702            padding: self.padding.unwrap_or_default(),
703            margin: self.margin.unwrap_or_default(),
704
705            display: self.display.unwrap_or_default(),
706
707            flex_direction: self.flex_direction.unwrap_or_default(),
708            justify_content: self.justify_content.unwrap_or_default(),
709            align_items: self.align_items.unwrap_or_default(),
710            align_self: self.align_self,
711            flex_wrap: self.flex_wrap.unwrap_or_default(),
712            align_content: self.align_content.unwrap_or_default(),
713            flex_grow: self.flex_grow.unwrap_or(0.0),
714            flex_shrink: self.flex_shrink.unwrap_or(1.0),
715            flex_basis: self
716                .flex_basis
717                .map(|d| match d {
718                    Dimension::Pt(v) => SizeConstraint::Fixed(v),
719                    Dimension::Percent(p) => SizeConstraint::Fixed(available_width * p / 100.0),
720                    Dimension::Auto => SizeConstraint::Auto,
721                })
722                .unwrap_or(SizeConstraint::Auto),
723            gap: self.gap.unwrap_or(0.0),
724            row_gap: self.row_gap.or(self.gap).unwrap_or(0.0),
725            column_gap: self.column_gap.or(self.gap).unwrap_or(0.0),
726
727            grid_template_columns: self.grid_template_columns.clone(),
728            grid_template_rows: self.grid_template_rows.clone(),
729            grid_auto_rows: self.grid_auto_rows.clone(),
730            grid_auto_columns: self.grid_auto_columns.clone(),
731            grid_placement: self.grid_placement.clone(),
732
733            font_family: self.font_family.clone().unwrap_or(parent_font_family),
734            font_size,
735            font_weight: self
736                .font_weight
737                .unwrap_or(parent.map(|p| p.font_weight).unwrap_or(400)),
738            font_style: self
739                .font_style
740                .unwrap_or(parent.map(|p| p.font_style).unwrap_or_default()),
741            line_height: self
742                .line_height
743                .unwrap_or(parent.map(|p| p.line_height).unwrap_or(1.4)),
744            text_align: {
745                let direction = self
746                    .direction
747                    .unwrap_or(parent.map(|p| p.direction).unwrap_or_default());
748                self.text_align.unwrap_or_else(|| {
749                    if matches!(direction, Direction::Rtl) {
750                        TextAlign::Right
751                    } else {
752                        parent.map(|p| p.text_align).unwrap_or_default()
753                    }
754                })
755            },
756            letter_spacing: self.letter_spacing.unwrap_or(0.0),
757            word_spacing: self.word_spacing.unwrap_or(0.0),
758            text_decoration: self
759                .text_decoration
760                .unwrap_or(parent.map(|p| p.text_decoration).unwrap_or_default()),
761            text_transform: self
762                .text_transform
763                .unwrap_or(parent.map(|p| p.text_transform).unwrap_or_default()),
764            hyphens: self
765                .hyphens
766                .unwrap_or(parent.map(|p| p.hyphens).unwrap_or_default()),
767            lang: self
768                .lang
769                .clone()
770                .or_else(|| parent.and_then(|p| p.lang.clone())),
771            direction: self
772                .direction
773                .unwrap_or(parent.map(|p| p.direction).unwrap_or_default()),
774            text_overflow: self.text_overflow.unwrap_or_default(),
775            line_breaking: self
776                .line_breaking
777                .unwrap_or(parent.map(|p| p.line_breaking).unwrap_or_default()),
778
779            color: self.color.unwrap_or(parent_color),
780            background_color: self.background_color,
781            opacity: self.opacity.unwrap_or(1.0),
782            overflow: self.overflow.unwrap_or_default(),
783            box_shadow: self.box_shadow,
784            background: self.background.clone(),
785
786            border_width: self
787                .border_width
788                .map(|e| Edges {
789                    top: e.top,
790                    right: e.right,
791                    bottom: e.bottom,
792                    left: e.left,
793                })
794                .unwrap_or_default(),
795
796            border_color: self
797                .border_color
798                .unwrap_or(EdgeValues::uniform(Color::BLACK)),
799            border_radius: self.border_radius.unwrap_or(CornerValues::uniform(0.0)),
800
801            position: self.position.unwrap_or_default(),
802            top: self.top,
803            right: self.right,
804            bottom: self.bottom,
805            left: self.left,
806
807            breakable: self.wrap.unwrap_or(true),
808            break_before: self.break_before.unwrap_or(false),
809            min_widow_lines: self.min_widow_lines.unwrap_or(2),
810            min_orphan_lines: self.min_orphan_lines.unwrap_or(2),
811
812            // Transform doesn't inherit — explicit per-element only.
813            transform: self.transform.clone().unwrap_or_default(),
814            transform_origin: self.transform_origin.unwrap_or((0.5, 0.5)),
815        }
816    }
817}