Skip to main content

azul_layout/solver3/
geometry.rs

1//! Box model geometry types and writing-mode support for the layout solver.
2//!
3//! Provides edge-size types (`EdgeSizes`, `ResolvedBoxProps`, `PackedBoxProps`),
4//! CSS value resolution (`UnresolvedMargin`, `UnresolvedEdge`, `ResolutionParams`),
5//! intrinsic sizing (`IntrinsicSizes`), and writing-mode context (`WritingModeContext`).
6
7use azul_core::{
8    geom::{LogicalPosition, LogicalRect, LogicalSize},
9    ui_solver::ResolvedOffsets,
10};
11use azul_css::props::{
12    basic::{pixel::PixelValue, PhysicalSize, PropertyContext, ResolutionContext, SizeMetric},
13    layout::LayoutWritingMode,
14    style::{StyleDirection, StyleTextOrientation},
15};
16
17#[derive(Copy, Debug, Clone, PartialEq, PartialOrd)]
18pub struct PositionedRectangle {
19    /// The outer bounds of the rectangle
20    pub bounds: LogicalRect,
21    /// Margin of the rectangle.
22    pub margin: ResolvedOffsets,
23    /// Border widths of the rectangle.
24    pub border: ResolvedOffsets,
25    /// Padding of the rectangle.
26    pub padding: ResolvedOffsets,
27}
28
29// +spec:box-model:83b3b8 - Box dimensions: content area with optional padding, border, margin areas
30/// Represents the four edges of a box for properties like margin, padding, border.
31// +spec:box-model:3b155c - "4 values assigned to sides" pattern (top, right, bottom, left) matching margin/inset shorthands
32// +spec:width-calculation:37f9e7 - CSS 2.2 §8.1 box dimensions: content, padding, border, margin areas with top/right/bottom/left segments
33#[derive(Debug, Clone, Copy, Default)]
34pub struct EdgeSizes {
35    pub top: f32,
36    pub right: f32,
37    pub bottom: f32,
38    pub left: f32,
39}
40
41impl EdgeSizes {
42    /// Sum of horizontal edges (left + right).
43    #[must_use] pub fn horizontal_sum(&self) -> f32 {
44        self.left + self.right
45    }
46
47    /// Sum of vertical edges (top + bottom).
48    #[must_use] pub fn vertical_sum(&self) -> f32 {
49        self.top + self.bottom
50    }
51
52    // +spec:block-formatting-context:440282 - vertical writing modes use analogous layout via main/cross axis abstraction
53    // +spec:block-formatting-context:a49f9e - line-relative directions mapped via writing mode
54    // +spec:block-formatting-context:387117 - writing-mode property maps block flow to vertical/horizontal axes
55    // +spec:box-model:4c01a3 - dimensional mapping: main=block axis, cross=inline axis per writing mode
56    // +spec:box-model:4c1a9f - physical-to-logical mapping of margin/padding/border for vertical writing modes
57    // +spec:box-model:9414ab - flow-relative mapping of box edges (margin/padding/border) per writing mode
58    // +spec:inline-formatting-context:2de457 - block/inline dimension mapping via writing mode
59    // +spec:inline-formatting-context:c6b91e - line-relative "over"/"under" mapped to physical top/bottom via writing mode
60    // +spec:writing-modes:00a918 - Abstract-to-physical mappings for block/inline to top/right/bottom/left
61    // +spec:writing-modes:14e6f0 - block-start/end depend only on writing-mode; inline-start/end also depend on direction (handled in positioning.rs)
62    // +spec:writing-modes:1c2101 - Abstract directional terms (top/right/bottom/left) to logical axes (main/cross) based on writing-mode
63    // +spec:writing-modes:1c5155 - line-relative mappings: over/under/line-left/line-right → top/bottom/left/right in horizontal-tb
64    // +spec:writing-modes:70daf1 - block/inline axis mapping per writing-mode for edge sizes
65    // +spec:writing-modes:f9af71 - flow-relative directions: block-start/end and inline-start/end mapped to physical edges
66    // +spec:writing-modes:60b023 - abstract-to-physical mapping: block axis = main, inline axis = cross
67    // +spec:writing-modes:829cd7 - flow-relative directions: block-start/end from writing-mode, inline-start/end from writing-mode+direction
68    // +spec:writing-modes:a2113d - block/inline axis mapping for writing modes (block-axis, inline-axis, block-start/end, inline-start/end)
69    // +spec:writing-modes:c0ae9c - abstract directional mappings from writing-mode/direction
70    // +spec:writing-modes:c91130 - Abstract box terminology: block/inline axis mapping per writing-mode
71    // +spec:writing-modes:cd31ce - flow-relative directions mapped to physical via writing mode
72    // +spec:writing-modes:fd8c18 - block/inline axis mapping based on writing mode
73    // +spec:writing-modes:0e549a - writing-mode computed value influences physical/logical axis mapping
74    /// Returns the size of the edge at the start of the main/block axis.
75    #[must_use] pub const fn main_start(&self, wm: LayoutWritingMode) -> f32 {
76        match wm {
77            LayoutWritingMode::HorizontalTb => self.top,
78            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.left,
79        }
80    }
81
82    /// Returns the size of the edge at the end of the main/block axis.
83    #[must_use] pub const fn main_end(&self, wm: LayoutWritingMode) -> f32 {
84        match wm {
85            LayoutWritingMode::HorizontalTb => self.bottom,
86            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.right,
87        }
88    }
89
90    /// Returns the sum of the start and end sizes on the main/block axis.
91    #[must_use] pub fn main_sum(&self, wm: LayoutWritingMode) -> f32 {
92        self.main_start(wm) + self.main_end(wm)
93    }
94
95    // +spec:block-formatting-context:6225cb - line-relative directions: vertical modes map line-over/under to top/bottom
96    /// Returns the size of the edge at the start of the cross/inline axis.
97    #[must_use] pub const fn cross_start(&self, wm: LayoutWritingMode) -> f32 {
98        match wm {
99            LayoutWritingMode::HorizontalTb => self.left,
100            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.top,
101        }
102    }
103
104    /// Returns the size of the edge at the end of the cross/inline axis.
105    #[must_use] pub const fn cross_end(&self, wm: LayoutWritingMode) -> f32 {
106        match wm {
107            LayoutWritingMode::HorizontalTb => self.right,
108            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.bottom,
109        }
110    }
111
112    /// Returns the sum of the start and end sizes on the cross/inline axis.
113    #[must_use] pub fn cross_sum(&self, wm: LayoutWritingMode) -> f32 {
114        self.cross_start(wm) + self.cross_end(wm)
115    }
116}
117
118// ============================================================================
119// UNRESOLVED VALUE TYPES (for lazy resolution during layout)
120// ============================================================================
121
122/// An unresolved CSS margin value.
123// +spec:box-model:ff1730 - margin properties apply to both continuous and paged media
124///
125/// Margins can be `auto` (for centering) or a length value that needs
126/// resolution against the containing block.
127#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
128pub enum UnresolvedMargin {
129    /// margin: 0 (default)
130    #[default]
131    Zero,
132    /// margin: auto (for centering, CSS 2.2 § 10.3.3)
133    Auto,
134    /// A length value (px, %, em, vh, etc.)
135    Length(PixelValue),
136}
137
138impl UnresolvedMargin {
139    /// Returns true if this is an auto margin
140    #[must_use] pub const fn is_auto(&self) -> bool {
141        matches!(self, Self::Auto)
142    }
143
144    /// Resolve this margin value to pixels.
145    ///
146    /// - `Auto` returns 0.0 (actual auto margin calculation happens in layout)
147    /// - `Zero` returns 0.0
148    /// - `Length` is resolved using the resolution context
149    #[allow(clippy::match_same_arms)] // enum/value mapping/dispatch table: one arm per input variant (or cross-type bindings that can't merge)
150    #[must_use] pub fn resolve(&self, ctx: &ResolutionContext) -> f32 {
151        match self {
152            Self::Zero => 0.0,
153            // +spec:box-model:c921aa - auto margin-top/bottom used value is 0 for block-level non-replaced elements in normal flow
154            // +spec:box-model:e25fdc - auto margins treated as zero for abspos size computation
155            Self::Auto => 0.0, // Auto is handled separately in layout
156            Self::Length(pv) => pv.resolve_with_context(ctx, PropertyContext::Margin),
157        }
158    }
159}
160
161/// Unresolved edge sizes for margin/padding/border.
162///
163/// This stores the raw CSS values before resolution, allowing us to
164/// defer resolution until the containing block size is known.
165#[derive(Debug, Clone, Copy, Default)]
166pub struct UnresolvedEdge<T> {
167    pub top: T,
168    pub right: T,
169    pub bottom: T,
170    pub left: T,
171}
172
173impl<T> UnresolvedEdge<T> {
174    pub const fn new(top: T, right: T, bottom: T, left: T) -> Self {
175        Self { top, right, bottom, left }
176    }
177}
178
179impl UnresolvedEdge<UnresolvedMargin> {
180    /// Resolve all margin edges to pixel values.
181    #[must_use] pub fn resolve(&self, ctx: &ResolutionContext) -> EdgeSizes {
182        EdgeSizes {
183            top: self.top.resolve(ctx),
184            right: self.right.resolve(ctx),
185            bottom: self.bottom.resolve(ctx),
186            left: self.left.resolve(ctx),
187        }
188    }
189
190    /// Extract which margins are set to `auto`.
191    #[must_use] pub const fn get_margin_auto(&self) -> MarginAuto {
192        MarginAuto {
193            top: self.top.is_auto(),
194            right: self.right.is_auto(),
195            bottom: self.bottom.is_auto(),
196            left: self.left.is_auto(),
197        }
198    }
199}
200
201impl UnresolvedEdge<PixelValue> {
202    /// Resolve all edges to pixel values.
203    #[must_use] pub fn resolve(&self, ctx: &ResolutionContext, prop_ctx: PropertyContext) -> EdgeSizes {
204        EdgeSizes {
205            top: self.top.resolve_with_context(ctx, prop_ctx),
206            right: self.right.resolve_with_context(ctx, prop_ctx),
207            bottom: self.bottom.resolve_with_context(ctx, prop_ctx),
208            left: self.left.resolve_with_context(ctx, prop_ctx),
209        }
210    }
211}
212
213/// Parameters needed to resolve CSS values to pixels.
214#[derive(Debug, Clone, Copy)]
215pub struct ResolutionParams {
216    // +spec:inline-formatting-context:26c933 - LogicalSize maps inline/block dimensions to physical width/height per writing mode
217    /// The containing block size (for % resolution)
218    pub containing_block: LogicalSize,
219    /// The viewport size (for vh/vw resolution)
220    pub viewport_size: LogicalSize,
221    /// The element's computed font-size (for em resolution)
222    pub element_font_size: f32,
223    /// The root element's font-size (for rem resolution)
224    pub root_font_size: f32,
225}
226
227impl ResolutionParams {
228    /// Create a `ResolutionContext` from these parameters.
229    #[must_use] pub const fn to_resolution_context(&self) -> ResolutionContext {
230        ResolutionContext {
231            element_font_size: self.element_font_size,
232            // For non-font properties, `em` resolves against the element's own
233            // computed font-size, so parent_font_size == element_font_size here.
234            // Do NOT use this context for font-size resolution itself.
235            parent_font_size: self.element_font_size,
236            root_font_size: self.root_font_size,
237            element_size: None,
238            containing_block_size: PhysicalSize::new(
239                self.containing_block.width,
240                self.containing_block.height,
241            ),
242            viewport_size: PhysicalSize::new(
243                self.viewport_size.width,
244                self.viewport_size.height,
245            ),
246        }
247    }
248}
249
250// ============================================================================
251// UNRESOLVED BOX PROPS (new design)
252// ============================================================================
253
254/// Box properties with unresolved CSS values.
255///
256/// This stores the raw CSS values as parsed, deferring resolution until
257/// layout time when the containing block size is known.
258#[derive(Debug, Clone, Copy, Default)]
259pub struct UnresolvedBoxProps {
260    pub margin: UnresolvedEdge<UnresolvedMargin>,
261    pub padding: UnresolvedEdge<PixelValue>,
262    pub border: UnresolvedEdge<PixelValue>,
263}
264
265impl UnresolvedBoxProps {
266    /// Resolve all box properties to pixel values.
267    #[must_use] pub fn resolve(&self, params: &ResolutionParams) -> ResolvedBoxProps {
268        let ctx = params.to_resolution_context();
269        ResolvedBoxProps {
270            margin: self.margin.resolve(&ctx),
271            padding: self.padding.resolve(&ctx, PropertyContext::Padding),
272            border: self.border.resolve(&ctx, PropertyContext::BorderWidth),
273            margin_auto: self.margin.get_margin_auto(),
274        }
275    }
276}
277
278// ============================================================================
279// RESOLVED BOX PROPS (legacy name: BoxProps)
280// ============================================================================
281
282/// Tracks which margins are set to `auto` (for centering calculations).
283#[derive(Debug, Clone, Copy, Default)]
284#[allow(clippy::struct_excessive_bools)] // one independent bool per margin edge (auto flags)
285pub struct MarginAuto {
286    pub left: bool,
287    pub right: bool,
288    pub top: bool,
289    pub bottom: bool,
290}
291
292/// A fully resolved representation of a node's box model properties.
293// +spec:box-model:3e083b - content/padding/border/margin box model layers
294// +spec:box-model:a227ff - content/padding/border/margin edges defining box extents for overflow
295// +spec:containing-block:bca691 - box model edges: padding/border/margin boxes with content-box, padding-box, margin-box methods
296///
297/// All values are in pixels. This is the result of resolving `UnresolvedBoxProps`
298/// against a containing block.
299#[derive(Debug, Clone, Copy, Default)]
300pub struct ResolvedBoxProps {
301    pub margin: EdgeSizes,
302    pub padding: EdgeSizes,
303    pub border: EdgeSizes,
304    /// Tracks which margins are set to `auto`.
305    /// CSS 2.2 § 10.3.3: If both margin-left and margin-right are auto,
306    /// their used values are equal, centering the element within its container.
307    pub margin_auto: MarginAuto,
308}
309
310impl ResolvedBoxProps {
311    // +spec:box-model:be08c6 - inner size (content-box) from outer size minus border+padding, floored at zero
312    // +spec:writing-modes:a58616 - abstract dimensions: inline size maps to physical width/height per writing-mode
313    /// Calculates the inner content-box size from an outer border-box size,
314    /// correctly accounting for the specified writing mode.
315    #[must_use] pub fn inner_size(&self, outer_size: LogicalSize, wm: LayoutWritingMode) -> LogicalSize {
316        let outer_main = outer_size.main(wm);
317        let outer_cross = outer_size.cross(wm);
318
319        // The sum of padding and border along the cross (inline) axis.
320        let cross_axis_spacing = self.padding.cross_sum(wm) + self.border.cross_sum(wm);
321
322        // The sum of padding and border along the main (block) axis.
323        let main_axis_spacing = self.padding.main_sum(wm) + self.border.main_sum(wm);
324
325        // +spec:box-model:2589b1 - content size = border-box - border - padding, floored at zero
326        // +spec:box-model:3ab53d - if padding+border > border-box, content floors at 0px
327        let inner_main = (outer_main - main_axis_spacing).max(0.0);
328        let inner_cross = (outer_cross - cross_axis_spacing).max(0.0);
329
330        LogicalSize::from_main_cross(inner_main, inner_cross, wm)
331    }
332
333    // +spec:box-model:aa585e - Content/padding/border/margin edge relationships
334    // +spec:height-calculation:6c9abb - box model edges: margin > border > padding > content
335    /// Returns the content-box rect from a border-box rect.
336    /// Shrinks inward by border + padding on each side.
337    // +spec:box-model:1720a5 - content of a block box is confined to its content edges
338    #[must_use] pub fn content_box(&self, border_box: LogicalRect) -> LogicalRect {
339        let x = border_box.origin.x + self.border.left + self.padding.left;
340        let y = border_box.origin.y + self.border.top + self.padding.top;
341        let w = (border_box.size.width - self.border.horizontal_sum() - self.padding.horizontal_sum()).max(0.0);
342        let h = (border_box.size.height - self.border.vertical_sum() - self.padding.vertical_sum()).max(0.0);
343        LogicalRect { origin: LogicalPosition { x, y }, size: LogicalSize { width: w, height: h } }
344    }
345
346    /// Returns the padding-box rect from a border-box rect.
347    /// Shrinks inward by border on each side.
348    #[must_use] pub fn padding_box(&self, border_box: LogicalRect) -> LogicalRect {
349        let x = border_box.origin.x + self.border.left;
350        let y = border_box.origin.y + self.border.top;
351        let w = (border_box.size.width - self.border.horizontal_sum()).max(0.0);
352        let h = (border_box.size.height - self.border.vertical_sum()).max(0.0);
353        LogicalRect { origin: LogicalPosition { x, y }, size: LogicalSize { width: w, height: h } }
354    }
355
356    /// Returns the margin-box rect from a border-box rect.
357    /// Expands outward by margin on each side.
358    #[must_use] pub fn margin_box(&self, border_box: LogicalRect) -> LogicalRect {
359        let x = border_box.origin.x - self.margin.left;
360        let y = border_box.origin.y - self.margin.top;
361        let w = border_box.size.width + self.margin.horizontal_sum();
362        let h = border_box.size.height + self.margin.vertical_sum();
363        LogicalRect { origin: LogicalPosition { x, y }, size: LogicalSize { width: w, height: h } }
364    }
365
366    // +spec:box-model:0e75c1 - margin, padding, border contribute to layout bounds (default line-fit-edge: leading uses line-height model)
367    /// Total horizontal space consumed by margin + border + padding.
368    #[must_use] pub fn horizontal_mbp(&self) -> f32 {
369        self.margin.horizontal_sum() + self.border.horizontal_sum() + self.padding.horizontal_sum()
370    }
371
372    /// Total vertical space consumed by margin + border + padding.
373    #[must_use] pub fn vertical_mbp(&self) -> f32 {
374        self.margin.vertical_sum() + self.border.vertical_sum() + self.padding.vertical_sum()
375    }
376
377    /// Total horizontal space consumed by border + padding only (no margin).
378    #[must_use] pub fn horizontal_bp(&self) -> f32 {
379        self.border.horizontal_sum() + self.padding.horizontal_sum()
380    }
381
382    /// Total vertical space consumed by border + padding only (no margin).
383    #[must_use] pub fn vertical_bp(&self) -> f32 {
384        self.border.vertical_sum() + self.padding.vertical_sum()
385    }
386}
387
388/// Type alias for backwards compatibility.
389/// TODO: Remove this once all code uses `ResolvedBoxProps` directly.
390pub type BoxProps = ResolvedBoxProps;
391
392/// Packed representation of box model properties using i16×10 encoding.
393///
394/// Stores margin/padding/border as i16 values scaled by 10 (0.1px precision),
395/// reducing the hot struct from 52B to 26B. Range: ±3276.7px per edge.
396///
397/// Only used for storage in `LayoutNodeHot`. The layout solver unpacks to
398/// `ResolvedBoxProps` (f32) for computation.
399#[derive(Debug, Clone, Copy, Default)]
400#[repr(C)]
401pub struct PackedBoxProps {
402    pub margin: [i16; 4],     // top, right, bottom, left — ×10
403    pub padding: [i16; 4],    // ×10
404    pub border: [i16; 4],     // ×10
405    pub margin_auto: MarginAuto,
406}
407
408impl PackedBoxProps {
409    /// Pack a `ResolvedBoxProps` into compact i16×10 encoding.
410    #[inline]
411    #[must_use] pub fn pack(bp: &ResolvedBoxProps) -> Self {
412        Self {
413            margin: Self::pack_edge(&bp.margin),
414            padding: Self::pack_edge(&bp.padding),
415            border: Self::pack_edge(&bp.border),
416            margin_auto: bp.margin_auto,
417        }
418    }
419
420    /// Unpack to full `ResolvedBoxProps` with f32 values.
421    #[inline]
422    #[must_use] pub fn unpack(&self) -> ResolvedBoxProps {
423        ResolvedBoxProps {
424            margin: Self::unpack_edge(&self.margin),
425            padding: Self::unpack_edge(&self.padding),
426            border: Self::unpack_edge(&self.border),
427            margin_auto: self.margin_auto,
428        }
429    }
430
431    /// Convenience: unpack and call `inner_size` on the result.
432    #[inline]
433    #[must_use] pub fn inner_size(&self, outer_size: LogicalSize, wm: LayoutWritingMode) -> LogicalSize {
434        self.unpack().inner_size(outer_size, wm)
435    }
436
437    /// Convenience: unpack and call `content_box` on the result.
438    #[inline]
439    #[must_use] pub fn content_box(&self, border_box: LogicalRect) -> LogicalRect {
440        self.unpack().content_box(border_box)
441    }
442
443    /// Convenience: unpack and call `padding_box` on the result.
444    #[inline]
445    #[must_use] pub fn padding_box(&self, border_box: LogicalRect) -> LogicalRect {
446        self.unpack().padding_box(border_box)
447    }
448
449    /// Convenience: unpack and call `margin_box` on the result.
450    #[inline]
451    #[must_use] pub fn margin_box(&self, border_box: LogicalRect) -> LogicalRect {
452        self.unpack().margin_box(border_box)
453    }
454
455    /// Convenience: unpack and return horizontal MBP.
456    #[inline]
457    #[must_use] pub fn horizontal_mbp(&self) -> f32 {
458        self.unpack().horizontal_mbp()
459    }
460
461    /// Convenience: unpack and return vertical MBP.
462    #[inline]
463    #[must_use] pub fn vertical_mbp(&self) -> f32 {
464        self.unpack().vertical_mbp()
465    }
466
467    /// Convenience: unpack and return horizontal BP.
468    #[inline]
469    #[must_use] pub fn horizontal_bp(&self) -> f32 {
470        self.unpack().horizontal_bp()
471    }
472
473    /// Convenience: unpack and return vertical BP.
474    #[inline]
475    #[must_use] pub fn vertical_bp(&self) -> f32 {
476        self.unpack().vertical_bp()
477    }
478
479    #[inline]
480    #[allow(clippy::cast_possible_truncation)] // bounded graphics/coord/counter/fixed-point cast
481    fn pack_edge(e: &EdgeSizes) -> [i16; 4] {
482        const MIN: f32 = i16::MIN as f32;
483        const MAX: f32 = i16::MAX as f32;
484        [
485            (e.top * 10.0).round().clamp(MIN, MAX) as i16,
486            (e.right * 10.0).round().clamp(MIN, MAX) as i16,
487            (e.bottom * 10.0).round().clamp(MIN, MAX) as i16,
488            (e.left * 10.0).round().clamp(MIN, MAX) as i16,
489        ]
490    }
491
492    #[inline]
493    #[allow(clippy::trivially_copy_pass_by_ref)] // <=8B Copy param kept by-ref intentionally (hot pixel/coord path or to avoid churning call sites for a perf-neutral change)
494    fn unpack_edge(e: &[i16; 4]) -> EdgeSizes {
495        EdgeSizes {
496            top: f32::from(e[0]) * 0.1,
497            right: f32::from(e[1]) * 0.1,
498            bottom: f32::from(e[2]) * 0.1,
499            left: f32::from(e[3]) * 0.1,
500        }
501    }
502}
503
504// Re-export float and clear types from azul_css
505pub use azul_css::props::layout::{LayoutClear, LayoutFloat};
506
507// +spec:intrinsic-sizing:af39b6 - min-content, max-content, and stretch fit size definitions
508// min-content constraint, max-content constraint definitions
509// and fit-content sizes for both inline and block axes
510// +spec:height-calculation:e9ec84 - replaced elements have natural dimensions (width, height, ratio)
511/// Represents the intrinsic sizing information for an element, calculated
512/// without knowledge of the final containing block size.
513// +spec:intrinsic-sizing:127a10 - min-content, max-content, fit-content size definitions (css-sizing-3 §2.1)
514// +spec:intrinsic-sizing:21f2cb - defines min-content, max-content, and stretch-fit size terminology
515// +spec:width-calculation:1583c4 - min-content, max-content, fit-content intrinsic size definitions (§2.1)
516#[derive(Debug, Clone, Copy, Default)]
517pub struct IntrinsicSizes {
518    // +spec:width-calculation:b83d0a - min-content width ("preferred minimum width" in CSS2.1§10.3.5)
519    // +spec:writing-modes:1583c4 - min-content size in inline axis = size fitting contents with all soft wraps taken
520    /// §2.1 min-content inline size: inline size fitting contents if all soft wraps taken.
521    pub min_content_width: f32,
522    // +spec:width-calculation:0c74d3 - max-content width ("preferred width" in CSS2.1§10.3.5)
523    // +spec:writing-modes:6e85d3 - max-content inline size is the "ideal" size in the inline axis (writing-mode-dependent)
524    /// §2.1 max-content inline size: narrowest inline size if no soft wraps taken.
525    pub max_content_width: f32,
526    /// The width specified by CSS properties, if any.
527    pub preferred_width: Option<f32>,
528    /// §2.1 min-content block size: for block containers, tables, and inline boxes,
529    /// equivalent to max-content block size.
530    pub min_content_height: f32,
531    // +spec:writing-modes:8c94e2 - max-content block size is the "ideal" block size after layout
532    /// §2.1 max-content block size: "ideal" block size, usually content height after layout.
533    pub max_content_height: f32,
534    /// The height specified by CSS properties, if any.
535    pub preferred_height: Option<f32>,
536}
537
538// ============================================================================
539// WRITING MODE SUPPORT
540// ============================================================================
541
542/// Captures the resolved writing mode context for a node.
543///
544/// This struct bundles together all the CSS properties that affect how
545/// logical directions (inline/block) map to physical directions (x/y).
546/// Spec agents should use this struct to implement writing-mode-aware layout.
547///
548/// # CSS Writing Modes Level 4
549///
550/// - `writing-mode` determines the block flow direction and inline base direction
551/// - `direction` determines the inline base direction (ltr or rtl)
552/// - `text-orientation` determines glyph orientation in vertical writing modes
553// +spec:block-formatting-context:333dcb - typographic mode captured by text_orientation field
554// +spec:block-formatting-context:66eb6d - text-orientation property (mixed|upright|sideways) integrated into WritingModeContext
555// +spec:block-formatting-context:8be1b0 - writing modes and vertical text orientation context (UTN#22)
556// +spec:display-property:0a39dc - text-orientation affects inline-level alignment via WritingModeContext
557// +spec:display-property:591355 - bidirectionality support via direction property in WritingModeContext
558#[derive(Debug, Clone, Copy, PartialEq, Eq)]
559pub struct WritingModeContext {
560    pub writing_mode: LayoutWritingMode,
561    pub direction: StyleDirection,
562    // +spec:block-formatting-context:925cfe - text-orientation mixed/upright for horizontal scripts in vertical mode
563    pub text_orientation: StyleTextOrientation,
564}
565
566impl Default for WritingModeContext {
567    fn default() -> Self {
568        Self {
569            writing_mode: LayoutWritingMode::HorizontalTb,
570            direction: StyleDirection::Ltr,
571            text_orientation: StyleTextOrientation::Mixed,
572        }
573    }
574}
575
576impl WritingModeContext {
577    /// Constructs a `WritingModeContext`, applying spec-mandated overrides.
578    // +spec:writing-modes:8307e4 - text-orientation: upright forces used direction to ltr
579    #[must_use] pub fn new(
580        writing_mode: LayoutWritingMode,
581        direction: StyleDirection,
582        text_orientation: StyleTextOrientation,
583    ) -> Self {
584        // CSS Writing Modes Level 4 §5.1: text-orientation: upright causes
585        // the used value of direction to be ltr, and all characters to be
586        // treated as strong LTR for bidi reordering purposes.
587        let used_direction = if text_orientation == StyleTextOrientation::Upright {
588            StyleDirection::Ltr
589        } else {
590            direction
591        };
592        Self {
593            writing_mode,
594            direction: used_direction,
595            text_orientation,
596        }
597    }
598
599    // +spec:writing-modes:458d31 - text-orientation:upright forces used direction to ltr
600    /// Returns the used value of `direction`.
601    ///
602    /// The upright override is already applied in `new()`, so this just
603    /// returns the stored direction.
604    #[must_use] pub const fn used_direction(&self) -> StyleDirection {
605        self.direction
606    }
607
608    // +spec:containing-block:c205e5 - orthogonal flow: child writing mode perpendicular to containing block's
609
610    // +spec:block-formatting-context:6225cb - vertical writing modes: line-over is right, line-under is left
611    // +spec:block-formatting-context:9a4269 - vertical vs horizontal script classification
612    /// Returns true if the writing mode is horizontal (`HorizontalTb`).
613    ///
614    /// When true, the inline axis is horizontal and the block axis is vertical.
615    #[must_use] pub const fn is_horizontal(&self) -> bool {
616        matches!(self.writing_mode, LayoutWritingMode::HorizontalTb)
617    }
618
619    /// Returns true if the inline size corresponds to the physical width.
620    ///
621    /// In horizontal writing modes, inline size = width.
622    /// In vertical writing modes, inline size = height.
623    // +spec:block-formatting-context:bb9845 - orthogonal flows: inline/block axis mapping
624    #[must_use] pub const fn inline_size_is_width(&self) -> bool {
625        self.is_horizontal()
626    }
627
628    /// Returns true if the block size corresponds to the physical height.
629    ///
630    /// In horizontal writing modes, block size = height.
631    /// In vertical writing modes, block size = width.
632    #[must_use] pub const fn block_size_is_height(&self) -> bool {
633        self.is_horizontal()
634    }
635
636    // +spec:writing-modes:32541a - direction property controls inline text direction via stylesheet
637    /// Returns true if the inline direction is reversed (RTL in horizontal,
638    /// or bottom-to-top in certain vertical modes).
639    #[must_use] pub fn is_inline_reversed(&self) -> bool {
640        self.used_direction() == StyleDirection::Rtl
641    }
642}