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