mirror of
https://github.com/lexmount/moli.git
synced 2026-10-06 00:00:56 +00:00
fix(layout): honor automatic content minimum for inline aspect ratios
This commit is contained in:
@@ -8,11 +8,12 @@
|
||||
use std::{fmt::Debug, hash::Hash};
|
||||
|
||||
use taffy::{
|
||||
AvailableSpace, LayoutInput, LayoutOutput, LayoutPartialTree, RequestedAxis, RunMode, Size,
|
||||
SizingPurpose,
|
||||
AvailableSpace, BoxSizing, Dimension, LayoutInput, LayoutOutput, LayoutPartialTree, MaybeMath,
|
||||
MaybeResolve, RequestedAxis, ResolveOrZero, RunMode, Size, SizingMode, SizingPurpose,
|
||||
WritingMode,
|
||||
};
|
||||
|
||||
use crate::{LayoutBoxId, LayoutDisplay, LayoutWorld};
|
||||
use crate::{LayoutBoxId, LayoutDisplay, LayoutWorld, style::resolve_stylo_calc_value};
|
||||
|
||||
impl<N> LayoutWorld<N>
|
||||
where
|
||||
@@ -86,10 +87,10 @@ where
|
||||
| LayoutDisplay::InlineTable
|
||||
);
|
||||
let AvailableSpace::Definite(available_width) = inputs.available_space.width else {
|
||||
return self.compute_child_layout(child.to_taffy(), inputs);
|
||||
return self.layout_atomic_inline_with_content_minimum(child, inputs);
|
||||
};
|
||||
if !uses_fit_content {
|
||||
return self.compute_child_layout(child.to_taffy(), inputs);
|
||||
return self.layout_atomic_inline_with_content_minimum(child, inputs);
|
||||
}
|
||||
|
||||
let intrinsic_inputs = LayoutInput {
|
||||
@@ -111,8 +112,8 @@ where
|
||||
height: inputs.definite_dimensions.height,
|
||||
};
|
||||
|
||||
self.compute_child_layout(
|
||||
child.to_taffy(),
|
||||
self.layout_atomic_inline_with_content_minimum(
|
||||
child,
|
||||
LayoutInput {
|
||||
known_dimensions,
|
||||
definite_dimensions,
|
||||
@@ -120,4 +121,107 @@ where
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn layout_atomic_inline_with_content_minimum(
|
||||
&mut self,
|
||||
child: LayoutBoxId,
|
||||
inputs: LayoutInput,
|
||||
) -> LayoutOutput {
|
||||
let Some(minimum) = self.atomic_ratio_content_minimum(child, inputs) else {
|
||||
return self.compute_child_layout(child.to_taffy(), inputs);
|
||||
};
|
||||
// This is a pass-local used minimum, not a computed-style mutation.
|
||||
// Taffy's leaf/block algorithms otherwise resolve the ratio into a
|
||||
// definite height before they have measured the box's content.
|
||||
let previous_minimum = std::mem::replace(
|
||||
&mut self.boxes[child.index()].style.taffy.min_size.height,
|
||||
minimum,
|
||||
);
|
||||
let output = self.compute_child_layout(child.to_taffy(), inputs);
|
||||
self.boxes[child.index()].style.taffy.min_size.height = previous_minimum;
|
||||
output
|
||||
}
|
||||
|
||||
/// CSS Sizing 4 §4.3: a non-replaced, non-scrolling atomic flow box with
|
||||
/// an automatic ratio-dependent height cannot shrink below its content.
|
||||
fn atomic_ratio_content_minimum(
|
||||
&mut self,
|
||||
child: LayoutBoxId,
|
||||
inputs: LayoutInput,
|
||||
) -> Option<Dimension> {
|
||||
let layout_box = &self.boxes[child.index()];
|
||||
let style = &layout_box.style.taffy;
|
||||
if layout_box.is_replaced()
|
||||
|| layout_box.style.display() != LayoutDisplay::InlineBlock
|
||||
|| layout_box.style.writing_mode() != WritingMode::HorizontalTb
|
||||
|| layout_box.resolved_aspect_ratio().is_none()
|
||||
|| !style.size.height.is_auto()
|
||||
|| !style.min_size.height.is_auto()
|
||||
|| style.overflow.y.is_scroll_container()
|
||||
|| inputs.known_dimensions.height.is_some()
|
||||
|| inputs.sizing_mode != SizingMode::InherentSize
|
||||
|| inputs.axis == RequestedAxis::Horizontal
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let padding_border = (style
|
||||
.padding
|
||||
.resolve_or_zero(inputs.parent_size.width, resolve_stylo_calc_value)
|
||||
+ style
|
||||
.border
|
||||
.resolve_or_zero(inputs.parent_size.width, resolve_stylo_calc_value))
|
||||
.sum_axes();
|
||||
let adjustment = if style.box_sizing == BoxSizing::ContentBox {
|
||||
padding_border
|
||||
} else {
|
||||
Size::ZERO
|
||||
};
|
||||
let width = inputs.known_dimensions.width.or_else(|| {
|
||||
style
|
||||
.size
|
||||
.width
|
||||
.maybe_resolve(inputs.parent_size.width, resolve_stylo_calc_value)
|
||||
.maybe_add(adjustment.width)
|
||||
.maybe_clamp(
|
||||
style
|
||||
.min_size
|
||||
.width
|
||||
.maybe_resolve(inputs.parent_size.width, resolve_stylo_calc_value)
|
||||
.maybe_add(adjustment.width),
|
||||
style
|
||||
.max_size
|
||||
.width
|
||||
.maybe_resolve(inputs.parent_size.width, resolve_stylo_calc_value)
|
||||
.maybe_add(adjustment.width),
|
||||
)
|
||||
})?;
|
||||
let maximum = style
|
||||
.max_size
|
||||
.height
|
||||
.maybe_resolve(inputs.parent_size.height, resolve_stylo_calc_value)
|
||||
.maybe_add(adjustment.height);
|
||||
let measured = self.compute_child_layout(
|
||||
child.to_taffy(),
|
||||
LayoutInput {
|
||||
run_mode: RunMode::ComputeSize,
|
||||
sizing_mode: SizingMode::ContentSize,
|
||||
known_dimensions: Size {
|
||||
width: Some(width),
|
||||
height: None,
|
||||
},
|
||||
definite_dimensions: Size {
|
||||
width: Some(width),
|
||||
height: None,
|
||||
},
|
||||
available_space: Size {
|
||||
width: AvailableSpace::Definite(width),
|
||||
height: AvailableSpace::MaxContent,
|
||||
},
|
||||
..inputs
|
||||
},
|
||||
);
|
||||
Some(Dimension::length(
|
||||
(measured.size.height.maybe_min(maximum) - adjustment.height).max(0.0),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,6 +243,86 @@ fn assert_close(actual: f32, expected: f32) {
|
||||
assert!((actual - expected).abs() < 0.05, "{actual} != {expected}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inline_block_ratio_height_respects_automatic_content_minimum_and_opt_outs() {
|
||||
let source = Source(vec![
|
||||
Node::element("root", "div", LayoutElementCategory::Generic, None, vec![1]),
|
||||
Node::element(
|
||||
"ratio",
|
||||
"div",
|
||||
LayoutElementCategory::Generic,
|
||||
None,
|
||||
vec![2],
|
||||
),
|
||||
Node::element(
|
||||
"content",
|
||||
"div",
|
||||
LayoutElementCategory::Generic,
|
||||
None,
|
||||
vec![],
|
||||
),
|
||||
]);
|
||||
for (minimum, maximum, overflow, explicit_height, expected) in [
|
||||
(
|
||||
Dimension::auto(),
|
||||
Dimension::auto(),
|
||||
Overflow::Visible,
|
||||
Dimension::auto(),
|
||||
20.0,
|
||||
),
|
||||
(
|
||||
Dimension::length(0.0),
|
||||
Dimension::auto(),
|
||||
Overflow::Visible,
|
||||
Dimension::auto(),
|
||||
10.0,
|
||||
),
|
||||
(
|
||||
Dimension::auto(),
|
||||
Dimension::auto(),
|
||||
Overflow::Hidden,
|
||||
Dimension::auto(),
|
||||
10.0,
|
||||
),
|
||||
(
|
||||
Dimension::auto(),
|
||||
Dimension::length(18.0),
|
||||
Overflow::Visible,
|
||||
Dimension::auto(),
|
||||
18.0,
|
||||
),
|
||||
(
|
||||
Dimension::auto(),
|
||||
Dimension::auto(),
|
||||
Overflow::Visible,
|
||||
Dimension::length(10.0),
|
||||
10.0,
|
||||
),
|
||||
] {
|
||||
let mut styles = Styles::default();
|
||||
styles.primary.insert(
|
||||
0,
|
||||
sized(LayoutDisplay::Block, 200.0, 100.0, PaintColor::TRANSPARENT),
|
||||
);
|
||||
styles.primary.insert(
|
||||
1,
|
||||
style(LayoutDisplay::InlineBlock, GREEN).tap_taffy(|style| {
|
||||
style.size.width = Dimension::length(40.0);
|
||||
style.size.height = explicit_height;
|
||||
style.aspect_ratio = Some(4.0);
|
||||
style.min_size.height = minimum;
|
||||
style.max_size.height = maximum;
|
||||
style.overflow.y = overflow;
|
||||
}),
|
||||
);
|
||||
styles
|
||||
.primary
|
||||
.insert(2, sized(LayoutDisplay::Block, 10.0, 20.0, BLUE));
|
||||
let snapshot = render(&source, &mut styles, 200, 100);
|
||||
assert_close(rect(&snapshot, GREEN).height, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_caption_tracks_rows_cells_and_common_spans_share_one_wrapper_geometry() {
|
||||
use LayoutElementCategory::Table;
|
||||
|
||||
Reference in New Issue
Block a user