1use 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 pub bounds: LogicalRect,
21 pub margin: ResolvedOffsets,
23 pub border: ResolvedOffsets,
25 pub padding: ResolvedOffsets,
27}
28
29#[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 #[must_use] pub fn horizontal_sum(&self) -> f32 {
44 self.left + self.right
45 }
46
47 #[must_use] pub fn vertical_sum(&self) -> f32 {
49 self.top + self.bottom
50 }
51
52 #[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 #[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 #[must_use] pub fn main_sum(&self, wm: LayoutWritingMode) -> f32 {
92 self.main_start(wm) + self.main_end(wm)
93 }
94
95 #[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 #[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 #[must_use] pub fn cross_sum(&self, wm: LayoutWritingMode) -> f32 {
114 self.cross_start(wm) + self.cross_end(wm)
115 }
116
117 #[must_use] pub const fn line_over(&self, wm: LayoutWritingMode) -> f32 {
130 match wm {
131 LayoutWritingMode::HorizontalTb => self.top,
132 LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.right,
133 }
134 }
135
136 #[must_use] pub const fn line_under(&self, wm: LayoutWritingMode) -> f32 {
141 match wm {
142 LayoutWritingMode::HorizontalTb => self.bottom,
143 LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.left,
144 }
145 }
146
147 #[must_use] pub fn line_over_under_sum(&self, wm: LayoutWritingMode) -> f32 {
149 self.line_over(wm) + self.line_under(wm)
150 }
151
152 #[must_use] pub const fn inline_start(&self, wm: LayoutWritingMode, dir: StyleDirection) -> f32 {
164 match dir {
165 StyleDirection::Ltr => self.cross_start(wm),
166 StyleDirection::Rtl => self.cross_end(wm),
167 }
168 }
169
170 #[must_use] pub const fn inline_end(&self, wm: LayoutWritingMode, dir: StyleDirection) -> f32 {
175 match dir {
176 StyleDirection::Ltr => self.cross_end(wm),
177 StyleDirection::Rtl => self.cross_start(wm),
178 }
179 }
180
181 #[must_use] pub fn inline_sum(&self, wm: LayoutWritingMode, dir: StyleDirection) -> f32 {
186 self.inline_start(wm, dir) + self.inline_end(wm, dir)
187 }
188}
189
190#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
200pub enum UnresolvedMargin {
201 #[default]
203 Zero,
204 Auto,
206 Length(PixelValue),
208}
209
210impl UnresolvedMargin {
211 #[must_use] pub const fn is_auto(&self) -> bool {
213 matches!(self, Self::Auto)
214 }
215
216 #[allow(clippy::match_same_arms)] #[must_use] pub fn resolve(&self, ctx: &ResolutionContext) -> f32 {
223 match self {
224 Self::Zero => 0.0,
225 Self::Auto => 0.0, Self::Length(pv) => pv.resolve_with_context(ctx, PropertyContext::Margin),
229 }
230 }
231}
232
233#[derive(Debug, Clone, Copy, Default)]
238pub struct UnresolvedEdge<T> {
239 pub top: T,
240 pub right: T,
241 pub bottom: T,
242 pub left: T,
243}
244
245impl<T> UnresolvedEdge<T> {
246 pub const fn new(top: T, right: T, bottom: T, left: T) -> Self {
247 Self { top, right, bottom, left }
248 }
249}
250
251impl UnresolvedEdge<UnresolvedMargin> {
252 #[must_use] pub fn resolve(&self, ctx: &ResolutionContext) -> EdgeSizes {
254 EdgeSizes {
255 top: self.top.resolve(ctx),
256 right: self.right.resolve(ctx),
257 bottom: self.bottom.resolve(ctx),
258 left: self.left.resolve(ctx),
259 }
260 }
261
262 #[must_use] pub const fn get_margin_auto(&self) -> MarginAuto {
264 MarginAuto {
265 top: self.top.is_auto(),
266 right: self.right.is_auto(),
267 bottom: self.bottom.is_auto(),
268 left: self.left.is_auto(),
269 }
270 }
271}
272
273impl UnresolvedEdge<PixelValue> {
274 #[must_use] pub fn resolve(&self, ctx: &ResolutionContext, prop_ctx: PropertyContext) -> EdgeSizes {
276 EdgeSizes {
277 top: self.top.resolve_with_context(ctx, prop_ctx),
278 right: self.right.resolve_with_context(ctx, prop_ctx),
279 bottom: self.bottom.resolve_with_context(ctx, prop_ctx),
280 left: self.left.resolve_with_context(ctx, prop_ctx),
281 }
282 }
283}
284
285#[derive(Debug, Clone, Copy)]
287pub struct ResolutionParams {
288 pub containing_block: LogicalSize,
291 pub viewport_size: LogicalSize,
293 pub element_font_size: f32,
295 pub root_font_size: f32,
297}
298
299impl ResolutionParams {
300 #[must_use] pub const fn to_resolution_context(&self) -> ResolutionContext {
302 ResolutionContext {
303 element_font_size: self.element_font_size,
304 parent_font_size: self.element_font_size,
308 root_font_size: self.root_font_size,
309 element_size: None,
310 containing_block_size: PhysicalSize::new(
311 self.containing_block.width,
312 self.containing_block.height,
313 ),
314 viewport_size: PhysicalSize::new(
315 self.viewport_size.width,
316 self.viewport_size.height,
317 ),
318 }
319 }
320}
321
322#[derive(Debug, Clone, Copy, Default)]
331pub struct UnresolvedBoxProps {
332 pub margin: UnresolvedEdge<UnresolvedMargin>,
333 pub padding: UnresolvedEdge<PixelValue>,
334 pub border: UnresolvedEdge<PixelValue>,
335}
336
337impl UnresolvedBoxProps {
338 #[must_use] pub fn resolve(&self, params: &ResolutionParams) -> ResolvedBoxProps {
340 let ctx = params.to_resolution_context();
341 ResolvedBoxProps {
342 margin: self.margin.resolve(&ctx),
343 padding: self.padding.resolve(&ctx, PropertyContext::Padding),
344 border: self.border.resolve(&ctx, PropertyContext::BorderWidth),
345 margin_auto: self.margin.get_margin_auto(),
346 }
347 }
348}
349
350#[derive(Debug, Clone, Copy, Default)]
356#[allow(clippy::struct_excessive_bools)] pub struct MarginAuto {
358 pub left: bool,
359 pub right: bool,
360 pub top: bool,
361 pub bottom: bool,
362}
363
364#[derive(Debug, Clone, Copy, Default)]
372pub struct ResolvedBoxProps {
373 pub margin: EdgeSizes,
374 pub padding: EdgeSizes,
375 pub border: EdgeSizes,
376 pub margin_auto: MarginAuto,
380}
381
382impl ResolvedBoxProps {
383 #[must_use] pub fn inner_size(&self, outer_size: LogicalSize, wm: LayoutWritingMode) -> LogicalSize {
388 let outer_main = outer_size.main(wm);
389 let outer_cross = outer_size.cross(wm);
390
391 let cross_axis_spacing = self.padding.cross_sum(wm) + self.border.cross_sum(wm);
393
394 let main_axis_spacing = self.padding.main_sum(wm) + self.border.main_sum(wm);
396
397 let inner_main = (outer_main - main_axis_spacing).max(0.0);
400 let inner_cross = (outer_cross - cross_axis_spacing).max(0.0);
401
402 LogicalSize::from_main_cross(inner_main, inner_cross, wm)
403 }
404
405 #[must_use] pub fn content_box(&self, border_box: LogicalRect) -> LogicalRect {
411 let x = border_box.origin.x + self.border.left + self.padding.left;
412 let y = border_box.origin.y + self.border.top + self.padding.top;
413 let w = (border_box.size.width - self.border.horizontal_sum() - self.padding.horizontal_sum()).max(0.0);
414 let h = (border_box.size.height - self.border.vertical_sum() - self.padding.vertical_sum()).max(0.0);
415 LogicalRect { origin: LogicalPosition { x, y }, size: LogicalSize { width: w, height: h } }
416 }
417
418 #[must_use] pub fn padding_box(&self, border_box: LogicalRect) -> LogicalRect {
421 let x = border_box.origin.x + self.border.left;
422 let y = border_box.origin.y + self.border.top;
423 let w = (border_box.size.width - self.border.horizontal_sum()).max(0.0);
424 let h = (border_box.size.height - self.border.vertical_sum()).max(0.0);
425 LogicalRect { origin: LogicalPosition { x, y }, size: LogicalSize { width: w, height: h } }
426 }
427
428 #[must_use] pub fn margin_box(&self, border_box: LogicalRect) -> LogicalRect {
431 let x = border_box.origin.x - self.margin.left;
432 let y = border_box.origin.y - self.margin.top;
433 let w = border_box.size.width + self.margin.horizontal_sum();
434 let h = border_box.size.height + self.margin.vertical_sum();
435 LogicalRect { origin: LogicalPosition { x, y }, size: LogicalSize { width: w, height: h } }
436 }
437
438 #[must_use] pub fn horizontal_mbp(&self) -> f32 {
441 self.margin.horizontal_sum() + self.border.horizontal_sum() + self.padding.horizontal_sum()
442 }
443
444 #[must_use] pub fn vertical_mbp(&self) -> f32 {
446 self.margin.vertical_sum() + self.border.vertical_sum() + self.padding.vertical_sum()
447 }
448
449 #[must_use] pub fn horizontal_bp(&self) -> f32 {
451 self.border.horizontal_sum() + self.padding.horizontal_sum()
452 }
453
454 #[must_use] pub fn vertical_bp(&self) -> f32 {
456 self.border.vertical_sum() + self.padding.vertical_sum()
457 }
458}
459
460pub type BoxProps = ResolvedBoxProps;
463
464#[derive(Debug, Clone, Copy, Default)]
472#[repr(C)]
473pub struct PackedBoxProps {
474 pub margin: [i16; 4], pub padding: [i16; 4], pub border: [i16; 4], pub margin_auto: MarginAuto,
478}
479
480impl PackedBoxProps {
481 #[inline]
483 #[must_use] pub fn pack(bp: &ResolvedBoxProps) -> Self {
484 Self {
485 margin: Self::pack_edge(&bp.margin),
486 padding: Self::pack_edge(&bp.padding),
487 border: Self::pack_edge(&bp.border),
488 margin_auto: bp.margin_auto,
489 }
490 }
491
492 #[inline]
494 #[must_use] pub fn unpack(&self) -> ResolvedBoxProps {
495 ResolvedBoxProps {
496 margin: Self::unpack_edge(&self.margin),
497 padding: Self::unpack_edge(&self.padding),
498 border: Self::unpack_edge(&self.border),
499 margin_auto: self.margin_auto,
500 }
501 }
502
503 #[inline]
505 #[must_use] pub fn inner_size(&self, outer_size: LogicalSize, wm: LayoutWritingMode) -> LogicalSize {
506 self.unpack().inner_size(outer_size, wm)
507 }
508
509 #[inline]
511 #[must_use] pub fn content_box(&self, border_box: LogicalRect) -> LogicalRect {
512 self.unpack().content_box(border_box)
513 }
514
515 #[inline]
517 #[must_use] pub fn padding_box(&self, border_box: LogicalRect) -> LogicalRect {
518 self.unpack().padding_box(border_box)
519 }
520
521 #[inline]
523 #[must_use] pub fn margin_box(&self, border_box: LogicalRect) -> LogicalRect {
524 self.unpack().margin_box(border_box)
525 }
526
527 #[inline]
529 #[must_use] pub fn horizontal_mbp(&self) -> f32 {
530 self.unpack().horizontal_mbp()
531 }
532
533 #[inline]
535 #[must_use] pub fn vertical_mbp(&self) -> f32 {
536 self.unpack().vertical_mbp()
537 }
538
539 #[inline]
541 #[must_use] pub fn horizontal_bp(&self) -> f32 {
542 self.unpack().horizontal_bp()
543 }
544
545 #[inline]
547 #[must_use] pub fn vertical_bp(&self) -> f32 {
548 self.unpack().vertical_bp()
549 }
550
551 #[inline]
552 #[allow(clippy::cast_possible_truncation)] fn pack_edge(e: &EdgeSizes) -> [i16; 4] {
554 const MIN: f32 = i16::MIN as f32;
555 const MAX: f32 = i16::MAX as f32;
556 [
557 (e.top * 10.0).round().clamp(MIN, MAX) as i16,
558 (e.right * 10.0).round().clamp(MIN, MAX) as i16,
559 (e.bottom * 10.0).round().clamp(MIN, MAX) as i16,
560 (e.left * 10.0).round().clamp(MIN, MAX) as i16,
561 ]
562 }
563
564 #[inline]
565 #[allow(clippy::trivially_copy_pass_by_ref)] fn unpack_edge(e: &[i16; 4]) -> EdgeSizes {
567 EdgeSizes {
568 top: f32::from(e[0]) * 0.1,
569 right: f32::from(e[1]) * 0.1,
570 bottom: f32::from(e[2]) * 0.1,
571 left: f32::from(e[3]) * 0.1,
572 }
573 }
574}
575
576pub use azul_css::props::layout::{LayoutClear, LayoutFloat};
578
579#[derive(Debug, Clone, Copy, Default)]
589pub struct IntrinsicSizes {
590 pub min_content_width: f32,
594 pub max_content_width: f32,
598 pub preferred_width: Option<f32>,
600 pub min_content_height: f32,
603 pub max_content_height: f32,
606 pub preferred_height: Option<f32>,
608 pub preferred_aspect_ratio: Option<f32>,
613}
614
615impl IntrinsicSizes {
616 #[must_use]
623 pub const fn fit_content_width(&self, available_inline_size: f32) -> f32 {
624 available_inline_size
625 .min(self.max_content_width)
626 .max(self.min_content_width)
627 }
628
629 #[must_use]
632 pub const fn fit_content_height(&self, available_block_size: f32) -> f32 {
633 available_block_size
634 .min(self.max_content_height)
635 .max(self.min_content_height)
636 }
637}
638
639#[derive(Debug, Clone, Copy, PartialEq, Eq)]
660pub struct WritingModeContext {
661 pub writing_mode: LayoutWritingMode,
662 pub direction: StyleDirection,
663 pub text_orientation: StyleTextOrientation,
665}
666
667impl Default for WritingModeContext {
668 fn default() -> Self {
669 Self {
670 writing_mode: LayoutWritingMode::HorizontalTb,
671 direction: StyleDirection::Ltr,
672 text_orientation: StyleTextOrientation::Mixed,
673 }
674 }
675}
676
677impl WritingModeContext {
678 #[must_use] pub fn new(
681 writing_mode: LayoutWritingMode,
682 direction: StyleDirection,
683 text_orientation: StyleTextOrientation,
684 ) -> Self {
685 let used_direction = if text_orientation == StyleTextOrientation::Upright {
689 StyleDirection::Ltr
690 } else {
691 direction
692 };
693 Self {
694 writing_mode,
695 direction: used_direction,
696 text_orientation,
697 }
698 }
699
700 #[must_use] pub const fn used_direction(&self) -> StyleDirection {
706 self.direction
707 }
708
709 #[must_use] pub const fn is_horizontal(&self) -> bool {
717 matches!(self.writing_mode, LayoutWritingMode::HorizontalTb)
718 }
719
720 #[must_use] pub const fn inline_size_is_width(&self) -> bool {
726 self.is_horizontal()
727 }
728
729 #[must_use] pub const fn block_size_is_height(&self) -> bool {
734 self.is_horizontal()
735 }
736
737 #[must_use] pub fn is_inline_reversed(&self) -> bool {
741 self.used_direction() == StyleDirection::Rtl
742 }
743}
744
745#[cfg(test)]
746#[allow(clippy::float_cmp, clippy::unreadable_literal)]
747mod autotest_generated {
748 use super::*;
749
750 const ALL_WM: [LayoutWritingMode; 3] = [
754 LayoutWritingMode::HorizontalTb,
755 LayoutWritingMode::VerticalRl,
756 LayoutWritingMode::VerticalLr,
757 ];
758
759 fn edges(top: f32, right: f32, bottom: f32, left: f32) -> EdgeSizes {
760 EdgeSizes { top, right, bottom, left }
761 }
762
763 fn rect(x: f32, y: f32, w: f32, h: f32) -> LogicalRect {
764 LogicalRect {
765 origin: LogicalPosition { x, y },
766 size: LogicalSize { width: w, height: h },
767 }
768 }
769
770 fn close(a: f32, b: f32, eps: f32) -> bool {
773 (a - b).abs() <= eps
774 }
775
776 fn params(cb: LogicalSize, vp: LogicalSize, font: f32, root: f32) -> ResolutionParams {
777 ResolutionParams {
778 containing_block: cb,
779 viewport_size: vp,
780 element_font_size: font,
781 root_font_size: root,
782 }
783 }
784
785 fn distinct_params() -> ResolutionParams {
788 params(
789 LogicalSize::new(800.0, 600.0),
790 LogicalSize::new(1000.0, 500.0),
791 16.0,
792 10.0,
793 )
794 }
795
796 fn props(margin: EdgeSizes, padding: EdgeSizes, border: EdgeSizes) -> ResolvedBoxProps {
797 ResolvedBoxProps { margin, padding, border, margin_auto: MarginAuto::default() }
798 }
799
800 #[test]
805 fn edge_sizes_sums_pick_the_right_pair_of_edges() {
806 let e = edges(1.0, 2.0, 4.0, 8.0); assert_eq!(e.horizontal_sum(), 10.0, "horizontal = left + right");
808 assert_eq!(e.vertical_sum(), 5.0, "vertical = top + bottom");
809 }
810
811 #[test]
812 fn edge_sizes_default_is_all_zero() {
813 let e = EdgeSizes::default();
814 assert_eq!(e.horizontal_sum(), 0.0);
815 assert_eq!(e.vertical_sum(), 0.0);
816 for wm in ALL_WM {
817 assert_eq!(e.main_sum(wm), 0.0);
818 assert_eq!(e.cross_sum(wm), 0.0);
819 }
820 }
821
822 #[test]
823 fn edge_sizes_sums_saturate_to_infinity_instead_of_panicking() {
824 let e = edges(f32::MAX, f32::MAX, f32::MAX, f32::MAX);
826 assert!(e.horizontal_sum().is_infinite() && e.horizontal_sum() > 0.0);
827 assert!(e.vertical_sum().is_infinite() && e.vertical_sum() > 0.0);
828 }
829
830 #[test]
831 fn edge_sizes_opposing_infinities_produce_nan_not_a_panic() {
832 let e = edges(f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY);
835 assert!(e.horizontal_sum().is_nan());
836 assert!(e.vertical_sum().is_nan());
837 }
838
839 #[test]
840 fn edge_sizes_nan_edges_propagate_without_panicking() {
841 let e = edges(f32::NAN, 1.0, 2.0, 3.0);
842 assert!(e.vertical_sum().is_nan());
843 assert_eq!(e.horizontal_sum(), 4.0, "a NaN top must not poison left+right");
844 for wm in ALL_WM {
845 let _ = e.main_sum(wm);
846 let _ = e.cross_sum(wm);
847 }
848 }
849
850 #[test]
855 fn edge_sizes_axis_mapping_is_exact_for_every_writing_mode() {
856 let e = edges(1.0, 2.0, 4.0, 8.0); let h = LayoutWritingMode::HorizontalTb;
860 assert_eq!(e.main_start(h), 1.0, "main-start = top");
861 assert_eq!(e.main_end(h), 4.0, "main-end = bottom");
862 assert_eq!(e.cross_start(h), 8.0, "cross-start = left");
863 assert_eq!(e.cross_end(h), 2.0, "cross-end = right");
864
865 for wm in [LayoutWritingMode::VerticalRl, LayoutWritingMode::VerticalLr] {
867 assert_eq!(e.main_start(wm), 8.0, "main-start = left");
868 assert_eq!(e.main_end(wm), 2.0, "main-end = right");
869 assert_eq!(e.cross_start(wm), 1.0, "cross-start = top");
870 assert_eq!(e.cross_end(wm), 4.0, "cross-end = bottom");
871 }
872 }
873
874 #[test]
876 fn edge_sizes_line_over_under_is_line_relative_not_physical() {
877 let e = edges(1.0, 2.0, 4.0, 8.0); let h = LayoutWritingMode::HorizontalTb;
881 assert_eq!(e.line_over(h), 1.0, "line-over = top in horizontal-tb");
882 assert_eq!(e.line_under(h), 4.0, "line-under = bottom in horizontal-tb");
883 assert_eq!(e.line_over(h), e.main_start(h), "over == main-start only in horizontal-tb");
884
885 for wm in [LayoutWritingMode::VerticalRl, LayoutWritingMode::VerticalLr] {
889 assert_eq!(e.line_over(wm), 2.0, "line-over = right in vertical modes");
890 assert_eq!(e.line_under(wm), 8.0, "line-under = left in vertical modes");
891 assert_ne!(e.line_over(wm), e.main_start(wm), "over != main-start in vertical modes");
892 }
893
894 assert_eq!(e.line_over_under_sum(h), 5.0);
896 assert_eq!(e.line_over_under_sum(LayoutWritingMode::VerticalRl), 10.0);
897 }
898
899 #[test]
901 fn edge_sizes_inline_start_end_depend_on_direction() {
902 let e = edges(1.0, 2.0, 4.0, 8.0); for wm in ALL_WM {
905 assert_eq!(
907 e.inline_start(wm, StyleDirection::Ltr),
908 e.cross_start(wm),
909 "LTR inline-start = cross-start (line-left)"
910 );
911 assert_eq!(
912 e.inline_end(wm, StyleDirection::Ltr),
913 e.cross_end(wm),
914 "LTR inline-end = cross-end (line-right)"
915 );
916
917 assert_eq!(
919 e.inline_start(wm, StyleDirection::Rtl),
920 e.cross_end(wm),
921 "RTL inline-start = cross-end (line-right)"
922 );
923 assert_eq!(
924 e.inline_end(wm, StyleDirection::Rtl),
925 e.cross_start(wm),
926 "RTL inline-end = cross-start (line-left)"
927 );
928
929 assert_eq!(
931 e.inline_sum(wm, StyleDirection::Ltr),
932 e.inline_sum(wm, StyleDirection::Rtl),
933 "inline-sum is direction-independent"
934 );
935 assert_eq!(e.inline_sum(wm, StyleDirection::Ltr), e.cross_sum(wm));
936 }
937
938 let h = LayoutWritingMode::HorizontalTb;
940 assert_eq!(e.inline_start(h, StyleDirection::Ltr), 8.0);
941 assert_eq!(e.inline_start(h, StyleDirection::Rtl), 2.0);
942 }
943
944 #[test]
945 fn edge_sizes_main_and_cross_sums_partition_the_four_edges() {
946 let e = edges(1.0, 2.0, 4.0, 8.0);
949 for wm in ALL_WM {
950 assert_eq!(e.main_start(wm) + e.main_end(wm), e.main_sum(wm));
951 assert_eq!(e.cross_start(wm) + e.cross_end(wm), e.cross_sum(wm));
952 assert_eq!(
953 e.main_sum(wm) + e.cross_sum(wm),
954 e.horizontal_sum() + e.vertical_sum(),
955 "{wm:?}: main+cross must cover all four edges"
956 );
957 }
958 }
959
960 #[test]
961 fn edge_sizes_axis_accessors_survive_extreme_values() {
962 let e = edges(f32::MAX, f32::MIN, f32::INFINITY, f32::NEG_INFINITY);
963 for wm in ALL_WM {
964 let _ = e.main_start(wm);
965 let _ = e.main_end(wm);
966 let _ = e.cross_start(wm);
967 let _ = e.cross_end(wm);
968 let _ = e.main_sum(wm);
969 let _ = e.cross_sum(wm);
970 }
971 }
972
973 #[test]
978 fn unresolved_margin_is_auto_only_for_the_auto_variant() {
979 assert!(UnresolvedMargin::Auto.is_auto());
980 assert!(!UnresolvedMargin::Zero.is_auto());
981 assert!(!UnresolvedMargin::Length(PixelValue::const_px(10)).is_auto());
982 assert!(!UnresolvedMargin::Length(PixelValue::zero()).is_auto());
984 assert!(!UnresolvedMargin::default().is_auto(), "default is Zero, not Auto");
985 }
986
987 #[test]
988 fn unresolved_margin_auto_and_zero_both_resolve_to_zero_px() {
989 let ctx = distinct_params().to_resolution_context();
990 assert_eq!(UnresolvedMargin::Zero.resolve(&ctx), 0.0);
991 assert_eq!(UnresolvedMargin::Auto.resolve(&ctx), 0.0);
993 }
994
995 #[test]
996 fn unresolved_margin_length_resolves_by_metric() {
997 let ctx = distinct_params().to_resolution_context(); let r = |pv: PixelValue| UnresolvedMargin::Length(pv).resolve(&ctx);
999
1000 assert_eq!(r(PixelValue::px(12.5)), 12.5);
1001 assert_eq!(r(PixelValue::em(2.0)), 32.0, "em = 2 x element font-size");
1002 assert_eq!(r(PixelValue::rem(2.0)), 20.0, "rem = 2 x root font-size");
1003 }
1004
1005 #[test]
1006 fn unresolved_margin_percent_resolves_against_containing_block_width() {
1007 let ctx = distinct_params().to_resolution_context();
1010 let got = UnresolvedMargin::Length(PixelValue::percent(50.0)).resolve(&ctx);
1011 assert_eq!(got, 400.0);
1012 assert_ne!(got, 300.0, "percentage margin must not use the block height");
1013 }
1014
1015 #[test]
1016 fn unresolved_margin_nan_length_resolves_to_zero_not_nan() {
1017 let ctx = distinct_params().to_resolution_context();
1020 let got = UnresolvedMargin::Length(PixelValue::px(f32::NAN)).resolve(&ctx);
1021 assert!(!got.is_nan(), "a NaN px value must not survive resolution");
1022 assert_eq!(got, 0.0);
1023 }
1024
1025 #[test]
1026 fn unresolved_margin_huge_length_saturates_to_a_finite_value() {
1027 let ctx = distinct_params().to_resolution_context();
1028 for v in [f32::MAX, f32::MIN, f32::INFINITY, f32::NEG_INFINITY] {
1029 let got = UnresolvedMargin::Length(PixelValue::px(v)).resolve(&ctx);
1030 assert!(got.is_finite(), "px({v}) resolved to a non-finite {got}");
1031 }
1032 }
1033
1034 #[test]
1035 fn unresolved_margin_percent_of_a_nan_containing_block_is_nan_not_a_panic() {
1036 let p = params(
1040 LogicalSize::new(f32::NAN, f32::NAN),
1041 LogicalSize::new(1000.0, 500.0),
1042 16.0,
1043 10.0,
1044 );
1045 let got = UnresolvedMargin::Length(PixelValue::percent(50.0)).resolve(&p.to_resolution_context());
1046 assert!(got.is_nan());
1047 }
1048
1049 #[test]
1054 fn unresolved_edge_new_assigns_fields_in_top_right_bottom_left_order() {
1055 let e = UnresolvedEdge::new(1_u8, 2, 4, 8);
1057 assert_eq!(e.top, 1);
1058 assert_eq!(e.right, 2);
1059 assert_eq!(e.bottom, 4);
1060 assert_eq!(e.left, 8);
1061 }
1062
1063 #[test]
1064 fn unresolved_edge_new_accepts_extreme_payloads() {
1065 let e = UnresolvedEdge::new(f32::NAN, f32::INFINITY, f32::MAX, f32::MIN);
1066 assert!(e.top.is_nan());
1067 assert!(e.right.is_infinite());
1068 assert_eq!(e.bottom, f32::MAX);
1069 assert_eq!(e.left, f32::MIN);
1070 }
1071
1072 #[test]
1073 fn get_margin_auto_flags_exactly_the_auto_sides() {
1074 let e = UnresolvedEdge::new(
1075 UnresolvedMargin::Zero, UnresolvedMargin::Auto, UnresolvedMargin::Length(PixelValue::const_px(5)), UnresolvedMargin::Auto, );
1080 let a = e.get_margin_auto();
1081 assert!(!a.top);
1082 assert!(a.right);
1083 assert!(!a.bottom);
1084 assert!(a.left);
1085 }
1086
1087 #[test]
1088 fn get_margin_auto_on_default_edge_flags_nothing() {
1089 let a = UnresolvedEdge::<UnresolvedMargin>::default().get_margin_auto();
1090 assert!(!a.top && !a.right && !a.bottom && !a.left);
1091 }
1092
1093 #[test]
1094 fn unresolved_margin_edge_resolve_keeps_each_side_separate() {
1095 let ctx = distinct_params().to_resolution_context();
1096 let e = UnresolvedEdge::new(
1097 UnresolvedMargin::Length(PixelValue::px(1.0)), UnresolvedMargin::Length(PixelValue::px(2.0)), UnresolvedMargin::Auto, UnresolvedMargin::Length(PixelValue::px(8.0)), );
1102 let r = e.resolve(&ctx);
1103 assert_eq!(r.top, 1.0);
1104 assert_eq!(r.right, 2.0);
1105 assert_eq!(r.bottom, 0.0, "auto resolves to 0 px here");
1106 assert_eq!(r.left, 8.0);
1107 }
1108
1109 #[test]
1110 fn pixel_edge_resolve_drops_percentages_on_border_width() {
1111 let ctx = distinct_params().to_resolution_context();
1114 let e = UnresolvedEdge::new(
1115 PixelValue::percent(50.0),
1116 PixelValue::percent(50.0),
1117 PixelValue::percent(50.0),
1118 PixelValue::percent(50.0),
1119 );
1120 let r = e.resolve(&ctx, PropertyContext::BorderWidth);
1121 assert_eq!(r.top, 0.0);
1122 assert_eq!(r.right, 0.0);
1123 assert_eq!(r.bottom, 0.0);
1124 assert_eq!(r.left, 0.0);
1125 }
1126
1127 #[test]
1128 fn pixel_edge_resolve_uses_block_width_for_vertical_padding_percentages() {
1129 let ctx = distinct_params().to_resolution_context();
1132 let e = UnresolvedEdge::new(
1133 PixelValue::percent(10.0),
1134 PixelValue::percent(10.0),
1135 PixelValue::percent(10.0),
1136 PixelValue::percent(10.0),
1137 );
1138 let r = e.resolve(&ctx, PropertyContext::Padding);
1139 assert_eq!(r.top, 80.0, "padding-top % must use the width");
1140 assert_eq!(r.bottom, 80.0, "padding-bottom % must use the width");
1141 assert_eq!(r.left, 80.0);
1142 assert_eq!(r.right, 80.0);
1143 }
1144
1145 #[test]
1146 fn pixel_edge_resolve_of_viewport_units_uses_the_viewport_not_the_block() {
1147 let ctx = distinct_params().to_resolution_context(); let e = UnresolvedEdge::new(
1149 PixelValue::from_metric(SizeMetric::Vw, 10.0),
1150 PixelValue::from_metric(SizeMetric::Vh, 10.0),
1151 PixelValue::from_metric(SizeMetric::Vmin, 10.0),
1152 PixelValue::from_metric(SizeMetric::Vmax, 10.0),
1153 );
1154 let r = e.resolve(&ctx, PropertyContext::Padding);
1155 assert_eq!(r.top, 100.0, "10vw of 1000");
1156 assert_eq!(r.right, 50.0, "10vh of 500");
1157 assert_eq!(r.bottom, 50.0, "10vmin of min(1000,500)");
1158 assert_eq!(r.left, 100.0, "10vmax of max(1000,500)");
1159 }
1160
1161 #[test]
1166 fn to_resolution_context_maps_every_field() {
1167 let ctx = distinct_params().to_resolution_context();
1168 assert_eq!(ctx.element_font_size, 16.0);
1169 assert_eq!(ctx.root_font_size, 10.0);
1170 assert_eq!(ctx.containing_block_size.width, 800.0);
1171 assert_eq!(ctx.containing_block_size.height, 600.0);
1172 assert_eq!(ctx.viewport_size.width, 1000.0);
1173 assert_eq!(ctx.viewport_size.height, 500.0);
1174 assert!(ctx.element_size.is_none(), "element size is unknown pre-layout");
1175 }
1176
1177 #[test]
1178 fn to_resolution_context_aliases_parent_font_size_onto_the_element_font_size() {
1179 let ctx = distinct_params().to_resolution_context();
1183 assert_eq!(ctx.parent_font_size, ctx.element_font_size);
1184 assert_eq!(ctx.parent_font_size, 16.0);
1185 }
1186
1187 #[test]
1188 fn to_resolution_context_passes_extreme_values_through_unchanged() {
1189 let p = params(
1190 LogicalSize::new(f32::MAX, f32::NAN),
1191 LogicalSize::new(f32::INFINITY, 0.0),
1192 0.0,
1193 f32::MIN,
1194 );
1195 let ctx = p.to_resolution_context();
1196 assert_eq!(ctx.containing_block_size.width, f32::MAX);
1197 assert!(ctx.containing_block_size.height.is_nan());
1198 assert!(ctx.viewport_size.width.is_infinite());
1199 assert_eq!(ctx.element_font_size, 0.0);
1200 assert_eq!(ctx.root_font_size, f32::MIN);
1201 }
1202
1203 #[test]
1204 fn zero_font_size_context_resolves_em_to_zero_without_dividing_by_it() {
1205 let p = params(LogicalSize::zero(), LogicalSize::zero(), 0.0, 0.0);
1206 let ctx = p.to_resolution_context();
1207 assert_eq!(PixelValue::em(10.0).resolve_with_context(&ctx, PropertyContext::Margin), 0.0);
1208 assert_eq!(PixelValue::rem(10.0).resolve_with_context(&ctx, PropertyContext::Margin), 0.0);
1209 let vmin = PixelValue::from_metric(SizeMetric::Vmin, 50.0);
1211 assert_eq!(vmin.resolve_with_context(&ctx, PropertyContext::Padding), 0.0);
1212 }
1213
1214 #[test]
1219 fn unresolved_box_props_default_resolves_to_an_all_zero_box() {
1220 let r = UnresolvedBoxProps::default().resolve(&distinct_params());
1221 assert_eq!(r.horizontal_mbp(), 0.0);
1222 assert_eq!(r.vertical_mbp(), 0.0);
1223 assert!(!r.margin_auto.left && !r.margin_auto.right);
1224 }
1225
1226 #[test]
1227 fn unresolved_box_props_resolve_applies_the_right_property_context_per_edge() {
1228 let p = distinct_params(); let b = UnresolvedBoxProps {
1230 margin: UnresolvedEdge::new(
1231 UnresolvedMargin::Auto,
1232 UnresolvedMargin::Length(PixelValue::percent(10.0)), UnresolvedMargin::Zero,
1234 UnresolvedMargin::Auto,
1235 ),
1236 padding: UnresolvedEdge::new(
1237 PixelValue::percent(10.0), PixelValue::px(4.0),
1239 PixelValue::px(4.0),
1240 PixelValue::px(4.0),
1241 ),
1242 border: UnresolvedEdge::new(
1243 PixelValue::percent(10.0), PixelValue::px(2.0),
1245 PixelValue::px(2.0),
1246 PixelValue::px(2.0),
1247 ),
1248 };
1249 let r = b.resolve(&p);
1250
1251 assert_eq!(r.margin.top, 0.0, "auto margin resolves to 0 px");
1252 assert_eq!(r.margin.right, 80.0);
1253 assert_eq!(r.padding.top, 80.0);
1254 assert_eq!(r.border.top, 0.0, "percent border-width must collapse to 0");
1255 assert_eq!(r.border.left, 2.0);
1256
1257 assert!(r.margin_auto.top);
1259 assert!(r.margin_auto.left);
1260 assert!(!r.margin_auto.right);
1261 assert!(!r.margin_auto.bottom);
1262 }
1263
1264 #[test]
1269 fn inner_size_of_a_zero_box_is_zero() {
1270 let bp = ResolvedBoxProps::default();
1271 for wm in ALL_WM {
1272 let s = bp.inner_size(LogicalSize::zero(), wm);
1273 assert_eq!(s.width, 0.0);
1274 assert_eq!(s.height, 0.0);
1275 }
1276 }
1277
1278 #[test]
1279 fn inner_size_subtracts_border_and_padding_but_not_margin() {
1280 let bp = props(
1281 edges(100.0, 100.0, 100.0, 100.0), edges(1.0, 2.0, 4.0, 8.0), edges(10.0, 20.0, 30.0, 40.0), );
1285 let s = bp.inner_size(LogicalSize::new(200.0, 100.0), LayoutWritingMode::HorizontalTb);
1288 assert_eq!(s.width, 130.0);
1289 assert_eq!(s.height, 55.0);
1290 }
1291
1292 #[test]
1293 fn inner_size_is_identical_in_every_writing_mode() {
1294 let bp = props(
1298 EdgeSizes::default(),
1299 edges(1.0, 2.0, 4.0, 8.0),
1300 edges(10.0, 20.0, 30.0, 40.0),
1301 );
1302 let outer = LogicalSize::new(200.0, 100.0);
1303 let base = bp.inner_size(outer, LayoutWritingMode::HorizontalTb);
1304 for wm in ALL_WM {
1305 let s = bp.inner_size(outer, wm);
1306 assert_eq!(s.width, base.width, "{wm:?} width diverged");
1307 assert_eq!(s.height, base.height, "{wm:?} height diverged");
1308 }
1309 }
1310
1311 #[test]
1312 fn inner_size_floors_at_zero_when_border_and_padding_exceed_the_box() {
1313 let bp = props(
1315 EdgeSizes::default(),
1316 edges(100.0, 100.0, 100.0, 100.0),
1317 edges(100.0, 100.0, 100.0, 100.0),
1318 );
1319 for wm in ALL_WM {
1320 let s = bp.inner_size(LogicalSize::new(10.0, 10.0), wm);
1321 assert_eq!(s.width, 0.0, "{wm:?}");
1322 assert_eq!(s.height, 0.0, "{wm:?}");
1323 assert!(s.width >= 0.0 && s.height >= 0.0);
1324 }
1325 }
1326
1327 #[test]
1328 fn inner_size_never_returns_nan() {
1329 let nan_bp = props(EdgeSizes::default(), edges(f32::NAN, 0.0, 0.0, 0.0), EdgeSizes::default());
1331 let cases = [
1332 (ResolvedBoxProps::default(), LogicalSize::new(f32::NAN, f32::NAN)),
1333 (nan_bp, LogicalSize::new(100.0, 100.0)),
1334 (
1335 props(EdgeSizes::default(), edges(f32::INFINITY, 0.0, f32::INFINITY, 0.0), EdgeSizes::default()),
1337 LogicalSize::new(f32::INFINITY, f32::INFINITY),
1338 ),
1339 ];
1340 for (bp, outer) in cases {
1341 for wm in ALL_WM {
1342 let s = bp.inner_size(outer, wm);
1343 assert!(!s.width.is_nan(), "{wm:?}: NaN width escaped inner_size");
1344 assert!(!s.height.is_nan(), "{wm:?}: NaN height escaped inner_size");
1345 assert!(s.width >= 0.0 && s.height >= 0.0);
1346 }
1347 }
1348 }
1349
1350 #[test]
1351 fn inner_size_at_f32_max_stays_finite_and_non_negative() {
1352 let bp = props(EdgeSizes::default(), edges(1.0, 2.0, 4.0, 8.0), edges(1.0, 1.0, 1.0, 1.0));
1353 for wm in ALL_WM {
1354 let s = bp.inner_size(LogicalSize::new(f32::MAX, f32::MAX), wm);
1355 assert!(s.width.is_finite() && s.height.is_finite());
1356 assert!(s.width > 0.0 && s.height > 0.0);
1357 }
1358 }
1359
1360 #[test]
1361 fn inner_size_with_negative_outer_size_floors_to_zero() {
1362 let bp = ResolvedBoxProps::default();
1363 for wm in ALL_WM {
1364 let s = bp.inner_size(LogicalSize::new(-100.0, -50.0), wm);
1365 assert_eq!(s.width, 0.0, "{wm:?}");
1366 assert_eq!(s.height, 0.0, "{wm:?}");
1367 }
1368 }
1369
1370 #[test]
1371 fn inner_size_with_negative_padding_grows_the_content_box() {
1372 let bp = props(EdgeSizes::default(), edges(-5.0, -5.0, -5.0, -5.0), EdgeSizes::default());
1376 let s = bp.inner_size(LogicalSize::new(100.0, 100.0), LayoutWritingMode::HorizontalTb);
1377 assert_eq!(s.width, 110.0);
1378 assert_eq!(s.height, 110.0);
1379 }
1380
1381 #[test]
1386 fn content_box_shrinks_by_border_plus_padding() {
1387 let bp = props(
1388 edges(100.0, 100.0, 100.0, 100.0), edges(1.0, 2.0, 4.0, 8.0), edges(10.0, 20.0, 30.0, 40.0), );
1392 let got = bp.content_box(rect(1000.0, 2000.0, 200.0, 100.0));
1393 assert_eq!(got, rect(1048.0, 2011.0, 130.0, 55.0));
1396 }
1397
1398 #[test]
1399 fn padding_box_shrinks_by_border_only() {
1400 let bp = props(
1401 EdgeSizes::default(),
1402 edges(1.0, 2.0, 4.0, 8.0),
1403 edges(10.0, 20.0, 30.0, 40.0),
1404 );
1405 let got = bp.padding_box(rect(1000.0, 2000.0, 200.0, 100.0));
1406 assert_eq!(got, rect(1040.0, 2010.0, 140.0, 60.0));
1407 }
1408
1409 #[test]
1410 fn margin_box_expands_by_margin_only() {
1411 let bp = props(
1412 edges(1.0, 2.0, 4.0, 8.0), edges(99.0, 99.0, 99.0, 99.0),
1414 edges(99.0, 99.0, 99.0, 99.0),
1415 );
1416 let got = bp.margin_box(rect(1000.0, 2000.0, 200.0, 100.0));
1417 assert_eq!(got, rect(992.0, 1999.0, 210.0, 105.0));
1419 }
1420
1421 #[test]
1422 fn box_rects_nest_content_inside_padding_inside_border_inside_margin() {
1423 let bp = props(
1424 edges(5.0, 6.0, 7.0, 8.0),
1425 edges(1.0, 2.0, 3.0, 4.0),
1426 edges(9.0, 10.0, 11.0, 12.0),
1427 );
1428 let border_box = rect(50.0, 60.0, 400.0, 300.0);
1429 let content = bp.content_box(border_box);
1430 let padding = bp.padding_box(border_box);
1431 let margin = bp.margin_box(border_box);
1432
1433 assert!(margin.origin.x <= border_box.origin.x);
1435 assert!(border_box.origin.x <= padding.origin.x);
1436 assert!(padding.origin.x <= content.origin.x);
1437 assert!(margin.origin.y <= border_box.origin.y);
1438 assert!(border_box.origin.y <= padding.origin.y);
1439 assert!(padding.origin.y <= content.origin.y);
1440
1441 assert!(content.size.width <= padding.size.width);
1442 assert!(padding.size.width <= border_box.size.width);
1443 assert!(border_box.size.width <= margin.size.width);
1444 assert!(content.size.height <= padding.size.height);
1445 assert!(padding.size.height <= border_box.size.height);
1446 assert!(border_box.size.height <= margin.size.height);
1447 }
1448
1449 #[test]
1450 fn content_box_size_floors_at_zero_but_the_origin_still_moves_in() {
1451 let bp = props(
1452 EdgeSizes::default(),
1453 edges(500.0, 500.0, 500.0, 500.0),
1454 edges(500.0, 500.0, 500.0, 500.0),
1455 );
1456 let got = bp.content_box(rect(0.0, 0.0, 10.0, 10.0));
1457 assert_eq!(got.size.width, 0.0, "size must clamp, not go negative");
1458 assert_eq!(got.size.height, 0.0);
1459 assert_eq!(got.origin.x, 1000.0, "origin is not clamped");
1460 assert_eq!(got.origin.y, 1000.0);
1461 }
1462
1463 #[test]
1464 fn padding_box_size_floors_at_zero_for_an_oversized_border() {
1465 let bp = props(EdgeSizes::default(), EdgeSizes::default(), edges(99.0, 99.0, 99.0, 99.0));
1466 let got = bp.padding_box(rect(0.0, 0.0, 10.0, 10.0));
1467 assert_eq!(got.size.width, 0.0);
1468 assert_eq!(got.size.height, 0.0);
1469 }
1470
1471 #[test]
1472 fn margin_box_with_negative_margins_can_shrink_below_zero() {
1473 let bp = props(edges(-10.0, -10.0, -10.0, -10.0), EdgeSizes::default(), EdgeSizes::default());
1477 let got = bp.margin_box(rect(0.0, 0.0, 10.0, 10.0));
1478 assert_eq!(got.size.width, -10.0);
1479 assert_eq!(got.size.height, -10.0);
1480 assert_eq!(got.origin.x, 10.0, "a negative left margin pushes the origin right");
1481 assert_eq!(got.origin.y, 10.0);
1482 }
1483
1484 #[test]
1485 fn box_rects_do_not_panic_on_nan_or_infinite_geometry() {
1486 let bp = props(
1487 edges(f32::NAN, f32::INFINITY, f32::NEG_INFINITY, f32::MAX),
1488 edges(f32::NAN, f32::MAX, 0.0, f32::MIN),
1489 edges(f32::INFINITY, 0.0, f32::NAN, 1.0),
1490 );
1491 let r = rect(f32::NAN, f32::INFINITY, f32::MAX, f32::MIN);
1492 let c = bp.content_box(r);
1493 let p = bp.padding_box(r);
1494 let m = bp.margin_box(r);
1495 for s in [c.size, p.size] {
1497 assert!(!s.width.is_nan() && !s.height.is_nan());
1498 assert!(s.width >= 0.0 && s.height >= 0.0);
1499 }
1500 let _ = m;
1501 }
1502
1503 #[test]
1508 fn mbp_and_bp_getters_sum_the_expected_layers() {
1509 let bp = props(
1510 edges(1.0, 2.0, 4.0, 8.0), edges(10.0, 20.0, 40.0, 80.0), edges(100.0, 200.0, 400.0, 800.0), );
1514 assert_eq!(bp.horizontal_bp(), 1100.0);
1515 assert_eq!(bp.vertical_bp(), 550.0);
1516 assert_eq!(bp.horizontal_mbp(), 1110.0);
1517 assert_eq!(bp.vertical_mbp(), 555.0);
1518
1519 assert_eq!(bp.horizontal_mbp(), bp.horizontal_bp() + bp.margin.horizontal_sum());
1521 assert_eq!(bp.vertical_mbp(), bp.vertical_bp() + bp.margin.vertical_sum());
1522 }
1523
1524 #[test]
1525 fn mbp_and_bp_getters_are_zero_on_a_default_box() {
1526 let bp = ResolvedBoxProps::default();
1527 assert_eq!(bp.horizontal_mbp(), 0.0);
1528 assert_eq!(bp.vertical_mbp(), 0.0);
1529 assert_eq!(bp.horizontal_bp(), 0.0);
1530 assert_eq!(bp.vertical_bp(), 0.0);
1531 }
1532
1533 #[test]
1534 fn mbp_getters_do_not_panic_on_extreme_boxes() {
1535 let bp = props(
1536 edges(f32::MAX, f32::MAX, f32::MAX, f32::MAX),
1537 edges(f32::NAN, 0.0, 0.0, 0.0),
1538 edges(f32::NEG_INFINITY, 0.0, 0.0, 0.0),
1539 );
1540 let _ = bp.horizontal_mbp();
1541 let _ = bp.vertical_mbp();
1542 let _ = bp.horizontal_bp();
1543 let _ = bp.vertical_bp();
1544 }
1545
1546 #[test]
1551 fn pack_edge_encodes_tenths_of_a_pixel() {
1552 let e = edges(0.0, 1.0, 2.5, 3276.7);
1553 let p = PackedBoxProps::pack_edge(&e);
1554 assert_eq!(p[0], 0);
1555 assert_eq!(p[1], 10);
1556 assert_eq!(p[2], 25);
1557 assert_eq!(p[3], 32767, "the documented +3276.7px maximum");
1558 }
1559
1560 #[test]
1561 fn pack_edge_rounds_to_the_nearest_tenth_rather_than_truncating() {
1562 let p = PackedBoxProps::pack_edge(&edges(1.04, 1.06, -1.04, -1.06));
1563 assert_eq!(p[0], 10, "1.04 rounds down");
1564 assert_eq!(p[1], 11, "1.06 rounds up — truncation would give 10");
1565 assert_eq!(p[2], -10);
1566 assert_eq!(p[3], -11);
1567 }
1568
1569 #[test]
1570 fn pack_edge_saturates_out_of_range_values_instead_of_wrapping() {
1571 let p = PackedBoxProps::pack_edge(&edges(10_000.0, -10_000.0, 1e30, -1e30));
1574 assert_eq!(p[0], i16::MAX);
1575 assert_eq!(p[1], i16::MIN);
1576 assert_eq!(p[2], i16::MAX);
1577 assert_eq!(p[3], i16::MIN);
1578 assert!(p.iter().all(|v| *v == i16::MAX || *v == i16::MIN));
1579 }
1580
1581 #[test]
1582 fn pack_edge_clamps_infinities_to_the_i16_bounds() {
1583 let p = PackedBoxProps::pack_edge(&edges(f32::INFINITY, f32::NEG_INFINITY, f32::MAX, f32::MIN));
1584 assert_eq!(p[0], i16::MAX);
1585 assert_eq!(p[1], i16::MIN);
1586 assert_eq!(p[2], i16::MAX);
1587 assert_eq!(p[3], i16::MIN);
1588 }
1589
1590 #[test]
1591 fn pack_edge_maps_nan_to_zero_without_panicking() {
1592 let p = PackedBoxProps::pack_edge(&edges(f32::NAN, f32::NAN, f32::NAN, f32::NAN));
1595 assert_eq!(p, [0, 0, 0, 0]);
1596 }
1597
1598 #[test]
1599 fn pack_edge_maps_negative_zero_to_zero() {
1600 let p = PackedBoxProps::pack_edge(&edges(-0.0, -0.0, -0.0, -0.0));
1601 assert_eq!(p, [0, 0, 0, 0]);
1602 }
1603
1604 #[test]
1605 fn unpack_edge_decodes_tenths_and_preserves_the_trbl_order() {
1606 let e = PackedBoxProps::unpack_edge(&[10, 25, -10, 32767]);
1607 assert!(close(e.top, 1.0, 1e-4), "top was {}", e.top);
1608 assert!(close(e.right, 2.5, 1e-4), "right was {}", e.right);
1609 assert!(close(e.bottom, -1.0, 1e-4), "bottom was {}", e.bottom);
1610 assert!(close(e.left, 3276.7, 1e-2), "left was {}", e.left);
1611 }
1612
1613 #[test]
1614 fn unpack_edge_at_the_i16_extremes_stays_finite() {
1615 let e = PackedBoxProps::unpack_edge(&[i16::MAX, i16::MIN, 0, 1]);
1616 assert!(e.top.is_finite() && e.right.is_finite());
1617 assert!(close(e.top, 3276.7, 1e-2));
1618 assert!(close(e.right, -3276.8, 1e-2));
1619 assert_eq!(e.bottom, 0.0);
1620 assert!(close(e.left, 0.1, 1e-6));
1621 }
1622
1623 #[test]
1628 fn every_i16_encoding_survives_unpack_then_pack_unchanged() {
1629 for n in i16::MIN..=i16::MAX {
1633 let encoded = [n; 4];
1634 let round_tripped = PackedBoxProps::pack_edge(&PackedBoxProps::unpack_edge(&encoded));
1635 assert_eq!(round_tripped, encoded, "i16 code {n} did not round-trip");
1636 }
1637 }
1638
1639 #[test]
1640 fn pack_then_unpack_preserves_values_to_a_tenth_of_a_pixel() {
1641 let bp = props(
1642 edges(1.0, 2.5, 0.1, 12.3),
1643 edges(0.0, 100.25, 3276.7, -3276.8),
1644 edges(0.5, 0.05, 7.0, 0.0),
1645 );
1646 let out = PackedBoxProps::pack(&bp).unpack();
1647 for (got, want) in [
1648 (out.margin.top, bp.margin.top),
1649 (out.margin.right, bp.margin.right),
1650 (out.margin.bottom, bp.margin.bottom),
1651 (out.margin.left, bp.margin.left),
1652 (out.padding.top, bp.padding.top),
1653 (out.padding.right, bp.padding.right),
1654 (out.padding.bottom, bp.padding.bottom),
1655 (out.padding.left, bp.padding.left),
1656 (out.border.top, bp.border.top),
1657 (out.border.right, bp.border.right),
1658 (out.border.bottom, bp.border.bottom),
1659 (out.border.left, bp.border.left),
1660 ] {
1661 assert!(close(got, want, 0.051), "{want} round-tripped to {got}");
1663 }
1664 }
1665
1666 #[test]
1667 fn pack_carries_the_margin_auto_flags_through_verbatim() {
1668 let bp = ResolvedBoxProps {
1670 margin_auto: MarginAuto { left: true, right: false, top: true, bottom: false },
1671 ..ResolvedBoxProps::default()
1672 };
1673 let out = PackedBoxProps::pack(&bp).unpack();
1674 assert!(out.margin_auto.left);
1675 assert!(!out.margin_auto.right);
1676 assert!(out.margin_auto.top);
1677 assert!(!out.margin_auto.bottom);
1678 }
1679
1680 #[test]
1681 fn pack_keeps_the_three_edge_groups_apart() {
1682 let bp = props(
1684 edges(1.0, 1.0, 1.0, 1.0),
1685 edges(2.0, 2.0, 2.0, 2.0),
1686 edges(3.0, 3.0, 3.0, 3.0),
1687 );
1688 let p = PackedBoxProps::pack(&bp);
1689 assert_eq!(p.margin, [10; 4]);
1690 assert_eq!(p.padding, [20; 4]);
1691 assert_eq!(p.border, [30; 4]);
1692 }
1693
1694 #[test]
1695 fn pack_of_an_out_of_range_box_clamps_rather_than_flipping_sign() {
1696 let bp = props(
1697 edges(5000.0, 5000.0, 5000.0, 5000.0), EdgeSizes::default(),
1699 EdgeSizes::default(),
1700 );
1701 let out = PackedBoxProps::pack(&bp).unpack();
1702 assert!(out.margin.top > 0.0, "a huge margin must not decode as negative");
1703 assert!(close(out.margin.top, 3276.7, 1e-2));
1704 }
1705
1706 #[test]
1707 fn packed_default_is_an_all_zero_box() {
1708 let p = PackedBoxProps::default();
1709 assert_eq!(p.margin, [0; 4]);
1710 assert_eq!(p.horizontal_mbp(), 0.0);
1711 assert_eq!(p.vertical_mbp(), 0.0);
1712 assert_eq!(p.horizontal_bp(), 0.0);
1713 assert_eq!(p.vertical_bp(), 0.0);
1714 let s = p.inner_size(LogicalSize::new(10.0, 10.0), LayoutWritingMode::HorizontalTb);
1715 assert_eq!(s.width, 10.0);
1716 assert_eq!(s.height, 10.0);
1717 }
1718
1719 #[test]
1724 fn packed_convenience_methods_match_the_unpacked_equivalents() {
1725 let bp = props(
1726 edges(1.0, 2.5, 4.0, 8.5),
1727 edges(0.5, 1.5, 2.5, 3.5),
1728 edges(2.0, 4.0, 6.0, 8.0),
1729 );
1730 let packed = PackedBoxProps::pack(&bp);
1731 let unpacked = packed.unpack();
1732 let r = rect(10.0, 20.0, 300.0, 200.0);
1733
1734 assert_eq!(packed.content_box(r), unpacked.content_box(r));
1735 assert_eq!(packed.padding_box(r), unpacked.padding_box(r));
1736 assert_eq!(packed.margin_box(r), unpacked.margin_box(r));
1737 assert_eq!(packed.horizontal_mbp(), unpacked.horizontal_mbp());
1738 assert_eq!(packed.vertical_mbp(), unpacked.vertical_mbp());
1739 assert_eq!(packed.horizontal_bp(), unpacked.horizontal_bp());
1740 assert_eq!(packed.vertical_bp(), unpacked.vertical_bp());
1741
1742 for wm in ALL_WM {
1743 let a = packed.inner_size(r.size, wm);
1744 let b = unpacked.inner_size(r.size, wm);
1745 assert_eq!(a.width, b.width, "{wm:?}");
1746 assert_eq!(a.height, b.height, "{wm:?}");
1747 }
1748 }
1749
1750 #[test]
1751 fn packed_inner_size_floors_at_zero_for_a_tiny_box() {
1752 let bp = props(
1753 EdgeSizes::default(),
1754 edges(50.0, 50.0, 50.0, 50.0),
1755 edges(50.0, 50.0, 50.0, 50.0),
1756 );
1757 let packed = PackedBoxProps::pack(&bp);
1758 for wm in ALL_WM {
1759 let s = packed.inner_size(LogicalSize::new(1.0, 1.0), wm);
1760 assert_eq!(s.width, 0.0, "{wm:?}");
1761 assert_eq!(s.height, 0.0, "{wm:?}");
1762 }
1763 }
1764
1765 #[test]
1766 fn packed_box_rects_do_not_panic_on_extreme_input_rects() {
1767 let packed = PackedBoxProps::pack(&props(
1768 edges(3276.7, 3276.7, 3276.7, 3276.7),
1769 edges(3276.7, 3276.7, 3276.7, 3276.7),
1770 edges(3276.7, 3276.7, 3276.7, 3276.7),
1771 ));
1772 for r in [
1773 rect(0.0, 0.0, 0.0, 0.0),
1774 rect(f32::MAX, f32::MIN, f32::MAX, f32::MAX),
1775 rect(f32::NAN, f32::NAN, f32::NAN, f32::NAN),
1776 rect(-1e30, -1e30, -1e30, -1e30),
1777 ] {
1778 let c = packed.content_box(r);
1779 let p = packed.padding_box(r);
1780 let _ = packed.margin_box(r);
1781 assert!(c.size.width >= 0.0 && !c.size.width.is_nan());
1782 assert!(p.size.height >= 0.0 && !p.size.height.is_nan());
1783 }
1784 }
1785
1786 #[test]
1791 fn writing_mode_context_new_stores_what_it_was_given() {
1792 let c = WritingModeContext::new(
1793 LayoutWritingMode::VerticalRl,
1794 StyleDirection::Rtl,
1795 StyleTextOrientation::Mixed,
1796 );
1797 assert_eq!(c.writing_mode, LayoutWritingMode::VerticalRl);
1798 assert_eq!(c.direction, StyleDirection::Rtl);
1799 assert_eq!(c.text_orientation, StyleTextOrientation::Mixed);
1800 }
1801
1802 #[test]
1803 fn text_orientation_upright_forces_the_used_direction_to_ltr() {
1804 let c = WritingModeContext::new(
1807 LayoutWritingMode::VerticalRl,
1808 StyleDirection::Rtl,
1809 StyleTextOrientation::Upright,
1810 );
1811 assert_eq!(c.used_direction(), StyleDirection::Ltr);
1812 assert!(!c.is_inline_reversed(), "upright must cancel the RTL reversal");
1813 }
1814
1815 #[test]
1816 fn only_upright_overrides_the_direction() {
1817 for orientation in [StyleTextOrientation::Mixed, StyleTextOrientation::Sideways] {
1818 let c = WritingModeContext::new(
1819 LayoutWritingMode::VerticalRl,
1820 StyleDirection::Rtl,
1821 orientation,
1822 );
1823 assert_eq!(
1824 c.used_direction(),
1825 StyleDirection::Rtl,
1826 "{orientation:?} must not touch the direction"
1827 );
1828 assert!(c.is_inline_reversed());
1829 }
1830 }
1831
1832 #[test]
1833 fn writing_mode_context_new_is_idempotent() {
1834 for wm in ALL_WM {
1837 for dir in [StyleDirection::Ltr, StyleDirection::Rtl] {
1838 for or in [
1839 StyleTextOrientation::Mixed,
1840 StyleTextOrientation::Upright,
1841 StyleTextOrientation::Sideways,
1842 ] {
1843 let once = WritingModeContext::new(wm, dir, or);
1844 let twice = WritingModeContext::new(
1845 once.writing_mode,
1846 once.direction,
1847 once.text_orientation,
1848 );
1849 assert_eq!(once, twice, "{wm:?}/{dir:?}/{or:?} is not a fixed point");
1850 }
1851 }
1852 }
1853 }
1854
1855 #[test]
1856 fn is_horizontal_is_true_only_for_horizontal_tb() {
1857 let mk = |wm| WritingModeContext::new(wm, StyleDirection::Ltr, StyleTextOrientation::Mixed);
1858 assert!(mk(LayoutWritingMode::HorizontalTb).is_horizontal());
1859 assert!(!mk(LayoutWritingMode::VerticalRl).is_horizontal());
1860 assert!(!mk(LayoutWritingMode::VerticalLr).is_horizontal());
1861 }
1862
1863 #[test]
1864 fn inline_and_block_axis_predicates_track_is_horizontal() {
1865 for wm in ALL_WM {
1868 for dir in [StyleDirection::Ltr, StyleDirection::Rtl] {
1869 let c = WritingModeContext::new(wm, dir, StyleTextOrientation::Mixed);
1870 assert_eq!(c.inline_size_is_width(), c.is_horizontal(), "{wm:?}");
1871 assert_eq!(c.block_size_is_height(), c.is_horizontal(), "{wm:?}");
1872 assert_eq!(
1873 c.inline_size_is_width(),
1874 c.block_size_is_height(),
1875 "{wm:?}: the two axes must flip together"
1876 );
1877 }
1878 }
1879 }
1880
1881 #[test]
1882 fn is_inline_reversed_follows_the_used_direction_not_the_writing_mode() {
1883 for wm in ALL_WM {
1884 let ltr = WritingModeContext::new(wm, StyleDirection::Ltr, StyleTextOrientation::Mixed);
1885 let rtl = WritingModeContext::new(wm, StyleDirection::Rtl, StyleTextOrientation::Mixed);
1886 assert!(!ltr.is_inline_reversed(), "{wm:?} ltr");
1887 assert!(rtl.is_inline_reversed(), "{wm:?} rtl");
1888 }
1889 }
1890
1891 #[test]
1892 fn default_writing_mode_context_is_horizontal_ltr_mixed() {
1893 let c = WritingModeContext::default();
1894 assert_eq!(c.writing_mode, LayoutWritingMode::HorizontalTb);
1895 assert_eq!(c.used_direction(), StyleDirection::Ltr);
1896 assert_eq!(c.text_orientation, StyleTextOrientation::Mixed);
1897 assert!(c.is_horizontal());
1898 assert!(c.inline_size_is_width());
1899 assert!(c.block_size_is_height());
1900 assert!(!c.is_inline_reversed());
1901 }
1902
1903 #[test]
1904 fn default_context_matches_new_with_the_same_arguments() {
1905 let built = WritingModeContext::new(
1906 LayoutWritingMode::HorizontalTb,
1907 StyleDirection::Ltr,
1908 StyleTextOrientation::Mixed,
1909 );
1910 assert_eq!(WritingModeContext::default(), built);
1911 }
1912
1913 #[test]
1914 fn fit_content_clamps_stretch_fit_between_min_and_max_content() {
1915 let is = IntrinsicSizes {
1916 min_content_width: 30.0,
1917 max_content_width: 100.0,
1918 min_content_height: 10.0,
1919 max_content_height: 40.0,
1920 ..Default::default()
1921 };
1922 assert!((is.fit_content_width(60.0) - 60.0).abs() < f32::EPSILON);
1924 assert!((is.fit_content_height(25.0) - 25.0).abs() < f32::EPSILON);
1925 assert!((is.fit_content_width(500.0) - 100.0).abs() < f32::EPSILON);
1927 assert!((is.fit_content_height(500.0) - 40.0).abs() < f32::EPSILON);
1928 assert!((is.fit_content_width(0.0) - 30.0).abs() < f32::EPSILON);
1930 assert!((is.fit_content_height(0.0) - 10.0).abs() < f32::EPSILON);
1931 }
1932
1933 #[test]
1934 fn intrinsic_sizes_default_has_no_preferred_aspect_ratio() {
1935 assert_eq!(IntrinsicSizes::default().preferred_aspect_ratio, None);
1936 }
1937}