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}