render: preserve responsive image grid geometry

This commit is contained in:
SGavrl committed 2026-07-23 14:48:27 +00:00
1 parent 7ddf52eb27
commit 9471591c6c
5 files changed
+104 -15

No files matched your search

+20 -10
View File
@@ -650,6 +650,7 @@ pub fn layout_dom_with_images(
continue; continue;
} }
if let Some(s) = styles.get_mut(&nid) { if let Some(s) = styles.get_mut(&nid) {
s.intrinsic_size = Some((iw, ih));
if s.aspect_ratio.is_none() { if s.aspect_ratio.is_none() {
s.aspect_ratio = Some(iw / ih); s.aspect_ratio = Some(iw / ih);
} }
@@ -675,16 +676,7 @@ pub fn layout_dom_with_images(
|known: taffy::Size<Option<f32>>, avail: taffy::Size<taffy::AvailableSpace>, _node, ctx: Option<&mut usize>, _style: &taffy::Style| { |known: taffy::Size<Option<f32>>, avail: taffy::Size<taffy::AvailableSpace>, _node, ctx: Option<&mut usize>, _style: &taffy::Style| {
match ctx { match ctx {
Some(&mut idx) => { Some(&mut idx) => {
// Width to line-break at: a known definite width, engine.measure_taffy(idx, known, avail)
// else the available width; MinContent -> narrowest
// (longest word), MaxContent -> unbounded (one line).
let width = known.width.or(match avail.width {
taffy::AvailableSpace::Definite(w) => Some(w),
taffy::AvailableSpace::MinContent => Some(0.0),
taffy::AvailableSpace::MaxContent => None,
});
let (w, h) = engine.measure(idx, width);
taffy::Size { width: w, height: h }
} }
None => taffy::Size::ZERO, None => taffy::Size::ZERO,
} }
@@ -1917,6 +1909,19 @@ fn build(
let mut taffy_style = to_taffy_style(style); let mut taffy_style = to_taffy_style(style);
// A replaced image is a measured leaf, even when CSS gives it a percentage
// width. Its intrinsic dimensions participate in an auto-sized ancestor's
// 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 context = engine.register_replaced(width, height);
let leaf = taffy_tree.new_leaf_with_context(taffy_style, context).ok()?;
id_map.insert(leaf, id);
return Some(leaf);
}
}
// If this container is a pure-text inline formatting context, collapse its // If this container is a pure-text inline formatting context, collapse its
// whole subtree to one leaf shaped and line-broken by cosmic-text (real // whole subtree to one leaf shaped and line-broken by cosmic-text (real
// text layout), sized on demand by the measure function. This is the fast, // text layout), sized on demand by the measure function. This is the fast,
@@ -1969,6 +1974,11 @@ fn build(
dom_children.retain(|&cid| { dom_children.retain(|&cid| {
tree.get_node(cid).map_or(false, |n| n.is_element()) || !tree.text_content(cid).trim().is_empty() tree.get_node(cid).map_or(false, |n| n.is_element()) || !tree.text_content(cid).trim().is_empty()
}); });
// Flex and grid placement consume the order-modified document order.
// Rust's stable sort preserves source order for equal values, exactly
// the CSS tie-break. Taffy has no CSS `order` style field, so feeding
// it the correctly ordered item sequence is the missing translation.
dom_children.sort_by_key(|cid| styles.get(cid).map(|style| style.order).unwrap_or(0));
} }
// `float` has no effect on a flex or grid item. Legacy stylesheets often // `float` has no effect on a flex or grid item. Legacy stylesheets often
// leave floats on children after a newer rule turns their parent into a // leave floats on children after a newer rule turns their parent into a
+66 -2
View File
@@ -105,8 +105,11 @@ pub struct TextEngine {
font_system: FontSystem, font_system: FontSystem,
swash: SwashCache, swash: SwashCache,
items: Vec<InlineItem>, items: Vec<InlineItem>,
replaced: Vec<(f32, f32)>,
} }
const REPLACED_CONTEXT_BIT: usize = 1usize << (usize::BITS - 1);
impl Default for TextEngine { impl Default for TextEngine {
fn default() -> Self { fn default() -> Self {
Self::new() Self::new()
@@ -129,7 +132,12 @@ impl TextEngine {
} }
db.set_sans_serif_family(FAMILY); db.set_sans_serif_family(FAMILY);
let font_system = FontSystem::new_with_locale_and_db("en-US".to_string(), db); let font_system = FontSystem::new_with_locale_and_db("en-US".to_string(), db);
TextEngine { font_system, swash: SwashCache::new(), items: Vec::new() } TextEngine {
font_system,
swash: SwashCache::new(),
items: Vec::new(),
replaced: Vec::new(),
}
} }
/// Number of inline formatting contexts collected (for debug/stats). /// Number of inline formatting contexts collected (for debug/stats).
@@ -258,6 +266,12 @@ impl TextEngine {
/// None for max-content), returning its shaped (width, height). Called by /// None for max-content), returning its shaped (width, height). Called by
/// taffy's measure function during layout. /// taffy's measure function during layout.
pub fn measure(&mut self, idx: usize, width: Option<f32>) -> (f32, f32) { pub fn measure(&mut self, idx: usize, width: Option<f32>) -> (f32, f32) {
if idx & REPLACED_CONTEXT_BIT != 0 {
let (iw, ih) = self.replaced[idx & !REPLACED_CONTEXT_BIT];
return width
.map(|width| (width, width * ih / iw))
.unwrap_or((iw, ih));
}
let TextEngine { font_system, items, .. } = self; let TextEngine { font_system, items, .. } = self;
let item = &mut items[idx]; let item = &mut items[idx];
item.buffer.set_size(font_system, width.map(|w| w.max(0.0)), None); item.buffer.set_size(font_system, width.map(|w| w.max(0.0)), None);
@@ -265,6 +279,51 @@ impl TextEngine {
buffer_size(&item.buffer) buffer_size(&item.buffer)
} }
/// Register a replaced element's intrinsic size as a taffy measure
/// 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) -> usize {
let index = self.replaced.len();
self.replaced.push((width, height));
REPLACED_CONTEXT_BIT | index
}
/// Measure either a shaped text context or an intrinsic replaced element.
/// Replaced boxes transfer a definite axis through their intrinsic ratio;
/// with neither axis definite they contribute their natural size.
pub fn measure_taffy(
&mut self,
idx: usize,
known: taffy::Size<Option<f32>>,
available: taffy::Size<taffy::AvailableSpace>,
) -> taffy::Size<f32> {
if idx & REPLACED_CONTEXT_BIT != 0 {
let (iw, ih) = self.replaced[idx & !REPLACED_CONTEXT_BIT];
return match (known.width, known.height) {
(Some(width), Some(height)) => taffy::Size { width, height },
(Some(width), None) => taffy::Size {
width,
height: width * ih / iw,
},
(None, Some(height)) => taffy::Size {
width: height * iw / ih,
height,
},
(None, None) => taffy::Size {
width: iw,
height: ih,
},
};
}
let width = known.width.or(match available.width {
taffy::AvailableSpace::Definite(width) => Some(width),
taffy::AvailableSpace::MinContent => Some(0.0),
taffy::AvailableSpace::MaxContent => None,
});
let (width, height) = self.measure(idx, width);
taffy::Size { width, height }
}
/// After layout, pin each context to its final content-box origin and /// After layout, pin each context to its final content-box origin and
/// clip, reshaping once at the resolved width so paint draws the same /// clip, reshaping once at the resolved width so paint draws the same
/// line breaks the box was sized for. /// line breaks the box was sized for.
@@ -628,7 +687,12 @@ impl TextEngine {
/// translate()` of the container's ancestors, shifting both the glyph /// translate()` of the container's ancestors, shifting both the glyph
/// origin and the clip so shaped text moves with a transformed box. /// origin and the clip so shaped text moves with a transformed box.
pub fn paint_item(&mut self, idx: usize, pixmap: &mut tiny_skia::Pixmap, offset: (f32, f32)) { pub fn paint_item(&mut self, idx: usize, pixmap: &mut tiny_skia::Pixmap, offset: (f32, f32)) {
let TextEngine { font_system, swash, items } = self; let TextEngine {
font_system,
swash,
items,
..
} = self;
let Some(item) = items.get_mut(idx) else { return }; let Some(item) = items.get_mut(idx) else { return };
let (ox, oy) = (item.origin.0 + offset.0, item.origin.1 + offset.1); let (ox, oy) = (item.origin.0 + offset.0, item.origin.1 + offset.1);
// The glyph origin shifts by the container's accumulated translate, // The glyph origin shifts by the container's accumulated translate,
+8
View File
@@ -191,6 +191,11 @@ pub struct LayoutStyle {
/// missing dimension from the given one, so a `width:100%` image gets a /// missing dimension from the given one, so a `width:100%` image gets a
/// real height instead of collapsing to zero. /// real height instead of collapsing to zero.
pub aspect_ratio: Option<f32>, pub aspect_ratio: Option<f32>,
/// Fetched intrinsic CSS-pixel size for a replaced element. Kept alongside
/// `aspect_ratio` so its taffy leaf can contribute a real min/max-content
/// size when percentage dimensions are resolved through an auto-sized
/// wrapper (`img { width:100%; height:auto }`).
pub intrinsic_size: Option<(f32, f32)>,
pub margin: Edges, pub margin: Edges,
/// Which margin sides are `auto` (top, right, bottom, left). `margin: 0 /// Which margin sides are `auto` (top, right, bottom, left). `margin: 0
/// auto` / `margin-inline: auto` centering needs a real Auto margin, which /// auto` / `margin-inline: auto` centering needs a real Auto margin, which
@@ -279,6 +284,9 @@ pub struct LayoutStyle {
pub justify_content: Option<taffy::JustifyContent>, pub justify_content: Option<taffy::JustifyContent>,
pub flex_grow: Option<f32>, pub flex_grow: Option<f32>,
pub flex_shrink: Option<f32>, pub flex_shrink: Option<f32>,
/// Flex/grid item order. The formatting algorithm consumes children in
/// order-modified document order, with source order breaking ties.
pub order: i32,
/// `flex-basis` (longhand, or the length in a `flex:` shorthand). `Auto` /// `flex-basis` (longhand, or the length in a `flex:` shorthand). `Auto`
/// is the default. Fixed-basis sidebars/columns (`flex: 0 0 260px`) /// is the default. Fixed-basis sidebars/columns (`flex: 0 0 260px`)
/// collapse to content width without it. /// collapse to content width without it.
+5 -3
View File
@@ -943,12 +943,14 @@ fn collect_image_intrinsics(
} }
let Some((url, density)) = resolve_img_url(tree, nid) else { continue }; let Some((url, density)) = resolve_img_url(tree, nid) else { continue };
let Some(bytes) = fetch_bytes(&url, base_url, cache) else { continue }; let Some(bytes) = fetch_bytes(&url, base_url, cache) else { continue };
if let Some((w, h)) = image_dimensions(&bytes) { let dimensions = image_dimensions(&bytes).map(|(width, height)| (width as f32, height as f32))
if w > 0 && h > 0 { .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 // A 2x (or w-descriptor) candidate's raw pixels are density
// times its CSS size; divide so layout sees CSS px, or every // times its CSS size; divide so layout sees CSS px, or every
// responsive image occupies twice its design size. // responsive image occupies twice its design size.
out.insert(nid, (w as f32 / density, h as f32 / density)); out.insert(nid, (w / density, h / density));
} }
} }
} }
+5
View File
@@ -432,6 +432,11 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
}, },
"flex-grow" => { if let Some(v) = token(value).and_then(|t| t.parse::<f32>().ok()) { style.flex_grow = Some(v); } } "flex-grow" => { if let Some(v) = token(value).and_then(|t| t.parse::<f32>().ok()) { style.flex_grow = Some(v); } }
"flex-shrink" => { if let Some(v) = token(value).and_then(|t| t.parse::<f32>().ok()) { style.flex_shrink = Some(v); } } "flex-shrink" => { if let Some(v) = token(value).and_then(|t| t.parse::<f32>().ok()) { style.flex_shrink = Some(v); } }
"order" => {
if let Ok(order) = value.trim().parse::<i32>() {
style.order = order;
}
}
"flex-basis" => { style.flex_basis = dimension_value(value.trim()); } "flex-basis" => { style.flex_basis = dimension_value(value.trim()); }
"flex" => parse_flex_shorthand(style, value), "flex" => parse_flex_shorthand(style, value),
"position" => { "position" => {