Improve authored tables and SVG intrinsic sizing
This commit is contained in:
1 parent
b4d11b4c9e
commit
0562fb4921
6 files changed
+947
-187
No files matched your search
+458
-125
@@ -2521,9 +2521,18 @@ pub fn layout_dom_with_resources(
|
||||
italic: None,
|
||||
})
|
||||
.collect();
|
||||
layout_dom_with_web_fonts(tree, viewport, intrinsic, &fonts)
|
||||
let intrinsic = intrinsic
|
||||
.iter()
|
||||
.filter_map(|(&nid, &(width, height))| {
|
||||
(width.is_finite() && height.is_finite() && width > 0.0 && height > 0.0)
|
||||
.then(|| (nid, crate::ReplacedIntrinsic::from_dimensions(width, height)))
|
||||
})
|
||||
.collect();
|
||||
layout_dom_with_web_fonts(tree, viewport, &intrinsic, &fonts)
|
||||
}
|
||||
|
||||
type ReplacedIntrinsicMap = HashMap<NodeId, crate::ReplacedIntrinsic>;
|
||||
|
||||
const CONTAINER_LAYOUT_SAFETY_LIMIT: usize = 512;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -3326,7 +3335,7 @@ fn retained_style_plan(
|
||||
pub(crate) fn layout_dom_with_web_fonts(
|
||||
tree: &DomTree,
|
||||
viewport: (f32, f32),
|
||||
intrinsic: &HashMap<NodeId, (f32, f32)>,
|
||||
intrinsic: &ReplacedIntrinsicMap,
|
||||
fonts: &[crate::inline::WebFont],
|
||||
) -> DomLayout {
|
||||
layout_dom_with_web_fonts_measured(tree, viewport, intrinsic, fonts).0
|
||||
@@ -3336,7 +3345,7 @@ pub(crate) fn layout_dom_with_web_fonts(
|
||||
pub(crate) fn layout_dom_with_web_fonts_and_stylesheet_cache(
|
||||
tree: &DomTree,
|
||||
viewport: (f32, f32),
|
||||
intrinsic: &HashMap<NodeId, (f32, f32)>,
|
||||
intrinsic: &ReplacedIntrinsicMap,
|
||||
fonts: &[crate::inline::WebFont],
|
||||
stylesheet_cache: &mut crate::css::StylesheetCache,
|
||||
) -> DomLayout {
|
||||
@@ -3353,7 +3362,7 @@ pub(crate) fn layout_dom_with_web_fonts_and_stylesheet_cache(
|
||||
pub(crate) fn layout_dom_with_web_fonts_and_stylesheet_cache_at_animation_time(
|
||||
tree: &DomTree,
|
||||
viewport: (f32, f32),
|
||||
intrinsic: &HashMap<NodeId, (f32, f32)>,
|
||||
intrinsic: &ReplacedIntrinsicMap,
|
||||
fonts: &[crate::inline::WebFont],
|
||||
stylesheet_cache: &mut crate::css::StylesheetCache,
|
||||
animation_sample_time: crate::AnimationSampleTime,
|
||||
@@ -3376,7 +3385,7 @@ pub(crate) fn layout_dom_with_web_fonts_and_stylesheet_cache_at_animation_time(
|
||||
pub(crate) fn layout_dom_with_web_fonts_and_stylesheet_cache_with_animation_state(
|
||||
tree: &DomTree,
|
||||
viewport: (f32, f32),
|
||||
intrinsic: &HashMap<NodeId, (f32, f32)>,
|
||||
intrinsic: &ReplacedIntrinsicMap,
|
||||
fonts: &[crate::inline::WebFont],
|
||||
stylesheet_cache: &mut crate::css::StylesheetCache,
|
||||
animation_sample: crate::AnimationSample,
|
||||
@@ -3401,7 +3410,7 @@ pub(crate) fn layout_dom_with_web_fonts_and_stylesheet_cache_with_animation_stat
|
||||
pub(crate) fn layout_dom_with_web_fonts_and_retained_styles(
|
||||
tree: &DomTree,
|
||||
viewport: (f32, f32),
|
||||
intrinsic: &HashMap<NodeId, (f32, f32)>,
|
||||
intrinsic: &ReplacedIntrinsicMap,
|
||||
fonts: &[crate::inline::WebFont],
|
||||
stylesheet_cache: &mut crate::css::StylesheetCache,
|
||||
retained: RetainedStyleMaps,
|
||||
@@ -3422,7 +3431,7 @@ pub(crate) fn layout_dom_with_web_fonts_and_retained_styles(
|
||||
pub(crate) fn layout_dom_with_web_fonts_and_retained_styles_at_animation_time(
|
||||
tree: &DomTree,
|
||||
viewport: (f32, f32),
|
||||
intrinsic: &HashMap<NodeId, (f32, f32)>,
|
||||
intrinsic: &ReplacedIntrinsicMap,
|
||||
fonts: &[crate::inline::WebFont],
|
||||
stylesheet_cache: &mut crate::css::StylesheetCache,
|
||||
retained: RetainedStyleMaps,
|
||||
@@ -3449,7 +3458,7 @@ pub(crate) fn layout_dom_with_web_fonts_and_retained_styles_at_animation_time(
|
||||
pub(crate) fn layout_dom_with_web_fonts_and_retained_styles_with_animation_state(
|
||||
tree: &DomTree,
|
||||
viewport: (f32, f32),
|
||||
intrinsic: &HashMap<NodeId, (f32, f32)>,
|
||||
intrinsic: &ReplacedIntrinsicMap,
|
||||
fonts: &[crate::inline::WebFont],
|
||||
stylesheet_cache: &mut crate::css::StylesheetCache,
|
||||
retained: RetainedStyleMaps,
|
||||
@@ -3475,7 +3484,7 @@ pub(crate) fn layout_dom_with_web_fonts_and_retained_styles_with_animation_state
|
||||
fn layout_dom_with_web_fonts_measured(
|
||||
tree: &DomTree,
|
||||
viewport: (f32, f32),
|
||||
intrinsic: &HashMap<NodeId, (f32, f32)>,
|
||||
intrinsic: &ReplacedIntrinsicMap,
|
||||
fonts: &[crate::inline::WebFont],
|
||||
) -> (DomLayout, ContainerLayoutTelemetry) {
|
||||
layout_dom_with_web_fonts_pass_limit(tree, viewport, intrinsic, fonts, None, None, None, &[])
|
||||
@@ -3484,7 +3493,7 @@ fn layout_dom_with_web_fonts_measured(
|
||||
fn layout_dom_with_web_fonts_pass_limit(
|
||||
tree: &DomTree,
|
||||
viewport: (f32, f32),
|
||||
intrinsic: &HashMap<NodeId, (f32, f32)>,
|
||||
intrinsic: &ReplacedIntrinsicMap,
|
||||
fonts: &[crate::inline::WebFont],
|
||||
pass_limit: Option<usize>,
|
||||
stylesheet_cache: Option<&mut crate::css::StylesheetCache>,
|
||||
@@ -3509,7 +3518,7 @@ fn layout_dom_with_web_fonts_pass_limit(
|
||||
fn layout_dom_with_web_fonts_pass_limit_at_animation_time(
|
||||
tree: &DomTree,
|
||||
viewport: (f32, f32),
|
||||
intrinsic: &HashMap<NodeId, (f32, f32)>,
|
||||
intrinsic: &ReplacedIntrinsicMap,
|
||||
fonts: &[crate::inline::WebFont],
|
||||
pass_limit: Option<usize>,
|
||||
stylesheet_cache: Option<&mut crate::css::StylesheetCache>,
|
||||
@@ -3782,7 +3791,7 @@ fn layout_dom_with_web_fonts_pass_limit_at_animation_time(
|
||||
fn layout_dom_once(
|
||||
tree: &DomTree,
|
||||
viewport: (f32, f32),
|
||||
intrinsic: &HashMap<NodeId, (f32, f32)>,
|
||||
intrinsic: &ReplacedIntrinsicMap,
|
||||
fonts: &[crate::inline::WebFont],
|
||||
sheet: &crate::css::Stylesheet,
|
||||
snapshot: Option<&crate::css::ContainerSnapshot>,
|
||||
@@ -3890,6 +3899,7 @@ fn layout_dom_once(
|
||||
is_inline_block: bool,
|
||||
flow_root: bool,
|
||||
is_table_box: bool,
|
||||
is_table_cell_box: bool,
|
||||
color: Option<[u8; 4]>,
|
||||
font_size: Option<f32>,
|
||||
font_weight: u16,
|
||||
@@ -3940,6 +3950,7 @@ fn layout_dom_once(
|
||||
is_inline_block: false,
|
||||
flow_root: false,
|
||||
is_table_box: false,
|
||||
is_table_cell_box: false,
|
||||
color: None,
|
||||
font_size: None,
|
||||
font_weight: 400,
|
||||
@@ -4039,6 +4050,7 @@ fn layout_dom_once(
|
||||
inh.is_inline_block = style.is_inline_block;
|
||||
inh.flow_root = style.flow_root;
|
||||
inh.is_table_box = style.is_table_box;
|
||||
inh.is_table_cell_box = style.is_table_cell_box;
|
||||
inh.color = style.color.or(inh.color);
|
||||
inh.font_size = style.font_size.or(inh.font_size);
|
||||
inh.font_weight = crate::style::used_font_weight(style);
|
||||
@@ -4205,9 +4217,14 @@ fn layout_dom_once(
|
||||
style.is_inline_block = inh.is_inline_block;
|
||||
style.flow_root = inh.flow_root;
|
||||
style.is_table_box = inh.is_table_box;
|
||||
// `internal_flex_container` describes an engine-only UA
|
||||
// approximation, not the computed CSS display value.
|
||||
style.internal_flex_container = false;
|
||||
style.is_table_cell_box = inh.is_table_cell_box;
|
||||
// Reconstruct the internal cell-content wrapper only when
|
||||
// the inherited computed display is table-cell.
|
||||
style.internal_flex_container = style.is_table_cell_box;
|
||||
if style.is_table_cell_box {
|
||||
style.flex_direction = Some(taffy::FlexDirection::Column);
|
||||
style.align_items = Some(taffy::AlignItems::FLEX_START);
|
||||
}
|
||||
style.display_inherit = false;
|
||||
}
|
||||
inh.display = style.display;
|
||||
@@ -4215,6 +4232,7 @@ fn layout_dom_once(
|
||||
inh.is_inline_block = style.is_inline_block;
|
||||
inh.flow_root = style.flow_root;
|
||||
inh.is_table_box = style.is_table_box;
|
||||
inh.is_table_cell_box = style.is_table_cell_box;
|
||||
match style.color {
|
||||
Some(c) => inh.color = Some(c),
|
||||
None => style.color = inh.color,
|
||||
@@ -4647,6 +4665,7 @@ fn layout_dom_once(
|
||||
let host_is_inline_block = style.is_inline_block;
|
||||
let host_flow_root = style.flow_root;
|
||||
let host_is_table_box = style.is_table_box;
|
||||
let host_is_table_cell_box = style.is_table_cell_box;
|
||||
let host_grid_auto_columns = style.grid_auto_columns.clone();
|
||||
let host_grid_auto_rows = style.grid_auto_rows.clone();
|
||||
let host_grid_auto_column_calcs = style.grid_calc_expressions[2].clone();
|
||||
@@ -4682,7 +4701,12 @@ fn layout_dom_once(
|
||||
pseudo.is_inline_block = host_is_inline_block;
|
||||
pseudo.flow_root = host_flow_root;
|
||||
pseudo.is_table_box = host_is_table_box;
|
||||
pseudo.internal_flex_container = false;
|
||||
pseudo.is_table_cell_box = host_is_table_cell_box;
|
||||
pseudo.internal_flex_container = pseudo.is_table_cell_box;
|
||||
if pseudo.is_table_cell_box {
|
||||
pseudo.flex_direction = Some(taffy::FlexDirection::Column);
|
||||
pseudo.align_items = Some(taffy::AlignItems::FLEX_START);
|
||||
}
|
||||
pseudo.display_inherit = false;
|
||||
}
|
||||
if pseudo.overflow_inherit_x {
|
||||
@@ -5261,14 +5285,17 @@ fn layout_dom_once(
|
||||
// 0x0 and never paints); with one dimension given, the aspect ratio
|
||||
// fills the other. An authored `max-width:100%` still caps it to the
|
||||
// container, and the aspect ratio keeps it proportional.
|
||||
for (&nid, &(iw, ih)) in intrinsic {
|
||||
if iw <= 0.0 || ih <= 0.0 {
|
||||
for (&nid, &metadata) in intrinsic {
|
||||
if metadata.natural_size().is_none() {
|
||||
continue;
|
||||
}
|
||||
if let Some(s) = styles.get_mut(&nid) {
|
||||
s.intrinsic_size = Some((iw, ih));
|
||||
if s.aspect_ratio.is_none() || s.aspect_ratio_is_mapped {
|
||||
s.aspect_ratio = Some(iw / ih);
|
||||
s.intrinsic_size = metadata.natural_size();
|
||||
s.replaced_intrinsic = Some(metadata);
|
||||
if (s.aspect_ratio.is_none() || s.aspect_ratio_is_mapped)
|
||||
&& metadata.ratio.is_some()
|
||||
{
|
||||
s.aspect_ratio = metadata.ratio;
|
||||
s.aspect_ratio_is_mapped = false;
|
||||
s.aspect_ratio_is_intrinsic = true;
|
||||
}
|
||||
@@ -5352,12 +5379,8 @@ fn layout_dom_once(
|
||||
// preferring the author's specified width when there is one.
|
||||
let mut tables: Vec<(taffy::NodeId, NodeId, usize)> = id_map
|
||||
.iter()
|
||||
.filter(|(_, dom)| {
|
||||
tree.get_node(**dom)
|
||||
.and_then(|n| n.as_element().map(|e| e.local.as_ref() == "table"))
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.map(|(t, d)| (*t, *d, table_ancestor_depth(tree, *d)))
|
||||
.filter(|(taffy, _)| ifc_items.table_rows.contains_key(taffy))
|
||||
.map(|(t, d)| (*t, *d, table_ancestor_depth(tree, *d, &styles)))
|
||||
.collect();
|
||||
// Outer tables consume their nested tables' intrinsic sizes.
|
||||
// Only after an outer depth is fixed can a root layout expose
|
||||
@@ -5610,87 +5633,35 @@ fn layout_dom_once(
|
||||
// contribution, not a min-content floor. Preserve the
|
||||
// content minimum and raise only the preferred width so
|
||||
// a constrained table can interpolate below the hint.
|
||||
// Percentage tracks retain the existing definite-table
|
||||
// behavior here; broader percent balancing is separate.
|
||||
let specified_columns = ifc_items.table_cols.get(&tnode);
|
||||
if let Some((spec_px, spec_pct)) = specified_columns {
|
||||
for j in 0..ncols {
|
||||
let fixed = spec_px.get(j).copied().flatten();
|
||||
if let Some(w) = fixed {
|
||||
let w = w.max(col_min[j]);
|
||||
col_max[j] = w;
|
||||
} else if let Some(w) = spec_pct
|
||||
.get(j)
|
||||
.copied()
|
||||
.flatten()
|
||||
.map(|percent| percent * target)
|
||||
{
|
||||
let w = w.max(col_min[j]);
|
||||
col_min[j] = w;
|
||||
col_max[j] = w;
|
||||
}
|
||||
}
|
||||
}
|
||||
let sum_min: f32 = col_min.iter().sum();
|
||||
let sum_max: f32 = col_max.iter().sum();
|
||||
let widths: Vec<f32> = if target <= sum_min {
|
||||
col_min.clone()
|
||||
} else if target >= sum_max {
|
||||
// Surplus follows the table-layout priority: auto
|
||||
// columns absorb it before fixed and percentage
|
||||
// columns. This is important even when every
|
||||
// column has min==max (a one-word auto cell still
|
||||
// fills the remainder of a definite-width table).
|
||||
let mut result = col_max.clone();
|
||||
let extra = target - sum_max;
|
||||
let is_px = |j: usize| {
|
||||
let fixed: Vec<Option<f32>> = (0..ncols)
|
||||
.map(|index| {
|
||||
specified_columns
|
||||
.and_then(|(px, _)| px.get(j))
|
||||
.and_then(|(px, _)| px.get(index))
|
||||
.copied()
|
||||
.flatten()
|
||||
.is_some()
|
||||
};
|
||||
let is_pct = |j: usize| {
|
||||
specified_columns
|
||||
.and_then(|(_, pct)| pct.get(j))
|
||||
.copied()
|
||||
.flatten()
|
||||
.is_some()
|
||||
};
|
||||
let mut candidates: Vec<usize> = (0..ncols)
|
||||
.filter(|&j| !is_px(j) && !is_pct(j) && col_max[j] > 0.0)
|
||||
})
|
||||
.collect();
|
||||
if candidates.is_empty() {
|
||||
candidates =
|
||||
(0..ncols).filter(|&j| !is_px(j) && !is_pct(j)).collect();
|
||||
let percentages: Vec<Option<f32>> = (0..ncols)
|
||||
.map(|index| {
|
||||
specified_columns
|
||||
.and_then(|(_, pct)| pct.get(index))
|
||||
.copied()
|
||||
.flatten()
|
||||
})
|
||||
.collect();
|
||||
for j in 0..ncols {
|
||||
if let Some(width) = fixed[j] {
|
||||
col_max[j] = width.max(col_min[j]);
|
||||
}
|
||||
if candidates.is_empty() {
|
||||
candidates = (0..ncols).filter(|&j| is_px(j)).collect();
|
||||
}
|
||||
if candidates.is_empty() {
|
||||
candidates = (0..ncols).filter(|&j| is_pct(j)).collect();
|
||||
}
|
||||
if candidates.is_empty() {
|
||||
candidates = (0..ncols).collect();
|
||||
}
|
||||
let weight_sum: f32 = candidates.iter().map(|&j| col_max[j]).sum();
|
||||
for &j in &candidates {
|
||||
let share = if weight_sum > 0.0 {
|
||||
extra * col_max[j] / weight_sum
|
||||
} else {
|
||||
extra / candidates.len() as f32
|
||||
};
|
||||
result[j] += share;
|
||||
}
|
||||
result
|
||||
} else {
|
||||
let scale = (target - sum_min) / (sum_max - sum_min);
|
||||
col_min
|
||||
.iter()
|
||||
.zip(&col_max)
|
||||
.map(|(mn, mx)| mn + (mx - mn) * scale)
|
||||
.collect()
|
||||
};
|
||||
let widths = distribute_auto_table_columns(
|
||||
target,
|
||||
&col_min,
|
||||
&col_max,
|
||||
&fixed,
|
||||
&percentages,
|
||||
);
|
||||
if let Ok(cur) = taffy_tree.style(tnode) {
|
||||
let mut s = cur.clone();
|
||||
s.size.width = length(used_declaration);
|
||||
@@ -9010,14 +8981,15 @@ fn synthesize_row_rects(tree: &DomTree, rects: &mut HashMap<NodeId, Rect>) {
|
||||
/// Number of ancestor tables containing `id`. Table sizing runs outer-first:
|
||||
/// an outer table consumes an inner table's intrinsic contributions, then a
|
||||
/// root layout establishes the inner table's real cell containing block.
|
||||
fn table_ancestor_depth(tree: &DomTree, id: NodeId) -> usize {
|
||||
fn table_ancestor_depth(
|
||||
tree: &DomTree,
|
||||
id: NodeId,
|
||||
styles: &HashMap<NodeId, crate::LayoutStyle>,
|
||||
) -> usize {
|
||||
let mut depth = 0usize;
|
||||
let mut cur = id;
|
||||
while let Some(p) = tree.get_node(cur).and_then(|n| n.parent) {
|
||||
if tree.get_node(p).is_some_and(|node| {
|
||||
node.as_element()
|
||||
.is_some_and(|element| element.local.as_ref() == "table")
|
||||
}) {
|
||||
if styles.get(&p).is_some_and(|style| style.is_table_box) {
|
||||
depth += 1;
|
||||
}
|
||||
cur = p;
|
||||
@@ -9378,6 +9350,163 @@ fn table_inline_outer_edges(style: &crate::LayoutStyle) -> f32 {
|
||||
+ spacing * 2.0
|
||||
}
|
||||
|
||||
/// Resolve auto-table column constraints into final track widths.
|
||||
///
|
||||
/// Gecko's auto-table strategy uses four monotonic guesses: all columns at
|
||||
/// min-content; percentage columns raised toward their percentage; fixed
|
||||
/// columns raised toward their preferred lengths; then auto columns raised to
|
||||
/// max-content. The used width interpolates only the category between the two
|
||||
/// surrounding guesses. This matters for Bootstrap input groups: a 100% input
|
||||
/// column must shrink by the intrinsic widths of adjacent `width:1%; nowrap`
|
||||
/// addon/button columns instead of making the table overflow.
|
||||
fn distribute_auto_table_columns(
|
||||
target: f32,
|
||||
minimums: &[f32],
|
||||
preferreds: &[f32],
|
||||
fixed: &[Option<f32>],
|
||||
percentages: &[Option<f32>],
|
||||
) -> Vec<f32> {
|
||||
let count = minimums.len();
|
||||
if count == 0
|
||||
|| preferreds.len() != count
|
||||
|| fixed.len() != count
|
||||
|| percentages.len() != count
|
||||
{
|
||||
return minimums.to_vec();
|
||||
}
|
||||
let target = target.max(0.0);
|
||||
let mins: Vec<f32> = minimums.iter().map(|value| value.max(0.0)).collect();
|
||||
let prefs: Vec<f32> = preferreds
|
||||
.iter()
|
||||
.zip(&mins)
|
||||
.map(|(preferred, minimum)| preferred.max(*minimum))
|
||||
.collect();
|
||||
|
||||
// Percentage column constraints are cumulatively clamped to 100% in
|
||||
// source order. Keep zero-valued entries as percentage columns: their
|
||||
// intrinsic minimum still participates in the percentage guess.
|
||||
let mut remaining = 1.0f32;
|
||||
let effective_percentages: Vec<Option<f32>> = percentages
|
||||
.iter()
|
||||
.map(|percentage| {
|
||||
percentage.map(|value| {
|
||||
let used = value.max(0.0).min(remaining);
|
||||
remaining = (remaining - used).max(0.0);
|
||||
used
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let guess_min = mins.clone();
|
||||
let guess_min_pct: Vec<f32> = (0..count)
|
||||
.map(|index| {
|
||||
effective_percentages[index]
|
||||
.map(|percentage| (percentage * target).max(mins[index]))
|
||||
.unwrap_or(mins[index])
|
||||
})
|
||||
.collect();
|
||||
let guess_min_spec: Vec<f32> = (0..count)
|
||||
.map(|index| {
|
||||
if effective_percentages[index].is_some() {
|
||||
guess_min_pct[index]
|
||||
} else if fixed[index].is_some() {
|
||||
prefs[index]
|
||||
} else {
|
||||
mins[index]
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let guess_pref: Vec<f32> = (0..count)
|
||||
.map(|index| {
|
||||
if effective_percentages[index].is_some() {
|
||||
guess_min_pct[index]
|
||||
} else {
|
||||
prefs[index]
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let total = |values: &[f32]| values.iter().sum::<f32>();
|
||||
let min_total = total(&guess_min);
|
||||
let min_pct_total = total(&guess_min_pct);
|
||||
let min_spec_total = total(&guess_min_spec);
|
||||
let pref_total = total(&guess_pref);
|
||||
|
||||
let interpolate = |lower: &[f32], upper: &[f32], wanted: f32| {
|
||||
let lower_total = total(lower);
|
||||
let delta_total = (total(upper) - lower_total).max(0.0);
|
||||
if delta_total <= f32::EPSILON {
|
||||
return lower.to_vec();
|
||||
}
|
||||
let scale = ((wanted - lower_total) / delta_total).clamp(0.0, 1.0);
|
||||
lower
|
||||
.iter()
|
||||
.zip(upper)
|
||||
.map(|(low, high)| low + (high - low).max(0.0) * scale)
|
||||
.collect()
|
||||
};
|
||||
|
||||
if target <= min_total {
|
||||
return guess_min;
|
||||
}
|
||||
if target < min_pct_total {
|
||||
return interpolate(&guess_min, &guess_min_pct, target);
|
||||
}
|
||||
if target < min_spec_total {
|
||||
return interpolate(&guess_min_pct, &guess_min_spec, target);
|
||||
}
|
||||
if target < pref_total {
|
||||
return interpolate(&guess_min_spec, &guess_pref, target);
|
||||
}
|
||||
|
||||
let mut result = guess_pref;
|
||||
let extra = target - pref_total;
|
||||
if extra <= f32::EPSILON {
|
||||
return result;
|
||||
}
|
||||
let mut candidates: Vec<(usize, f32)> = (0..count)
|
||||
.filter(|index| effective_percentages[*index].is_none() && fixed[*index].is_none())
|
||||
.filter_map(|index| (prefs[index] > 0.0).then_some((index, prefs[index])))
|
||||
.collect();
|
||||
if candidates.is_empty() {
|
||||
candidates = (0..count)
|
||||
.filter(|index| {
|
||||
effective_percentages[*index].is_none() && fixed[*index].is_none()
|
||||
})
|
||||
.map(|index| (index, 1.0))
|
||||
.collect();
|
||||
}
|
||||
if candidates.is_empty() {
|
||||
candidates = (0..count)
|
||||
.filter(|index| effective_percentages[*index].is_none() && fixed[*index].is_some())
|
||||
.map(|index| (index, prefs[index].max(0.0)))
|
||||
.collect();
|
||||
}
|
||||
if candidates.is_empty() {
|
||||
candidates = effective_percentages
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, percentage)| {
|
||||
(*percentage)
|
||||
.filter(|value| *value > 0.0)
|
||||
.map(|value| (index, value))
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
if candidates.is_empty() {
|
||||
candidates = (0..count).map(|index| (index, 1.0)).collect();
|
||||
}
|
||||
let weight: f32 = candidates.iter().map(|(_, value)| *value).sum();
|
||||
let candidate_count = candidates.len() as f32;
|
||||
for (index, value) in candidates {
|
||||
result[index] += if weight > 0.0 {
|
||||
extra * value / weight
|
||||
} else {
|
||||
extra / candidate_count
|
||||
};
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Build a `<table>` as a CSS grid. Modeling the table as a grid is what makes
|
||||
/// columns negotiate a shared width across every row (min-content/max-content
|
||||
/// track sizing), which the old flex-row-per-`<tr>` stack could not do: each
|
||||
@@ -9400,11 +9529,51 @@ fn build_table(
|
||||
styles: &HashMap<NodeId, crate::LayoutStyle>,
|
||||
) -> Option<taffy::NodeId> {
|
||||
let style = styles.get(&id)?;
|
||||
let native_html_table = tree.get_node(id).is_some_and(|node| {
|
||||
node.as_element()
|
||||
.is_some_and(|element| element.local.as_ref() == "table")
|
||||
});
|
||||
let authored_row_children = if native_html_table {
|
||||
None
|
||||
} else {
|
||||
let children = rendered_children(tree, id);
|
||||
let mut flattened = Vec::new();
|
||||
flatten_contents_children(tree, &children, styles, &mut flattened);
|
||||
// Full anonymous-table fixup is not represented yet. Falling back to
|
||||
// ordinary box construction preserves every child; partially taking
|
||||
// the dedicated path would silently discard direct text/non-cell
|
||||
// boxes once one real table cell made the build succeed.
|
||||
let all_rendered_children_are_cells = flattened.iter().all(|child| {
|
||||
let hidden = styles
|
||||
.get(child)
|
||||
.is_some_and(|child_style| child_style.display == crate::Display::None);
|
||||
let ignorable_whitespace = tree.get_node(*child).is_some_and(|node| {
|
||||
!node.is_element() && tree.text_content(*child).trim().is_empty()
|
||||
});
|
||||
hidden
|
||||
|| ignorable_whitespace
|
||||
|| styles
|
||||
.get(child)
|
||||
.is_some_and(|child_style| child_style.is_table_cell_box)
|
||||
});
|
||||
if !all_rendered_children_are_cells {
|
||||
return None;
|
||||
}
|
||||
Some(flattened)
|
||||
};
|
||||
let mut rows: Vec<(NodeId, usize)> = Vec::new();
|
||||
if native_html_table {
|
||||
collect_table_rows(tree, id, &mut rows);
|
||||
if rows.is_empty() {
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
// CSS table fixup inserts an anonymous row around table-cell children
|
||||
// that are direct children of a table. The grid representation does
|
||||
// not need a material row node, but retaining one logical row gives
|
||||
// those cells the same shared-track negotiation as native cells.
|
||||
rows.push((id, 1));
|
||||
}
|
||||
// Bounds so a crafted table cannot exhaust memory or time: `colspan`/
|
||||
// `rowspan` and the column count are page-controlled, and the occupancy
|
||||
// fill is O(rows x cols) native code that neither the V8 watchdog nor a
|
||||
@@ -9434,11 +9603,23 @@ fn build_table(
|
||||
let mut ncols = 0usize;
|
||||
for (r, &(tr, group_end)) in rows.iter().enumerate() {
|
||||
let mut c = 0usize;
|
||||
for cid in tree.children(tr) {
|
||||
let row_children = if native_html_table {
|
||||
tree.children(tr)
|
||||
} else {
|
||||
authored_row_children.clone().unwrap_or_default()
|
||||
};
|
||||
for cid in row_children {
|
||||
let local = tree
|
||||
.get_node(cid)
|
||||
.and_then(|n| n.as_element().map(|e| e.local.to_string()));
|
||||
if !matches!(local.as_deref(), Some("td") | Some("th")) {
|
||||
let is_cell = if native_html_table {
|
||||
matches!(local.as_deref(), Some("td") | Some("th"))
|
||||
} else {
|
||||
styles
|
||||
.get(&cid)
|
||||
.is_some_and(|cell| cell.is_table_cell_box)
|
||||
};
|
||||
if !is_cell {
|
||||
continue;
|
||||
}
|
||||
// A hidden cell is removed from the table model entirely (it must
|
||||
@@ -9456,10 +9637,18 @@ fn build_table(
|
||||
if c >= MAX_COLS {
|
||||
break;
|
||||
}
|
||||
let cs = span_attr(cid, "colspan").clamp(1, MAX_SPAN);
|
||||
let cs = if native_html_table {
|
||||
span_attr(cid, "colspan").clamp(1, MAX_SPAN)
|
||||
} else {
|
||||
1
|
||||
};
|
||||
// rowspan=0 means "span to the end of this row group". Explicit
|
||||
// spans are clipped at the same boundary by the effective cell map.
|
||||
let rs_raw = span_attr(cid, "rowspan");
|
||||
let rs_raw = if native_html_table {
|
||||
span_attr(cid, "rowspan")
|
||||
} else {
|
||||
1
|
||||
};
|
||||
let rows_left_in_group = group_end.min(nrows).saturating_sub(r).max(1);
|
||||
let rs = if rs_raw == 0 {
|
||||
rows_left_in_group
|
||||
@@ -9507,6 +9696,18 @@ fn build_table(
|
||||
// grid area. Left in place, a `width:50%` cell would shrink to
|
||||
// half of its own already-halved track.
|
||||
cstyle.size.width = Dimension::auto();
|
||||
// Taffy's grid-item automatic minimum is an engine artifact here:
|
||||
// the table track has already collected the cell's min-content
|
||||
// contribution and owns final column sizing. Remove only the
|
||||
// initial `auto` minimum at translation time; an authored
|
||||
// min-width remains a real constraint and computed style stays
|
||||
// declaration-order independent.
|
||||
if styles
|
||||
.get(cid)
|
||||
.is_some_and(|cell_style| cell_style.min_width == crate::Dimension::Auto)
|
||||
{
|
||||
cstyle.min_size.width = taffy::Dimension::length(0.0);
|
||||
}
|
||||
let _ = taffy_tree.set_style(cell_node, cstyle);
|
||||
}
|
||||
children.push(cell_node);
|
||||
@@ -9618,6 +9819,7 @@ fn build_table(
|
||||
// Row sizing: a `height` on the row or a rowspan-1 cell is a MINIMUM
|
||||
// (content can always grow a row), matching how tables treat heights.
|
||||
let mut row_min: Vec<Option<f32>> = vec![None; nrows];
|
||||
if native_html_table {
|
||||
for (r, &(tr, _)) in rows.iter().enumerate() {
|
||||
if let Some(crate::Dimension::Px(h)) = styles.get(&tr).map(|s| s.height) {
|
||||
if h > 0.0 {
|
||||
@@ -9625,6 +9827,7 @@ fn build_table(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (cid, r, _c, rs, _cs) in &placed {
|
||||
if *rs != 1 {
|
||||
continue;
|
||||
@@ -10218,11 +10421,11 @@ fn build(
|
||||
return None;
|
||||
}
|
||||
|
||||
// Real table layout: a <table> becomes a CSS grid so columns negotiate
|
||||
// Real table layout: every computed table box becomes a CSS grid so columns negotiate
|
||||
// across rows (the flex approximation could not) and colspan/rowspan map to
|
||||
// grid spans. Falls through to the generic path only if the table has no
|
||||
// usable rows/cells.
|
||||
if _name.local.as_ref() == "table" {
|
||||
if style.is_table_box {
|
||||
if let Some(node) = build_table(
|
||||
tree, id, taffy_tree, id_map, words, engine, ifc_items, styles,
|
||||
) {
|
||||
@@ -10387,13 +10590,13 @@ fn build(
|
||||
// max-content measurement; once the percentage axis becomes definite, the
|
||||
// measure callback derives the other axis through the intrinsic ratio.
|
||||
if _name.local.as_ref() == "img" {
|
||||
if let Some((width, height)) = style.intrinsic_size {
|
||||
let intrinsic_ratio =
|
||||
if width.is_finite() && height.is_finite() && width > 0.0 && height > 0.0 {
|
||||
width / height
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
if let Some(intrinsic) = style.replaced_intrinsic.or_else(|| {
|
||||
style
|
||||
.intrinsic_size
|
||||
.map(|(width, height)| crate::ReplacedIntrinsic::from_dimensions(width, height))
|
||||
}) {
|
||||
let (width, height) = intrinsic.natural_size().unwrap_or((300.0, 150.0));
|
||||
let intrinsic_ratio = intrinsic.ratio.unwrap_or(2.0);
|
||||
let preferred_ratio = style
|
||||
.aspect_ratio
|
||||
.filter(|ratio| ratio.is_finite() && *ratio > 0.0)
|
||||
@@ -10455,9 +10658,13 @@ fn build(
|
||||
.iter()
|
||||
.flatten()
|
||||
.any(|expression| expression.contains('%'));
|
||||
let ratio_only = intrinsic.width.is_none()
|
||||
&& intrinsic.height.is_none()
|
||||
&& intrinsic.ratio.is_some();
|
||||
if matches!(style.width, crate::Dimension::Auto)
|
||||
&& matches!(style.height, crate::Dimension::Auto)
|
||||
&& !has_percentage_constraint
|
||||
&& !ratio_only
|
||||
&& !is_in_flow_grid_item(tree, id, style, styles)
|
||||
{
|
||||
let constrained =
|
||||
@@ -10494,7 +10701,7 @@ fn build(
|
||||
if measured_axis_constraint {
|
||||
taffy_style.aspect_ratio = None;
|
||||
}
|
||||
let context = engine.register_replaced(width, height, style);
|
||||
let context = engine.register_replaced_intrinsic(intrinsic, style);
|
||||
let leaf = taffy_tree
|
||||
.new_leaf_with_context(taffy_style, context)
|
||||
.ok()?;
|
||||
@@ -10657,9 +10864,7 @@ fn build(
|
||||
let has_in_flow_block_child = dom_children
|
||||
.iter()
|
||||
.any(|cid| styles.get(cid).map_or(false, is_in_flow_block_level));
|
||||
let is_native_table_cell = node.as_element().map_or(false, |element| {
|
||||
matches!(element.local.as_ref(), "td" | "th") && style.internal_flex_container
|
||||
});
|
||||
let is_internal_table_cell = style.is_table_cell_box && style.internal_flex_container;
|
||||
|
||||
// `text-align` affects inline content, never the used width or placement
|
||||
// of an in-flow block child. `to_taffy_style` promotes a centered/right
|
||||
@@ -10685,7 +10890,8 @@ fn build(
|
||||
// block children (e.g. a `position:relative` hero wrapper whose only
|
||||
// child is out-of-flow) to width 0 and let text and blocks share lines.
|
||||
// Floats still take the legacy zone path below.
|
||||
if (style.display == crate::Display::Block || (is_native_table_cell && has_in_flow_block_child))
|
||||
if (style.display == crate::Display::Block
|
||||
|| (is_internal_table_cell && has_in_flow_block_child))
|
||||
&& has_inline_ish_content
|
||||
&& !has_float_child
|
||||
{
|
||||
@@ -11683,7 +11889,10 @@ fn max_definite_table_content_width(
|
||||
) -> Option<f32> {
|
||||
let mut best: Option<f32> = None;
|
||||
for descendant in tree.descendants(id) {
|
||||
let structural = tree.get_node(descendant).is_some_and(|node| {
|
||||
let structural = styles
|
||||
.get(&descendant)
|
||||
.is_some_and(|style| style.is_table_cell_box)
|
||||
|| tree.get_node(descendant).is_some_and(|node| {
|
||||
node.as_element().is_some_and(|element| {
|
||||
matches!(
|
||||
element.local.as_ref(),
|
||||
@@ -12438,6 +12647,14 @@ fn build_children_with_float_zone(
|
||||
flex_direction: taffy::FlexDirection::Row,
|
||||
flex_wrap: taffy::FlexWrap::Wrap,
|
||||
align_items: Some(taffy::AlignItems::FLEX_START),
|
||||
// This anonymous row represents the float band's available
|
||||
// inline size. Make that size definite before flex line
|
||||
// collection: leaving it auto lets intrinsic sizing wrap a set
|
||||
// of percentage floats before the parent later stretches the row.
|
||||
size: taffy::Size {
|
||||
width: taffy::Dimension::percent(1.0),
|
||||
height: taffy::Dimension::auto(),
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
if let Ok(row) = taffy_tree.new_with_children(row_style, &run_children) {
|
||||
@@ -13381,6 +13598,42 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn consecutive_percentage_floats_collect_against_the_full_band_width() {
|
||||
let tree = parse_html(
|
||||
r#"<style>
|
||||
html,body{margin:0}
|
||||
#container{box-sizing:border-box;width:780px}
|
||||
.cell{float:left;box-sizing:border-box;width:25%;padding:0 15px}
|
||||
#a{height:87px} #b{height:85px} #c{height:150px} #d{height:76px}
|
||||
</style>
|
||||
<div id="container">
|
||||
<div id="a" class="cell"></div>
|
||||
<div id="b" class="cell"></div>
|
||||
<div id="c" class="cell"></div>
|
||||
<div id="d" class="cell"></div>
|
||||
</div>"#,
|
||||
);
|
||||
let laid = layout_dom(&tree, (900.0, 300.0));
|
||||
for (index, id) in ["a", "b", "c", "d"].into_iter().enumerate() {
|
||||
let rect = laid.rects[&tree.get_element_by_id(id).unwrap()];
|
||||
assert!((rect.width - 195.0).abs() < 0.01, "{id}: {rect:?}");
|
||||
assert!(
|
||||
rect.y.abs() < 0.01,
|
||||
"four 25% floats must share the 780px band; {id} wrapped: {rect:?}"
|
||||
);
|
||||
assert!(
|
||||
(rect.x - index as f32 * 195.0).abs() < 0.01,
|
||||
"{id}: {rect:?}"
|
||||
);
|
||||
}
|
||||
let container = laid.rects[&tree.get_element_by_id("container").unwrap()];
|
||||
assert!(
|
||||
(container.height - 150.0).abs() < 0.01,
|
||||
"one float band must be as tall as its tallest float: {container:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ineligible_nearest_inline_container_does_not_fall_through() {
|
||||
let tree = parse_html(
|
||||
@@ -15782,6 +16035,86 @@ mod tests {
|
||||
assert_eq!(width("border-cell"), 100.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authored_table_with_mixed_direct_content_preserves_non_cell_child() {
|
||||
let tree = parse_html(
|
||||
r#"<style>
|
||||
html,body { margin:0 }
|
||||
#group { display:table; width:300px }
|
||||
#cell { display:table-cell; width:100px; height:20px }
|
||||
#ordinary { display:block; width:80px; height:30px }
|
||||
</style>
|
||||
<div id="group">
|
||||
<div id="cell"></div>
|
||||
direct text
|
||||
<div id="ordinary"></div>
|
||||
</div>"#,
|
||||
);
|
||||
let laid = layout_dom(&tree, (500.0, 200.0));
|
||||
let id = |name: &str| tree.get_element_by_id(name).unwrap();
|
||||
|
||||
assert!(laid.rects.contains_key(&id("group")));
|
||||
assert!(laid.rects.contains_key(&id("cell")));
|
||||
let ordinary = laid.rects[&id("ordinary")];
|
||||
assert!((ordinary.width - 80.0).abs() < 0.1, "{ordinary:?}");
|
||||
assert!((ordinary.height - 30.0).abs() < 0.1, "{ordinary:?}");
|
||||
// Before the preflight check, finding `cell` was enough to take the
|
||||
// dedicated table path, which filtered `ordinary` (and the text) out.
|
||||
// Its geometry proves the mixed subtree instead used ordinary box
|
||||
// construction and kept the non-cell sibling.
|
||||
}
|
||||
|
||||
#[cfg(feature = "paint")]
|
||||
#[test]
|
||||
fn authored_table_cells_allocate_bootstrap_input_group_columns() {
|
||||
let tree = parse_html(
|
||||
r#"<style>
|
||||
* { box-sizing:border-box }
|
||||
html,body { margin:0 }
|
||||
#group { display:table; width:600px; border-collapse:separate }
|
||||
#field,#addon,#button { display:table-cell; height:34px }
|
||||
#field { width:100% }
|
||||
#addon,#button {
|
||||
width:1%; white-space:nowrap; padding:0 12px;
|
||||
font:16px/normal "Liberation Sans";
|
||||
}
|
||||
</style>
|
||||
<div id="group"><div id="field"></div><div id="addon">Addon label</div><div id="button">Button</div></div>"#,
|
||||
);
|
||||
let laid = layout_dom(&tree, (800.0, 200.0));
|
||||
let rect = |id: &str| laid.rects[&tree.get_element_by_id(id).unwrap()];
|
||||
let group = rect("group");
|
||||
let field = rect("field");
|
||||
let addon = rect("addon");
|
||||
let button = rect("button");
|
||||
|
||||
assert!((group.width - 600.0).abs() < 0.1, "{group:?}");
|
||||
assert!((field.x - group.x).abs() < 0.1, "{field:?} {group:?}");
|
||||
assert!((addon.x - (field.x + field.width)).abs() < 0.1);
|
||||
assert!((button.x - (addon.x + addon.width)).abs() < 0.1);
|
||||
assert!(((button.x + button.width) - (group.x + group.width)).abs() < 0.1);
|
||||
assert!(addon.width > 80.0, "nowrap addon must keep its intrinsic width: {addon:?}");
|
||||
assert!(button.width > 50.0, "nowrap button must keep its intrinsic width: {button:?}");
|
||||
assert!(field.width > addon.width + button.width, "{field:?} {addon:?} {button:?}");
|
||||
for cell in [field, addon, button] {
|
||||
assert!((cell.height - 34.0).abs() < 0.1, "{cell:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_table_percent_guess_reserves_intrinsic_neighbor_columns() {
|
||||
let widths = distribute_auto_table_columns(
|
||||
600.0,
|
||||
&[0.0, 100.0, 70.0],
|
||||
&[0.0, 100.0, 70.0],
|
||||
&[None, None, None],
|
||||
&[Some(1.0), Some(0.01), Some(0.01)],
|
||||
);
|
||||
assert!((widths[0] - 430.0).abs() < 0.01, "{widths:?}");
|
||||
assert!((widths[1] - 100.0).abs() < 0.01, "{widths:?}");
|
||||
assert!((widths[2] - 70.0).abs() < 0.01, "{widths:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_space_inherits_into_a_block_descendant_inline_context() {
|
||||
let tree = parse_html(
|
||||
|
||||
@@ -732,7 +732,8 @@ const REPLACED_CONTEXT_BIT: usize = 1usize << (usize::BITS - 1);
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct ReplacedItem {
|
||||
intrinsic_width: f32,
|
||||
intrinsic_width: Option<f32>,
|
||||
intrinsic_height: Option<f32>,
|
||||
preferred_width: Option<f32>,
|
||||
preferred_height: Option<f32>,
|
||||
preferred_ratio: f32,
|
||||
@@ -740,6 +741,10 @@ struct ReplacedItem {
|
||||
min_height: Option<f32>,
|
||||
max_width: Option<f32>,
|
||||
max_height: Option<f32>,
|
||||
/// Both intrinsic axes are absent but a real preferred ratio exists.
|
||||
/// CSS replaced sizing stretch-fits this case to a definite available
|
||||
/// inline size instead of using the 300x150 default-object contribution.
|
||||
ratio_only: bool,
|
||||
/// CSS Sizing's cyclic-percentage rule makes a proper replaced element's
|
||||
/// inline min-content contribution zero when its preferred or maximum
|
||||
/// inline size contains a percentage. The natural size still participates
|
||||
@@ -749,6 +754,10 @@ struct ReplacedItem {
|
||||
|
||||
impl ReplacedItem {
|
||||
fn from_style(width: f32, height: f32, style: &LayoutStyle) -> Self {
|
||||
Self::from_intrinsic(crate::ReplacedIntrinsic::from_dimensions(width, height), style)
|
||||
}
|
||||
|
||||
fn from_intrinsic(intrinsic: crate::ReplacedIntrinsic, style: &LayoutStyle) -> Self {
|
||||
let px = |dimension| match dimension {
|
||||
Dimension::Px(value) => Some(value.max(0.0)),
|
||||
_ => None,
|
||||
@@ -758,24 +767,40 @@ impl ReplacedItem {
|
||||
.as_deref()
|
||||
.map_or(false, |expression| expression.contains('%'))
|
||||
};
|
||||
let intrinsic_ratio =
|
||||
if width.is_finite() && height.is_finite() && width > 0.0 && height > 0.0 {
|
||||
width / height
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
ReplacedItem {
|
||||
intrinsic_width: width,
|
||||
preferred_width: px(style.width),
|
||||
preferred_height: px(style.height),
|
||||
preferred_ratio: style
|
||||
let explicit_ratio = style
|
||||
.aspect_ratio
|
||||
.filter(|ratio| ratio.is_finite() && *ratio > 0.0)
|
||||
.unwrap_or(intrinsic_ratio),
|
||||
.or_else(|| {
|
||||
intrinsic
|
||||
.ratio
|
||||
.filter(|ratio| ratio.is_finite() && *ratio > 0.0)
|
||||
});
|
||||
let intrinsic_ratio = intrinsic
|
||||
.ratio
|
||||
.filter(|ratio| ratio.is_finite() && *ratio > 0.0)
|
||||
.or_else(|| {
|
||||
let (width, height) = intrinsic.natural_size()?;
|
||||
(width.is_finite() && height.is_finite() && width > 0.0 && height > 0.0)
|
||||
.then_some(width / height)
|
||||
})
|
||||
.unwrap_or(2.0);
|
||||
ReplacedItem {
|
||||
intrinsic_width: intrinsic
|
||||
.width
|
||||
.filter(|width| width.is_finite() && *width > 0.0),
|
||||
intrinsic_height: intrinsic
|
||||
.height
|
||||
.filter(|height| height.is_finite() && *height > 0.0),
|
||||
preferred_width: px(style.width),
|
||||
preferred_height: px(style.height),
|
||||
preferred_ratio: explicit_ratio.unwrap_or(intrinsic_ratio),
|
||||
min_width: px(style.min_width),
|
||||
min_height: px(style.min_height),
|
||||
max_width: px(style.max_width),
|
||||
max_height: px(style.max_height),
|
||||
ratio_only: intrinsic.width.is_none()
|
||||
&& intrinsic.height.is_none()
|
||||
&& explicit_ratio.is_some(),
|
||||
zero_inline_min_content: matches!(style.width, Dimension::Percent(_))
|
||||
|| matches!(style.max_width, Dimension::Percent(_))
|
||||
|| expression_has_percentage(0)
|
||||
@@ -846,10 +871,22 @@ impl ReplacedItem {
|
||||
(Some(width), Some(height)) => (width, height),
|
||||
(Some(width), None) => (width, width / self.preferred_ratio),
|
||||
(None, Some(height)) => (height * self.preferred_ratio, height),
|
||||
(None, None) => (
|
||||
self.intrinsic_width,
|
||||
self.intrinsic_width / self.preferred_ratio,
|
||||
),
|
||||
(None, None) => match (self.intrinsic_width, self.intrinsic_height) {
|
||||
(Some(width), Some(height)) => (width, height),
|
||||
(Some(width), None) => (width, width / self.preferred_ratio),
|
||||
(None, Some(height)) => (height * self.preferred_ratio, height),
|
||||
// Contain the intrinsic ratio inside CSS Images' 300x150
|
||||
// default object size. A definite authored/known axis is
|
||||
// handled above and transfers through the same ratio.
|
||||
(None, None) => {
|
||||
let width = if self.preferred_ratio >= 2.0 {
|
||||
300.0
|
||||
} else {
|
||||
150.0 * self.preferred_ratio
|
||||
};
|
||||
(width, width / self.preferred_ratio)
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
let tentative = taffy::Size { width, height };
|
||||
@@ -1472,9 +1509,20 @@ impl TextEngine {
|
||||
/// context. Percentage-sized image leaves still need their intrinsic
|
||||
/// max-content contribution while an auto-sized ancestor is measured.
|
||||
pub fn register_replaced(&mut self, width: f32, height: f32, style: &LayoutStyle) -> usize {
|
||||
self.register_replaced_intrinsic(
|
||||
crate::ReplacedIntrinsic::from_dimensions(width, height),
|
||||
style,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn register_replaced_intrinsic(
|
||||
&mut self,
|
||||
intrinsic: crate::ReplacedIntrinsic,
|
||||
style: &LayoutStyle,
|
||||
) -> usize {
|
||||
let index = self.replaced.len();
|
||||
self.replaced
|
||||
.push(ReplacedItem::from_style(width, height, style));
|
||||
.push(ReplacedItem::from_intrinsic(intrinsic, style));
|
||||
REPLACED_CONTEXT_BIT | index
|
||||
}
|
||||
|
||||
@@ -1489,7 +1537,19 @@ impl TextEngine {
|
||||
) -> taffy::Size<f32> {
|
||||
if idx & REPLACED_CONTEXT_BIT != 0 {
|
||||
let replaced = self.replaced[idx & !REPLACED_CONTEXT_BIT];
|
||||
let mut size = replaced.size(known);
|
||||
let stretch_width = (replaced.ratio_only
|
||||
&& replaced.preferred_width.is_none()
|
||||
&& replaced.preferred_height.is_none()
|
||||
&& known.width.is_none())
|
||||
.then(|| match available.width {
|
||||
taffy::AvailableSpace::Definite(width) => Some(width.max(0.0)),
|
||||
_ => None,
|
||||
})
|
||||
.flatten();
|
||||
let mut size = replaced.size(taffy::Size {
|
||||
width: known.width.or(stretch_width),
|
||||
height: known.height,
|
||||
});
|
||||
if replaced.zero_inline_min_content
|
||||
&& known.width.is_none()
|
||||
&& matches!(available.width, taffy::AvailableSpace::MinContent)
|
||||
@@ -3714,6 +3774,36 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_replaced_intrinsics_use_the_default_object_axis() {
|
||||
let unknown = taffy::Size {
|
||||
width: None,
|
||||
height: None,
|
||||
};
|
||||
let style = LayoutStyle::default();
|
||||
let width_only = ReplacedItem::from_intrinsic(
|
||||
crate::ReplacedIntrinsic {
|
||||
width: Some(100.0),
|
||||
height: None,
|
||||
ratio: None,
|
||||
},
|
||||
&style,
|
||||
)
|
||||
.size(unknown);
|
||||
assert_eq!((width_only.width, width_only.height), (100.0, 150.0));
|
||||
|
||||
let height_only = ReplacedItem::from_intrinsic(
|
||||
crate::ReplacedIntrinsic {
|
||||
width: None,
|
||||
height: Some(100.0),
|
||||
ratio: None,
|
||||
},
|
||||
&style,
|
||||
)
|
||||
.size(unknown);
|
||||
assert_eq!((height_only.width, height_only.height), (300.0, 100.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn both_auto_replaced_constraints_transfer_through_preferred_ratio() {
|
||||
let unknown = taffy::Size {
|
||||
@@ -3721,7 +3811,9 @@ mod tests {
|
||||
height: None,
|
||||
};
|
||||
let measure =
|
||||
|style: &LayoutStyle| ReplacedItem::from_style(512.0, 323.0, style).size(unknown);
|
||||
|style: &LayoutStyle| {
|
||||
ReplacedItem::from_style(512.0, 323.0, style).size(unknown)
|
||||
};
|
||||
|
||||
let max_height = LayoutStyle {
|
||||
max_height: Dimension::Px(128.0),
|
||||
|
||||
@@ -763,6 +763,12 @@ pub struct LayoutStyle {
|
||||
/// features such as container-query axis availability that depend on the
|
||||
/// computed display rather than our internal layout approximation.
|
||||
pub(crate) is_table_box: bool,
|
||||
/// The computed inner display is `table-cell`.
|
||||
///
|
||||
/// Authored internal table boxes need the same cell sizing path as native
|
||||
/// `<td>`/`<th>` elements even though Taffy represents their cell-content
|
||||
/// wrapper as an internal column flexbox.
|
||||
pub(crate) is_table_cell_box: bool,
|
||||
/// The HTML UA sheet's vendor `text-align` behavior for `<center>`.
|
||||
/// Unlike ordinary `text-align:center`, it also centers fixed-width block
|
||||
/// descendants while leaving auto-width blocks fill-available.
|
||||
@@ -807,6 +813,10 @@ pub struct LayoutStyle {
|
||||
/// size when percentage dimensions are resolved through an auto-sized
|
||||
/// wrapper (`img { width:100%; height:auto }`).
|
||||
pub intrinsic_size: Option<(f32, f32)>,
|
||||
/// Per-axis decoded intrinsic metadata used by the replaced sizing path.
|
||||
/// SVG can expose only one dimension or a `viewBox` ratio, distinctions
|
||||
/// that the stable public `intrinsic_size` tuple cannot represent.
|
||||
pub(crate) replaced_intrinsic: Option<ReplacedIntrinsic>,
|
||||
/// The current ratio came from HTML width/height presentation hints
|
||||
/// (`<img>` or its selected `<picture><source>`), rather than authored
|
||||
/// CSS. A decoded image's natural ratio replaces this provisional ratio.
|
||||
@@ -1320,6 +1330,50 @@ pub struct LayoutStyle {
|
||||
pub box_shadow: Option<BoxShadow>,
|
||||
}
|
||||
|
||||
/// Intrinsic metadata exposed by a decoded replaced resource.
|
||||
///
|
||||
/// Presence of this value means metadata is available; each dimension may
|
||||
/// still be absent independently. This is required for SVG, whose root can
|
||||
/// declare one dimension, both dimensions, or only a `viewBox` ratio.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub(crate) struct ReplacedIntrinsic {
|
||||
pub(crate) width: Option<f32>,
|
||||
pub(crate) height: Option<f32>,
|
||||
pub(crate) ratio: Option<f32>,
|
||||
}
|
||||
|
||||
impl ReplacedIntrinsic {
|
||||
pub fn from_dimensions(width: f32, height: f32) -> Self {
|
||||
Self {
|
||||
width: Some(width),
|
||||
height: Some(height),
|
||||
ratio: (width.is_finite() && height.is_finite() && width > 0.0 && height > 0.0)
|
||||
.then_some(width / height),
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve the concrete natural size from CSS Images' 300x150 default
|
||||
/// object size. A definite authored layout axis still overrides this
|
||||
/// fallback and transfers through the intrinsic ratio.
|
||||
pub fn natural_size(self) -> Option<(f32, f32)> {
|
||||
let width = self.width.filter(|value| value.is_finite() && *value > 0.0);
|
||||
let height = self
|
||||
.height
|
||||
.filter(|value| value.is_finite() && *value > 0.0);
|
||||
let ratio = self.ratio.filter(|value| value.is_finite() && *value > 0.0);
|
||||
match (width, height, ratio) {
|
||||
(Some(width), Some(height), _) => Some((width, height)),
|
||||
(Some(width), None, Some(ratio)) => Some((width, width / ratio)),
|
||||
(None, Some(height), Some(ratio)) => Some((height * ratio, height)),
|
||||
(Some(width), None, None) => Some((width, 150.0)),
|
||||
(None, Some(height), None) => Some((300.0, height)),
|
||||
(None, None, Some(ratio)) if ratio >= 2.0 => Some((300.0, 300.0 / ratio)),
|
||||
(None, None, Some(ratio)) => Some((150.0 * ratio, 150.0)),
|
||||
(None, None, None) => Some((300.0, 150.0)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this box has an inline outer display and participates in an inline
|
||||
/// formatting context. `display` retains the inner layout mode for
|
||||
/// inline-flex/grid, so this cannot be inferred from `Display::Inline` alone.
|
||||
|
||||
@@ -127,7 +127,7 @@ fn network_resource_url(url: &str) -> String {
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct RememberedContentImageIntrinsic {
|
||||
resolved_url: String,
|
||||
dimensions: (f32, f32),
|
||||
intrinsic: crate::ReplacedIntrinsic,
|
||||
}
|
||||
|
||||
/// Page-scoped raw resource bytes shared by layout preparation and repeated
|
||||
@@ -488,7 +488,7 @@ impl RenderResourceCache {
|
||||
fn seed_content_image_intrinsics(
|
||||
&mut self,
|
||||
tree: &DomTree,
|
||||
intrinsic: &mut HashMap<obscura_dom::tree::NodeId, (f32, f32)>,
|
||||
intrinsic: &mut HashMap<obscura_dom::tree::NodeId, crate::ReplacedIntrinsic>,
|
||||
selected: &mut HashMap<obscura_dom::tree::NodeId, SelectedImage>,
|
||||
) -> HashSet<obscura_dom::tree::NodeId> {
|
||||
let remembered = self
|
||||
@@ -506,7 +506,7 @@ impl RenderResourceCache {
|
||||
self.forget_content_image_intrinsic(nid);
|
||||
continue;
|
||||
}
|
||||
intrinsic.insert(nid, value.dimensions);
|
||||
intrinsic.insert(nid, value.intrinsic);
|
||||
selected.insert(
|
||||
nid,
|
||||
SelectedImage {
|
||||
@@ -524,7 +524,7 @@ impl RenderResourceCache {
|
||||
&mut self,
|
||||
nid: obscura_dom::tree::NodeId,
|
||||
resolved_url: String,
|
||||
dimensions: (f32, f32),
|
||||
intrinsic: crate::ReplacedIntrinsic,
|
||||
) {
|
||||
if self.max_content_image_intrinsics == 0 {
|
||||
return;
|
||||
@@ -544,7 +544,7 @@ impl RenderResourceCache {
|
||||
nid,
|
||||
RememberedContentImageIntrinsic {
|
||||
resolved_url,
|
||||
dimensions,
|
||||
intrinsic,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -6920,13 +6920,20 @@ fn image_dimensions(bytes: &[u8]) -> Option<(u32, u32)> {
|
||||
}
|
||||
|
||||
fn image_metadata_from_bytes(bytes: &[u8]) -> Option<(f32, f32)> {
|
||||
let (width, height) = image_dimensions(bytes)
|
||||
.map(|(width, height)| (width as f32, height as f32))
|
||||
.or_else(|| svg_intrinsic(bytes))?;
|
||||
image_intrinsic_metadata(bytes)?.natural_size()
|
||||
}
|
||||
|
||||
fn image_intrinsic_metadata(bytes: &[u8]) -> Option<crate::ReplacedIntrinsic> {
|
||||
let metadata = image_dimensions(bytes)
|
||||
.map(|(width, height)| {
|
||||
crate::ReplacedIntrinsic::from_dimensions(width as f32, height as f32)
|
||||
})
|
||||
.or_else(|| svg_image_intrinsic_metadata(bytes))?;
|
||||
let (width, height) = metadata.natural_size()?;
|
||||
if !width.is_finite() || !height.is_finite() || width <= 0.0 || height <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
Some((width, height))
|
||||
Some(metadata)
|
||||
}
|
||||
|
||||
fn background_image_rect(
|
||||
@@ -7546,7 +7553,7 @@ fn collect_image_intrinsics(
|
||||
base_url: Option<&str>,
|
||||
cache: &mut RenderResourceCache,
|
||||
) -> (
|
||||
HashMap<obscura_dom::tree::NodeId, (f32, f32)>,
|
||||
HashMap<obscura_dom::tree::NodeId, crate::ReplacedIntrinsic>,
|
||||
HashMap<obscura_dom::tree::NodeId, SelectedImage>,
|
||||
) {
|
||||
let mut out = std::collections::HashMap::new();
|
||||
@@ -7578,16 +7585,12 @@ fn collect_image_intrinsics(
|
||||
let Some(bytes) = fetch_profiled_image_bytes(&resolved_url, None, cache, profile) else {
|
||||
continue;
|
||||
};
|
||||
let dimensions = image_dimensions(&bytes)
|
||||
.map(|(width, height)| (width as f32, height as f32))
|
||||
.or_else(|| svg_intrinsic(&bytes));
|
||||
if let Some((w, h)) = dimensions {
|
||||
if w > 0.0 && h > 0.0 {
|
||||
// A 2x (or w-descriptor) candidate's raw pixels are density
|
||||
// times its CSS size; divide so layout sees CSS px, or every
|
||||
// responsive image occupies twice its design size.
|
||||
out.insert(nid, (w / density, h / density));
|
||||
}
|
||||
if let Some(mut intrinsic) = image_intrinsic_metadata(&bytes) {
|
||||
// A 2x (or w-descriptor) candidate's raw pixels are density times
|
||||
// its CSS size. A ratio is dimensionless and remains unchanged.
|
||||
intrinsic.width = intrinsic.width.map(|width| width / density);
|
||||
intrinsic.height = intrinsic.height.map(|height| height / density);
|
||||
out.insert(nid, intrinsic);
|
||||
}
|
||||
}
|
||||
(out, selected)
|
||||
@@ -7601,9 +7604,9 @@ fn collect_content_image_intrinsics(
|
||||
styles: &std::collections::HashMap<obscura_dom::tree::NodeId, crate::LayoutStyle>,
|
||||
base_url: Option<&str>,
|
||||
cache: &mut RenderResourceCache,
|
||||
out: &mut std::collections::HashMap<obscura_dom::tree::NodeId, (f32, f32)>,
|
||||
out: &mut std::collections::HashMap<obscura_dom::tree::NodeId, crate::ReplacedIntrinsic>,
|
||||
selected: &mut HashMap<obscura_dom::tree::NodeId, SelectedImage>,
|
||||
source_intrinsic: &HashMap<obscura_dom::tree::NodeId, (f32, f32)>,
|
||||
source_intrinsic: &HashMap<obscura_dom::tree::NodeId, crate::ReplacedIntrinsic>,
|
||||
source_selected: &HashMap<obscura_dom::tree::NodeId, SelectedImage>,
|
||||
seeded: &HashSet<obscura_dom::tree::NodeId>,
|
||||
) -> bool {
|
||||
@@ -7645,23 +7648,14 @@ fn collect_content_image_intrinsics(
|
||||
cache.forget_content_image_intrinsic(nid);
|
||||
continue;
|
||||
};
|
||||
let dimensions = image_dimensions(&bytes)
|
||||
.map(|(width, height)| (width as f32, height as f32))
|
||||
.or_else(|| svg_intrinsic(&bytes));
|
||||
let Some((width, height)) = dimensions else {
|
||||
let Some(intrinsic) = image_intrinsic_metadata(&bytes) else {
|
||||
changed |= previous_dimensions.is_some() || remembered_url_changed;
|
||||
cache.forget_content_image_intrinsic(nid);
|
||||
continue;
|
||||
};
|
||||
if width <= 0.0 || height <= 0.0 {
|
||||
changed |= previous_dimensions.is_some() || remembered_url_changed;
|
||||
cache.forget_content_image_intrinsic(nid);
|
||||
continue;
|
||||
}
|
||||
let dimensions = (width, height);
|
||||
changed |= previous_dimensions != Some(dimensions) || remembered_url_changed;
|
||||
out.insert(nid, dimensions);
|
||||
cache.remember_content_image_intrinsic(nid, resolved_url, dimensions);
|
||||
changed |= previous_dimensions != Some(intrinsic) || remembered_url_changed;
|
||||
out.insert(nid, intrinsic);
|
||||
cache.remember_content_image_intrinsic(nid, resolved_url, intrinsic);
|
||||
}
|
||||
|
||||
// A remembered selection whose computed content disappeared must stop
|
||||
@@ -8125,6 +8119,173 @@ fn svg_intrinsic(bytes: &[u8]) -> Option<(f32, f32)> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Read intrinsic SVG dimensions without treating `viewBox` user-space
|
||||
/// coordinates as CSS-pixel dimensions. `usvg::Tree::size()` necessarily
|
||||
/// resolves percentage/missing root dimensions to its rendering viewport, so
|
||||
/// it cannot preserve the distinction required by CSS replaced sizing.
|
||||
fn svg_image_intrinsic_metadata(bytes: &[u8]) -> Option<crate::ReplacedIntrinsic> {
|
||||
// The lightweight attribute pass below preserves missing/percentage axes,
|
||||
// which `Tree::size()` necessarily resolves. It must not, however, turn a
|
||||
// malformed XML prefix that merely resembles an SVG root into successful
|
||||
// image metadata. Use the same parser as paint as the validity gate first.
|
||||
let _validated = usvg::Tree::from_data(bytes, &usvg::Options::default()).ok()?;
|
||||
let source = std::str::from_utf8(bytes).ok()?;
|
||||
let mut remaining = source.trim_start_matches('\u{feff}').trim_start();
|
||||
let tail = loop {
|
||||
if let Some(after) = remaining.strip_prefix("<!--") {
|
||||
let end = after.find("-->")?;
|
||||
remaining = after[end + 3..].trim_start();
|
||||
continue;
|
||||
}
|
||||
if let Some(after) = remaining.strip_prefix("<?") {
|
||||
let end = after.find("?>")?;
|
||||
remaining = after[end + 2..].trim_start();
|
||||
continue;
|
||||
}
|
||||
if remaining.starts_with("<!") {
|
||||
// Skip a validated DOCTYPE/declaration, including an internal
|
||||
// subset whose quoted declarations may themselves contain `>`.
|
||||
let mut quote = None;
|
||||
let mut subset_depth = 0usize;
|
||||
let end = remaining.char_indices().find_map(|(index, ch)| match quote {
|
||||
Some(open) if ch == open => {
|
||||
quote = None;
|
||||
None
|
||||
}
|
||||
Some(_) => None,
|
||||
None if ch == '\'' || ch == '"' => {
|
||||
quote = Some(ch);
|
||||
None
|
||||
}
|
||||
None if ch == '[' => {
|
||||
subset_depth += 1;
|
||||
None
|
||||
}
|
||||
None if ch == ']' => {
|
||||
subset_depth = subset_depth.saturating_sub(1);
|
||||
None
|
||||
}
|
||||
None if ch == '>' && subset_depth == 0 => Some(index),
|
||||
None => None,
|
||||
})?;
|
||||
remaining = remaining[end + 1..].trim_start();
|
||||
continue;
|
||||
}
|
||||
let after_open = remaining.strip_prefix('<')?;
|
||||
let name_end = after_open
|
||||
.find(|ch: char| ch.is_ascii_whitespace() || matches!(ch, '/' | '>'))
|
||||
.unwrap_or(after_open.len());
|
||||
let root_name = &after_open[..name_end];
|
||||
if root_name.rsplit(':').next()? != "svg" {
|
||||
return None;
|
||||
}
|
||||
break &after_open[name_end..];
|
||||
};
|
||||
let mut quote = None;
|
||||
let end = tail.char_indices().find_map(|(index, ch)| match quote {
|
||||
Some(open) if ch == open => {
|
||||
quote = None;
|
||||
None
|
||||
}
|
||||
Some(_) => None,
|
||||
None if ch == '\'' || ch == '"' => {
|
||||
quote = Some(ch);
|
||||
None
|
||||
}
|
||||
None if ch == '>' => Some(index),
|
||||
None => None,
|
||||
})?;
|
||||
let attributes = &tail[..end];
|
||||
let attribute = |name: &str| -> Option<&str> {
|
||||
let bytes = attributes.as_bytes();
|
||||
let mut cursor = 0;
|
||||
while cursor < bytes.len() {
|
||||
while cursor < bytes.len()
|
||||
&& (bytes[cursor].is_ascii_whitespace() || bytes[cursor] == b'/')
|
||||
{
|
||||
cursor += 1;
|
||||
}
|
||||
let name_start = cursor;
|
||||
while cursor < bytes.len()
|
||||
&& !bytes[cursor].is_ascii_whitespace()
|
||||
&& bytes[cursor] != b'='
|
||||
{
|
||||
cursor += 1;
|
||||
}
|
||||
let found = &attributes[name_start..cursor];
|
||||
while cursor < bytes.len() && bytes[cursor].is_ascii_whitespace() {
|
||||
cursor += 1;
|
||||
}
|
||||
if cursor >= bytes.len() || bytes[cursor] != b'=' {
|
||||
continue;
|
||||
}
|
||||
cursor += 1;
|
||||
while cursor < bytes.len() && bytes[cursor].is_ascii_whitespace() {
|
||||
cursor += 1;
|
||||
}
|
||||
if cursor >= bytes.len() || !matches!(bytes[cursor], b'\'' | b'"') {
|
||||
continue;
|
||||
}
|
||||
let quote = bytes[cursor];
|
||||
cursor += 1;
|
||||
let value_start = cursor;
|
||||
while cursor < bytes.len() && bytes[cursor] != quote {
|
||||
cursor += 1;
|
||||
}
|
||||
let value = &attributes[value_start..cursor];
|
||||
cursor = cursor.saturating_add(1);
|
||||
if found == name {
|
||||
return Some(value);
|
||||
}
|
||||
}
|
||||
None
|
||||
};
|
||||
let length = |value: &str| -> Option<f32> {
|
||||
let value = value.trim();
|
||||
if value.is_empty() || value.ends_with('%') || value.eq_ignore_ascii_case("auto") {
|
||||
return None;
|
||||
}
|
||||
let split = value
|
||||
.find(|ch: char| !(ch.is_ascii_digit() || matches!(ch, '+' | '-' | '.' | 'e' | 'E')))
|
||||
.unwrap_or(value.len());
|
||||
let number = value[..split].parse::<f32>().ok()?;
|
||||
let factor = match value[split..].trim().to_ascii_lowercase().as_str() {
|
||||
"" | "px" => 1.0,
|
||||
"in" => 96.0,
|
||||
"cm" => 96.0 / 2.54,
|
||||
"mm" => 96.0 / 25.4,
|
||||
"q" => 96.0 / 101.6,
|
||||
"pt" => 96.0 / 72.0,
|
||||
"pc" => 16.0,
|
||||
_ => return None,
|
||||
};
|
||||
let result = number * factor;
|
||||
(result.is_finite() && result > 0.0).then_some(result)
|
||||
};
|
||||
let width = attribute("width").and_then(length);
|
||||
let height = attribute("height").and_then(length);
|
||||
let view_box_ratio = attribute("viewBox").and_then(|value| {
|
||||
let values = value
|
||||
.split(|ch: char| ch.is_ascii_whitespace() || ch == ',')
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::parse::<f32>)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.ok()?;
|
||||
(values.len() == 4 && values[2].is_finite() && values[3].is_finite()
|
||||
&& values[2] > 0.0 && values[3] > 0.0)
|
||||
.then_some(values[2] / values[3])
|
||||
});
|
||||
let ratio = match (width, height) {
|
||||
(Some(width), Some(height)) => Some(width / height),
|
||||
_ => view_box_ratio,
|
||||
};
|
||||
Some(crate::ReplacedIntrinsic {
|
||||
width,
|
||||
height,
|
||||
ratio,
|
||||
})
|
||||
}
|
||||
|
||||
/// A full-pixmap clip mask admitting only the pixels inside `rect`, used to
|
||||
/// crop an `object-fit: cover|none` image to its element box.
|
||||
fn box_clip_mask(pw: u32, ph: u32, rect: &crate::Rect) -> Option<tiny_skia::Mask> {
|
||||
@@ -9152,6 +9313,72 @@ mod tests {
|
||||
use crate::dom::layout_dom_with_web_fonts;
|
||||
use obscura_dom::tree_sink::parse_html;
|
||||
|
||||
#[test]
|
||||
fn svg_image_metadata_keeps_view_box_as_ratio_only() {
|
||||
let ratio_only = svg_image_intrinsic_metadata(
|
||||
br#"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 576 576'/>"#,
|
||||
)
|
||||
.expect("ratio-only SVG metadata");
|
||||
assert_eq!(ratio_only.width, None);
|
||||
assert_eq!(ratio_only.height, None);
|
||||
assert_eq!(ratio_only.ratio, Some(1.0));
|
||||
assert_eq!(ratio_only.natural_size(), Some((150.0, 150.0)));
|
||||
|
||||
let explicit = svg_image_intrinsic_metadata(
|
||||
br#"<svg xmlns='http://www.w3.org/2000/svg' width='120' height='80' viewBox='0 0 200 100'/>"#,
|
||||
)
|
||||
.expect("explicit SVG metadata");
|
||||
assert_eq!(explicit.width, Some(120.0));
|
||||
assert_eq!(explicit.height, Some(80.0));
|
||||
assert_eq!(explicit.ratio, Some(1.5));
|
||||
assert_eq!(explicit.natural_size(), Some((120.0, 80.0)));
|
||||
|
||||
let commented = svg_image_intrinsic_metadata(
|
||||
br#"<!-- <svg width='999' height='999'/> --><svg xmlns='http://www.w3.org/2000/svg' width='12' height='8'/>"#,
|
||||
)
|
||||
.expect("the real root after a comment");
|
||||
assert_eq!(commented.natural_size(), Some((12.0, 8.0)));
|
||||
|
||||
assert!(svg_image_intrinsic_metadata(
|
||||
br#"<svg xmlns='http://www.w3.org/2000/svg' width='10' height='20'><g>"#,
|
||||
)
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_box_only_svg_transfers_definite_css_width_without_using_view_box_units() {
|
||||
let square = br#"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'/>"#;
|
||||
assert_eq!(
|
||||
image_metadata_from_bytes(square),
|
||||
Some((150.0, 150.0)),
|
||||
"natural dimensions contain the ratio in the 300x150 default object"
|
||||
);
|
||||
let tree = parse_html(
|
||||
r#"<html><head><style>
|
||||
html,body { margin:0 }
|
||||
#host { width:360px }
|
||||
img { display:block }
|
||||
#ratio { width:100%; height:auto }
|
||||
</style></head><body><div id="host">
|
||||
<img id="auto" src="data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20100%20100'/%3E">
|
||||
<img id="ratio" src="data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20200%20100'/%3E">
|
||||
<img id="explicit" src="data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='120'%20height='80'%20viewBox='0%200%20200%20100'/%3E">
|
||||
</div></body></html>"#,
|
||||
);
|
||||
let mut resources = RenderResourceCache::default();
|
||||
let prepared = prepare_dom(&tree, (500.0, 400.0), None, &mut resources).expect("layout");
|
||||
let rect = |selector| {
|
||||
let id = tree.query_selector(selector).unwrap().unwrap();
|
||||
prepared.layout.rects[&id]
|
||||
};
|
||||
let auto = rect("#auto");
|
||||
assert_eq!((auto.width, auto.height), (360.0, 360.0));
|
||||
let ratio = rect("#ratio");
|
||||
assert_eq!((ratio.width, ratio.height), (360.0, 180.0));
|
||||
let explicit = rect("#explicit");
|
||||
assert_eq!((explicit.width, explicit.height), (120.0, 80.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_placeholders_honor_default_author_color_opacity_and_value_state() {
|
||||
let tree = parse_html(
|
||||
@@ -10883,11 +11110,27 @@ mod tests {
|
||||
.expect("selector")
|
||||
.expect("image")
|
||||
});
|
||||
resources.remember_content_image_intrinsic(ids[0], "a".into(), (1.0, 1.0));
|
||||
resources.remember_content_image_intrinsic(ids[1], "b".into(), (2.0, 2.0));
|
||||
resources.remember_content_image_intrinsic(
|
||||
ids[0],
|
||||
"a".into(),
|
||||
crate::ReplacedIntrinsic::from_dimensions(1.0, 1.0),
|
||||
);
|
||||
resources.remember_content_image_intrinsic(
|
||||
ids[1],
|
||||
"b".into(),
|
||||
crate::ReplacedIntrinsic::from_dimensions(2.0, 2.0),
|
||||
);
|
||||
// Refreshing id 1 makes id 2 the oldest entry.
|
||||
resources.remember_content_image_intrinsic(ids[0], "a2".into(), (3.0, 3.0));
|
||||
resources.remember_content_image_intrinsic(ids[2], "c".into(), (4.0, 4.0));
|
||||
resources.remember_content_image_intrinsic(
|
||||
ids[0],
|
||||
"a2".into(),
|
||||
crate::ReplacedIntrinsic::from_dimensions(3.0, 3.0),
|
||||
);
|
||||
resources.remember_content_image_intrinsic(
|
||||
ids[2],
|
||||
"c".into(),
|
||||
crate::ReplacedIntrinsic::from_dimensions(4.0, 4.0),
|
||||
);
|
||||
|
||||
assert_eq!(resources.content_image_intrinsics.len(), 2);
|
||||
assert!(resources.content_image_intrinsics.contains_key(&ids[0]));
|
||||
|
||||
@@ -248,6 +248,7 @@ pub fn ua_style(tag: &str) -> LayoutStyle {
|
||||
// grows to fit unshrinkable content, needs real table layout.)
|
||||
style.min_width = crate::Dimension::Px(0.0);
|
||||
if tag == "table" {
|
||||
style.is_table_box = true;
|
||||
// Chromium's HTML UA sheet makes the table grid border-box and
|
||||
// supplies the traditional two-pixel separate-border spacing.
|
||||
// Author declarations and the legacy `cellspacing` hint cascade
|
||||
@@ -272,6 +273,7 @@ pub fn ua_style(tag: &str) -> LayoutStyle {
|
||||
} else if tag == "td" || tag == "th" {
|
||||
style.display = Display::Flex;
|
||||
style.internal_flex_container = true;
|
||||
style.is_table_cell_box = true;
|
||||
style.flex_direction = Some(taffy::FlexDirection::Column);
|
||||
style.align_items = Some(taffy::AlignItems::FLEX_START);
|
||||
style.padding = Edges {
|
||||
@@ -713,6 +715,7 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
||||
| "flow-root"
|
||||
| "table"
|
||||
| "inline-table"
|
||||
| "table-cell"
|
||||
| "-webkit-box"
|
||||
| "-webkit-inline-box"
|
||||
| "contents"
|
||||
@@ -726,6 +729,7 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
||||
// internal enum happens to equal the newly specified value.
|
||||
style.internal_flex_container = false;
|
||||
style.is_table_box = false;
|
||||
style.is_table_cell_box = false;
|
||||
style.is_inline_block = false;
|
||||
style.flow_root = false;
|
||||
style.display_contents = false;
|
||||
@@ -769,6 +773,17 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
||||
style.flow_root = true;
|
||||
style.is_table_box = true;
|
||||
}
|
||||
"table-cell" => {
|
||||
// A table cell establishes an internal flow container. A
|
||||
// column flexbox is our cell-content wrapper stand-in; it
|
||||
// is marked internal so descendants do not become CSS flex
|
||||
// items and the table grid owns the cell's inline size.
|
||||
style.display = crate::Display::Flex;
|
||||
style.internal_flex_container = true;
|
||||
style.flex_direction = Some(taffy::FlexDirection::Column);
|
||||
style.align_items = Some(taffy::AlignItems::FLEX_START);
|
||||
style.is_table_cell_box = true;
|
||||
}
|
||||
"-webkit-box" => {
|
||||
// The unclamped legacy display is an old flexbox. When it
|
||||
// forms a vertical line-clamp root, the post-declaration
|
||||
@@ -7684,9 +7699,24 @@ mod tests {
|
||||
assert!(inline_table.flow_root);
|
||||
assert!(inline_table.is_table_box);
|
||||
|
||||
let table_cell = compute_style("div", Some("display:block; display:table-cell"));
|
||||
assert_eq!(table_cell.display, Display::Flex);
|
||||
assert!(table_cell.internal_flex_container);
|
||||
assert!(table_cell.is_table_cell_box);
|
||||
|
||||
let min_before = compute_style("div", Some("min-width:50px;display:table-cell"));
|
||||
let min_after = compute_style("div", Some("display:table-cell;min-width:50px"));
|
||||
assert_eq!(min_before.min_width, crate::Dimension::Px(50.0));
|
||||
assert_eq!(min_after.min_width, crate::Dimension::Px(50.0));
|
||||
|
||||
let reset_table = compute_style("div", Some("display:table; display:grid"));
|
||||
assert_eq!(reset_table.display, Display::Grid);
|
||||
assert!(!reset_table.is_table_box);
|
||||
|
||||
let reset_cell = compute_style("div", Some("display:table-cell; display:block"));
|
||||
assert_eq!(reset_cell.display, Display::Block);
|
||||
assert!(!reset_cell.internal_flex_container);
|
||||
assert!(!reset_cell.is_table_cell_box);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -3854,7 +3854,8 @@ fn display_inherit_transitively_preserves_contents_and_block_display() {
|
||||
</div>
|
||||
<section id="block-parent"><span id="block-child"></span></section>
|
||||
<table><tr><td id="native-cell">
|
||||
<div id="native-inherit" style="display:inherit"></div>
|
||||
<div id="native-inherit" style="min-width:50px;display:inherit"></div>
|
||||
<div id="native-inherit-reversed" style="display:inherit;min-width:50px"></div>
|
||||
</td></tr></table>
|
||||
"#,
|
||||
);
|
||||
@@ -3880,11 +3881,18 @@ fn display_inherit_transitively_preserves_contents_and_block_display() {
|
||||
|
||||
let native_cell = &layout.styles[&id("native-cell")];
|
||||
let native_inherit = &layout.styles[&id("native-inherit")];
|
||||
let native_inherit_reversed = &layout.styles[&id("native-inherit-reversed")];
|
||||
assert!(native_cell.internal_flex_container);
|
||||
assert_eq!(native_inherit.display, obscura_render::Display::Flex);
|
||||
assert!(
|
||||
!native_inherit.internal_flex_container,
|
||||
"display inheritance must not copy the engine's UA-only table approximation"
|
||||
native_inherit.internal_flex_container,
|
||||
"inherited table-cell display must reconstruct its internal cell-content wrapper"
|
||||
);
|
||||
assert_eq!(native_inherit.min_width, obscura_render::Dimension::Px(50.0));
|
||||
assert!(native_inherit_reversed.internal_flex_container);
|
||||
assert_eq!(
|
||||
native_inherit_reversed.min_width,
|
||||
obscura_render::Dimension::Px(50.0)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user