render: clamp intrinsic replaced sizing

This commit is contained in:
SGavrl committed 2026-07-23 19:22:27 +00:00
1 parent 436b69f50f
commit 07d2c6691f
2 files changed
+94 -25

No files matched your search

+28 -1
View File
@@ -2064,7 +2064,34 @@ fn build(
// measure callback derives the other axis through the intrinsic ratio. // measure callback derives the other axis through the intrinsic ratio.
if _name.local.as_ref() == "img" { if _name.local.as_ref() == "img" {
if let Some((width, height)) = style.intrinsic_size { if let Some((width, height)) = style.intrinsic_size {
let context = engine.register_replaced(width, height); // When an auto axis has a definite min/max constraint, the
// measured replaced leaf must own intrinsic-ratio transfer so it
// can clamp the derived size. Leaving the same ratio on taffy's
// style lets taffy synthesize that axis before measurement and
// bypass the constraint (width:100%; height:auto;
// max-height:200px became the uncapped intrinsic height).
//
// Keep taffy's ratio in the unconstrained case: percentage-sized
// images in auto wrappers can be measured at intrinsic size before
// their percentage width becomes definite, and taffy then needs
// the ratio to transfer that final width to height.
let measured_axis_constraint =
(!matches!(style.height, crate::Dimension::Auto)
&& matches!(style.width, crate::Dimension::Auto)
&& matches!(
(style.min_width, style.max_width),
(crate::Dimension::Px(_), _) | (_, crate::Dimension::Px(_))
))
|| (!matches!(style.width, crate::Dimension::Auto)
&& matches!(style.height, crate::Dimension::Auto)
&& matches!(
(style.min_height, style.max_height),
(crate::Dimension::Px(_), _) | (_, crate::Dimension::Px(_))
));
if measured_axis_constraint {
taffy_style.aspect_ratio = None;
}
let context = engine.register_replaced(width, height, style);
let leaf = taffy_tree.new_leaf_with_context(taffy_style, context).ok()?; let leaf = taffy_tree.new_leaf_with_context(taffy_style, context).ok()?;
id_map.insert(leaf, id); id_map.insert(leaf, id);
return Some(leaf); return Some(leaf);
+66 -24
View File
@@ -18,7 +18,7 @@ use cosmic_text::{Align, Attrs, Buffer, Color, Family, FontSystem, Metrics, Shap
use obscura_dom::tree::{DomTree, NodeId}; use obscura_dom::tree::{DomTree, NodeId};
use crate::{Display, LayoutStyle, Rect, TextTransform}; use crate::{Dimension, Display, LayoutStyle, Rect, TextTransform};
// Bundled faces. Chrome on this class of host renders `sans-serif` and the // Bundled faces. Chrome on this class of host renders `sans-serif` and the
// ubiquitous Arial/Helvetica/system-ui stacks as Liberation Sans, `serif` as // ubiquitous Arial/Helvetica/system-ui stacks as Liberation Sans, `serif` as
@@ -105,11 +105,63 @@ pub struct TextEngine {
font_system: FontSystem, font_system: FontSystem,
swash: SwashCache, swash: SwashCache,
items: Vec<InlineItem>, items: Vec<InlineItem>,
replaced: Vec<(f32, f32)>, replaced: Vec<ReplacedItem>,
} }
const REPLACED_CONTEXT_BIT: usize = 1usize << (usize::BITS - 1); const REPLACED_CONTEXT_BIT: usize = 1usize << (usize::BITS - 1);
#[derive(Clone, Copy)]
struct ReplacedItem {
intrinsic_width: f32,
intrinsic_height: f32,
min_width: Option<f32>,
min_height: Option<f32>,
max_width: Option<f32>,
max_height: Option<f32>,
}
impl ReplacedItem {
fn from_style(width: f32, height: f32, style: &LayoutStyle) -> Self {
let px = |dimension| match dimension {
Dimension::Px(value) => Some(value.max(0.0)),
_ => None,
};
ReplacedItem {
intrinsic_width: width,
intrinsic_height: height,
min_width: px(style.min_width),
min_height: px(style.min_height),
max_width: px(style.max_width),
max_height: px(style.max_height),
}
}
fn clamp(value: f32, min: Option<f32>, max: Option<f32>) -> f32 {
// CSS sizing gives the minimum precedence when min > max.
let value = max.map_or(value, |max| value.min(max));
min.map_or(value, |min| value.max(min))
}
fn size(self, known: taffy::Size<Option<f32>>) -> taffy::Size<f32> {
let (width, height) = match (known.width, known.height) {
(Some(width), Some(height)) => (width, height),
(Some(width), None) => (
width,
width * self.intrinsic_height / self.intrinsic_width,
),
(None, Some(height)) => (
height * self.intrinsic_width / self.intrinsic_height,
height,
),
(None, None) => (self.intrinsic_width, self.intrinsic_height),
};
taffy::Size {
width: Self::clamp(width, self.min_width, self.max_width),
height: Self::clamp(height, self.min_height, self.max_height),
}
}
}
impl Default for TextEngine { impl Default for TextEngine {
fn default() -> Self { fn default() -> Self {
Self::new() Self::new()
@@ -267,10 +319,9 @@ impl TextEngine {
/// 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 { if idx & REPLACED_CONTEXT_BIT != 0 {
let (iw, ih) = self.replaced[idx & !REPLACED_CONTEXT_BIT]; let size = self.replaced[idx & !REPLACED_CONTEXT_BIT]
return width .size(taffy::Size { width, height: None });
.map(|width| (width, width * ih / iw)) return (size.width, size.height);
.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];
@@ -282,9 +333,15 @@ impl TextEngine {
/// Register a replaced element's intrinsic size as a taffy measure /// Register a replaced element's intrinsic size as a taffy measure
/// context. Percentage-sized image leaves still need their intrinsic /// context. Percentage-sized image leaves still need their intrinsic
/// max-content contribution while an auto-sized ancestor is measured. /// max-content contribution while an auto-sized ancestor is measured.
pub fn register_replaced(&mut self, width: f32, height: f32) -> usize { pub fn register_replaced(
&mut self,
width: f32,
height: f32,
style: &LayoutStyle,
) -> usize {
let index = self.replaced.len(); let index = self.replaced.len();
self.replaced.push((width, height)); self.replaced
.push(ReplacedItem::from_style(width, height, style));
REPLACED_CONTEXT_BIT | index REPLACED_CONTEXT_BIT | index
} }
@@ -298,22 +355,7 @@ impl TextEngine {
available: taffy::Size<taffy::AvailableSpace>, available: taffy::Size<taffy::AvailableSpace>,
) -> taffy::Size<f32> { ) -> taffy::Size<f32> {
if idx & REPLACED_CONTEXT_BIT != 0 { if idx & REPLACED_CONTEXT_BIT != 0 {
let (iw, ih) = self.replaced[idx & !REPLACED_CONTEXT_BIT]; return self.replaced[idx & !REPLACED_CONTEXT_BIT].size(known);
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 { let width = known.width.or(match available.width {
taffy::AvailableSpace::Definite(width) => Some(width), taffy::AvailableSpace::Definite(width) => Some(width),