use azul_core::{
geom::{LogicalPosition, LogicalRect, LogicalSize},
ui_solver::ResolvedOffsets,
};
use azul_css::props::{
basic::{pixel::PixelValue, PhysicalSize, PropertyContext, ResolutionContext, SizeMetric},
layout::LayoutWritingMode,
style::{StyleDirection, StyleTextOrientation},
};
#[derive(Copy, Debug, Clone, PartialEq, PartialOrd)]
pub struct PositionedRectangle {
pub bounds: LogicalRect,
pub margin: ResolvedOffsets,
pub border: ResolvedOffsets,
pub padding: ResolvedOffsets,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct EdgeSizes {
pub top: f32,
pub right: f32,
pub bottom: f32,
pub left: f32,
}
impl EdgeSizes {
#[must_use] pub fn horizontal_sum(&self) -> f32 {
self.left + self.right
}
#[must_use] pub fn vertical_sum(&self) -> f32 {
self.top + self.bottom
}
#[must_use] pub const fn main_start(&self, wm: LayoutWritingMode) -> f32 {
match wm {
LayoutWritingMode::HorizontalTb => self.top,
LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.left,
}
}
#[must_use] pub const fn main_end(&self, wm: LayoutWritingMode) -> f32 {
match wm {
LayoutWritingMode::HorizontalTb => self.bottom,
LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.right,
}
}
#[must_use] pub fn main_sum(&self, wm: LayoutWritingMode) -> f32 {
self.main_start(wm) + self.main_end(wm)
}
#[must_use] pub const fn cross_start(&self, wm: LayoutWritingMode) -> f32 {
match wm {
LayoutWritingMode::HorizontalTb => self.left,
LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.top,
}
}
#[must_use] pub const fn cross_end(&self, wm: LayoutWritingMode) -> f32 {
match wm {
LayoutWritingMode::HorizontalTb => self.right,
LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.bottom,
}
}
#[must_use] pub fn cross_sum(&self, wm: LayoutWritingMode) -> f32 {
self.cross_start(wm) + self.cross_end(wm)
}
#[must_use] pub const fn line_over(&self, wm: LayoutWritingMode) -> f32 {
match wm {
LayoutWritingMode::HorizontalTb => self.top,
LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.right,
}
}
#[must_use] pub const fn line_under(&self, wm: LayoutWritingMode) -> f32 {
match wm {
LayoutWritingMode::HorizontalTb => self.bottom,
LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.left,
}
}
#[must_use] pub fn line_over_under_sum(&self, wm: LayoutWritingMode) -> f32 {
self.line_over(wm) + self.line_under(wm)
}
#[must_use] pub const fn inline_start(&self, wm: LayoutWritingMode, dir: StyleDirection) -> f32 {
match dir {
StyleDirection::Ltr => self.cross_start(wm),
StyleDirection::Rtl => self.cross_end(wm),
}
}
#[must_use] pub const fn inline_end(&self, wm: LayoutWritingMode, dir: StyleDirection) -> f32 {
match dir {
StyleDirection::Ltr => self.cross_end(wm),
StyleDirection::Rtl => self.cross_start(wm),
}
}
#[must_use] pub fn inline_sum(&self, wm: LayoutWritingMode, dir: StyleDirection) -> f32 {
self.inline_start(wm, dir) + self.inline_end(wm, dir)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum UnresolvedMargin {
#[default]
Zero,
Auto,
Length(PixelValue),
}
impl UnresolvedMargin {
#[must_use] pub const fn is_auto(&self) -> bool {
matches!(self, Self::Auto)
}
#[allow(clippy::match_same_arms)] #[must_use] pub fn resolve(&self, ctx: &ResolutionContext) -> f32 {
match self {
Self::Zero => 0.0,
Self::Auto => 0.0, Self::Length(pv) => pv.resolve_with_context(ctx, PropertyContext::Margin),
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct UnresolvedEdge<T> {
pub top: T,
pub right: T,
pub bottom: T,
pub left: T,
}
impl<T> UnresolvedEdge<T> {
pub const fn new(top: T, right: T, bottom: T, left: T) -> Self {
Self { top, right, bottom, left }
}
}
impl UnresolvedEdge<UnresolvedMargin> {
#[must_use] pub fn resolve(&self, ctx: &ResolutionContext) -> EdgeSizes {
EdgeSizes {
top: self.top.resolve(ctx),
right: self.right.resolve(ctx),
bottom: self.bottom.resolve(ctx),
left: self.left.resolve(ctx),
}
}
#[must_use] pub const fn get_margin_auto(&self) -> MarginAuto {
MarginAuto {
top: self.top.is_auto(),
right: self.right.is_auto(),
bottom: self.bottom.is_auto(),
left: self.left.is_auto(),
}
}
}
impl UnresolvedEdge<PixelValue> {
#[must_use] pub fn resolve(&self, ctx: &ResolutionContext, prop_ctx: PropertyContext) -> EdgeSizes {
EdgeSizes {
top: self.top.resolve_with_context(ctx, prop_ctx),
right: self.right.resolve_with_context(ctx, prop_ctx),
bottom: self.bottom.resolve_with_context(ctx, prop_ctx),
left: self.left.resolve_with_context(ctx, prop_ctx),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ResolutionParams {
pub containing_block: LogicalSize,
pub viewport_size: LogicalSize,
pub element_font_size: f32,
pub root_font_size: f32,
}
impl ResolutionParams {
#[must_use] pub const fn to_resolution_context(&self) -> ResolutionContext {
ResolutionContext {
element_font_size: self.element_font_size,
parent_font_size: self.element_font_size,
root_font_size: self.root_font_size,
element_size: None,
containing_block_size: PhysicalSize::new(
self.containing_block.width,
self.containing_block.height,
),
viewport_size: PhysicalSize::new(
self.viewport_size.width,
self.viewport_size.height,
),
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct UnresolvedBoxProps {
pub margin: UnresolvedEdge<UnresolvedMargin>,
pub padding: UnresolvedEdge<PixelValue>,
pub border: UnresolvedEdge<PixelValue>,
}
impl UnresolvedBoxProps {
#[must_use] pub fn resolve(&self, params: &ResolutionParams) -> ResolvedBoxProps {
let ctx = params.to_resolution_context();
ResolvedBoxProps {
margin: self.margin.resolve(&ctx),
padding: self.padding.resolve(&ctx, PropertyContext::Padding),
border: self.border.resolve(&ctx, PropertyContext::BorderWidth),
margin_auto: self.margin.get_margin_auto(),
}
}
}
#[derive(Debug, Clone, Copy, Default)]
#[allow(clippy::struct_excessive_bools)] pub struct MarginAuto {
pub left: bool,
pub right: bool,
pub top: bool,
pub bottom: bool,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ResolvedBoxProps {
pub margin: EdgeSizes,
pub padding: EdgeSizes,
pub border: EdgeSizes,
pub margin_auto: MarginAuto,
}
impl ResolvedBoxProps {
#[must_use] pub fn inner_size(&self, outer_size: LogicalSize, wm: LayoutWritingMode) -> LogicalSize {
let outer_main = outer_size.main(wm);
let outer_cross = outer_size.cross(wm);
let cross_axis_spacing = self.padding.cross_sum(wm) + self.border.cross_sum(wm);
let main_axis_spacing = self.padding.main_sum(wm) + self.border.main_sum(wm);
let inner_main = (outer_main - main_axis_spacing).max(0.0);
let inner_cross = (outer_cross - cross_axis_spacing).max(0.0);
LogicalSize::from_main_cross(inner_main, inner_cross, wm)
}
#[must_use] pub fn content_box(&self, border_box: LogicalRect) -> LogicalRect {
let x = border_box.origin.x + self.border.left + self.padding.left;
let y = border_box.origin.y + self.border.top + self.padding.top;
let w = (border_box.size.width - self.border.horizontal_sum() - self.padding.horizontal_sum()).max(0.0);
let h = (border_box.size.height - self.border.vertical_sum() - self.padding.vertical_sum()).max(0.0);
LogicalRect { origin: LogicalPosition { x, y }, size: LogicalSize { width: w, height: h } }
}
#[must_use] pub fn padding_box(&self, border_box: LogicalRect) -> LogicalRect {
let x = border_box.origin.x + self.border.left;
let y = border_box.origin.y + self.border.top;
let w = (border_box.size.width - self.border.horizontal_sum()).max(0.0);
let h = (border_box.size.height - self.border.vertical_sum()).max(0.0);
LogicalRect { origin: LogicalPosition { x, y }, size: LogicalSize { width: w, height: h } }
}
#[must_use] pub fn margin_box(&self, border_box: LogicalRect) -> LogicalRect {
let x = border_box.origin.x - self.margin.left;
let y = border_box.origin.y - self.margin.top;
let w = border_box.size.width + self.margin.horizontal_sum();
let h = border_box.size.height + self.margin.vertical_sum();
LogicalRect { origin: LogicalPosition { x, y }, size: LogicalSize { width: w, height: h } }
}
#[must_use] pub fn horizontal_mbp(&self) -> f32 {
self.margin.horizontal_sum() + self.border.horizontal_sum() + self.padding.horizontal_sum()
}
#[must_use] pub fn vertical_mbp(&self) -> f32 {
self.margin.vertical_sum() + self.border.vertical_sum() + self.padding.vertical_sum()
}
#[must_use] pub fn horizontal_bp(&self) -> f32 {
self.border.horizontal_sum() + self.padding.horizontal_sum()
}
#[must_use] pub fn vertical_bp(&self) -> f32 {
self.border.vertical_sum() + self.padding.vertical_sum()
}
}
pub type BoxProps = ResolvedBoxProps;
#[derive(Debug, Clone, Copy, Default)]
#[repr(C)]
pub struct PackedBoxProps {
pub margin: [i16; 4], pub padding: [i16; 4], pub border: [i16; 4], pub margin_auto: MarginAuto,
}
impl PackedBoxProps {
#[inline]
#[must_use] pub fn pack(bp: &ResolvedBoxProps) -> Self {
Self {
margin: Self::pack_edge(&bp.margin),
padding: Self::pack_edge(&bp.padding),
border: Self::pack_edge(&bp.border),
margin_auto: bp.margin_auto,
}
}
#[inline]
#[must_use] pub fn unpack(&self) -> ResolvedBoxProps {
ResolvedBoxProps {
margin: Self::unpack_edge(&self.margin),
padding: Self::unpack_edge(&self.padding),
border: Self::unpack_edge(&self.border),
margin_auto: self.margin_auto,
}
}
#[inline]
#[must_use] pub fn inner_size(&self, outer_size: LogicalSize, wm: LayoutWritingMode) -> LogicalSize {
self.unpack().inner_size(outer_size, wm)
}
#[inline]
#[must_use] pub fn content_box(&self, border_box: LogicalRect) -> LogicalRect {
self.unpack().content_box(border_box)
}
#[inline]
#[must_use] pub fn padding_box(&self, border_box: LogicalRect) -> LogicalRect {
self.unpack().padding_box(border_box)
}
#[inline]
#[must_use] pub fn margin_box(&self, border_box: LogicalRect) -> LogicalRect {
self.unpack().margin_box(border_box)
}
#[inline]
#[must_use] pub fn horizontal_mbp(&self) -> f32 {
self.unpack().horizontal_mbp()
}
#[inline]
#[must_use] pub fn vertical_mbp(&self) -> f32 {
self.unpack().vertical_mbp()
}
#[inline]
#[must_use] pub fn horizontal_bp(&self) -> f32 {
self.unpack().horizontal_bp()
}
#[inline]
#[must_use] pub fn vertical_bp(&self) -> f32 {
self.unpack().vertical_bp()
}
#[inline]
#[allow(clippy::cast_possible_truncation)] fn pack_edge(e: &EdgeSizes) -> [i16; 4] {
const MIN: f32 = i16::MIN as f32;
const MAX: f32 = i16::MAX as f32;
[
(e.top * 10.0).round().clamp(MIN, MAX) as i16,
(e.right * 10.0).round().clamp(MIN, MAX) as i16,
(e.bottom * 10.0).round().clamp(MIN, MAX) as i16,
(e.left * 10.0).round().clamp(MIN, MAX) as i16,
]
}
#[inline]
#[allow(clippy::trivially_copy_pass_by_ref)] fn unpack_edge(e: &[i16; 4]) -> EdgeSizes {
EdgeSizes {
top: f32::from(e[0]) * 0.1,
right: f32::from(e[1]) * 0.1,
bottom: f32::from(e[2]) * 0.1,
left: f32::from(e[3]) * 0.1,
}
}
}
pub use azul_css::props::layout::{LayoutClear, LayoutFloat};
#[derive(Debug, Clone, Copy, Default)]
pub struct IntrinsicSizes {
pub min_content_width: f32,
pub max_content_width: f32,
pub preferred_width: Option<f32>,
pub min_content_height: f32,
pub max_content_height: f32,
pub preferred_height: Option<f32>,
pub preferred_aspect_ratio: Option<f32>,
}
impl IntrinsicSizes {
#[must_use]
pub const fn fit_content_width(&self, available_inline_size: f32) -> f32 {
available_inline_size
.min(self.max_content_width)
.max(self.min_content_width)
}
#[must_use]
pub const fn fit_content_height(&self, available_block_size: f32) -> f32 {
available_block_size
.min(self.max_content_height)
.max(self.min_content_height)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WritingModeContext {
pub writing_mode: LayoutWritingMode,
pub direction: StyleDirection,
pub text_orientation: StyleTextOrientation,
}
impl Default for WritingModeContext {
fn default() -> Self {
Self {
writing_mode: LayoutWritingMode::HorizontalTb,
direction: StyleDirection::Ltr,
text_orientation: StyleTextOrientation::Mixed,
}
}
}
impl WritingModeContext {
#[must_use] pub fn new(
writing_mode: LayoutWritingMode,
direction: StyleDirection,
text_orientation: StyleTextOrientation,
) -> Self {
let used_direction = if text_orientation == StyleTextOrientation::Upright {
StyleDirection::Ltr
} else {
direction
};
Self {
writing_mode,
direction: used_direction,
text_orientation,
}
}
#[must_use] pub const fn used_direction(&self) -> StyleDirection {
self.direction
}
#[must_use] pub const fn is_horizontal(&self) -> bool {
matches!(self.writing_mode, LayoutWritingMode::HorizontalTb)
}
#[must_use] pub const fn inline_size_is_width(&self) -> bool {
self.is_horizontal()
}
#[must_use] pub const fn block_size_is_height(&self) -> bool {
self.is_horizontal()
}
#[must_use] pub fn is_inline_reversed(&self) -> bool {
self.used_direction() == StyleDirection::Rtl
}
}
#[cfg(test)]
#[allow(clippy::float_cmp, clippy::unreadable_literal)]
mod autotest_generated {
use super::*;
const ALL_WM: [LayoutWritingMode; 3] = [
LayoutWritingMode::HorizontalTb,
LayoutWritingMode::VerticalRl,
LayoutWritingMode::VerticalLr,
];
fn edges(top: f32, right: f32, bottom: f32, left: f32) -> EdgeSizes {
EdgeSizes { top, right, bottom, left }
}
fn rect(x: f32, y: f32, w: f32, h: f32) -> LogicalRect {
LogicalRect {
origin: LogicalPosition { x, y },
size: LogicalSize { width: w, height: h },
}
}
fn close(a: f32, b: f32, eps: f32) -> bool {
(a - b).abs() <= eps
}
fn params(cb: LogicalSize, vp: LogicalSize, font: f32, root: f32) -> ResolutionParams {
ResolutionParams {
containing_block: cb,
viewport_size: vp,
element_font_size: font,
root_font_size: root,
}
}
fn distinct_params() -> ResolutionParams {
params(
LogicalSize::new(800.0, 600.0),
LogicalSize::new(1000.0, 500.0),
16.0,
10.0,
)
}
fn props(margin: EdgeSizes, padding: EdgeSizes, border: EdgeSizes) -> ResolvedBoxProps {
ResolvedBoxProps { margin, padding, border, margin_auto: MarginAuto::default() }
}
#[test]
fn edge_sizes_sums_pick_the_right_pair_of_edges() {
let e = edges(1.0, 2.0, 4.0, 8.0); assert_eq!(e.horizontal_sum(), 10.0, "horizontal = left + right");
assert_eq!(e.vertical_sum(), 5.0, "vertical = top + bottom");
}
#[test]
fn edge_sizes_default_is_all_zero() {
let e = EdgeSizes::default();
assert_eq!(e.horizontal_sum(), 0.0);
assert_eq!(e.vertical_sum(), 0.0);
for wm in ALL_WM {
assert_eq!(e.main_sum(wm), 0.0);
assert_eq!(e.cross_sum(wm), 0.0);
}
}
#[test]
fn edge_sizes_sums_saturate_to_infinity_instead_of_panicking() {
let e = edges(f32::MAX, f32::MAX, f32::MAX, f32::MAX);
assert!(e.horizontal_sum().is_infinite() && e.horizontal_sum() > 0.0);
assert!(e.vertical_sum().is_infinite() && e.vertical_sum() > 0.0);
}
#[test]
fn edge_sizes_opposing_infinities_produce_nan_not_a_panic() {
let e = edges(f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY);
assert!(e.horizontal_sum().is_nan());
assert!(e.vertical_sum().is_nan());
}
#[test]
fn edge_sizes_nan_edges_propagate_without_panicking() {
let e = edges(f32::NAN, 1.0, 2.0, 3.0);
assert!(e.vertical_sum().is_nan());
assert_eq!(e.horizontal_sum(), 4.0, "a NaN top must not poison left+right");
for wm in ALL_WM {
let _ = e.main_sum(wm);
let _ = e.cross_sum(wm);
}
}
#[test]
fn edge_sizes_axis_mapping_is_exact_for_every_writing_mode() {
let e = edges(1.0, 2.0, 4.0, 8.0);
let h = LayoutWritingMode::HorizontalTb;
assert_eq!(e.main_start(h), 1.0, "main-start = top");
assert_eq!(e.main_end(h), 4.0, "main-end = bottom");
assert_eq!(e.cross_start(h), 8.0, "cross-start = left");
assert_eq!(e.cross_end(h), 2.0, "cross-end = right");
for wm in [LayoutWritingMode::VerticalRl, LayoutWritingMode::VerticalLr] {
assert_eq!(e.main_start(wm), 8.0, "main-start = left");
assert_eq!(e.main_end(wm), 2.0, "main-end = right");
assert_eq!(e.cross_start(wm), 1.0, "cross-start = top");
assert_eq!(e.cross_end(wm), 4.0, "cross-end = bottom");
}
}
#[test]
fn edge_sizes_line_over_under_is_line_relative_not_physical() {
let e = edges(1.0, 2.0, 4.0, 8.0);
let h = LayoutWritingMode::HorizontalTb;
assert_eq!(e.line_over(h), 1.0, "line-over = top in horizontal-tb");
assert_eq!(e.line_under(h), 4.0, "line-under = bottom in horizontal-tb");
assert_eq!(e.line_over(h), e.main_start(h), "over == main-start only in horizontal-tb");
for wm in [LayoutWritingMode::VerticalRl, LayoutWritingMode::VerticalLr] {
assert_eq!(e.line_over(wm), 2.0, "line-over = right in vertical modes");
assert_eq!(e.line_under(wm), 8.0, "line-under = left in vertical modes");
assert_ne!(e.line_over(wm), e.main_start(wm), "over != main-start in vertical modes");
}
assert_eq!(e.line_over_under_sum(h), 5.0);
assert_eq!(e.line_over_under_sum(LayoutWritingMode::VerticalRl), 10.0);
}
#[test]
fn edge_sizes_inline_start_end_depend_on_direction() {
let e = edges(1.0, 2.0, 4.0, 8.0);
for wm in ALL_WM {
assert_eq!(
e.inline_start(wm, StyleDirection::Ltr),
e.cross_start(wm),
"LTR inline-start = cross-start (line-left)"
);
assert_eq!(
e.inline_end(wm, StyleDirection::Ltr),
e.cross_end(wm),
"LTR inline-end = cross-end (line-right)"
);
assert_eq!(
e.inline_start(wm, StyleDirection::Rtl),
e.cross_end(wm),
"RTL inline-start = cross-end (line-right)"
);
assert_eq!(
e.inline_end(wm, StyleDirection::Rtl),
e.cross_start(wm),
"RTL inline-end = cross-start (line-left)"
);
assert_eq!(
e.inline_sum(wm, StyleDirection::Ltr),
e.inline_sum(wm, StyleDirection::Rtl),
"inline-sum is direction-independent"
);
assert_eq!(e.inline_sum(wm, StyleDirection::Ltr), e.cross_sum(wm));
}
let h = LayoutWritingMode::HorizontalTb;
assert_eq!(e.inline_start(h, StyleDirection::Ltr), 8.0);
assert_eq!(e.inline_start(h, StyleDirection::Rtl), 2.0);
}
#[test]
fn edge_sizes_main_and_cross_sums_partition_the_four_edges() {
let e = edges(1.0, 2.0, 4.0, 8.0);
for wm in ALL_WM {
assert_eq!(e.main_start(wm) + e.main_end(wm), e.main_sum(wm));
assert_eq!(e.cross_start(wm) + e.cross_end(wm), e.cross_sum(wm));
assert_eq!(
e.main_sum(wm) + e.cross_sum(wm),
e.horizontal_sum() + e.vertical_sum(),
"{wm:?}: main+cross must cover all four edges"
);
}
}
#[test]
fn edge_sizes_axis_accessors_survive_extreme_values() {
let e = edges(f32::MAX, f32::MIN, f32::INFINITY, f32::NEG_INFINITY);
for wm in ALL_WM {
let _ = e.main_start(wm);
let _ = e.main_end(wm);
let _ = e.cross_start(wm);
let _ = e.cross_end(wm);
let _ = e.main_sum(wm);
let _ = e.cross_sum(wm);
}
}
#[test]
fn unresolved_margin_is_auto_only_for_the_auto_variant() {
assert!(UnresolvedMargin::Auto.is_auto());
assert!(!UnresolvedMargin::Zero.is_auto());
assert!(!UnresolvedMargin::Length(PixelValue::const_px(10)).is_auto());
assert!(!UnresolvedMargin::Length(PixelValue::zero()).is_auto());
assert!(!UnresolvedMargin::default().is_auto(), "default is Zero, not Auto");
}
#[test]
fn unresolved_margin_auto_and_zero_both_resolve_to_zero_px() {
let ctx = distinct_params().to_resolution_context();
assert_eq!(UnresolvedMargin::Zero.resolve(&ctx), 0.0);
assert_eq!(UnresolvedMargin::Auto.resolve(&ctx), 0.0);
}
#[test]
fn unresolved_margin_length_resolves_by_metric() {
let ctx = distinct_params().to_resolution_context(); let r = |pv: PixelValue| UnresolvedMargin::Length(pv).resolve(&ctx);
assert_eq!(r(PixelValue::px(12.5)), 12.5);
assert_eq!(r(PixelValue::em(2.0)), 32.0, "em = 2 x element font-size");
assert_eq!(r(PixelValue::rem(2.0)), 20.0, "rem = 2 x root font-size");
}
#[test]
fn unresolved_margin_percent_resolves_against_containing_block_width() {
let ctx = distinct_params().to_resolution_context();
let got = UnresolvedMargin::Length(PixelValue::percent(50.0)).resolve(&ctx);
assert_eq!(got, 400.0);
assert_ne!(got, 300.0, "percentage margin must not use the block height");
}
#[test]
fn unresolved_margin_nan_length_resolves_to_zero_not_nan() {
let ctx = distinct_params().to_resolution_context();
let got = UnresolvedMargin::Length(PixelValue::px(f32::NAN)).resolve(&ctx);
assert!(!got.is_nan(), "a NaN px value must not survive resolution");
assert_eq!(got, 0.0);
}
#[test]
fn unresolved_margin_huge_length_saturates_to_a_finite_value() {
let ctx = distinct_params().to_resolution_context();
for v in [f32::MAX, f32::MIN, f32::INFINITY, f32::NEG_INFINITY] {
let got = UnresolvedMargin::Length(PixelValue::px(v)).resolve(&ctx);
assert!(got.is_finite(), "px({v}) resolved to a non-finite {got}");
}
}
#[test]
fn unresolved_margin_percent_of_a_nan_containing_block_is_nan_not_a_panic() {
let p = params(
LogicalSize::new(f32::NAN, f32::NAN),
LogicalSize::new(1000.0, 500.0),
16.0,
10.0,
);
let got = UnresolvedMargin::Length(PixelValue::percent(50.0)).resolve(&p.to_resolution_context());
assert!(got.is_nan());
}
#[test]
fn unresolved_edge_new_assigns_fields_in_top_right_bottom_left_order() {
let e = UnresolvedEdge::new(1_u8, 2, 4, 8);
assert_eq!(e.top, 1);
assert_eq!(e.right, 2);
assert_eq!(e.bottom, 4);
assert_eq!(e.left, 8);
}
#[test]
fn unresolved_edge_new_accepts_extreme_payloads() {
let e = UnresolvedEdge::new(f32::NAN, f32::INFINITY, f32::MAX, f32::MIN);
assert!(e.top.is_nan());
assert!(e.right.is_infinite());
assert_eq!(e.bottom, f32::MAX);
assert_eq!(e.left, f32::MIN);
}
#[test]
fn get_margin_auto_flags_exactly_the_auto_sides() {
let e = UnresolvedEdge::new(
UnresolvedMargin::Zero, UnresolvedMargin::Auto, UnresolvedMargin::Length(PixelValue::const_px(5)), UnresolvedMargin::Auto, );
let a = e.get_margin_auto();
assert!(!a.top);
assert!(a.right);
assert!(!a.bottom);
assert!(a.left);
}
#[test]
fn get_margin_auto_on_default_edge_flags_nothing() {
let a = UnresolvedEdge::<UnresolvedMargin>::default().get_margin_auto();
assert!(!a.top && !a.right && !a.bottom && !a.left);
}
#[test]
fn unresolved_margin_edge_resolve_keeps_each_side_separate() {
let ctx = distinct_params().to_resolution_context();
let e = UnresolvedEdge::new(
UnresolvedMargin::Length(PixelValue::px(1.0)), UnresolvedMargin::Length(PixelValue::px(2.0)), UnresolvedMargin::Auto, UnresolvedMargin::Length(PixelValue::px(8.0)), );
let r = e.resolve(&ctx);
assert_eq!(r.top, 1.0);
assert_eq!(r.right, 2.0);
assert_eq!(r.bottom, 0.0, "auto resolves to 0 px here");
assert_eq!(r.left, 8.0);
}
#[test]
fn pixel_edge_resolve_drops_percentages_on_border_width() {
let ctx = distinct_params().to_resolution_context();
let e = UnresolvedEdge::new(
PixelValue::percent(50.0),
PixelValue::percent(50.0),
PixelValue::percent(50.0),
PixelValue::percent(50.0),
);
let r = e.resolve(&ctx, PropertyContext::BorderWidth);
assert_eq!(r.top, 0.0);
assert_eq!(r.right, 0.0);
assert_eq!(r.bottom, 0.0);
assert_eq!(r.left, 0.0);
}
#[test]
fn pixel_edge_resolve_uses_block_width_for_vertical_padding_percentages() {
let ctx = distinct_params().to_resolution_context();
let e = UnresolvedEdge::new(
PixelValue::percent(10.0),
PixelValue::percent(10.0),
PixelValue::percent(10.0),
PixelValue::percent(10.0),
);
let r = e.resolve(&ctx, PropertyContext::Padding);
assert_eq!(r.top, 80.0, "padding-top % must use the width");
assert_eq!(r.bottom, 80.0, "padding-bottom % must use the width");
assert_eq!(r.left, 80.0);
assert_eq!(r.right, 80.0);
}
#[test]
fn pixel_edge_resolve_of_viewport_units_uses_the_viewport_not_the_block() {
let ctx = distinct_params().to_resolution_context(); let e = UnresolvedEdge::new(
PixelValue::from_metric(SizeMetric::Vw, 10.0),
PixelValue::from_metric(SizeMetric::Vh, 10.0),
PixelValue::from_metric(SizeMetric::Vmin, 10.0),
PixelValue::from_metric(SizeMetric::Vmax, 10.0),
);
let r = e.resolve(&ctx, PropertyContext::Padding);
assert_eq!(r.top, 100.0, "10vw of 1000");
assert_eq!(r.right, 50.0, "10vh of 500");
assert_eq!(r.bottom, 50.0, "10vmin of min(1000,500)");
assert_eq!(r.left, 100.0, "10vmax of max(1000,500)");
}
#[test]
fn to_resolution_context_maps_every_field() {
let ctx = distinct_params().to_resolution_context();
assert_eq!(ctx.element_font_size, 16.0);
assert_eq!(ctx.root_font_size, 10.0);
assert_eq!(ctx.containing_block_size.width, 800.0);
assert_eq!(ctx.containing_block_size.height, 600.0);
assert_eq!(ctx.viewport_size.width, 1000.0);
assert_eq!(ctx.viewport_size.height, 500.0);
assert!(ctx.element_size.is_none(), "element size is unknown pre-layout");
}
#[test]
fn to_resolution_context_aliases_parent_font_size_onto_the_element_font_size() {
let ctx = distinct_params().to_resolution_context();
assert_eq!(ctx.parent_font_size, ctx.element_font_size);
assert_eq!(ctx.parent_font_size, 16.0);
}
#[test]
fn to_resolution_context_passes_extreme_values_through_unchanged() {
let p = params(
LogicalSize::new(f32::MAX, f32::NAN),
LogicalSize::new(f32::INFINITY, 0.0),
0.0,
f32::MIN,
);
let ctx = p.to_resolution_context();
assert_eq!(ctx.containing_block_size.width, f32::MAX);
assert!(ctx.containing_block_size.height.is_nan());
assert!(ctx.viewport_size.width.is_infinite());
assert_eq!(ctx.element_font_size, 0.0);
assert_eq!(ctx.root_font_size, f32::MIN);
}
#[test]
fn zero_font_size_context_resolves_em_to_zero_without_dividing_by_it() {
let p = params(LogicalSize::zero(), LogicalSize::zero(), 0.0, 0.0);
let ctx = p.to_resolution_context();
assert_eq!(PixelValue::em(10.0).resolve_with_context(&ctx, PropertyContext::Margin), 0.0);
assert_eq!(PixelValue::rem(10.0).resolve_with_context(&ctx, PropertyContext::Margin), 0.0);
let vmin = PixelValue::from_metric(SizeMetric::Vmin, 50.0);
assert_eq!(vmin.resolve_with_context(&ctx, PropertyContext::Padding), 0.0);
}
#[test]
fn unresolved_box_props_default_resolves_to_an_all_zero_box() {
let r = UnresolvedBoxProps::default().resolve(&distinct_params());
assert_eq!(r.horizontal_mbp(), 0.0);
assert_eq!(r.vertical_mbp(), 0.0);
assert!(!r.margin_auto.left && !r.margin_auto.right);
}
#[test]
fn unresolved_box_props_resolve_applies_the_right_property_context_per_edge() {
let p = distinct_params(); let b = UnresolvedBoxProps {
margin: UnresolvedEdge::new(
UnresolvedMargin::Auto,
UnresolvedMargin::Length(PixelValue::percent(10.0)), UnresolvedMargin::Zero,
UnresolvedMargin::Auto,
),
padding: UnresolvedEdge::new(
PixelValue::percent(10.0), PixelValue::px(4.0),
PixelValue::px(4.0),
PixelValue::px(4.0),
),
border: UnresolvedEdge::new(
PixelValue::percent(10.0), PixelValue::px(2.0),
PixelValue::px(2.0),
PixelValue::px(2.0),
),
};
let r = b.resolve(&p);
assert_eq!(r.margin.top, 0.0, "auto margin resolves to 0 px");
assert_eq!(r.margin.right, 80.0);
assert_eq!(r.padding.top, 80.0);
assert_eq!(r.border.top, 0.0, "percent border-width must collapse to 0");
assert_eq!(r.border.left, 2.0);
assert!(r.margin_auto.top);
assert!(r.margin_auto.left);
assert!(!r.margin_auto.right);
assert!(!r.margin_auto.bottom);
}
#[test]
fn inner_size_of_a_zero_box_is_zero() {
let bp = ResolvedBoxProps::default();
for wm in ALL_WM {
let s = bp.inner_size(LogicalSize::zero(), wm);
assert_eq!(s.width, 0.0);
assert_eq!(s.height, 0.0);
}
}
#[test]
fn inner_size_subtracts_border_and_padding_but_not_margin() {
let bp = props(
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), );
let s = bp.inner_size(LogicalSize::new(200.0, 100.0), LayoutWritingMode::HorizontalTb);
assert_eq!(s.width, 130.0);
assert_eq!(s.height, 55.0);
}
#[test]
fn inner_size_is_identical_in_every_writing_mode() {
let bp = props(
EdgeSizes::default(),
edges(1.0, 2.0, 4.0, 8.0),
edges(10.0, 20.0, 30.0, 40.0),
);
let outer = LogicalSize::new(200.0, 100.0);
let base = bp.inner_size(outer, LayoutWritingMode::HorizontalTb);
for wm in ALL_WM {
let s = bp.inner_size(outer, wm);
assert_eq!(s.width, base.width, "{wm:?} width diverged");
assert_eq!(s.height, base.height, "{wm:?} height diverged");
}
}
#[test]
fn inner_size_floors_at_zero_when_border_and_padding_exceed_the_box() {
let bp = props(
EdgeSizes::default(),
edges(100.0, 100.0, 100.0, 100.0),
edges(100.0, 100.0, 100.0, 100.0),
);
for wm in ALL_WM {
let s = bp.inner_size(LogicalSize::new(10.0, 10.0), wm);
assert_eq!(s.width, 0.0, "{wm:?}");
assert_eq!(s.height, 0.0, "{wm:?}");
assert!(s.width >= 0.0 && s.height >= 0.0);
}
}
#[test]
fn inner_size_never_returns_nan() {
let nan_bp = props(EdgeSizes::default(), edges(f32::NAN, 0.0, 0.0, 0.0), EdgeSizes::default());
let cases = [
(ResolvedBoxProps::default(), LogicalSize::new(f32::NAN, f32::NAN)),
(nan_bp, LogicalSize::new(100.0, 100.0)),
(
props(EdgeSizes::default(), edges(f32::INFINITY, 0.0, f32::INFINITY, 0.0), EdgeSizes::default()),
LogicalSize::new(f32::INFINITY, f32::INFINITY),
),
];
for (bp, outer) in cases {
for wm in ALL_WM {
let s = bp.inner_size(outer, wm);
assert!(!s.width.is_nan(), "{wm:?}: NaN width escaped inner_size");
assert!(!s.height.is_nan(), "{wm:?}: NaN height escaped inner_size");
assert!(s.width >= 0.0 && s.height >= 0.0);
}
}
}
#[test]
fn inner_size_at_f32_max_stays_finite_and_non_negative() {
let bp = props(EdgeSizes::default(), edges(1.0, 2.0, 4.0, 8.0), edges(1.0, 1.0, 1.0, 1.0));
for wm in ALL_WM {
let s = bp.inner_size(LogicalSize::new(f32::MAX, f32::MAX), wm);
assert!(s.width.is_finite() && s.height.is_finite());
assert!(s.width > 0.0 && s.height > 0.0);
}
}
#[test]
fn inner_size_with_negative_outer_size_floors_to_zero() {
let bp = ResolvedBoxProps::default();
for wm in ALL_WM {
let s = bp.inner_size(LogicalSize::new(-100.0, -50.0), wm);
assert_eq!(s.width, 0.0, "{wm:?}");
assert_eq!(s.height, 0.0, "{wm:?}");
}
}
#[test]
fn inner_size_with_negative_padding_grows_the_content_box() {
let bp = props(EdgeSizes::default(), edges(-5.0, -5.0, -5.0, -5.0), EdgeSizes::default());
let s = bp.inner_size(LogicalSize::new(100.0, 100.0), LayoutWritingMode::HorizontalTb);
assert_eq!(s.width, 110.0);
assert_eq!(s.height, 110.0);
}
#[test]
fn content_box_shrinks_by_border_plus_padding() {
let bp = props(
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), );
let got = bp.content_box(rect(1000.0, 2000.0, 200.0, 100.0));
assert_eq!(got, rect(1048.0, 2011.0, 130.0, 55.0));
}
#[test]
fn padding_box_shrinks_by_border_only() {
let bp = props(
EdgeSizes::default(),
edges(1.0, 2.0, 4.0, 8.0),
edges(10.0, 20.0, 30.0, 40.0),
);
let got = bp.padding_box(rect(1000.0, 2000.0, 200.0, 100.0));
assert_eq!(got, rect(1040.0, 2010.0, 140.0, 60.0));
}
#[test]
fn margin_box_expands_by_margin_only() {
let bp = props(
edges(1.0, 2.0, 4.0, 8.0), edges(99.0, 99.0, 99.0, 99.0),
edges(99.0, 99.0, 99.0, 99.0),
);
let got = bp.margin_box(rect(1000.0, 2000.0, 200.0, 100.0));
assert_eq!(got, rect(992.0, 1999.0, 210.0, 105.0));
}
#[test]
fn box_rects_nest_content_inside_padding_inside_border_inside_margin() {
let bp = props(
edges(5.0, 6.0, 7.0, 8.0),
edges(1.0, 2.0, 3.0, 4.0),
edges(9.0, 10.0, 11.0, 12.0),
);
let border_box = rect(50.0, 60.0, 400.0, 300.0);
let content = bp.content_box(border_box);
let padding = bp.padding_box(border_box);
let margin = bp.margin_box(border_box);
assert!(margin.origin.x <= border_box.origin.x);
assert!(border_box.origin.x <= padding.origin.x);
assert!(padding.origin.x <= content.origin.x);
assert!(margin.origin.y <= border_box.origin.y);
assert!(border_box.origin.y <= padding.origin.y);
assert!(padding.origin.y <= content.origin.y);
assert!(content.size.width <= padding.size.width);
assert!(padding.size.width <= border_box.size.width);
assert!(border_box.size.width <= margin.size.width);
assert!(content.size.height <= padding.size.height);
assert!(padding.size.height <= border_box.size.height);
assert!(border_box.size.height <= margin.size.height);
}
#[test]
fn content_box_size_floors_at_zero_but_the_origin_still_moves_in() {
let bp = props(
EdgeSizes::default(),
edges(500.0, 500.0, 500.0, 500.0),
edges(500.0, 500.0, 500.0, 500.0),
);
let got = bp.content_box(rect(0.0, 0.0, 10.0, 10.0));
assert_eq!(got.size.width, 0.0, "size must clamp, not go negative");
assert_eq!(got.size.height, 0.0);
assert_eq!(got.origin.x, 1000.0, "origin is not clamped");
assert_eq!(got.origin.y, 1000.0);
}
#[test]
fn padding_box_size_floors_at_zero_for_an_oversized_border() {
let bp = props(EdgeSizes::default(), EdgeSizes::default(), edges(99.0, 99.0, 99.0, 99.0));
let got = bp.padding_box(rect(0.0, 0.0, 10.0, 10.0));
assert_eq!(got.size.width, 0.0);
assert_eq!(got.size.height, 0.0);
}
#[test]
fn margin_box_with_negative_margins_can_shrink_below_zero() {
let bp = props(edges(-10.0, -10.0, -10.0, -10.0), EdgeSizes::default(), EdgeSizes::default());
let got = bp.margin_box(rect(0.0, 0.0, 10.0, 10.0));
assert_eq!(got.size.width, -10.0);
assert_eq!(got.size.height, -10.0);
assert_eq!(got.origin.x, 10.0, "a negative left margin pushes the origin right");
assert_eq!(got.origin.y, 10.0);
}
#[test]
fn box_rects_do_not_panic_on_nan_or_infinite_geometry() {
let bp = props(
edges(f32::NAN, f32::INFINITY, f32::NEG_INFINITY, f32::MAX),
edges(f32::NAN, f32::MAX, 0.0, f32::MIN),
edges(f32::INFINITY, 0.0, f32::NAN, 1.0),
);
let r = rect(f32::NAN, f32::INFINITY, f32::MAX, f32::MIN);
let c = bp.content_box(r);
let p = bp.padding_box(r);
let m = bp.margin_box(r);
for s in [c.size, p.size] {
assert!(!s.width.is_nan() && !s.height.is_nan());
assert!(s.width >= 0.0 && s.height >= 0.0);
}
let _ = m;
}
#[test]
fn mbp_and_bp_getters_sum_the_expected_layers() {
let bp = props(
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), );
assert_eq!(bp.horizontal_bp(), 1100.0);
assert_eq!(bp.vertical_bp(), 550.0);
assert_eq!(bp.horizontal_mbp(), 1110.0);
assert_eq!(bp.vertical_mbp(), 555.0);
assert_eq!(bp.horizontal_mbp(), bp.horizontal_bp() + bp.margin.horizontal_sum());
assert_eq!(bp.vertical_mbp(), bp.vertical_bp() + bp.margin.vertical_sum());
}
#[test]
fn mbp_and_bp_getters_are_zero_on_a_default_box() {
let bp = ResolvedBoxProps::default();
assert_eq!(bp.horizontal_mbp(), 0.0);
assert_eq!(bp.vertical_mbp(), 0.0);
assert_eq!(bp.horizontal_bp(), 0.0);
assert_eq!(bp.vertical_bp(), 0.0);
}
#[test]
fn mbp_getters_do_not_panic_on_extreme_boxes() {
let bp = props(
edges(f32::MAX, f32::MAX, f32::MAX, f32::MAX),
edges(f32::NAN, 0.0, 0.0, 0.0),
edges(f32::NEG_INFINITY, 0.0, 0.0, 0.0),
);
let _ = bp.horizontal_mbp();
let _ = bp.vertical_mbp();
let _ = bp.horizontal_bp();
let _ = bp.vertical_bp();
}
#[test]
fn pack_edge_encodes_tenths_of_a_pixel() {
let e = edges(0.0, 1.0, 2.5, 3276.7);
let p = PackedBoxProps::pack_edge(&e);
assert_eq!(p[0], 0);
assert_eq!(p[1], 10);
assert_eq!(p[2], 25);
assert_eq!(p[3], 32767, "the documented +3276.7px maximum");
}
#[test]
fn pack_edge_rounds_to_the_nearest_tenth_rather_than_truncating() {
let p = PackedBoxProps::pack_edge(&edges(1.04, 1.06, -1.04, -1.06));
assert_eq!(p[0], 10, "1.04 rounds down");
assert_eq!(p[1], 11, "1.06 rounds up — truncation would give 10");
assert_eq!(p[2], -10);
assert_eq!(p[3], -11);
}
#[test]
fn pack_edge_saturates_out_of_range_values_instead_of_wrapping() {
let p = PackedBoxProps::pack_edge(&edges(10_000.0, -10_000.0, 1e30, -1e30));
assert_eq!(p[0], i16::MAX);
assert_eq!(p[1], i16::MIN);
assert_eq!(p[2], i16::MAX);
assert_eq!(p[3], i16::MIN);
assert!(p.iter().all(|v| *v == i16::MAX || *v == i16::MIN));
}
#[test]
fn pack_edge_clamps_infinities_to_the_i16_bounds() {
let p = PackedBoxProps::pack_edge(&edges(f32::INFINITY, f32::NEG_INFINITY, f32::MAX, f32::MIN));
assert_eq!(p[0], i16::MAX);
assert_eq!(p[1], i16::MIN);
assert_eq!(p[2], i16::MAX);
assert_eq!(p[3], i16::MIN);
}
#[test]
fn pack_edge_maps_nan_to_zero_without_panicking() {
let p = PackedBoxProps::pack_edge(&edges(f32::NAN, f32::NAN, f32::NAN, f32::NAN));
assert_eq!(p, [0, 0, 0, 0]);
}
#[test]
fn pack_edge_maps_negative_zero_to_zero() {
let p = PackedBoxProps::pack_edge(&edges(-0.0, -0.0, -0.0, -0.0));
assert_eq!(p, [0, 0, 0, 0]);
}
#[test]
fn unpack_edge_decodes_tenths_and_preserves_the_trbl_order() {
let e = PackedBoxProps::unpack_edge(&[10, 25, -10, 32767]);
assert!(close(e.top, 1.0, 1e-4), "top was {}", e.top);
assert!(close(e.right, 2.5, 1e-4), "right was {}", e.right);
assert!(close(e.bottom, -1.0, 1e-4), "bottom was {}", e.bottom);
assert!(close(e.left, 3276.7, 1e-2), "left was {}", e.left);
}
#[test]
fn unpack_edge_at_the_i16_extremes_stays_finite() {
let e = PackedBoxProps::unpack_edge(&[i16::MAX, i16::MIN, 0, 1]);
assert!(e.top.is_finite() && e.right.is_finite());
assert!(close(e.top, 3276.7, 1e-2));
assert!(close(e.right, -3276.8, 1e-2));
assert_eq!(e.bottom, 0.0);
assert!(close(e.left, 0.1, 1e-6));
}
#[test]
fn every_i16_encoding_survives_unpack_then_pack_unchanged() {
for n in i16::MIN..=i16::MAX {
let encoded = [n; 4];
let round_tripped = PackedBoxProps::pack_edge(&PackedBoxProps::unpack_edge(&encoded));
assert_eq!(round_tripped, encoded, "i16 code {n} did not round-trip");
}
}
#[test]
fn pack_then_unpack_preserves_values_to_a_tenth_of_a_pixel() {
let bp = props(
edges(1.0, 2.5, 0.1, 12.3),
edges(0.0, 100.25, 3276.7, -3276.8),
edges(0.5, 0.05, 7.0, 0.0),
);
let out = PackedBoxProps::pack(&bp).unpack();
for (got, want) in [
(out.margin.top, bp.margin.top),
(out.margin.right, bp.margin.right),
(out.margin.bottom, bp.margin.bottom),
(out.margin.left, bp.margin.left),
(out.padding.top, bp.padding.top),
(out.padding.right, bp.padding.right),
(out.padding.bottom, bp.padding.bottom),
(out.padding.left, bp.padding.left),
(out.border.top, bp.border.top),
(out.border.right, bp.border.right),
(out.border.bottom, bp.border.bottom),
(out.border.left, bp.border.left),
] {
assert!(close(got, want, 0.051), "{want} round-tripped to {got}");
}
}
#[test]
fn pack_carries_the_margin_auto_flags_through_verbatim() {
let bp = ResolvedBoxProps {
margin_auto: MarginAuto { left: true, right: false, top: true, bottom: false },
..ResolvedBoxProps::default()
};
let out = PackedBoxProps::pack(&bp).unpack();
assert!(out.margin_auto.left);
assert!(!out.margin_auto.right);
assert!(out.margin_auto.top);
assert!(!out.margin_auto.bottom);
}
#[test]
fn pack_keeps_the_three_edge_groups_apart() {
let bp = props(
edges(1.0, 1.0, 1.0, 1.0),
edges(2.0, 2.0, 2.0, 2.0),
edges(3.0, 3.0, 3.0, 3.0),
);
let p = PackedBoxProps::pack(&bp);
assert_eq!(p.margin, [10; 4]);
assert_eq!(p.padding, [20; 4]);
assert_eq!(p.border, [30; 4]);
}
#[test]
fn pack_of_an_out_of_range_box_clamps_rather_than_flipping_sign() {
let bp = props(
edges(5000.0, 5000.0, 5000.0, 5000.0), EdgeSizes::default(),
EdgeSizes::default(),
);
let out = PackedBoxProps::pack(&bp).unpack();
assert!(out.margin.top > 0.0, "a huge margin must not decode as negative");
assert!(close(out.margin.top, 3276.7, 1e-2));
}
#[test]
fn packed_default_is_an_all_zero_box() {
let p = PackedBoxProps::default();
assert_eq!(p.margin, [0; 4]);
assert_eq!(p.horizontal_mbp(), 0.0);
assert_eq!(p.vertical_mbp(), 0.0);
assert_eq!(p.horizontal_bp(), 0.0);
assert_eq!(p.vertical_bp(), 0.0);
let s = p.inner_size(LogicalSize::new(10.0, 10.0), LayoutWritingMode::HorizontalTb);
assert_eq!(s.width, 10.0);
assert_eq!(s.height, 10.0);
}
#[test]
fn packed_convenience_methods_match_the_unpacked_equivalents() {
let bp = props(
edges(1.0, 2.5, 4.0, 8.5),
edges(0.5, 1.5, 2.5, 3.5),
edges(2.0, 4.0, 6.0, 8.0),
);
let packed = PackedBoxProps::pack(&bp);
let unpacked = packed.unpack();
let r = rect(10.0, 20.0, 300.0, 200.0);
assert_eq!(packed.content_box(r), unpacked.content_box(r));
assert_eq!(packed.padding_box(r), unpacked.padding_box(r));
assert_eq!(packed.margin_box(r), unpacked.margin_box(r));
assert_eq!(packed.horizontal_mbp(), unpacked.horizontal_mbp());
assert_eq!(packed.vertical_mbp(), unpacked.vertical_mbp());
assert_eq!(packed.horizontal_bp(), unpacked.horizontal_bp());
assert_eq!(packed.vertical_bp(), unpacked.vertical_bp());
for wm in ALL_WM {
let a = packed.inner_size(r.size, wm);
let b = unpacked.inner_size(r.size, wm);
assert_eq!(a.width, b.width, "{wm:?}");
assert_eq!(a.height, b.height, "{wm:?}");
}
}
#[test]
fn packed_inner_size_floors_at_zero_for_a_tiny_box() {
let bp = props(
EdgeSizes::default(),
edges(50.0, 50.0, 50.0, 50.0),
edges(50.0, 50.0, 50.0, 50.0),
);
let packed = PackedBoxProps::pack(&bp);
for wm in ALL_WM {
let s = packed.inner_size(LogicalSize::new(1.0, 1.0), wm);
assert_eq!(s.width, 0.0, "{wm:?}");
assert_eq!(s.height, 0.0, "{wm:?}");
}
}
#[test]
fn packed_box_rects_do_not_panic_on_extreme_input_rects() {
let packed = PackedBoxProps::pack(&props(
edges(3276.7, 3276.7, 3276.7, 3276.7),
edges(3276.7, 3276.7, 3276.7, 3276.7),
edges(3276.7, 3276.7, 3276.7, 3276.7),
));
for r in [
rect(0.0, 0.0, 0.0, 0.0),
rect(f32::MAX, f32::MIN, f32::MAX, f32::MAX),
rect(f32::NAN, f32::NAN, f32::NAN, f32::NAN),
rect(-1e30, -1e30, -1e30, -1e30),
] {
let c = packed.content_box(r);
let p = packed.padding_box(r);
let _ = packed.margin_box(r);
assert!(c.size.width >= 0.0 && !c.size.width.is_nan());
assert!(p.size.height >= 0.0 && !p.size.height.is_nan());
}
}
#[test]
fn writing_mode_context_new_stores_what_it_was_given() {
let c = WritingModeContext::new(
LayoutWritingMode::VerticalRl,
StyleDirection::Rtl,
StyleTextOrientation::Mixed,
);
assert_eq!(c.writing_mode, LayoutWritingMode::VerticalRl);
assert_eq!(c.direction, StyleDirection::Rtl);
assert_eq!(c.text_orientation, StyleTextOrientation::Mixed);
}
#[test]
fn text_orientation_upright_forces_the_used_direction_to_ltr() {
let c = WritingModeContext::new(
LayoutWritingMode::VerticalRl,
StyleDirection::Rtl,
StyleTextOrientation::Upright,
);
assert_eq!(c.used_direction(), StyleDirection::Ltr);
assert!(!c.is_inline_reversed(), "upright must cancel the RTL reversal");
}
#[test]
fn only_upright_overrides_the_direction() {
for orientation in [StyleTextOrientation::Mixed, StyleTextOrientation::Sideways] {
let c = WritingModeContext::new(
LayoutWritingMode::VerticalRl,
StyleDirection::Rtl,
orientation,
);
assert_eq!(
c.used_direction(),
StyleDirection::Rtl,
"{orientation:?} must not touch the direction"
);
assert!(c.is_inline_reversed());
}
}
#[test]
fn writing_mode_context_new_is_idempotent() {
for wm in ALL_WM {
for dir in [StyleDirection::Ltr, StyleDirection::Rtl] {
for or in [
StyleTextOrientation::Mixed,
StyleTextOrientation::Upright,
StyleTextOrientation::Sideways,
] {
let once = WritingModeContext::new(wm, dir, or);
let twice = WritingModeContext::new(
once.writing_mode,
once.direction,
once.text_orientation,
);
assert_eq!(once, twice, "{wm:?}/{dir:?}/{or:?} is not a fixed point");
}
}
}
}
#[test]
fn is_horizontal_is_true_only_for_horizontal_tb() {
let mk = |wm| WritingModeContext::new(wm, StyleDirection::Ltr, StyleTextOrientation::Mixed);
assert!(mk(LayoutWritingMode::HorizontalTb).is_horizontal());
assert!(!mk(LayoutWritingMode::VerticalRl).is_horizontal());
assert!(!mk(LayoutWritingMode::VerticalLr).is_horizontal());
}
#[test]
fn inline_and_block_axis_predicates_track_is_horizontal() {
for wm in ALL_WM {
for dir in [StyleDirection::Ltr, StyleDirection::Rtl] {
let c = WritingModeContext::new(wm, dir, StyleTextOrientation::Mixed);
assert_eq!(c.inline_size_is_width(), c.is_horizontal(), "{wm:?}");
assert_eq!(c.block_size_is_height(), c.is_horizontal(), "{wm:?}");
assert_eq!(
c.inline_size_is_width(),
c.block_size_is_height(),
"{wm:?}: the two axes must flip together"
);
}
}
}
#[test]
fn is_inline_reversed_follows_the_used_direction_not_the_writing_mode() {
for wm in ALL_WM {
let ltr = WritingModeContext::new(wm, StyleDirection::Ltr, StyleTextOrientation::Mixed);
let rtl = WritingModeContext::new(wm, StyleDirection::Rtl, StyleTextOrientation::Mixed);
assert!(!ltr.is_inline_reversed(), "{wm:?} ltr");
assert!(rtl.is_inline_reversed(), "{wm:?} rtl");
}
}
#[test]
fn default_writing_mode_context_is_horizontal_ltr_mixed() {
let c = WritingModeContext::default();
assert_eq!(c.writing_mode, LayoutWritingMode::HorizontalTb);
assert_eq!(c.used_direction(), StyleDirection::Ltr);
assert_eq!(c.text_orientation, StyleTextOrientation::Mixed);
assert!(c.is_horizontal());
assert!(c.inline_size_is_width());
assert!(c.block_size_is_height());
assert!(!c.is_inline_reversed());
}
#[test]
fn default_context_matches_new_with_the_same_arguments() {
let built = WritingModeContext::new(
LayoutWritingMode::HorizontalTb,
StyleDirection::Ltr,
StyleTextOrientation::Mixed,
);
assert_eq!(WritingModeContext::default(), built);
}
#[test]
fn fit_content_clamps_stretch_fit_between_min_and_max_content() {
let is = IntrinsicSizes {
min_content_width: 30.0,
max_content_width: 100.0,
min_content_height: 10.0,
max_content_height: 40.0,
..Default::default()
};
assert!((is.fit_content_width(60.0) - 60.0).abs() < f32::EPSILON);
assert!((is.fit_content_height(25.0) - 25.0).abs() < f32::EPSILON);
assert!((is.fit_content_width(500.0) - 100.0).abs() < f32::EPSILON);
assert!((is.fit_content_height(500.0) - 40.0).abs() < f32::EPSILON);
assert!((is.fit_content_width(0.0) - 30.0).abs() < f32::EPSILON);
assert!((is.fit_content_height(0.0) - 10.0).abs() < f32::EPSILON);
}
#[test]
fn intrinsic_sizes_default_has_no_preferred_aspect_ratio() {
assert_eq!(IntrinsicSizes::default().preferred_aspect_ratio, None);
}
}