render: real block layout for mixed content, owner-space overflow clips
Two structural fixes to the layout and paint model. Mixed inline+block content: a block holding both was promoted wholesale to a flex-row-wrap container to approximate the inline formatting context. That collapsed contentless block children to width 0 (a position:relative hero wrapper whose only child is an absolute fill image vanished with it) and let text share lines with sibling blocks. Now the parent stays a real block, block-level children stay direct children (auto width fills the containing block), and each maximal run of consecutive inline-level siblings becomes one anonymous block-level box carrying the IFC. A run of pure text and foldable inline wrappers collapses to a single shaped cosmic-text leaf (one taffy node per run instead of one per word); runs with atomic inline boxes fall back to a flex-wrap wrapper. Absolutely positioned children neither join nor break runs and no longer count as inline content, so their containing block keeps its real used width and taffy resolves width:100%/inset:0 fills against it. Overflow clips: an inherited overflow:hidden clip was shifted by the painted node's own accumulated translate, so a carousel track's translateX dragged the viewport's clip along with every slide and all slides stayed visible stacked. Clips are now stored in the clip owner's coordinate space (owner translate baked in at resolve time) and no longer move with descendants, for boxes, per-word text, and shaped inline items alike. The same walk records per-node accumulated translates once, replacing the per-painted-node ancestor walks. Also parse translate3d (Swiper-style carousels position their track exclusively through it) and make transform:none reset a lower-priority transform. Verified: abs-fill repros that rendered 0x0 now fill across block/flex/grid parents; carousel repro shows exactly one slide; danluu.com mismatch vs Chromium 13.0 to 5.7 percent; no regressions across the prose and framework spot sets; obstacle course 33/33; 36 percent faster on a layout-bound synthetic page with neutral real-page memory.
This commit is contained in:
@@ -38,9 +38,15 @@ pub struct DomLayout {
|
||||
pub rects: HashMap<NodeId, Rect>,
|
||||
pub styles: HashMap<NodeId, crate::LayoutStyle>,
|
||||
/// The clip rect inherited from ancestor `overflow: hidden` boxes, keyed
|
||||
/// per node. `None` means unclipped. Does not include the node's own
|
||||
/// overflow (that only clips its children, not itself).
|
||||
/// per node, in SCREEN space (the clip owner's box shifted by the owner's
|
||||
/// accumulated translate; see `resolve_clip_rects`). `None` means
|
||||
/// unclipped. Does not include the node's own overflow (that only clips
|
||||
/// its children, not itself).
|
||||
pub clip_rects: HashMap<NodeId, Option<Rect>>,
|
||||
/// Accumulated `transform: translate()` per node (own + ancestors), in CSS
|
||||
/// px. Only nodes with a non-zero accumulation are present; paint shifts
|
||||
/// each box by this to reach screen space.
|
||||
pub translates: HashMap<NodeId, (f32, f32)>,
|
||||
/// Per-word geometry for text nodes: a text DOM node lays out as one
|
||||
/// taffy leaf per word (see `build_text_words`), each wrapping
|
||||
/// independently within its container, so its rendered content is a list
|
||||
@@ -55,6 +61,23 @@ pub struct DomLayout {
|
||||
pub text_engine: crate::inline::TextEngine,
|
||||
#[cfg(feature = "paint")]
|
||||
pub ifc_items: HashMap<NodeId, usize>,
|
||||
/// Anonymous inline-run contexts: a mixed block's consecutive inline
|
||||
/// children folded to one shaped leaf each (see `build_mixed_block`).
|
||||
/// Keyed by the parent block's `NodeId`, in document order; the leaves
|
||||
/// have no DOM node of their own.
|
||||
#[cfg(feature = "paint")]
|
||||
pub run_ifc_items: HashMap<NodeId, Vec<usize>>,
|
||||
}
|
||||
|
||||
/// Registry of cosmic-text inline formatting contexts created during the
|
||||
/// taffy-tree build: `whole` holds single-leaf containers keyed by the
|
||||
/// container's own DOM id, `runs` holds anonymous inline-run leaves keyed by
|
||||
/// the parent block's DOM id (those leaves have no DOM node, so their final
|
||||
/// rects are captured separately; see `compute_absolute_rects`).
|
||||
#[derive(Default)]
|
||||
struct IfcRegistry {
|
||||
whole: HashMap<NodeId, usize>,
|
||||
runs: HashMap<NodeId, Vec<usize>>,
|
||||
}
|
||||
|
||||
/// Walk the tree top-down accumulating the clip rect imposed by ancestor
|
||||
@@ -63,26 +86,66 @@ pub struct DomLayout {
|
||||
/// "visually hidden but accessible" pattern: a 1x1 absolutely-positioned box
|
||||
/// with clipped overflow) actually hide its contents instead of letting text
|
||||
/// paint wherever the box's static position happens to land.
|
||||
/// Clips are stored in SCREEN space: the clip owner's border box offset by the
|
||||
/// owner's own accumulated `transform: translate()`. A clip belongs to its
|
||||
/// owner's coordinate space, not the painted descendant's; shifting an
|
||||
/// inherited clip by the descendant's translate (the old behavior) let a
|
||||
/// carousel track's `translateX` drag the viewport's clip along with every
|
||||
/// slide, so all slides stayed visible inside their own shifted clip.
|
||||
///
|
||||
/// The same walk records each node's accumulated translate in `translates`
|
||||
/// (one pass here instead of an ancestor walk per painted node).
|
||||
fn resolve_clip_rects(
|
||||
tree: &DomTree,
|
||||
id: NodeId,
|
||||
inherited: Option<Rect>,
|
||||
tx: f32,
|
||||
ty: f32,
|
||||
rects: &HashMap<NodeId, Rect>,
|
||||
styles: &HashMap<NodeId, crate::LayoutStyle>,
|
||||
clip_rects: &mut HashMap<NodeId, Option<Rect>>,
|
||||
translates: &mut HashMap<NodeId, (f32, f32)>,
|
||||
) {
|
||||
clip_rects.insert(id, inherited);
|
||||
// This node's own translate joins the accumulation for its box and its
|
||||
// whole subtree (percentages resolve against its own border box).
|
||||
let (tx, ty) = match (styles.get(&id).and_then(|s| s.transform_translate), rects.get(&id)) {
|
||||
(Some((dx, dy)), Some(rect)) => (tx + resolve_translate(dx, rect.width), ty + resolve_translate(dy, rect.height)),
|
||||
(Some((dx, dy)), None) => (tx + resolve_translate(dx, 0.0), ty + resolve_translate(dy, 0.0)),
|
||||
_ => (tx, ty),
|
||||
};
|
||||
if tx != 0.0 || ty != 0.0 {
|
||||
translates.insert(id, (tx, ty));
|
||||
}
|
||||
let next = match (styles.get(&id), rects.get(&id)) {
|
||||
(Some(style), Some(rect)) if style.overflow_hidden => {
|
||||
let own = Rect { x: rect.x + tx, y: rect.y + ty, width: rect.width, height: rect.height };
|
||||
Some(match inherited {
|
||||
Some(clip) => clip.intersect(rect).unwrap_or(Rect::default()),
|
||||
None => *rect,
|
||||
Some(clip) => clip.intersect(&own).unwrap_or(Rect::default()),
|
||||
None => own,
|
||||
})
|
||||
}
|
||||
_ => inherited,
|
||||
};
|
||||
for cid in tree.children(id) {
|
||||
resolve_clip_rects(tree, cid, next, rects, styles, clip_rects);
|
||||
resolve_clip_rects(tree, cid, next, tx, ty, rects, styles, clip_rects, translates);
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve one `transform: translate()` component to px: a length passes
|
||||
/// through, a percentage is taken against `basis` (the element's own
|
||||
/// border-box extent on that axis). Font/viewport-relative leftovers fall
|
||||
/// back to a coarse px value (translate rarely uses them).
|
||||
pub(crate) fn resolve_translate(d: crate::Dimension, basis: f32) -> f32 {
|
||||
match d {
|
||||
crate::Dimension::Px(px) => px,
|
||||
crate::Dimension::Percent(p) => p * basis,
|
||||
crate::Dimension::Em(v) | crate::Dimension::Rem(v) => v * 16.0,
|
||||
crate::Dimension::Vw(v)
|
||||
| crate::Dimension::Vh(v)
|
||||
| crate::Dimension::Vmin(v)
|
||||
| crate::Dimension::Vmax(v) => v,
|
||||
crate::Dimension::Auto => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,7 +354,7 @@ pub fn layout_dom_with_images(
|
||||
let mut id_map: HashMap<taffy::NodeId, NodeId> = HashMap::new();
|
||||
let mut words: HashMap<taffy::NodeId, (NodeId, String)> = HashMap::new();
|
||||
let mut engine = crate::inline::TextEngine::new();
|
||||
let mut ifc_items: HashMap<NodeId, usize> = HashMap::new();
|
||||
let mut ifc_items = IfcRegistry::default();
|
||||
|
||||
// The document node itself is not an element; lay out from the first
|
||||
// element descendant (the <html> root).
|
||||
@@ -302,6 +365,9 @@ pub fn layout_dom_with_images(
|
||||
|
||||
let mut rects = HashMap::new();
|
||||
let mut text_runs = HashMap::new();
|
||||
// Final absolute rects of anonymous inline-run leaves, keyed by the
|
||||
// engine item index (they have no DOM id to key `rects` by).
|
||||
let mut anon_rects: HashMap<usize, Rect> = HashMap::new();
|
||||
if let Some(root_id) = root {
|
||||
// Top-down inheritance of the properties CSS inherits by default.
|
||||
#[derive(Clone)]
|
||||
@@ -746,20 +812,21 @@ pub fn layout_dom_with_images(
|
||||
{
|
||||
let _ = taffy_tree.compute_layout(taffy_root, available);
|
||||
}
|
||||
compute_absolute_rects(&taffy_tree, taffy_root, 0.0, 0.0, &id_map, &words, &mut rects, &mut text_runs);
|
||||
compute_absolute_rects(&taffy_tree, taffy_root, 0.0, 0.0, &id_map, &words, &mut rects, &mut text_runs, &mut anon_rects);
|
||||
synthesize_row_rects(tree, &mut rects);
|
||||
}
|
||||
}
|
||||
|
||||
let mut clip_rects = HashMap::new();
|
||||
let mut translates = HashMap::new();
|
||||
if let Some(root_id) = root {
|
||||
resolve_clip_rects(tree, root_id, None, &rects, &styles, &mut clip_rects);
|
||||
resolve_clip_rects(tree, root_id, None, 0.0, 0.0, &rects, &styles, &mut clip_rects, &mut translates);
|
||||
}
|
||||
|
||||
// Pin each shaped inline context to its final content-box origin/width now
|
||||
// that layout is done, so paint draws the same line breaks it was sized for.
|
||||
#[cfg(feature = "paint")]
|
||||
for (nid, &idx) in &ifc_items {
|
||||
for (nid, &idx) in &ifc_items.whole {
|
||||
if let (Some(rect), Some(style)) = (rects.get(nid), styles.get(nid)) {
|
||||
let origin = crate::inline::content_origin(rect, style);
|
||||
let cw = crate::inline::content_width(rect, style);
|
||||
@@ -767,11 +834,15 @@ pub fn layout_dom_with_images(
|
||||
// `overflow: hidden` on the container clips it (this is what keeps
|
||||
// the 1x1 "visually hidden" skip-link box from painting its text,
|
||||
// now that the text is one leaf rather than clipped word boxes).
|
||||
// Clips live in screen space; the container's own box joins the
|
||||
// chain shifted by its accumulated translate.
|
||||
let inherited = clip_rects.get(nid).copied().flatten();
|
||||
let clip = if style.overflow_hidden {
|
||||
let (tx, ty) = translates.get(nid).copied().unwrap_or((0.0, 0.0));
|
||||
let own = crate::Rect { x: rect.x + tx, y: rect.y + ty, width: rect.width, height: rect.height };
|
||||
Some(match inherited {
|
||||
Some(c) => c.intersect(rect).unwrap_or(crate::Rect::default()),
|
||||
None => *rect,
|
||||
Some(c) => c.intersect(&own).unwrap_or(crate::Rect::default()),
|
||||
None => own,
|
||||
})
|
||||
} else {
|
||||
inherited
|
||||
@@ -779,16 +850,42 @@ pub fn layout_dom_with_images(
|
||||
engine.finalize(idx, origin, cw, clip);
|
||||
}
|
||||
}
|
||||
// Anonymous run leaves have no DOM node and no border/padding of their
|
||||
// own: the leaf rect IS the content box. Clipping comes from the parent
|
||||
// block (its inherited chain, plus its own overflow like any child).
|
||||
#[cfg(feature = "paint")]
|
||||
for (parent, items) in &ifc_items.runs {
|
||||
let inherited = clip_rects.get(parent).copied().flatten();
|
||||
let clip = match (styles.get(parent), rects.get(parent)) {
|
||||
(Some(style), Some(prect)) if style.overflow_hidden => {
|
||||
let (tx, ty) = translates.get(parent).copied().unwrap_or((0.0, 0.0));
|
||||
let own = crate::Rect { x: prect.x + tx, y: prect.y + ty, width: prect.width, height: prect.height };
|
||||
Some(match inherited {
|
||||
Some(c) => c.intersect(&own).unwrap_or(crate::Rect::default()),
|
||||
None => own,
|
||||
})
|
||||
}
|
||||
_ => inherited,
|
||||
};
|
||||
for &idx in items {
|
||||
if let Some(rect) = anon_rects.get(&idx) {
|
||||
engine.finalize(idx, (rect.x, rect.y), rect.width, clip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DomLayout {
|
||||
rects,
|
||||
styles,
|
||||
clip_rects,
|
||||
translates,
|
||||
text_runs,
|
||||
#[cfg(feature = "paint")]
|
||||
text_engine: engine,
|
||||
#[cfg(feature = "paint")]
|
||||
ifc_items,
|
||||
ifc_items: ifc_items.whole,
|
||||
#[cfg(feature = "paint")]
|
||||
run_ifc_items: ifc_items.runs,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -996,6 +1093,7 @@ fn compute_absolute_rects(
|
||||
words: &HashMap<taffy::NodeId, (NodeId, String)>,
|
||||
rects: &mut HashMap<NodeId, Rect>,
|
||||
text_runs: &mut HashMap<NodeId, Vec<(Rect, String)>>,
|
||||
anon_rects: &mut HashMap<usize, Rect>,
|
||||
) {
|
||||
if let Ok(layout) = taffy_tree.layout(taffy_id) {
|
||||
let x = abs_x + layout.location.x;
|
||||
@@ -1004,6 +1102,11 @@ fn compute_absolute_rects(
|
||||
|
||||
if let Some(dom_id) = id_map.get(&taffy_id) {
|
||||
rects.insert(*dom_id, rect);
|
||||
} else if let Some(&item) = taffy_tree.get_node_context(taffy_id) {
|
||||
// A taffy leaf with an engine-item context but no DOM id is an
|
||||
// anonymous inline-run leaf (see `build_mixed_block`); record its
|
||||
// final rect by item index so the finalize pass can pin it.
|
||||
anon_rects.insert(item, rect);
|
||||
}
|
||||
// A word leaf's dom_id is its owning text node, shared by every other
|
||||
// word from the same node, so this appends rather than overwrites.
|
||||
@@ -1013,7 +1116,7 @@ fn compute_absolute_rects(
|
||||
|
||||
if let Ok(children) = taffy_tree.children(taffy_id) {
|
||||
for child_id in children {
|
||||
compute_absolute_rects(taffy_tree, child_id, x, y, id_map, words, rects, text_runs);
|
||||
compute_absolute_rects(taffy_tree, child_id, x, y, id_map, words, rects, text_runs, anon_rects);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1036,7 +1139,13 @@ fn has_inline_content(tree: &DomTree, id: NodeId, styles: &HashMap<NodeId, crate
|
||||
if s.display_contents && s.display != crate::Display::None {
|
||||
has_inline_content(tree, cid, styles)
|
||||
} else {
|
||||
// Out-of-flow boxes (absolutely positioned, floated)
|
||||
// are not inline content: an inline that is the sole
|
||||
// child of a hero wrapper must not drag the wrapper
|
||||
// into the inline-formatting path.
|
||||
s.display == crate::Display::Inline
|
||||
&& !matches!(s.position, Some(taffy::Position::Absolute))
|
||||
&& s.float.is_none()
|
||||
}
|
||||
})
|
||||
.unwrap_or(false),
|
||||
@@ -1060,7 +1169,7 @@ fn build_any(
|
||||
id_map: &mut HashMap<taffy::NodeId, NodeId>,
|
||||
words: &mut HashMap<taffy::NodeId, (NodeId, String)>,
|
||||
engine: &mut crate::inline::TextEngine,
|
||||
ifc_items: &mut HashMap<NodeId, usize>,
|
||||
ifc_items: &mut IfcRegistry,
|
||||
styles: &HashMap<NodeId, crate::LayoutStyle>,
|
||||
) -> Vec<taffy::NodeId> {
|
||||
let is_text = tree
|
||||
@@ -1341,7 +1450,7 @@ fn build_table(
|
||||
id_map: &mut HashMap<taffy::NodeId, NodeId>,
|
||||
words: &mut HashMap<taffy::NodeId, (NodeId, String)>,
|
||||
engine: &mut crate::inline::TextEngine,
|
||||
ifc_items: &mut HashMap<NodeId, usize>,
|
||||
ifc_items: &mut IfcRegistry,
|
||||
styles: &HashMap<NodeId, crate::LayoutStyle>,
|
||||
) -> Option<taffy::NodeId> {
|
||||
let style = styles.get(&id)?;
|
||||
@@ -1473,7 +1582,7 @@ fn build(
|
||||
id_map: &mut HashMap<taffy::NodeId, NodeId>,
|
||||
words: &mut HashMap<taffy::NodeId, (NodeId, String)>,
|
||||
engine: &mut crate::inline::TextEngine,
|
||||
ifc_items: &mut HashMap<NodeId, usize>,
|
||||
ifc_items: &mut IfcRegistry,
|
||||
styles: &HashMap<NodeId, crate::LayoutStyle>,
|
||||
) -> Option<taffy::NodeId> {
|
||||
let node = tree.get_node(id)?;
|
||||
@@ -1504,7 +1613,7 @@ fn build(
|
||||
if let Some(item) = engine.try_build(tree, id, styles) {
|
||||
let leaf = taffy_tree.new_leaf_with_context(taffy_style, item).ok()?;
|
||||
id_map.insert(leaf, id);
|
||||
ifc_items.insert(id, item);
|
||||
ifc_items.whole.insert(id, item);
|
||||
return Some(leaf);
|
||||
}
|
||||
|
||||
@@ -1529,6 +1638,35 @@ fn build(
|
||||
|| (style.display == crate::Display::Flex && style.flex_direction == Some(taffy::FlexDirection::Column));
|
||||
let has_inline_ish_content =
|
||||
has_inline_content(tree, id, styles) || style.before_content.is_some() || style.after_content.is_some();
|
||||
|
||||
let mut dom_children = tree.children(id);
|
||||
// In a grid formatting context, whitespace-only text between items does not
|
||||
// generate an anonymous grid item (CSS Grid). taffy 0.12 places each stray
|
||||
// whitespace text node in its own cell, which offsets every real item, so a
|
||||
// Tailwind `grid-cols-3` block with newlines between its children lays out
|
||||
// diagonally instead of in one row (this collapsed the whole modern
|
||||
// framework cluster after the taffy upgrade). Drop them before building.
|
||||
if style.display == crate::Display::Grid {
|
||||
dom_children.retain(|&cid| {
|
||||
tree.get_node(cid).map_or(false, |n| n.is_element()) || !tree.text_content(cid).trim().is_empty()
|
||||
});
|
||||
}
|
||||
let has_float_child = dom_children.iter().any(|&cid| styles.get(&cid).map(|s| s.float.is_some()).unwrap_or(false));
|
||||
|
||||
// A block with mixed inline + block children keeps real block layout:
|
||||
// block-level children become direct block children (full available
|
||||
// width, exactly what CSS block layout gives them), while each maximal
|
||||
// run of consecutive inline-level siblings is wrapped in one anonymous
|
||||
// block-level box that carries the inline formatting context. This is
|
||||
// how real engines structure mixed content, and it replaces the old
|
||||
// whole-container flex-row-wrap promotion, which collapsed contentless
|
||||
// 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 && has_inline_ish_content && !has_float_child {
|
||||
return build_mixed_block(tree, id, style, taffy_style, &dom_children, taffy_tree, id_map, words, engine, ifc_items, styles);
|
||||
}
|
||||
|
||||
if stacks_children_vertically && has_inline_ish_content {
|
||||
taffy_style.display = taffy::style::Display::Flex;
|
||||
taffy_style.flex_direction = taffy::FlexDirection::Row;
|
||||
@@ -1553,19 +1691,6 @@ fn build(
|
||||
taffy_style.align_items = Some(taffy::AlignItems::FLEX_START);
|
||||
}
|
||||
|
||||
let mut dom_children = tree.children(id);
|
||||
// In a grid formatting context, whitespace-only text between items does not
|
||||
// generate an anonymous grid item (CSS Grid). taffy 0.12 places each stray
|
||||
// whitespace text node in its own cell, which offsets every real item, so a
|
||||
// Tailwind `grid-cols-3` block with newlines between its children lays out
|
||||
// diagonally instead of in one row (this collapsed the whole modern
|
||||
// framework cluster after the taffy upgrade). Drop them before building.
|
||||
if style.display == crate::Display::Grid {
|
||||
dom_children.retain(|&cid| {
|
||||
tree.get_node(cid).map_or(false, |n| n.is_element()) || !tree.text_content(cid).trim().is_empty()
|
||||
});
|
||||
}
|
||||
let has_float_child = dom_children.iter().any(|&cid| styles.get(&cid).map(|s| s.float.is_some()).unwrap_or(false));
|
||||
let mut child_ids: Vec<taffy::NodeId> = if has_float_child {
|
||||
build_children_with_float_zone(tree, &dom_children, taffy_tree, id_map, words, engine, ifc_items, styles)
|
||||
} else {
|
||||
@@ -1589,6 +1714,202 @@ fn build(
|
||||
Some(taffy_id)
|
||||
}
|
||||
|
||||
/// Build a block container whose children mix inline-level and block-level
|
||||
/// content, preserving real block layout for the block children.
|
||||
///
|
||||
/// Structure produced (mirroring how CSS block containers actually work):
|
||||
/// - block-level in-flow children stay direct children of the (taffy Block)
|
||||
/// parent, so an auto width fills the containing block even when the child
|
||||
/// has no in-flow content of its own;
|
||||
/// - each maximal run of consecutive inline-level siblings becomes ONE
|
||||
/// anonymous block-level box carrying the inline formatting context. A run
|
||||
/// of pure text and foldable inline wrappers collapses to a single shaped
|
||||
/// cosmic-text leaf (one taffy node for the whole run instead of one per
|
||||
/// word: faster and lighter than the old whole-container promotion). Runs
|
||||
/// holding atomic inline boxes (img, inline-block, ...) fall back to an
|
||||
/// anonymous flex-wrap wrapper around the run's boxes;
|
||||
/// - out-of-flow (absolutely positioned) children neither join nor break a
|
||||
/// run; they are appended after the flow children so their containing
|
||||
/// block is this parent, whose used width they resolve percentages against.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn build_mixed_block(
|
||||
tree: &DomTree,
|
||||
id: NodeId,
|
||||
style: &crate::LayoutStyle,
|
||||
mut taffy_style: taffy::Style,
|
||||
dom_children: &[NodeId],
|
||||
taffy_tree: &mut TaffyTree<usize>,
|
||||
id_map: &mut HashMap<taffy::NodeId, NodeId>,
|
||||
words: &mut HashMap<taffy::NodeId, (NodeId, String)>,
|
||||
engine: &mut crate::inline::TextEngine,
|
||||
ifc_items: &mut IfcRegistry,
|
||||
styles: &HashMap<NodeId, crate::LayoutStyle>,
|
||||
) -> Option<taffy::NodeId> {
|
||||
// The parent is a genuine block container. `to_taffy_style` may have
|
||||
// promoted it to a flex column for `text-align` (taffy block layout has
|
||||
// no align-items); undo that: text-align moves line content, never
|
||||
// block children, so it is applied to the runs below instead.
|
||||
taffy_style.display = taffy::style::Display::Block;
|
||||
|
||||
// Splice display:contents children and drop nothing else, so runs
|
||||
// partition over the child list exactly as CSS sees it.
|
||||
let mut flat: Vec<NodeId> = Vec::new();
|
||||
flatten_contents_children(tree, dom_children, styles, &mut flat);
|
||||
|
||||
enum Seg {
|
||||
Run(Vec<NodeId>),
|
||||
Block(NodeId),
|
||||
}
|
||||
let mut segs: Vec<Seg> = Vec::new();
|
||||
let mut out_of_flow: Vec<NodeId> = Vec::new();
|
||||
for &cid in &flat {
|
||||
let Some(node) = tree.get_node(cid) else { continue };
|
||||
let is_text = matches!(node.data, obscura_dom::tree::NodeData::Text { .. });
|
||||
let inline_level = if is_text {
|
||||
true
|
||||
} else if let Some(s) = styles.get(&cid) {
|
||||
if s.display == crate::Display::None {
|
||||
continue;
|
||||
}
|
||||
if matches!(s.position, Some(taffy::Position::Absolute)) {
|
||||
out_of_flow.push(cid);
|
||||
continue;
|
||||
}
|
||||
s.display == crate::Display::Inline
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if inline_level {
|
||||
match segs.last_mut() {
|
||||
Some(Seg::Run(run)) => run.push(cid),
|
||||
_ => segs.push(Seg::Run(vec![cid])),
|
||||
}
|
||||
} else {
|
||||
segs.push(Seg::Block(cid));
|
||||
}
|
||||
}
|
||||
|
||||
// ::before joins the first inline run and ::after the last (a list
|
||||
// marker must share its item text's lines); when the adjacent segment
|
||||
// is a block, the pseudo content gets its own anonymous run instead.
|
||||
let before_leaves = style.before_content.as_ref().map(|c| build_pseudo_content(id, c, style, taffy_tree, words)).unwrap_or_default();
|
||||
let after_leaves = style.after_content.as_ref().map(|c| build_pseudo_content(id, c, style, taffy_tree, words)).unwrap_or_default();
|
||||
let mut before_pending = !before_leaves.is_empty();
|
||||
let mut after_pending = !after_leaves.is_empty();
|
||||
|
||||
let n_segs = segs.len();
|
||||
let mut child_ids: Vec<taffy::NodeId> = Vec::new();
|
||||
for (i, seg) in segs.into_iter().enumerate() {
|
||||
match seg {
|
||||
Seg::Block(cid) => {
|
||||
child_ids.extend(build_any(tree, cid, taffy_tree, id_map, words, engine, ifc_items, styles));
|
||||
}
|
||||
Seg::Run(run) => {
|
||||
let join_before = before_pending && i == 0;
|
||||
let join_after = after_pending && i + 1 == n_segs;
|
||||
// Fast path: the whole run folds to one shaped leaf, unless
|
||||
// pseudo-content word leaves must share its lines.
|
||||
if !join_before && !join_after {
|
||||
if let Some(item) = engine.try_build_run(tree, id, &run, styles) {
|
||||
let leaf = taffy_tree.new_leaf_with_context(run_leaf_style(), item).ok()?;
|
||||
ifc_items.runs.entry(id).or_default().push(item);
|
||||
child_ids.push(leaf);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let mut atoms: Vec<taffy::NodeId> = Vec::new();
|
||||
if join_before {
|
||||
atoms.extend(before_leaves.iter().copied());
|
||||
before_pending = false;
|
||||
}
|
||||
for &rc in &run {
|
||||
atoms.extend(build_any(tree, rc, taffy_tree, id_map, words, engine, ifc_items, styles));
|
||||
}
|
||||
if join_after {
|
||||
atoms.extend(after_leaves.iter().copied());
|
||||
after_pending = false;
|
||||
}
|
||||
if atoms.is_empty() {
|
||||
// Whitespace-only run between blocks: no anonymous box.
|
||||
continue;
|
||||
}
|
||||
let wrapper = taffy_tree.new_with_children(run_wrapper_style(style), &atoms).ok()?;
|
||||
child_ids.push(wrapper);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Pseudo content that found no adjacent run to join.
|
||||
if before_pending {
|
||||
let wrapper = taffy_tree.new_with_children(run_wrapper_style(style), &before_leaves).ok()?;
|
||||
child_ids.insert(0, wrapper);
|
||||
}
|
||||
if after_pending {
|
||||
let wrapper = taffy_tree.new_with_children(run_wrapper_style(style), &after_leaves).ok()?;
|
||||
child_ids.push(wrapper);
|
||||
}
|
||||
for cid in out_of_flow {
|
||||
child_ids.extend(build_any(tree, cid, taffy_tree, id_map, words, engine, ifc_items, styles));
|
||||
}
|
||||
|
||||
let taffy_id = if child_ids.is_empty() {
|
||||
taffy_tree.new_leaf(taffy_style).ok()?
|
||||
} else {
|
||||
taffy_tree.new_with_children(taffy_style, &child_ids).ok()?
|
||||
};
|
||||
id_map.insert(taffy_id, id);
|
||||
Some(taffy_id)
|
||||
}
|
||||
|
||||
/// Expand `display: contents` wrappers so their children partition into the
|
||||
/// caller's segment list as if they were direct children (CSS Display 3).
|
||||
fn flatten_contents_children(
|
||||
tree: &DomTree,
|
||||
children: &[NodeId],
|
||||
styles: &HashMap<NodeId, crate::LayoutStyle>,
|
||||
out: &mut Vec<NodeId>,
|
||||
) {
|
||||
for &cid in children {
|
||||
let splices = styles.get(&cid).map(|s| s.display_contents && s.display != crate::Display::None).unwrap_or(false);
|
||||
if splices {
|
||||
let kids = tree.children(cid);
|
||||
flatten_contents_children(tree, &kids, styles, out);
|
||||
} else {
|
||||
out.push(cid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Style for a single-leaf inline run: a block-level box that fills the
|
||||
/// containing block's width, with height from the shaped text (measure fn).
|
||||
fn run_leaf_style() -> taffy::Style {
|
||||
taffy::Style {
|
||||
size: taffy::Size { width: taffy::style::Dimension::percent(1.0), height: taffy::style::Dimension::auto() },
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Style for an anonymous inline-run wrapper: a full-width block-level box
|
||||
/// whose interior is the flex-wrap approximation of an inline formatting
|
||||
/// context. The parent's `text-align` stand-in (align_items) moves the run's
|
||||
/// line content via justify-content, exactly as the old whole-container
|
||||
/// promotion did, but scoped to the run so sibling blocks stay full width.
|
||||
fn run_wrapper_style(parent: &crate::LayoutStyle) -> taffy::Style {
|
||||
let justify = match parent.align_items {
|
||||
Some(taffy::AlignItems::FLEX_END) => Some(taffy::JustifyContent::FLEX_END),
|
||||
Some(taffy::AlignItems::CENTER) => Some(taffy::JustifyContent::CENTER),
|
||||
_ => None,
|
||||
};
|
||||
taffy::Style {
|
||||
display: taffy::style::Display::Flex,
|
||||
flex_direction: taffy::FlexDirection::Row,
|
||||
flex_wrap: taffy::FlexWrap::Wrap,
|
||||
align_items: Some(taffy::AlignItems::FLEX_START),
|
||||
justify_content: justify,
|
||||
size: taffy::Size { width: taffy::style::Dimension::percent(1.0), height: taffy::style::Dimension::auto() },
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Approximate `float: left|right` without real per-line reflow (which
|
||||
/// taffy's block/flex/grid modes do not provide): place the float alongside
|
||||
/// the flow siblings that follow it, up to whichever comes first of the next
|
||||
@@ -1679,7 +2000,7 @@ fn build_children_with_float_zone(
|
||||
id_map: &mut HashMap<taffy::NodeId, NodeId>,
|
||||
words: &mut HashMap<taffy::NodeId, (NodeId, String)>,
|
||||
engine: &mut crate::inline::TextEngine,
|
||||
ifc_items: &mut HashMap<NodeId, usize>,
|
||||
ifc_items: &mut IfcRegistry,
|
||||
styles: &HashMap<NodeId, crate::LayoutStyle>,
|
||||
) -> Vec<taffy::NodeId> {
|
||||
let is_float = |cid: NodeId| styles.get(&cid).map(|s| s.float.is_some()).unwrap_or(false);
|
||||
|
||||
@@ -150,28 +150,55 @@ impl TextEngine {
|
||||
return None;
|
||||
}
|
||||
let base = styles.get(&id)?;
|
||||
let base_size = base.font_size.unwrap_or(16.0);
|
||||
// `-webkit-background-clip: text` on a transparent-colored element paints
|
||||
// its background *through* the glyphs (gradient/solid text). When active,
|
||||
// shape the glyphs in opaque white so their coverage renders, then recolor
|
||||
// them from the background at paint time; otherwise transparent text stays
|
||||
// transparent (and invisible), unchanged.
|
||||
let clip_fill = clip_text_fill(base);
|
||||
let default_color = if clip_fill.is_some() {
|
||||
[255, 255, 255, 255]
|
||||
} else {
|
||||
base.color.unwrap_or([0, 0, 0, 255])
|
||||
};
|
||||
|
||||
let root_transform = base.text_transform.unwrap_or(TextTransform::None);
|
||||
let root_underline = base.underline.unwrap_or(false);
|
||||
let root_italic = base.font_style_italic.unwrap_or(false);
|
||||
let root_bold = base.font_weight.as_deref() == Some("bold");
|
||||
let root_family = resolve_font_family(base.font_family.as_deref());
|
||||
let (ctx, clip_fill) = base_span_ctx(base);
|
||||
let mut spans: Vec<(String, SpanAttrs)> = Vec::new();
|
||||
let mut collector = Collector { last_was_space: true };
|
||||
let ctx = SpanCtx { color: default_color, bold: root_bold, italic: root_italic, underline: root_underline, transform: root_transform, family: root_family };
|
||||
collect_spans(tree, id, styles, ctx, &mut spans, &mut collector);
|
||||
self.push_shaped_item(base, spans, clip_fill)
|
||||
}
|
||||
|
||||
/// Build an inline formatting context from a *run* of consecutive
|
||||
/// inline-level siblings inside `parent` (a mixed-content block whose
|
||||
/// other children are block-level). The run folds to one shaped buffer
|
||||
/// exactly like a whole-container IFC, using the parent's style as the
|
||||
/// base. Returns `None` when any node in the run cannot fold (atomic
|
||||
/// inline, replaced element, ...) or the run has no visible text; the
|
||||
/// caller then falls back to the flex-wrap wrapper for that run.
|
||||
pub fn try_build_run(
|
||||
&mut self,
|
||||
tree: &DomTree,
|
||||
parent: NodeId,
|
||||
run: &[NodeId],
|
||||
styles: &std::collections::HashMap<NodeId, LayoutStyle>,
|
||||
) -> Option<usize> {
|
||||
let mut has_text = false;
|
||||
for &cid in run {
|
||||
if !inline_child_ok(tree, cid, styles, &mut has_text) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
if !has_text {
|
||||
return None;
|
||||
}
|
||||
let base = styles.get(&parent)?;
|
||||
let (ctx, clip_fill) = base_span_ctx(base);
|
||||
let mut spans: Vec<(String, SpanAttrs)> = Vec::new();
|
||||
let mut collector = Collector { last_was_space: true };
|
||||
for &cid in run {
|
||||
collect_node_spans(tree, cid, styles, ctx, &mut spans, &mut collector);
|
||||
}
|
||||
self.push_shaped_item(base, spans, clip_fill)
|
||||
}
|
||||
|
||||
/// Shared tail of [`try_build`] / [`try_build_run`]: shape the collected
|
||||
/// spans into a cosmic-text buffer under `base`'s font metrics and
|
||||
/// alignment, and store it as a new inline item.
|
||||
fn push_shaped_item(
|
||||
&mut self,
|
||||
base: &LayoutStyle,
|
||||
mut spans: Vec<(String, SpanAttrs)>,
|
||||
clip_fill: Option<(f32, Vec<([u8; 4], Option<f32>)>)>,
|
||||
) -> Option<usize> {
|
||||
// Trim a single trailing space so it does not widen the last line.
|
||||
if let Some((text, _)) = spans.last_mut() {
|
||||
if text.ends_with(' ') {
|
||||
@@ -182,6 +209,7 @@ impl TextEngine {
|
||||
return None;
|
||||
}
|
||||
|
||||
let base_size = base.font_size.unwrap_or(16.0);
|
||||
// Real `line-height` drives vertical rhythm; a fixed ratio made
|
||||
// real-site prose (e.g. Wikipedia's 1.6) noticeably tighter than
|
||||
// Chromium. `normal` is font-relative; ~1.2 matches DejaVu closely.
|
||||
@@ -293,6 +321,32 @@ struct Collector {
|
||||
/// spans. Collapsing spans HTML's insignificant whitespace (runs of spaces,
|
||||
/// tabs, and newlines fold to one space; leading space at the start of the
|
||||
/// context is dropped) exactly as `white-space: normal` requires.
|
||||
/// Root span context (and background-clip-text fill, when active) for an IFC
|
||||
/// whose base style is `base`.
|
||||
///
|
||||
/// `-webkit-background-clip: text` on a transparent-colored element paints
|
||||
/// its background *through* the glyphs (gradient/solid text). When active,
|
||||
/// shape the glyphs in opaque white so their coverage renders, then recolor
|
||||
/// them from the background at paint time; otherwise transparent text stays
|
||||
/// transparent (and invisible), unchanged.
|
||||
fn base_span_ctx(base: &LayoutStyle) -> (SpanCtx, Option<(f32, Vec<([u8; 4], Option<f32>)>)>) {
|
||||
let clip_fill = clip_text_fill(base);
|
||||
let default_color = if clip_fill.is_some() {
|
||||
[255, 255, 255, 255]
|
||||
} else {
|
||||
base.color.unwrap_or([0, 0, 0, 255])
|
||||
};
|
||||
let ctx = SpanCtx {
|
||||
color: default_color,
|
||||
bold: base.font_weight.as_deref() == Some("bold"),
|
||||
italic: base.font_style_italic.unwrap_or(false),
|
||||
underline: base.underline.unwrap_or(false),
|
||||
transform: base.text_transform.unwrap_or(TextTransform::None),
|
||||
family: resolve_font_family(base.font_family.as_deref()),
|
||||
};
|
||||
(ctx, clip_fill)
|
||||
}
|
||||
|
||||
fn collect_spans(
|
||||
tree: &DomTree,
|
||||
id: NodeId,
|
||||
@@ -302,38 +356,53 @@ fn collect_spans(
|
||||
c: &mut Collector,
|
||||
) {
|
||||
for cid in tree.children(id) {
|
||||
let Some(node) = tree.get_node(cid) else { continue };
|
||||
match &node.data {
|
||||
obscura_dom::tree::NodeData::Text { contents } => {
|
||||
let attrs = SpanAttrs { bold: ctx.bold, italic: ctx.italic, underline: ctx.underline, color: ctx.color, family: ctx.family };
|
||||
push_text(contents, ctx.transform, &attrs, out, c);
|
||||
collect_node_spans(tree, cid, styles, ctx, out, c);
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect the spans contributed by one node (a text node's runs, or an
|
||||
/// element's whole subtree with its style threaded through). Split out of
|
||||
/// [`collect_spans`] so an inline *run* (a slice of siblings, not a whole
|
||||
/// container) can also be folded into one shaped buffer.
|
||||
fn collect_node_spans(
|
||||
tree: &DomTree,
|
||||
cid: NodeId,
|
||||
styles: &std::collections::HashMap<NodeId, LayoutStyle>,
|
||||
ctx: SpanCtx,
|
||||
out: &mut Vec<(String, SpanAttrs)>,
|
||||
c: &mut Collector,
|
||||
) {
|
||||
let Some(node) = tree.get_node(cid) else { return };
|
||||
match &node.data {
|
||||
obscura_dom::tree::NodeData::Text { contents } => {
|
||||
let attrs = SpanAttrs { bold: ctx.bold, italic: ctx.italic, underline: ctx.underline, color: ctx.color, family: ctx.family };
|
||||
push_text(contents, ctx.transform, &attrs, out, c);
|
||||
}
|
||||
_ => {
|
||||
let Some(elem) = node.as_element() else { return };
|
||||
let style = styles.get(&cid);
|
||||
if style.map(|s| s.display == Display::None).unwrap_or(false) {
|
||||
return;
|
||||
}
|
||||
_ => {
|
||||
let Some(elem) = node.as_element() else { continue };
|
||||
let style = styles.get(&cid);
|
||||
if style.map(|s| s.display == Display::None).unwrap_or(false) {
|
||||
continue;
|
||||
}
|
||||
if elem.local.as_ref() == "br" {
|
||||
out.push(("\n".to_string(), SpanAttrs { bold: ctx.bold, italic: ctx.italic, underline: ctx.underline, color: ctx.color, family: ctx.family }));
|
||||
c.last_was_space = true;
|
||||
continue;
|
||||
}
|
||||
let child = SpanCtx {
|
||||
color: style.and_then(|s| s.color).unwrap_or(ctx.color),
|
||||
bold: ctx.bold || style.map(|s| s.font_weight.as_deref() == Some("bold")).unwrap_or(false),
|
||||
italic: ctx.italic || style.and_then(|s| s.font_style_italic).unwrap_or(false),
|
||||
// Underline propagates in: an ancestor's underline covers
|
||||
// descendant text; an element only sets its own via CSS.
|
||||
underline: ctx.underline || style.and_then(|s| s.underline).unwrap_or(false),
|
||||
transform: style.and_then(|s| s.text_transform).unwrap_or(ctx.transform),
|
||||
family: style
|
||||
.and_then(|s| s.font_family.as_deref())
|
||||
.map(|f| resolve_font_family(Some(f)))
|
||||
.unwrap_or(ctx.family),
|
||||
};
|
||||
collect_spans(tree, cid, styles, child, out, c);
|
||||
if elem.local.as_ref() == "br" {
|
||||
out.push(("\n".to_string(), SpanAttrs { bold: ctx.bold, italic: ctx.italic, underline: ctx.underline, color: ctx.color, family: ctx.family }));
|
||||
c.last_was_space = true;
|
||||
return;
|
||||
}
|
||||
let child = SpanCtx {
|
||||
color: style.and_then(|s| s.color).unwrap_or(ctx.color),
|
||||
bold: ctx.bold || style.map(|s| s.font_weight.as_deref() == Some("bold")).unwrap_or(false),
|
||||
italic: ctx.italic || style.and_then(|s| s.font_style_italic).unwrap_or(false),
|
||||
// Underline propagates in: an ancestor's underline covers
|
||||
// descendant text; an element only sets its own via CSS.
|
||||
underline: ctx.underline || style.and_then(|s| s.underline).unwrap_or(false),
|
||||
transform: style.and_then(|s| s.text_transform).unwrap_or(ctx.transform),
|
||||
family: style
|
||||
.and_then(|s| s.font_family.as_deref())
|
||||
.map(|f| resolve_font_family(Some(f)))
|
||||
.unwrap_or(ctx.family),
|
||||
};
|
||||
collect_spans(tree, cid, styles, child, out, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -562,9 +631,11 @@ impl TextEngine {
|
||||
let TextEngine { font_system, swash, items } = self;
|
||||
let Some(item) = items.get_mut(idx) else { return };
|
||||
let (ox, oy) = (item.origin.0 + offset.0, item.origin.1 + offset.1);
|
||||
let clip = item
|
||||
.clip
|
||||
.map(|c| Rect { x: c.x + offset.0, y: c.y + offset.1, width: c.width, height: c.height });
|
||||
// The glyph origin shifts by the container's accumulated translate,
|
||||
// but the clip is already in screen space (owner-shifted at
|
||||
// `resolve_clip_rects`) and must not move with the container, or a
|
||||
// translated slide would drag its viewport's clip along with it.
|
||||
let clip = item.clip;
|
||||
let pw = pixmap.width() as i32;
|
||||
let ph = pixmap.height() as i32;
|
||||
let clip_bounds = clip.map(|c| (c.x, c.y, c.x + c.width, c.y + c.height));
|
||||
|
||||
@@ -51,6 +51,17 @@ pub mod inline {
|
||||
pub fn try_build(&mut self, _tree: &DomTree, _id: NodeId, _styles: &HashMap<NodeId, crate::LayoutStyle>) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
/// See `try_build`: inline runs likewise fall back to word-split
|
||||
/// geometry in layout-only builds.
|
||||
pub fn try_build_run(
|
||||
&mut self,
|
||||
_tree: &DomTree,
|
||||
_parent: NodeId,
|
||||
_run: &[NodeId],
|
||||
_styles: &HashMap<NodeId, crate::LayoutStyle>,
|
||||
) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ pub fn paint_dom(tree: &DomTree, viewport: (f32, f32), base_url: Option<&str>) -
|
||||
// (the overwhelmingly common case), every node's accumulated offset is
|
||||
// zero, so skip the per-node ancestor walk entirely and keep the paint
|
||||
// path free of any added cost.
|
||||
let any_transform = laid.styles.values().any(|s| s.transform_translate.is_some());
|
||||
|
||||
// Tree order so later elements paint over earlier ones (normal flow).
|
||||
for nid in tree.descendants(tree.document()) {
|
||||
if svg_subtree_skip.contains(&nid) {
|
||||
@@ -60,7 +60,7 @@ pub fn paint_dom(tree: &DomTree, viewport: (f32, f32), base_url: Option<&str>) -
|
||||
};
|
||||
|
||||
if node.is_text() {
|
||||
paint_text_node(tree, nid, &laid, &mut pixmap, any_transform);
|
||||
paint_text_node(tree, nid, &laid, &mut pixmap);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -84,25 +84,19 @@ pub fn paint_dom(tree: &DomTree, viewport: (f32, f32), base_url: Option<&str>) -
|
||||
|
||||
// A `transform: translate()` on this element or any ancestor offsets
|
||||
// this element's whole painted box (and, applied per node, its whole
|
||||
// subtree). Shift the border box and the inherited clip by the same
|
||||
// accumulated vector so everything painted for this node moves together.
|
||||
let (ox, oy) = if any_transform {
|
||||
accumulated_translate(tree, nid, &laid)
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
};
|
||||
// subtree). The box shifts into screen space; the inherited clip is
|
||||
// ALREADY in screen space (shifted by its own owner's translate at
|
||||
// `resolve_clip_rects`), so it must not move with this descendant:
|
||||
// that is what lets a clip cull a slide the carousel track has
|
||||
// translated out of its viewport.
|
||||
let (ox, oy) = laid.translates.get(&nid).copied().unwrap_or((0.0, 0.0));
|
||||
let rect = crate::Rect { x: rect.x + ox, y: rect.y + oy, width: rect.width, height: rect.height };
|
||||
|
||||
// Ancestor `overflow: hidden` clip, if any. Skip painting entirely
|
||||
// once the box has no visible overlap with it (this is what makes the
|
||||
// ubiquitous 1x1 clipped "visually hidden" accessibility pattern
|
||||
// actually invisible instead of painting text wherever it lands).
|
||||
let clip = laid
|
||||
.clip_rects
|
||||
.get(&nid)
|
||||
.copied()
|
||||
.flatten()
|
||||
.map(|c| crate::Rect { x: c.x + ox, y: c.y + oy, width: c.width, height: c.height });
|
||||
let clip = laid.clip_rects.get(&nid).copied().flatten();
|
||||
let visible_rect = match clip {
|
||||
Some(c) => match rect.intersect(&c) {
|
||||
Some(r) => r,
|
||||
@@ -344,68 +338,33 @@ pub fn paint_dom(tree: &DomTree, viewport: (f32, f32), base_url: Option<&str>) -
|
||||
if svg_subtree_skip.contains(&nid) {
|
||||
continue;
|
||||
}
|
||||
if let Some(&idx) = laid.ifc_items.get(&nid) {
|
||||
if laid.styles.get(&nid).map(|s| s.effectively_invisible).unwrap_or(false) {
|
||||
continue;
|
||||
}
|
||||
// Shift the shaped glyphs by the same accumulated translate as the
|
||||
// container's box so text under a transformed ancestor moves with
|
||||
// it. Computed before the mutable `paint_item` borrow.
|
||||
let off = if any_transform {
|
||||
accumulated_translate(tree, nid, &laid)
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
};
|
||||
let whole = laid.ifc_items.get(&nid).copied();
|
||||
let run_items = laid.run_ifc_items.get(&nid).cloned();
|
||||
if whole.is_none() && run_items.is_none() {
|
||||
continue;
|
||||
}
|
||||
if laid.styles.get(&nid).map(|s| s.effectively_invisible).unwrap_or(false) {
|
||||
continue;
|
||||
}
|
||||
// Shift the shaped glyphs by the same accumulated translate as the
|
||||
// container's box so text under a transformed ancestor moves with
|
||||
// it. Computed before the mutable `paint_item` borrow.
|
||||
let off = laid.translates.get(&nid).copied().unwrap_or((0.0, 0.0));
|
||||
if let Some(idx) = whole {
|
||||
laid.text_engine.paint_item(idx, &mut pixmap, off);
|
||||
}
|
||||
// Anonymous inline-run leaves of a mixed block (see
|
||||
// `build_mixed_block`), pinned to their own boxes at finalize.
|
||||
if let Some(items) = run_items {
|
||||
for idx in items {
|
||||
laid.text_engine.paint_item(idx, &mut pixmap, off);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(pixmap)
|
||||
}
|
||||
|
||||
/// Sum of `transform: translate()` offsets of `nid` and every ancestor, in CSS
|
||||
/// px. A translate shifts an element's own box and its entire subtree by the
|
||||
/// same vector, so a node's painted position is its normal-flow position plus
|
||||
/// every translate at or above it. Percentage components resolve against each
|
||||
/// contributing element's own border box (read from the laid-out rects), per
|
||||
/// CSS. Nodes with no style/rect entry (e.g. text nodes) contribute nothing but
|
||||
/// are still walked through so element ancestors above them count.
|
||||
fn accumulated_translate(
|
||||
tree: &DomTree,
|
||||
nid: obscura_dom::tree::NodeId,
|
||||
laid: &crate::DomLayout,
|
||||
) -> (f32, f32) {
|
||||
let (mut ox, mut oy) = (0.0f32, 0.0f32);
|
||||
let mut cur = Some(nid);
|
||||
while let Some(id) = cur {
|
||||
if let (Some(style), Some(rect)) = (laid.styles.get(&id), laid.rects.get(&id)) {
|
||||
if let Some((dx, dy)) = style.transform_translate {
|
||||
ox += resolve_translate(dx, rect.width);
|
||||
oy += resolve_translate(dy, rect.height);
|
||||
}
|
||||
}
|
||||
cur = tree.get_node(id).and_then(|n| n.parent);
|
||||
}
|
||||
(ox, oy)
|
||||
}
|
||||
|
||||
/// Resolve one translate component to px: a length passes through, a percentage
|
||||
/// is taken against `basis` (the element's own border-box extent on that axis).
|
||||
/// Font/viewport-relative leftovers fall back to a coarse px value rather than
|
||||
/// panicking (translate rarely uses them; px/% are the required forms).
|
||||
fn resolve_translate(d: crate::Dimension, basis: f32) -> f32 {
|
||||
match d {
|
||||
crate::Dimension::Px(px) => px,
|
||||
crate::Dimension::Percent(p) => p * basis,
|
||||
crate::Dimension::Em(v) | crate::Dimension::Rem(v) => v * 16.0,
|
||||
crate::Dimension::Vw(v)
|
||||
| crate::Dimension::Vh(v)
|
||||
| crate::Dimension::Vmin(v)
|
||||
| crate::Dimension::Vmax(v) => v,
|
||||
crate::Dimension::Auto => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// A closed rounded-rectangle path, corners approximated by quadratic curves
|
||||
/// (visually indistinguishable from true arcs at typical UI radii). `r` is
|
||||
/// clamped so it never exceeds half the shorter side.
|
||||
@@ -587,7 +546,7 @@ fn paint_text_node(
|
||||
nid: obscura_dom::tree::NodeId,
|
||||
laid: &crate::DomLayout,
|
||||
pixmap: &mut Pixmap,
|
||||
any_transform: bool,
|
||||
|
||||
) -> Option<()> {
|
||||
let runs = laid.text_runs.get(&nid)?;
|
||||
let node = tree.get_node(nid)?;
|
||||
@@ -600,18 +559,10 @@ fn paint_text_node(
|
||||
let fsize = style.font_size.unwrap_or(16.0);
|
||||
let is_bold = style.font_weight.as_deref() == Some("bold");
|
||||
// A text node has no transform of its own, but any transformed element
|
||||
// ancestor offsets it; the ancestor walk from the text node picks those up.
|
||||
let (ox, oy) = if any_transform {
|
||||
accumulated_translate(tree, nid, laid)
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
};
|
||||
let clip = laid
|
||||
.clip_rects
|
||||
.get(&nid)
|
||||
.copied()
|
||||
.flatten()
|
||||
.map(|c| crate::Rect { x: c.x + ox, y: c.y + oy, width: c.width, height: c.height });
|
||||
// ancestor offsets it (the accumulation covers text nodes too). The clip
|
||||
// is already in screen space and stays put.
|
||||
let (ox, oy) = laid.translates.get(&nid).copied().unwrap_or((0.0, 0.0));
|
||||
let clip = laid.clip_rects.get(&nid).copied().flatten();
|
||||
|
||||
for (rect, word) in runs {
|
||||
draw_text(pixmap, word, rect.x + ox, rect.y + oy, color, fsize, is_bold, clip);
|
||||
|
||||
@@ -537,7 +537,15 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
||||
/// translate and scale parts.
|
||||
fn parse_transform(style: &mut LayoutStyle, value: &str) {
|
||||
let v = value.trim();
|
||||
if v.is_empty() || v.eq_ignore_ascii_case("none") {
|
||||
if v.is_empty() {
|
||||
return;
|
||||
}
|
||||
// `transform: none` resets any transform from a lower-priority rule (the
|
||||
// carousel idiom: a class translates the track, an inline style clears it
|
||||
// for the untransformed state).
|
||||
if v.eq_ignore_ascii_case("none") {
|
||||
style.transform_translate = None;
|
||||
style.transform_scale = None;
|
||||
return;
|
||||
}
|
||||
let zero = crate::Dimension::Px(0.0);
|
||||
@@ -546,7 +554,10 @@ fn parse_transform(style: &mut LayoutStyle, value: &str) {
|
||||
for (func, args) in transform_functions(v) {
|
||||
let parts: Vec<&str> = args.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()).collect();
|
||||
match func.to_ascii_lowercase().as_str() {
|
||||
"translate" => {
|
||||
// translate3d's z component is ignored (no perspective model);
|
||||
// Swiper and similar carousels position their track exclusively
|
||||
// through translate3d, so dropping it left every slide stacked.
|
||||
"translate" | "translate3d" => {
|
||||
if let Some(a) = parts.first() {
|
||||
tx = Some(dimension_value(a));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user