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