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
178/// A dimension that can be points, percentage, or auto.
179#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
180pub enum Dimension {
181    /// Fixed size in points (1/72 inch).
182    Pt(f64),
183    /// Percentage of parent's corresponding dimension.
184    Percent(f64),
185    /// Size determined by content.
186    Auto,
187}
188
189impl Dimension {
190    /// Resolve this dimension given a parent size.
191    /// Returns None for Auto.
192    pub fn resolve(&self, parent_size: f64) -> Option<f64> {
193        match self {
194            Dimension::Pt(v) => Some(*v),
195            Dimension::Percent(p) => Some(parent_size * p / 100.0),
196            Dimension::Auto => None,
197        }
198    }
199}
200
201/// Layout display mode.
202#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
203pub enum Display {
204    /// Flexbox layout (default).
205    #[default]
206    Flex,
207    /// CSS Grid layout.
208    Grid,
209}
210
211/// A single grid track size definition.
212#[derive(Debug, Clone, Serialize, Deserialize)]
213pub enum GridTrackSize {
214    /// Fixed size in points.
215    Pt(f64),
216    /// Fractional unit (distributes remaining space proportionally).
217    Fr(f64),
218    /// Size determined by content.
219    Auto,
220    /// Clamped between min and max.
221    MinMax(Box<GridTrackSize>, Box<GridTrackSize>),
222}
223
224/// Grid item placement.
225#[derive(Debug, Clone, Default, Serialize, Deserialize)]
226#[serde(rename_all = "camelCase")]
227pub struct GridPlacement {
228    /// Column start line (1-based).
229    pub column_start: Option<i32>,
230    /// Column end line (1-based).
231    pub column_end: Option<i32>,
232    /// Row start line (1-based).
233    pub row_start: Option<i32>,
234    /// Row end line (1-based).
235    pub row_end: Option<i32>,
236    /// Number of columns to span.
237    pub column_span: Option<u32>,
238    /// Number of rows to span.
239    pub row_span: Option<u32>,
240}
241
242#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
243pub enum FlexDirection {
244    #[default]
245    Column,
246    Row,
247    ColumnReverse,
248    RowReverse,
249}
250
251#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
252pub enum JustifyContent {
253    #[default]
254    FlexStart,
255    FlexEnd,
256    Center,
257    SpaceBetween,
258    SpaceAround,
259    SpaceEvenly,
260}
261
262#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
263pub enum AlignItems {
264    FlexStart,
265    FlexEnd,
266    Center,
267    #[default]
268    Stretch,
269    Baseline,
270}
271
272#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
273pub enum FlexWrap {
274    #[default]
275    NoWrap,
276    Wrap,
277    WrapReverse,
278}
279
280#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
281pub enum AlignContent {
282    #[default]
283    FlexStart,
284    FlexEnd,
285    Center,
286    SpaceBetween,
287    SpaceAround,
288    SpaceEvenly,
289    Stretch,
290}
291
292#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
293pub enum FontStyle {
294    #[default]
295    Normal,
296    Italic,
297    Oblique,
298}
299
300#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
301pub enum TextAlign {
302    #[default]
303    Left,
304    Right,
305    Center,
306    Justify,
307}
308
309#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
310pub enum TextDecoration {
311    #[default]
312    None,
313    Underline,
314    LineThrough,
315}
316
317#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
318pub enum TextTransform {
319    #[default]
320    None,
321    Uppercase,
322    Lowercase,
323    Capitalize,
324}
325
326/// Overflow behavior for container elements.
327#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
328pub enum Overflow {
329    /// Content can overflow the container bounds (default).
330    #[default]
331    Visible,
332    /// Content is clipped to the container bounds.
333    Hidden,
334}
335
336/// Text overflow behavior when text exceeds available width.
337#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
338pub enum TextOverflow {
339    /// Normal wrapping (default).
340    #[default]
341    Wrap,
342    /// Single-line truncation with "..." appended.
343    Ellipsis,
344    /// Single-line truncation without any indicator.
345    Clip,
346}
347
348/// Text direction for BiDi support.
349#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
350#[serde(rename_all = "lowercase")]
351pub enum Direction {
352    /// Left-to-right (default).
353    #[default]
354    Ltr,
355    /// Right-to-left (Arabic, Hebrew).
356    Rtl,
357    /// Auto-detect from first strong character.
358    Auto,
359}
360
361/// Line breaking algorithm.
362#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
363#[serde(rename_all = "lowercase")]
364pub enum LineBreaking {
365    /// Knuth-Plass optimal line breaking (default). Minimizes global raggedness.
366    #[default]
367    Optimal,
368    /// Simple greedy line breaking. Fills lines left-to-right, breaks at first overflow.
369    Greedy,
370}
371
372/// CSS `hyphens` property controlling word hyphenation.
373#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
374#[serde(rename_all = "lowercase")]
375pub enum Hyphens {
376    /// No hyphenation, not even at soft hyphens.
377    None,
378    /// Only break at soft hyphens (U+00AD) in the text.
379    #[default]
380    Manual,
381    /// Algorithmic hyphenation using language rules.
382    Auto,
383}
384
385/// An RGBA color.
386#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
387pub struct Color {
388    pub r: f64, // 0.0 - 1.0
389    pub g: f64,
390    pub b: f64,
391    pub a: f64,
392}
393
394/// CSS-style drop shadow. v1 paints a filled rectangle offset behind the
395/// element with the given color; the `blur` field is parsed for forward
396/// compatibility but ignored. Honors `border_radius` for rounded shadows.
397#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
398#[serde(rename_all = "camelCase")]
399pub struct BoxShadow {
400    pub offset_x: f64,
401    pub offset_y: f64,
402    /// Currently ignored (no blur in v1).
403    #[serde(default)]
404    pub blur: f64,
405    pub color: Color,
406}
407
408/// CSS-style background that may be a solid color or a gradient. The
409/// React layer parses CSS strings (`linear-gradient(180deg, #fff, #000)`,
410/// `radial-gradient(circle, #fff, #000)`, `#abcdef`) into this shape.
411///
412/// v1 supports 2-stop gradients via PDF Type 2 (axial) and Type 3
413/// (radial) shading dictionaries. Multi-stop gradients fall back to the
414/// first and last stop in v1; full stitching is a v2 task.
415#[derive(Debug, Clone, Serialize, Deserialize)]
416#[serde(tag = "type", rename_all = "camelCase")]
417pub enum Background {
418    Color(Color),
419    Linear(LinearGradient),
420    Radial(RadialGradient),
421}
422
423#[derive(Debug, Clone, Serialize, Deserialize)]
424#[serde(rename_all = "camelCase")]
425pub struct LinearGradient {
426    /// CSS angle in degrees. 0deg = bottom-to-top, 90deg = left-to-right,
427    /// 180deg = top-to-bottom (CSS clockwise-from-up convention).
428    pub angle_deg: f64,
429    pub stops: Vec<GradientStop>,
430}
431
432#[derive(Debug, Clone, Serialize, Deserialize)]
433#[serde(rename_all = "camelCase")]
434pub struct RadialGradient {
435    pub stops: Vec<GradientStop>,
436}
437
438#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
439#[serde(rename_all = "camelCase")]
440pub struct GradientStop {
441    /// Position along the gradient axis (0.0 to 1.0).
442    pub position: f64,
443    pub color: Color,
444}
445
446impl Color {
447    pub const BLACK: Color = Color {
448        r: 0.0,
449        g: 0.0,
450        b: 0.0,
451        a: 1.0,
452    };
453    pub const WHITE: Color = Color {
454        r: 1.0,
455        g: 1.0,
456        b: 1.0,
457        a: 1.0,
458    };
459    pub const TRANSPARENT: Color = Color {
460        r: 0.0,
461        g: 0.0,
462        b: 0.0,
463        a: 0.0,
464    };
465
466    pub fn rgb(r: f64, g: f64, b: f64) -> Self {
467        Self { r, g, b, a: 1.0 }
468    }
469
470    pub fn hex(hex: &str) -> Self {
471        let hex = hex.trim_start_matches('#');
472        let (r, g, b) = match hex.len() {
473            3 => {
474                let r = u8::from_str_radix(&hex[0..1].repeat(2), 16).unwrap_or(0);
475                let g = u8::from_str_radix(&hex[1..2].repeat(2), 16).unwrap_or(0);
476                let b = u8::from_str_radix(&hex[2..3].repeat(2), 16).unwrap_or(0);
477                (r, g, b)
478            }
479            6 => {
480                let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0);
481                let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0);
482                let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0);
483                (r, g, b)
484            }
485            _ => (0, 0, 0),
486        };
487        Self {
488            r: r as f64 / 255.0,
489            g: g as f64 / 255.0,
490            b: b as f64 / 255.0,
491            a: 1.0,
492        }
493    }
494}
495
496impl Default for Color {
497    fn default() -> Self {
498        Color::BLACK
499    }
500}
501
502/// Values for each edge (top, right, bottom, left).
503#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
504pub struct EdgeValues<T: Copy> {
505    pub top: T,
506    pub right: T,
507    pub bottom: T,
508    pub left: T,
509}
510
511impl<T: Copy> EdgeValues<T> {
512    pub fn uniform(v: T) -> Self {
513        Self {
514            top: v,
515            right: v,
516            bottom: v,
517            left: v,
518        }
519    }
520}
521
522/// Values for each corner (top-left, top-right, bottom-right, bottom-left).
523#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
524pub struct CornerValues {
525    pub top_left: f64,
526    pub top_right: f64,
527    pub bottom_right: f64,
528    pub bottom_left: f64,
529}
530
531impl CornerValues {
532    pub fn uniform(v: f64) -> Self {
533        Self {
534            top_left: v,
535            top_right: v,
536            bottom_right: v,
537            bottom_left: v,
538        }
539    }
540}
541
542/// Resolved style: all values are concrete (no Option, no Auto for computed values).
543/// This is what the layout engine works with after style resolution.
544#[derive(Debug, Clone)]
545pub struct ResolvedStyle {
546    // Box model
547    pub width: SizeConstraint,
548    pub height: SizeConstraint,
549    pub min_width: f64,
550    pub min_height: f64,
551    pub max_width: f64,
552    pub max_height: f64,
553    pub padding: Edges,
554    pub margin: MarginEdges,
555
556    // Display
557    pub display: Display,
558
559    // Flex
560    pub flex_direction: FlexDirection,
561    pub justify_content: JustifyContent,
562    pub align_items: AlignItems,
563    pub align_self: Option<AlignItems>,
564    pub flex_wrap: FlexWrap,
565    pub align_content: AlignContent,
566    pub flex_grow: f64,
567    pub flex_shrink: f64,
568    pub flex_basis: SizeConstraint,
569    pub gap: f64,
570    pub row_gap: f64,
571    pub column_gap: f64,
572
573    // Grid
574    pub grid_template_columns: Option<Vec<GridTrackSize>>,
575    pub grid_template_rows: Option<Vec<GridTrackSize>>,
576    pub grid_auto_rows: Option<GridTrackSize>,
577    pub grid_auto_columns: Option<GridTrackSize>,
578    pub grid_placement: Option<GridPlacement>,
579
580    // Text
581    pub font_family: String,
582    pub font_size: f64,
583    pub font_weight: u32,
584    pub font_style: FontStyle,
585    pub line_height: f64,
586    pub text_align: TextAlign,
587    pub letter_spacing: f64,
588    pub word_spacing: f64,
589    pub text_decoration: TextDecoration,
590    pub text_transform: TextTransform,
591    pub hyphens: Hyphens,
592    pub lang: Option<String>,
593    pub direction: Direction,
594    pub text_overflow: TextOverflow,
595    pub line_breaking: LineBreaking,
596
597    // Visual
598    pub color: Color,
599    pub background_color: Option<Color>,
600    pub opacity: f64,
601    pub overflow: Overflow,
602    pub border_width: Edges,
603    pub border_color: EdgeValues<Color>,
604    pub border_radius: CornerValues,
605    pub box_shadow: Option<BoxShadow>,
606    pub background: Option<Background>,
607
608    // Positioning
609    pub position: Position,
610    pub top: Option<f64>,
611    pub right: Option<f64>,
612    pub bottom: Option<f64>,
613    pub left: Option<f64>,
614
615    // Page behavior
616    pub breakable: bool,
617    pub break_before: bool,
618    pub min_widow_lines: u32,
619    pub min_orphan_lines: u32,
620}
621
622#[derive(Debug, Clone, Copy)]
623pub enum SizeConstraint {
624    Fixed(f64),
625    Auto,
626}
627
628impl Style {
629    /// Resolve this style against a parent's resolved style and available dimensions.
630    pub fn resolve(&self, parent: Option<&ResolvedStyle>, available_width: f64) -> ResolvedStyle {
631        let parent_font_size = parent.map(|p| p.font_size).unwrap_or(12.0);
632        let parent_color = parent.map(|p| p.color).unwrap_or(Color::BLACK);
633        let parent_font_family = parent
634            .map(|p| p.font_family.clone())
635            .unwrap_or_else(|| "Helvetica".to_string());
636
637        let font_size = self.font_size.unwrap_or(parent_font_size);
638
639        ResolvedStyle {
640            width: self
641                .width
642                .map(|d| match d {
643                    Dimension::Pt(v) => SizeConstraint::Fixed(v),
644                    Dimension::Percent(p) => SizeConstraint::Fixed(available_width * p / 100.0),
645                    Dimension::Auto => SizeConstraint::Auto,
646                })
647                .unwrap_or(SizeConstraint::Auto),
648
649            height: self
650                .height
651                .map(|d| match d {
652                    Dimension::Pt(v) => SizeConstraint::Fixed(v),
653                    Dimension::Percent(p) => SizeConstraint::Fixed(p), // height % is complex, simplified
654                    Dimension::Auto => SizeConstraint::Auto,
655                })
656                .unwrap_or(SizeConstraint::Auto),
657
658            min_width: self
659                .min_width
660                .and_then(|d| d.resolve(available_width))
661                .unwrap_or(0.0),
662            min_height: self.min_height.and_then(|d| d.resolve(0.0)).unwrap_or(0.0),
663            max_width: self
664                .max_width
665                .and_then(|d| d.resolve(available_width))
666                .unwrap_or(f64::INFINITY),
667            max_height: self
668                .max_height
669                .and_then(|d| d.resolve(0.0))
670                .unwrap_or(f64::INFINITY),
671
672            padding: self.padding.unwrap_or_default(),
673            margin: self.margin.unwrap_or_default(),
674
675            display: self.display.unwrap_or_default(),
676
677            flex_direction: self.flex_direction.unwrap_or_default(),
678            justify_content: self.justify_content.unwrap_or_default(),
679            align_items: self.align_items.unwrap_or_default(),
680            align_self: self.align_self,
681            flex_wrap: self.flex_wrap.unwrap_or_default(),
682            align_content: self.align_content.unwrap_or_default(),
683            flex_grow: self.flex_grow.unwrap_or(0.0),
684            flex_shrink: self.flex_shrink.unwrap_or(1.0),
685            flex_basis: self
686                .flex_basis
687                .map(|d| match d {
688                    Dimension::Pt(v) => SizeConstraint::Fixed(v),
689                    Dimension::Percent(p) => SizeConstraint::Fixed(available_width * p / 100.0),
690                    Dimension::Auto => SizeConstraint::Auto,
691                })
692                .unwrap_or(SizeConstraint::Auto),
693            gap: self.gap.unwrap_or(0.0),
694            row_gap: self.row_gap.or(self.gap).unwrap_or(0.0),
695            column_gap: self.column_gap.or(self.gap).unwrap_or(0.0),
696
697            grid_template_columns: self.grid_template_columns.clone(),
698            grid_template_rows: self.grid_template_rows.clone(),
699            grid_auto_rows: self.grid_auto_rows.clone(),
700            grid_auto_columns: self.grid_auto_columns.clone(),
701            grid_placement: self.grid_placement.clone(),
702
703            font_family: self.font_family.clone().unwrap_or(parent_font_family),
704            font_size,
705            font_weight: self
706                .font_weight
707                .unwrap_or(parent.map(|p| p.font_weight).unwrap_or(400)),
708            font_style: self
709                .font_style
710                .unwrap_or(parent.map(|p| p.font_style).unwrap_or_default()),
711            line_height: self
712                .line_height
713                .unwrap_or(parent.map(|p| p.line_height).unwrap_or(1.4)),
714            text_align: {
715                let direction = self
716                    .direction
717                    .unwrap_or(parent.map(|p| p.direction).unwrap_or_default());
718                self.text_align.unwrap_or_else(|| {
719                    if matches!(direction, Direction::Rtl) {
720                        TextAlign::Right
721                    } else {
722                        parent.map(|p| p.text_align).unwrap_or_default()
723                    }
724                })
725            },
726            letter_spacing: self.letter_spacing.unwrap_or(0.0),
727            word_spacing: self.word_spacing.unwrap_or(0.0),
728            text_decoration: self
729                .text_decoration
730                .unwrap_or(parent.map(|p| p.text_decoration).unwrap_or_default()),
731            text_transform: self
732                .text_transform
733                .unwrap_or(parent.map(|p| p.text_transform).unwrap_or_default()),
734            hyphens: self
735                .hyphens
736                .unwrap_or(parent.map(|p| p.hyphens).unwrap_or_default()),
737            lang: self
738                .lang
739                .clone()
740                .or_else(|| parent.and_then(|p| p.lang.clone())),
741            direction: self
742                .direction
743                .unwrap_or(parent.map(|p| p.direction).unwrap_or_default()),
744            text_overflow: self.text_overflow.unwrap_or_default(),
745            line_breaking: self
746                .line_breaking
747                .unwrap_or(parent.map(|p| p.line_breaking).unwrap_or_default()),
748
749            color: self.color.unwrap_or(parent_color),
750            background_color: self.background_color,
751            opacity: self.opacity.unwrap_or(1.0),
752            overflow: self.overflow.unwrap_or_default(),
753            box_shadow: self.box_shadow,
754            background: self.background.clone(),
755
756            border_width: self
757                .border_width
758                .map(|e| Edges {
759                    top: e.top,
760                    right: e.right,
761                    bottom: e.bottom,
762                    left: e.left,
763                })
764                .unwrap_or_default(),
765
766            border_color: self
767                .border_color
768                .unwrap_or(EdgeValues::uniform(Color::BLACK)),
769            border_radius: self.border_radius.unwrap_or(CornerValues::uniform(0.0)),
770
771            position: self.position.unwrap_or_default(),
772            top: self.top,
773            right: self.right,
774            bottom: self.bottom,
775            left: self.left,
776
777            breakable: self.wrap.unwrap_or(true),
778            break_before: self.break_before.unwrap_or(false),
779            min_widow_lines: self.min_widow_lines.unwrap_or(2),
780            min_orphan_lines: self.min_orphan_lines.unwrap_or(2),
781        }
782    }
783}