mirror of
https://github.com/lexmount/moli.git
synced 2026-10-05 16:00:54 +00:00
fix(layout): share absolute automatic sizing across containing blocks
Use Taffy's shared absolute sizing contract for deferred inline containing blocks, preserving formatter-owned content sizes and logical-axis constraints. Remove the duplicate physical-width sizing path. Add 384 font-independent screenshot geometry checks across block, flex, grid and inline owners in three writing modes. Verified against Chromium; fixed sizing WPT improves 220 to 223 of 293 with no regressions, and the 565-case border guard is unchanged. Root fmt, full Clippy and all 17217 Nextest tests pass.
This commit is contained in:
Generated
+1
-1
@@ -4842,7 +4842,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "taffy"
|
||||
version = "0.13.0"
|
||||
source = "git+https://github.com/ldm0/taffy?rev=d9915f3dbfa6dae40afd9e97d214b4220fd90359#d9915f3dbfa6dae40afd9e97d214b4220fd90359"
|
||||
source = "git+https://github.com/ldm0/taffy?rev=b5a55e2b3b7aa3b71ae9f8dfb124fb018c4a2cf2#b5a55e2b3b7aa3b71ae9f8dfb124fb018c4a2cf2"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"serde",
|
||||
|
||||
+1
-1
@@ -102,7 +102,7 @@ stylo_atoms = { git = "https://github.com/ldm0/stylo", rev = "cfa170da90c467e937
|
||||
stylo_dom = { git = "https://github.com/ldm0/stylo", rev = "cfa170da90c467e93786ec0fa5526bd83f201933" }
|
||||
stylo_static_prefs = { git = "https://github.com/ldm0/stylo", rev = "cfa170da90c467e93786ec0fa5526bd83f201933" }
|
||||
stylo_traits = { git = "https://github.com/ldm0/stylo", rev = "cfa170da90c467e93786ec0fa5526bd83f201933" }
|
||||
taffy = { git = "https://github.com/ldm0/taffy", rev = "d9915f3dbfa6dae40afd9e97d214b4220fd90359" }
|
||||
taffy = { git = "https://github.com/ldm0/taffy", rev = "b5a55e2b3b7aa3b71ae9f8dfb124fb018c4a2cf2" }
|
||||
|
||||
[workspace.lints.clippy]
|
||||
disallowed_methods = "deny"
|
||||
|
||||
@@ -18,26 +18,6 @@ impl<N> LayoutWorld<N>
|
||||
where
|
||||
N: Copy + Debug + Eq + Hash,
|
||||
{
|
||||
/// Measure the border-box width selected by CSS fit-content sizing.
|
||||
///
|
||||
/// Taffy's block and flex absolute-layout paths use the same two intrinsic
|
||||
/// measurements internally. Moli also needs the operation at the
|
||||
/// IFC seam, where an out-of-flow placeholder is owned by Parley and the
|
||||
/// positioned box cannot remain a normal child of Taffy's numeric tree.
|
||||
pub(crate) fn measure_fit_content_width(
|
||||
&mut self,
|
||||
child: LayoutBoxId,
|
||||
inputs: LayoutInput,
|
||||
available_width: f32,
|
||||
) -> f32 {
|
||||
self.measure_fit_content_inline_size(
|
||||
child,
|
||||
inputs,
|
||||
WritingMode::HorizontalTb,
|
||||
available_width,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn measure_fit_content_inline_size(
|
||||
&mut self,
|
||||
child: LayoutBoxId,
|
||||
|
||||
@@ -89,13 +89,18 @@ impl PhysicalStaticPosition {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fit-content width available on the side(s) selected by the static
|
||||
/// position. A center edge constrains both sides, unlike a start point.
|
||||
pub(crate) fn available_width(self, containing_width: f32) -> f32 {
|
||||
let bounds = self
|
||||
.horizontal_axis()
|
||||
.inset_modified_bounds(containing_width);
|
||||
bounds.end - bounds.start
|
||||
/// Fit-content space on both physical axes selected by the static edges.
|
||||
/// The child chooses its inline axis from its own writing mode; a center
|
||||
/// edge constrains both sides, unlike a start point.
|
||||
pub(crate) fn available_size(self, containing_size: Size<f32>) -> Size<f32> {
|
||||
let extent = |axis: StaticPositionAxis, size| {
|
||||
let bounds = axis.inset_modified_bounds(size);
|
||||
bounds.end - bounds.start
|
||||
};
|
||||
Size {
|
||||
width: extent(self.horizontal_axis(), containing_size.width),
|
||||
height: extent(self.vertical_axis(), containing_size.height),
|
||||
}
|
||||
}
|
||||
|
||||
fn horizontal_axis(self) -> StaticPositionAxis {
|
||||
@@ -110,6 +115,18 @@ impl PhysicalStaticPosition {
|
||||
}
|
||||
}
|
||||
|
||||
fn vertical_axis(self) -> StaticPositionAxis {
|
||||
StaticPositionAxis {
|
||||
offset: self.point.y,
|
||||
edge: match self.vertical_edge {
|
||||
VerticalStaticEdge::Top => PhysicalAxisStaticEdge::Min,
|
||||
VerticalStaticEdge::Center => PhysicalAxisStaticEdge::Center,
|
||||
VerticalStaticEdge::Bottom => PhysicalAxisStaticEdge::Max,
|
||||
},
|
||||
safety: self.safety.height,
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve the border-box origin from the static margin-box edge and used
|
||||
/// margins. Auto margins on a static-position axis have already become zero
|
||||
/// during absolute sizing; this step must not distribute them a second time.
|
||||
@@ -122,15 +139,7 @@ impl PhysicalStaticPosition {
|
||||
containing_direction: Direction,
|
||||
) -> Point<f32> {
|
||||
let horizontal = self.horizontal_axis();
|
||||
let vertical = StaticPositionAxis {
|
||||
offset: self.point.y,
|
||||
edge: match self.vertical_edge {
|
||||
VerticalStaticEdge::Top => PhysicalAxisStaticEdge::Min,
|
||||
VerticalStaticEdge::Center => PhysicalAxisStaticEdge::Center,
|
||||
VerticalStaticEdge::Bottom => PhysicalAxisStaticEdge::Max,
|
||||
},
|
||||
safety: self.safety.height,
|
||||
};
|
||||
let vertical = self.vertical_axis();
|
||||
Point {
|
||||
x: horizontal.border_box_start(
|
||||
containing_size.width,
|
||||
@@ -463,6 +472,25 @@ mod tests {
|
||||
direction: Direction::Ltr,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn static_position_constrains_both_physical_available_axes() {
|
||||
let position = PhysicalStaticPosition::new(
|
||||
Point { x: 40.0, y: 70.0 },
|
||||
HorizontalStaticEdge::Center,
|
||||
VerticalStaticEdge::Bottom,
|
||||
);
|
||||
assert_eq!(
|
||||
position.available_size(Size {
|
||||
width: 200.0,
|
||||
height: 100.0
|
||||
}),
|
||||
Size {
|
||||
width: 80.0,
|
||||
height: 70.0
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn centered_static_position_centers_the_margin_box() {
|
||||
let position = PhysicalStaticPosition::new(
|
||||
|
||||
+36
-186
@@ -7,9 +7,9 @@ use inline_containing_block::InlineContainingBlocks;
|
||||
use parley::{AlignmentOptions, PositionedLayoutItem};
|
||||
use style::Atom;
|
||||
use taffy::{
|
||||
AbsoluteAxis, AlignContent, AlignContentKeyword, AlignmentSafety, AutoSizeBehavior,
|
||||
AvailableSpace, BlockContext, BlockFormattingContext, BoxSizing, CacheTree, DetailedGridInfo,
|
||||
Dimension, Display, FlexDirection, Layout, LayoutBlockContainer, LayoutFlexboxContainer,
|
||||
AlignContent, AlignContentKeyword, AlignmentSafety, AutoSizeBehavior, AvailableSpace,
|
||||
BlockContext, BlockFormattingContext, BoxSizing, CacheTree, DetailedGridInfo, Dimension,
|
||||
Display, FlexDirection, Layout, LayoutBlockContainer, LayoutFlexboxContainer,
|
||||
LayoutGridContainer, LayoutInput, LayoutOutput, LayoutPartialTree, LeafLayoutContext, Line,
|
||||
MaybeMath, MaybeResolve, NodeId, Point, ResolveOrZero, RoundTree, RunMode, Size, SizingMode,
|
||||
SizingPurpose, Style, TraversePartialTree, TraverseTree, compute_block_layout,
|
||||
@@ -1202,221 +1202,71 @@ fn layout_deferred_absolute_child<N>(
|
||||
) where
|
||||
N: Copy + Debug + Eq + Hash,
|
||||
{
|
||||
let child_writing_mode = world.boxes[child.index()].style.writing_mode();
|
||||
let style = world.boxes[child.index()].style.taffy.clone();
|
||||
let style = &world.boxes[child.index()].style.taffy;
|
||||
if style.display == Display::None || style.position != taffy::Position::Absolute {
|
||||
return;
|
||||
}
|
||||
|
||||
let area_width = area.size.width;
|
||||
let area_height = area.size.height;
|
||||
let percentage_basis = area.writing_mode.to_logical(area.size).inline_size;
|
||||
let aspect_ratio = style.aspect_ratio;
|
||||
let margin = style
|
||||
.margin
|
||||
.map(|value| value.maybe_resolve(percentage_basis, resolve_stylo_calc_value));
|
||||
let padding = style
|
||||
.padding
|
||||
.resolve_or_zero(Some(percentage_basis), resolve_stylo_calc_value);
|
||||
let border = style
|
||||
.border
|
||||
.resolve_or_zero(Some(percentage_basis), resolve_stylo_calc_value);
|
||||
let padding_border_sum = (padding + border).sum_axes();
|
||||
let box_sizing_adjustment = if style.box_sizing == BoxSizing::ContentBox {
|
||||
padding_border_sum
|
||||
} else {
|
||||
Size::ZERO
|
||||
};
|
||||
let left = style
|
||||
.inset
|
||||
.left
|
||||
.maybe_resolve(area_width, resolve_stylo_calc_value);
|
||||
let right = style
|
||||
.inset
|
||||
.right
|
||||
.maybe_resolve(area_width, resolve_stylo_calc_value);
|
||||
let top = style
|
||||
.inset
|
||||
.top
|
||||
.maybe_resolve(area_height, resolve_stylo_calc_value);
|
||||
let bottom = style
|
||||
.inset
|
||||
.bottom
|
||||
.maybe_resolve(area_height, resolve_stylo_calc_value);
|
||||
let block_auto_behavior = match child_writing_mode.block_axis() {
|
||||
AbsoluteAxis::Horizontal if left.is_some() && right.is_some() => {
|
||||
AutoSizeBehavior::StretchExplicit
|
||||
}
|
||||
AbsoluteAxis::Vertical if top.is_some() && bottom.is_some() => {
|
||||
AutoSizeBehavior::StretchExplicit
|
||||
}
|
||||
_ => AutoSizeBehavior::FitContent,
|
||||
};
|
||||
let style_size = style
|
||||
.size
|
||||
.maybe_resolve(area.size, resolve_stylo_calc_value)
|
||||
.maybe_apply_aspect_ratio(aspect_ratio)
|
||||
.maybe_add(box_sizing_adjustment);
|
||||
let min_size = style
|
||||
.min_size
|
||||
.maybe_resolve(area.size, resolve_stylo_calc_value)
|
||||
.maybe_apply_aspect_ratio(aspect_ratio)
|
||||
.maybe_add(box_sizing_adjustment)
|
||||
.or(padding_border_sum.map(Some))
|
||||
.maybe_max(padding_border_sum);
|
||||
let max_size = style
|
||||
.max_size
|
||||
.maybe_resolve(area.size, resolve_stylo_calc_value)
|
||||
.maybe_apply_aspect_ratio(aspect_ratio)
|
||||
.maybe_add(box_sizing_adjustment);
|
||||
let mut known_dimensions = style_size.maybe_clamp(min_size, max_size);
|
||||
|
||||
if let (None, Some(left), Some(right)) = (known_dimensions.width, left, right) {
|
||||
known_dimensions.width = Some(
|
||||
(area_width.maybe_sub(margin.left).maybe_sub(margin.right) - left - right).max(0.0),
|
||||
);
|
||||
known_dimensions = known_dimensions
|
||||
.maybe_apply_aspect_ratio(aspect_ratio)
|
||||
.maybe_clamp(min_size, max_size);
|
||||
}
|
||||
if let (None, Some(top), Some(bottom)) = (known_dimensions.height, top, bottom) {
|
||||
known_dimensions.height = Some(
|
||||
(area_height.maybe_sub(margin.top).maybe_sub(margin.bottom) - top - bottom).max(0.0),
|
||||
);
|
||||
known_dimensions = known_dimensions
|
||||
.maybe_apply_aspect_ratio(aspect_ratio)
|
||||
.maybe_clamp(min_size, max_size);
|
||||
}
|
||||
|
||||
let available_space = Size {
|
||||
width: AvailableSpace::Definite(area_width.maybe_clamp(min_size.width, max_size.width)),
|
||||
height: AvailableSpace::Definite(area_height.maybe_clamp(min_size.height, max_size.height)),
|
||||
};
|
||||
if known_dimensions.width.is_none() {
|
||||
// CSS 2.2 §10.3.7 resolves an auto-width absolute box with at least
|
||||
// one auto horizontal inset as fit-content. Taffy's block/flex paths
|
||||
// already implement this contract, but an IFC's Parley placeholder
|
||||
// requires Moli to perform the same sizing at this custom seam.
|
||||
let non_auto_margin_width = margin.left.unwrap_or(0.0) + margin.right.unwrap_or(0.0);
|
||||
let available_width = match (left, right) {
|
||||
(Some(left), None) => area_width - left,
|
||||
(None, Some(right)) => area_width - right,
|
||||
(None, None) => static_position.available_width(area_width),
|
||||
(Some(_), Some(_)) => unreachable!("both insets already resolve auto width"),
|
||||
} - non_auto_margin_width;
|
||||
known_dimensions.width = Some(world.measure_fit_content_width(
|
||||
child,
|
||||
LayoutInput {
|
||||
known_dimensions,
|
||||
definite_dimensions: known_dimensions,
|
||||
parent_size: area.size.map(Some),
|
||||
parent_writing_mode: area.writing_mode,
|
||||
available_space,
|
||||
sizing_mode: SizingMode::ContentSize,
|
||||
sizing_purpose: SizingPurpose::IntrinsicContribution,
|
||||
run_mode: RunMode::ComputeSize,
|
||||
axis: taffy::RequestedAxis::Horizontal,
|
||||
block_auto_behavior,
|
||||
block_margins_are_collapsible: Line::FALSE,
|
||||
},
|
||||
available_width,
|
||||
));
|
||||
known_dimensions = known_dimensions
|
||||
.maybe_apply_aspect_ratio(aspect_ratio)
|
||||
.maybe_clamp(min_size, max_size);
|
||||
}
|
||||
let measured_size = world
|
||||
.compute_child_layout(
|
||||
child.to_taffy(),
|
||||
LayoutInput {
|
||||
known_dimensions,
|
||||
definite_dimensions: known_dimensions,
|
||||
parent_size: area.size.map(Some),
|
||||
parent_writing_mode: area.writing_mode,
|
||||
available_space,
|
||||
sizing_mode: SizingMode::ContentSize,
|
||||
sizing_purpose: SizingPurpose::Layout,
|
||||
run_mode: RunMode::ComputeSize,
|
||||
axis: taffy::RequestedAxis::Both,
|
||||
block_auto_behavior,
|
||||
block_margins_are_collapsible: Line::FALSE,
|
||||
},
|
||||
)
|
||||
.size;
|
||||
let final_size = known_dimensions
|
||||
.unwrap_or(measured_size)
|
||||
.maybe_clamp(min_size, max_size);
|
||||
let output = world.compute_child_layout(
|
||||
// Inline-containing-block geometry becomes available after the numeric
|
||||
// traversal, but sizing uses the same absolute contract as block/flex/grid.
|
||||
let computed = taffy::compute::compute_absolute_layout(
|
||||
world,
|
||||
child.to_taffy(),
|
||||
LayoutInput {
|
||||
known_dimensions: final_size.map(Some),
|
||||
definite_dimensions: known_dimensions,
|
||||
parent_size: area.size.map(Some),
|
||||
parent_writing_mode: area.writing_mode,
|
||||
available_space,
|
||||
sizing_mode: SizingMode::ContentSize,
|
||||
sizing_purpose: SizingPurpose::Layout,
|
||||
run_mode: RunMode::PerformLayout,
|
||||
axis: taffy::RequestedAxis::Both,
|
||||
block_auto_behavior,
|
||||
block_margins_are_collapsible: Line::FALSE,
|
||||
},
|
||||
);
|
||||
|
||||
let resolved_margin = taffy::compute::resolve_absolute_margins(
|
||||
margin,
|
||||
taffy::Rect {
|
||||
left,
|
||||
right,
|
||||
top,
|
||||
bottom,
|
||||
},
|
||||
area.size,
|
||||
final_size,
|
||||
taffy::WritingDirection {
|
||||
mode: area.writing_mode,
|
||||
direction: area.direction,
|
||||
taffy::compute::AbsoluteConstraintSpace {
|
||||
containing_block_size: area.size,
|
||||
writing_direction: taffy::WritingDirection {
|
||||
mode: area.writing_mode,
|
||||
direction: area.direction,
|
||||
},
|
||||
static_available_space: static_position.available_size(area.size),
|
||||
},
|
||||
);
|
||||
let output = computed.output;
|
||||
let size = output.size;
|
||||
let margin = computed.margin;
|
||||
let inset = computed.inset;
|
||||
let static_origin = static_position.border_box_origin(
|
||||
final_size,
|
||||
resolved_margin,
|
||||
size,
|
||||
margin,
|
||||
area.size,
|
||||
area.writing_mode,
|
||||
area.direction,
|
||||
);
|
||||
let x = match (left, right) {
|
||||
let x = match (inset.left, inset.right) {
|
||||
(Some(left), Some(right)) => {
|
||||
if area.direction == taffy::Direction::Rtl {
|
||||
area_width - final_size.width - right - resolved_margin.right
|
||||
area.size.width - size.width - right - margin.right
|
||||
} else {
|
||||
left + resolved_margin.left
|
||||
left + margin.left
|
||||
}
|
||||
}
|
||||
(Some(left), None) => left + resolved_margin.left,
|
||||
(None, Some(right)) => area_width - final_size.width - right - resolved_margin.right,
|
||||
(Some(left), None) => left + margin.left,
|
||||
(None, Some(right)) => area.size.width - size.width - right - margin.right,
|
||||
(None, None) => static_origin.x,
|
||||
};
|
||||
let y = top
|
||||
.map(|top| top + resolved_margin.top)
|
||||
let y = inset
|
||||
.top
|
||||
.map(|top| top + margin.top)
|
||||
.or_else(|| {
|
||||
bottom.map(|bottom| area_height - final_size.height - bottom - resolved_margin.bottom)
|
||||
inset
|
||||
.bottom
|
||||
.map(|bottom| area.size.height - size.height - bottom - margin.bottom)
|
||||
})
|
||||
.unwrap_or(static_origin.y);
|
||||
let scrollbar_size = world.get_scrollbar_insets(child.to_taffy()).sum_axes();
|
||||
world.boxes[child.index()].unrounded_layout = Layout {
|
||||
order: 0,
|
||||
in_flow: None,
|
||||
size: final_size,
|
||||
size,
|
||||
content_size: output.content_size,
|
||||
scrollbar_size,
|
||||
location: Point {
|
||||
x: area.origin.x + x - numeric_parent_origin.x,
|
||||
y: area.origin.y + y - numeric_parent_origin.y,
|
||||
},
|
||||
padding,
|
||||
border,
|
||||
margin: resolved_margin,
|
||||
padding: computed.padding,
|
||||
border: computed.border,
|
||||
margin,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,50 @@
|
||||
use super::*;
|
||||
|
||||
/// WPT abspos-005/012/013 with authored stretch, limits and percentage-basis
|
||||
/// controls across all formatting owners, including inline containing blocks.
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn screenshot_resolves_absolute_automatic_sizes_in_logical_order() {
|
||||
run_page_vm_async_test(async move {
|
||||
let loader = crate::network::ResourceRequestClient::new(&FetchConfig::default())?;
|
||||
let mut page = test_page_vm_with_loader_and_document_url(
|
||||
&loader,
|
||||
Vec::new(),
|
||||
Url::parse("https://example.com/absolute-automatic-sizing.html")?,
|
||||
);
|
||||
page.vm_mut()
|
||||
.set_layout_policy(moli_page_types::LayoutPolicy::OnDemand);
|
||||
let fixture = include_str!("../../../../tests/fixtures/absolute-automatic-sizing.html");
|
||||
page.vm_mut().eval(&format!(
|
||||
"document.open();document.write({});document.close()",
|
||||
serde_json::to_string(fixture)?,
|
||||
))?;
|
||||
page.vm_mut()
|
||||
.prime_document_lifecycle_processing_and_record_stylesheet_network_results();
|
||||
// Synchronous geometry reads intentionally use the last snapshot.
|
||||
page.vm_mut()
|
||||
.screenshot_layout_snapshot(moli_layout::PaintViewport::new(1280, 1600, 1.0))?
|
||||
.expect("absolute sizing fixture must retain a layout root");
|
||||
let checks: serde_json::Value = serde_json::from_str(
|
||||
&page
|
||||
.vm_mut()
|
||||
.eval("JSON.stringify(collectAbsoluteAutoSizingChecks())")?,
|
||||
)?;
|
||||
let checks = checks.as_array().expect("absolute sizing checks");
|
||||
assert_eq!(checks.len(), 384);
|
||||
let failures: Vec<_> = checks
|
||||
.iter()
|
||||
.filter(|check| check["actual"] != check["expected"])
|
||||
.collect();
|
||||
assert!(
|
||||
failures.is_empty(),
|
||||
"absolute sizing mismatches: {failures:#?}"
|
||||
);
|
||||
Ok::<_, anyhow::Error>(())
|
||||
})
|
||||
.await
|
||||
.expect("absolute automatic sizing fixture should run");
|
||||
}
|
||||
|
||||
/// WPT block-aspect-ratio-015/017/043/056, with explicit limits, overflow, sizing-box
|
||||
/// semantics, and logical-axis controls shared with the Chromium comparison.
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Absolute automatic sizing across formatting and containing blocks</title>
|
||||
<link rel="help" href="https://drafts.csswg.org/css-position-3/#abspos-auto-size">
|
||||
<style>
|
||||
html, body { margin:0; padding:0; }
|
||||
html { overflow:hidden; }
|
||||
.slot { position:absolute; inline-size:240px; block-size:300px; font:0/0 sans-serif; }
|
||||
.owner { inline-size:240px; block-size:300px; }
|
||||
.item { position:absolute; inset:0; box-sizing:content-box; background:rgb(31,127,63); }
|
||||
</style>
|
||||
<body>
|
||||
<script>
|
||||
// Literal logical border-box dimensions. Empty and fixed-size content avoid
|
||||
// font metrics and isolate the inset/alignment/ratio sizing contract.
|
||||
const cases = [
|
||||
['inline-ratio-normal', 'inline-size:100px;aspect-ratio:1', null, [100,100]],
|
||||
['inline-ratio-stretch', 'inline-size:100px;aspect-ratio:1;align-self:stretch', null, [100,300]],
|
||||
['inline-ratio-start', 'inline-size:100px;aspect-ratio:1;align-self:start', null, [100,100]],
|
||||
['both-auto-normal', 'aspect-ratio:1', null, [240,240]],
|
||||
['both-auto-block-stretch', 'aspect-ratio:1;align-self:stretch', null, [300,300]],
|
||||
['both-auto-both-stretch', 'aspect-ratio:1;align-self:stretch;justify-self:stretch', null, [240,300]],
|
||||
['both-auto-inline-stretch', 'aspect-ratio:1;justify-self:stretch', null, [240,240]],
|
||||
['block-ratio-normal', 'block-size:100px;aspect-ratio:1/2', null, [50,100]],
|
||||
['block-ratio-inline-stretch', 'block-size:100px;aspect-ratio:1/2;justify-self:stretch', null, [240,100]],
|
||||
['block-content-minimum', 'inline-size:100px;aspect-ratio:2;inset-block-end:auto', [100,100], [100,100]],
|
||||
['block-min-zero', 'inline-size:100px;aspect-ratio:2;inset-block-end:auto;min-block-size:0', [100,100], [100,50]],
|
||||
['block-overflow-hidden', 'inline-size:100px;aspect-ratio:2;inset-block-end:auto;overflow:hidden', [100,100], [100,50]],
|
||||
['block-authored-maximum', 'inline-size:100px;aspect-ratio:2;inset-block-end:auto;max-block-size:75px', [100,100], [100,75]],
|
||||
['block-transferred-maximum', 'inline-size:200px;max-inline-size:100px;aspect-ratio:2;inset-block-end:auto', [100,100], [100,100]],
|
||||
['inline-content-minimum', 'block-size:100px;aspect-ratio:1/2;inset-inline-end:auto', [100,20], [100,100]],
|
||||
['inline-min-zero', 'block-size:100px;aspect-ratio:1/2;inset-inline-end:auto;min-inline-size:0', [100,20], [50,100]],
|
||||
['inline-overflow-hidden', 'block-size:100px;aspect-ratio:1/2;inset-inline-end:auto;overflow:hidden', [100,20], [50,100]],
|
||||
['inline-authored-maximum', 'block-size:100px;aspect-ratio:1/2;inset-inline-end:auto;max-inline-size:75px', [100,20], [75,100]],
|
||||
['inline-transferred-maximum', 'block-size:200px;max-block-size:100px;aspect-ratio:1/2;inset-inline-end:auto', [100,20], [100,100]],
|
||||
['no-ratio-stretch', '', null, [240,300]],
|
||||
['no-ratio-start', 'align-self:start;justify-self:start', [80,120], [80,120]],
|
||||
['auto-inset-overrides-stretch', 'inline-size:100px;aspect-ratio:1;align-self:stretch;inset-block-end:auto', null, [100,100]],
|
||||
['ratio-content-box-edges', 'inline-size:100px;aspect-ratio:1;padding:5px;border:2px solid black', null, [114,114]],
|
||||
['ratio-stretch-content-box-edges', 'inline-size:100px;aspect-ratio:1;align-self:stretch;padding:5px;border:2px solid black', null, [114,300]],
|
||||
['block-insets-determine-ratio', 'aspect-ratio:3;inset-inline:auto;inset-block-start:30%;inset-block-end:50%', null, [180,60]],
|
||||
['fit-content-cap-and-ratio-minimum', 'max-inline-size:50px;aspect-ratio:3;inset-inline-end:auto;inset-block-end:auto', 'wrappable', [50,90]],
|
||||
['content-block-is-indefinite', 'inline-size:100px;inset-block-end:auto', {offset:0}, [100,20]],
|
||||
['ratio-block-is-definite', 'inline-size:100px;aspect-ratio:1;inset-block-end:auto', {offset:100}, [100,100]],
|
||||
['stretch-block-is-definite', 'inline-size:100px', {offset:300}, [100,300]],
|
||||
];
|
||||
let index = 0;
|
||||
for (const mode of ['horizontal-tb','vertical-rl','vertical-lr']) {
|
||||
for (const ownerType of ['block','flex','grid','inline']) {
|
||||
for (const [name, style, contentSize, expected] of cases) {
|
||||
const slot = document.createElement('div');
|
||||
slot.className = 'slot';
|
||||
slot.style.cssText = `left:${index%3*400}px;top:${Math.floor(index/3)*360}px;writing-mode:${mode}`;
|
||||
const owner = document.createElement('div');
|
||||
owner.className = 'owner';
|
||||
owner.style.display = ownerType === 'inline' ? 'block' : ownerType;
|
||||
if (ownerType !== 'inline') owner.style.position = 'relative';
|
||||
const item = document.createElement('div');
|
||||
item.className = 'item';
|
||||
item.id = `${mode}-${ownerType}-${name}`;
|
||||
item.style.cssText = style;
|
||||
// aspect-ratio is physical width/height; the cases describe a logical
|
||||
// inline/block ratio, so invert it for vertical writing modes.
|
||||
if (mode !== 'horizontal-tb' && item.style.aspectRatio) {
|
||||
const parts = item.style.aspectRatio.split('/').map(Number);
|
||||
item.style.aspectRatio = `${parts[1] || 1}/${parts[0]}`;
|
||||
}
|
||||
item.dataset.expected = JSON.stringify(mode === 'horizontal-tb' ? expected : [expected[1],expected[0]]);
|
||||
if (Array.isArray(contentSize)) {
|
||||
const content = document.createElement('div');
|
||||
content.style.cssText = `inline-size:${contentSize[0]}px;block-size:${contentSize[1]}px`;
|
||||
item.append(content);
|
||||
} else if (contentSize === 'wrappable') {
|
||||
for (let i = 0; i < 9; i++) {
|
||||
if (i) item.append(document.createTextNode(' '));
|
||||
const word = document.createElement('span');
|
||||
word.style.cssText = 'display:inline-block;inline-size:40px;block-size:10px;vertical-align:top';
|
||||
item.append(word);
|
||||
}
|
||||
} else if (contentSize) {
|
||||
const relative = document.createElement('div');
|
||||
relative.style.cssText = 'inline-size:50px;block-size:20px;position:relative;inset-block-start:100%';
|
||||
relative.dataset.offset = contentSize.offset;
|
||||
relative.dataset.mode = mode;
|
||||
item.append(relative);
|
||||
}
|
||||
owner.append(item);
|
||||
if (ownerType === 'inline') {
|
||||
const inline = document.createElement('span');
|
||||
inline.style.position = 'relative';
|
||||
inline.append(owner);
|
||||
slot.append(inline);
|
||||
} else {
|
||||
slot.append(owner);
|
||||
}
|
||||
document.body.append(slot);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
function collectAbsoluteAutoSizingChecks() {
|
||||
const checks = Array.from(document.querySelectorAll('.item'), item => {
|
||||
const rect = item.getBoundingClientRect();
|
||||
return {id:item.id, actual:[rect.width,rect.height], expected:JSON.parse(item.dataset.expected)};
|
||||
});
|
||||
for (const child of document.querySelectorAll('[data-offset]')) {
|
||||
const rect = child.getBoundingClientRect();
|
||||
const parent = child.parentElement.getBoundingClientRect();
|
||||
const offset = child.dataset.mode === 'horizontal-tb' ? rect.top-parent.top
|
||||
: child.dataset.mode === 'vertical-rl' ? parent.right-rect.right : rect.left-parent.left;
|
||||
checks.push({id:`${child.parentElement.id}-relative-offset`, actual:[offset], expected:[Number(child.dataset.offset)]});
|
||||
}
|
||||
return checks;
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user