Improve authored tables and SVG intrinsic sizing
This commit is contained in:
1 parent
b4d11b4c9e
commit
0562fb4921
6 files changed
+970
-210
No files matched your search
+482
-149
@@ -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)
|
||||
let fixed: Vec<Option<f32>> = (0..ncols)
|
||||
.map(|index| {
|
||||
specified_columns
|
||||
.and_then(|(px, _)| px.get(index))
|
||||
.copied()
|
||||
.flatten()
|
||||
.map(|percent| percent * target)
|
||||
{
|
||||
let w = w.max(col_min[j]);
|
||||
col_min[j] = w;
|
||||
col_max[j] = w;
|
||||
}
|
||||
})
|
||||
.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]);
|
||||
}
|
||||
}
|
||||
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| {
|
||||
specified_columns
|
||||
.and_then(|(px, _)| px.get(j))
|
||||
.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();
|
||||
}
|
||||
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,10 +9529,50 @@ 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();
|
||||
collect_table_rows(tree, id, &mut rows);
|
||||
if rows.is_empty() {
|
||||
return None;
|
||||
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
|
||||
@@ -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,10 +9819,12 @@ 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];
|
||||
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 {
|
||||
row_min[r] = Some(h);
|
||||
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 {
|
||||
row_min[r] = Some(h);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,22 +11889,25 @@ 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| {
|
||||
node.as_element().is_some_and(|element| {
|
||||
matches!(
|
||||
element.local.as_ref(),
|
||||
"caption"
|
||||
| "col"
|
||||
| "colgroup"
|
||||
| "thead"
|
||||
| "tbody"
|
||||
| "tfoot"
|
||||
| "tr"
|
||||
| "td"
|
||||
| "th"
|
||||
)
|
||||
})
|
||||
});
|
||||
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(),
|
||||
"caption"
|
||||
| "col"
|
||||
| "colgroup"
|
||||
| "thead"
|
||||
| "tbody"
|
||||
| "tfoot"
|
||||
| "tr"
|
||||
| "td"
|
||||
| "th"
|
||||
)
|
||||
})
|
||||
});
|
||||
if structural {
|
||||
continue;
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user