Expose canonical inline fragments
This commit is contained in:
1 parent
85884ca01a
commit
1a521220d0
2 files changed
+663
-45
No files matched your search
@@ -4284,6 +4284,71 @@ fn layout_dom_once(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pure-text IFC descendants do not own Taffy nodes. Once their shared
|
||||||
|
// buffer has its final line breaks, derive their real continuations from
|
||||||
|
// shaping provenance and install those fragments as canonical DOM geometry.
|
||||||
|
// The common paragraph with no nested inline owner skips both this work and
|
||||||
|
// the second clip-tree walk.
|
||||||
|
#[cfg(feature = "paint")]
|
||||||
|
if engine.has_inline_owners() {
|
||||||
|
let canonical = synthesize_shaped_inline_fragments(tree, &mut rects, &styles, &engine);
|
||||||
|
if !canonical.is_empty() {
|
||||||
|
let relative_offsets = canonical
|
||||||
|
.keys()
|
||||||
|
.filter_map(|&owner| {
|
||||||
|
folded_inline_relative_offset(tree, owner, &rects, &styles)
|
||||||
|
.filter(|offset| *offset != (0.0, 0.0))
|
||||||
|
.map(|offset| (owner, offset))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
engine.set_inline_owner_offsets(&relative_offsets);
|
||||||
|
inline_fragments.extend(canonical);
|
||||||
|
clip_rects.clear();
|
||||||
|
translates.clear();
|
||||||
|
transforms.clear();
|
||||||
|
if let Some(root_id) = root {
|
||||||
|
let root_font_size = styles
|
||||||
|
.get(&root_id)
|
||||||
|
.and_then(|style| style.font_size)
|
||||||
|
.unwrap_or(16.0);
|
||||||
|
resolve_clip_rects(
|
||||||
|
tree,
|
||||||
|
root_id,
|
||||||
|
None,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
&rects,
|
||||||
|
&styles,
|
||||||
|
&mut clip_rects,
|
||||||
|
&mut translates,
|
||||||
|
crate::Affine2::IDENTITY,
|
||||||
|
&mut transforms,
|
||||||
|
root_font_size,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (nid, &idx) in &ifc_items.whole {
|
||||||
|
engine.set_clip(
|
||||||
|
idx,
|
||||||
|
shaped_item_clip(*nid, &rects, &styles, &clip_rects, &translates, viewport),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (parent, items) in &ifc_items.runs {
|
||||||
|
let clip = shaped_item_clip(
|
||||||
|
*parent,
|
||||||
|
&rects,
|
||||||
|
&styles,
|
||||||
|
&clip_rects,
|
||||||
|
&translates,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
for &idx in items {
|
||||||
|
engine.set_clip(idx, clip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let generated_boxes = ifc_items
|
let generated_boxes = ifc_items
|
||||||
.generated
|
.generated
|
||||||
.iter()
|
.iter()
|
||||||
@@ -4371,6 +4436,180 @@ fn synthesize_ordinary_inline_fragments(
|
|||||||
fragments
|
fragments
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "paint")]
|
||||||
|
fn shaped_item_clip(
|
||||||
|
owner: NodeId,
|
||||||
|
rects: &HashMap<NodeId, Rect>,
|
||||||
|
styles: &HashMap<NodeId, crate::LayoutStyle>,
|
||||||
|
clip_rects: &HashMap<NodeId, Option<OverflowClip>>,
|
||||||
|
translates: &HashMap<NodeId, (f32, f32)>,
|
||||||
|
viewport: (f32, f32),
|
||||||
|
) -> Option<Rect> {
|
||||||
|
let inherited = clip_rects.get(&owner).copied().flatten();
|
||||||
|
let clip = match (styles.get(&owner), rects.get(&owner)) {
|
||||||
|
(Some(style), Some(rect)) if style.overflow_hidden => {
|
||||||
|
let (tx, ty) = translates.get(&owner).copied().unwrap_or((0.0, 0.0));
|
||||||
|
let own = OverflowClip::for_box(rect, style, tx, ty);
|
||||||
|
Some(match inherited {
|
||||||
|
Some(clip) => clip.intersect(own),
|
||||||
|
None => own,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => inherited,
|
||||||
|
};
|
||||||
|
clip.map(|clip| clip.viewport_rect(viewport))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "paint")]
|
||||||
|
fn synthesize_shaped_inline_fragments(
|
||||||
|
tree: &DomTree,
|
||||||
|
rects: &mut HashMap<NodeId, Rect>,
|
||||||
|
styles: &HashMap<NodeId, crate::LayoutStyle>,
|
||||||
|
engine: &crate::inline::TextEngine,
|
||||||
|
) -> HashMap<NodeId, Vec<Rect>> {
|
||||||
|
let mut fragments: HashMap<NodeId, Vec<((usize, usize), Rect)>> = HashMap::new();
|
||||||
|
for shaped in engine.inline_owner_line_fragments() {
|
||||||
|
let Some(style) = styles.get(&shaped.owner) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let (ascent, descent) = engine.inline_font_box_metrics(style);
|
||||||
|
let Some((relative_x, relative_y)) =
|
||||||
|
folded_inline_relative_offset(tree, shaped.owner, rects, styles)
|
||||||
|
else {
|
||||||
|
// Keep the pre-existing Taffy surrogate geometry when an inset
|
||||||
|
// cannot be resolved faithfully. Installing a knowingly wrong
|
||||||
|
// shaped fragment would make the DOM geometry canonical.
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let rect = Rect {
|
||||||
|
x: shaped.x + relative_x,
|
||||||
|
y: shaped.baseline_y - ascent - style.padding.top - style.border.top + relative_y,
|
||||||
|
width: shaped.width,
|
||||||
|
height: ascent
|
||||||
|
+ descent
|
||||||
|
+ style.padding.top
|
||||||
|
+ style.padding.bottom
|
||||||
|
+ style.border.top
|
||||||
|
+ style.border.bottom,
|
||||||
|
};
|
||||||
|
fragments
|
||||||
|
.entry(shaped.owner)
|
||||||
|
.or_default()
|
||||||
|
.push(((shaped.item_index, shaped.line_index), rect));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut canonical = HashMap::with_capacity(fragments.len());
|
||||||
|
for (owner, mut pieces) in fragments {
|
||||||
|
pieces.sort_by(|(order_a, rect_a), (order_b, rect_b)| {
|
||||||
|
order_a
|
||||||
|
.cmp(order_b)
|
||||||
|
.then_with(|| rect_a.x.total_cmp(&rect_b.x))
|
||||||
|
});
|
||||||
|
let fragments = pieces.into_iter().map(|(_, rect)| rect).collect::<Vec<_>>();
|
||||||
|
let mut union = fragments[0];
|
||||||
|
for fragment in &fragments[1..] {
|
||||||
|
let left = union.x.min(fragment.x);
|
||||||
|
let top = union.y.min(fragment.y);
|
||||||
|
let right = (union.x + union.width).max(fragment.x + fragment.width);
|
||||||
|
let bottom = (union.y + union.height).max(fragment.y + fragment.height);
|
||||||
|
union = Rect {
|
||||||
|
x: left,
|
||||||
|
y: top,
|
||||||
|
width: (right - left).max(0.0),
|
||||||
|
height: (bottom - top).max(0.0),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
rects.insert(owner, union);
|
||||||
|
canonical.insert(owner, fragments);
|
||||||
|
}
|
||||||
|
canonical
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "paint")]
|
||||||
|
fn folded_inline_relative_offset(
|
||||||
|
tree: &DomTree,
|
||||||
|
owner: NodeId,
|
||||||
|
rects: &HashMap<NodeId, Rect>,
|
||||||
|
styles: &HashMap<NodeId, crate::LayoutStyle>,
|
||||||
|
) -> Option<(f32, f32)> {
|
||||||
|
fn containing_block_size(
|
||||||
|
tree: &DomTree,
|
||||||
|
owner: NodeId,
|
||||||
|
rects: &HashMap<NodeId, Rect>,
|
||||||
|
styles: &HashMap<NodeId, crate::LayoutStyle>,
|
||||||
|
) -> Option<(f32, f32)> {
|
||||||
|
let mut ancestor = tree.get_node(owner).and_then(|node| node.parent);
|
||||||
|
while let Some(id) = ancestor {
|
||||||
|
let style = styles.get(&id)?;
|
||||||
|
if !style.ignores_used_box_sizes() && !style.display_contents {
|
||||||
|
let rect = rects.get(&id)?;
|
||||||
|
return Some((
|
||||||
|
(rect.width
|
||||||
|
- style.border.left
|
||||||
|
- style.border.right
|
||||||
|
- style.padding.left
|
||||||
|
- style.padding.right)
|
||||||
|
.max(0.0),
|
||||||
|
(rect.height
|
||||||
|
- style.border.top
|
||||||
|
- style.border.bottom
|
||||||
|
- style.padding.top
|
||||||
|
- style.padding.bottom)
|
||||||
|
.max(0.0),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
ancestor = tree.get_node(id).and_then(|node| node.parent);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut offset = (0.0, 0.0);
|
||||||
|
let mut current = Some(owner);
|
||||||
|
while let Some(id) = current {
|
||||||
|
let Some(style) = styles.get(&id) else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
if !style.ignores_used_box_sizes() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if style.position == Some(taffy::Position::Relative) && !style.position_sticky {
|
||||||
|
let basis = containing_block_size(tree, id, rects, styles);
|
||||||
|
let resolve = |value: Option<crate::Dimension>, horizontal: bool| match value {
|
||||||
|
None | Some(crate::Dimension::Auto) => Ok(None),
|
||||||
|
Some(crate::Dimension::Px(value)) if value.is_finite() => Ok(Some(value)),
|
||||||
|
Some(crate::Dimension::Percent(value)) if value.is_finite() => basis
|
||||||
|
.map(|size| Some(value * if horizontal { size.0 } else { size.1 }))
|
||||||
|
.ok_or(()),
|
||||||
|
_ => Err(()),
|
||||||
|
};
|
||||||
|
// Failed calc()/var() resolution is represented by an expression
|
||||||
|
// with no corresponding used Dimension. Do not reinterpret it as
|
||||||
|
// `auto` merely because this sidecar runs after style resolution.
|
||||||
|
for index in 0..4 {
|
||||||
|
if style.inset_expressions[index].is_some() && style.inset[index].is_none() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let left = resolve(style.inset[3], true).ok()?;
|
||||||
|
let right = resolve(style.inset[1], true).ok()?;
|
||||||
|
let top = resolve(style.inset[0], false).ok()?;
|
||||||
|
let bottom = resolve(style.inset[2], false).ok()?;
|
||||||
|
if let Some(left) = left {
|
||||||
|
offset.0 += left;
|
||||||
|
} else if let Some(right) = right {
|
||||||
|
offset.0 -= right;
|
||||||
|
}
|
||||||
|
if let Some(top) = top {
|
||||||
|
offset.1 += top;
|
||||||
|
} else if let Some(bottom) = bottom {
|
||||||
|
offset.1 -= bottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current = tree.get_node(id).and_then(|node| node.parent);
|
||||||
|
}
|
||||||
|
Some(offset)
|
||||||
|
}
|
||||||
|
|
||||||
fn container_snapshot(
|
fn container_snapshot(
|
||||||
tree: &DomTree,
|
tree: &DomTree,
|
||||||
layout: &DomLayout,
|
layout: &DomLayout,
|
||||||
@@ -11487,7 +11726,7 @@ mod tests {
|
|||||||
</section>"#,
|
</section>"#,
|
||||||
);
|
);
|
||||||
let laid = layout_dom(&tree, (1280.0, 720.0));
|
let laid = layout_dom(&tree, (1280.0, 720.0));
|
||||||
let rect = |id| laid.rects[&tree.get_element_by_id(id).unwrap()];
|
let rect = |id: &str| -> Rect { laid.rects[&tree.get_element_by_id(id).unwrap()] };
|
||||||
let row = rect("row");
|
let row = rect("row");
|
||||||
let column = rect("column");
|
let column = rect("column");
|
||||||
let quote = rect("quote");
|
let quote = rect("quote");
|
||||||
@@ -11526,7 +11765,7 @@ mod tests {
|
|||||||
</section>"#
|
</section>"#
|
||||||
));
|
));
|
||||||
let laid = layout_dom(&tree, (1280.0, 720.0));
|
let laid = layout_dom(&tree, (1280.0, 720.0));
|
||||||
let rect = |id| laid.rects[&tree.get_element_by_id(id).unwrap()];
|
let rect = |id: &str| -> Rect { laid.rects[&tree.get_element_by_id(id).unwrap()] };
|
||||||
let column = rect("column");
|
let column = rect("column");
|
||||||
let quote = rect("quote");
|
let quote = rect("quote");
|
||||||
let logo = rect("logo");
|
let logo = rect("logo");
|
||||||
@@ -12016,9 +12255,12 @@ mod tests {
|
|||||||
assert_eq!(laid.rects[&control_host].height, 40.0);
|
assert_eq!(laid.rects[&control_host].height, 40.0);
|
||||||
assert_eq!(laid.rects[&token].height, expected_fragment_height);
|
assert_eq!(laid.rects[&token].height, expected_fragment_height);
|
||||||
assert_eq!(laid.inline_fragments[&token], vec![laid.rects[&token]]);
|
assert_eq!(laid.inline_fragments[&token], vec![laid.rects[&token]]);
|
||||||
assert_eq!(
|
assert!(
|
||||||
laid.rects[&token].y - laid.rects[&host].y,
|
((laid.rects[&token].y - laid.rects[&host].y)
|
||||||
(40.0 - raw_font_height) / 2.0 - 5.0 - 2.0
|
- ((40.0 - raw_font_height) / 2.0 - 5.0 - 2.0))
|
||||||
|
.abs()
|
||||||
|
< 0.5,
|
||||||
|
"final shaped baseline must retain the expected centered font box"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
laid.rects[&control].y - laid.rects[&control_host].y,
|
laid.rects[&control].y - laid.rects[&control_host].y,
|
||||||
@@ -12028,7 +12270,111 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn fallback_word_boxes_shape_and_paint_with_the_loaded_webfont() {
|
fn nested_inline_uses_final_shaping_for_its_canonical_fragment() {
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"<style>
|
||||||
|
html,body,p { margin:0 }
|
||||||
|
p { width:300px; font:16px/20px monospace }
|
||||||
|
</style>
|
||||||
|
<p id="host">before <span id="token">plain nested text</span> after</p>"#,
|
||||||
|
);
|
||||||
|
let laid = layout_dom(&tree, (400.0, 100.0));
|
||||||
|
let token = tree.get_element_by_id("token").unwrap();
|
||||||
|
let fragments = &laid.inline_fragments[&token];
|
||||||
|
|
||||||
|
assert_eq!(fragments.len(), 1, "{fragments:?}");
|
||||||
|
assert_eq!(laid.rects[&token], fragments[0]);
|
||||||
|
assert!(fragments[0].width > 100.0, "{fragments:?}");
|
||||||
|
assert_eq!(laid.ifc_items.len(), 1, "the span must stay in one shaped IFC");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wrapped_decorated_inline_exposes_three_ordered_font_box_fragments() {
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"<style>
|
||||||
|
html,body,p { margin:0 }
|
||||||
|
p { width:80px; font:16px/20px monospace }
|
||||||
|
code {
|
||||||
|
padding-top:2px; padding-bottom:3px;
|
||||||
|
border-top:1px solid; border-bottom:2px solid;
|
||||||
|
background:red
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<p><code id="token">alpha beta gamma</code></p>"#,
|
||||||
|
);
|
||||||
|
let laid = layout_dom(&tree, (300.0, 120.0));
|
||||||
|
let token = tree.get_element_by_id("token").unwrap();
|
||||||
|
let fragments = &laid.inline_fragments[&token];
|
||||||
|
|
||||||
|
assert_eq!(fragments.len(), 3, "one canonical fragment per visual line: {fragments:?}");
|
||||||
|
assert!(fragments.windows(2).all(|pair| pair[0].y < pair[1].y));
|
||||||
|
assert!(fragments.iter().all(|fragment| fragment.width > 0.0));
|
||||||
|
let left = fragments.iter().map(|rect| rect.x).fold(f32::INFINITY, f32::min);
|
||||||
|
let top = fragments.iter().map(|rect| rect.y).fold(f32::INFINITY, f32::min);
|
||||||
|
let right = fragments
|
||||||
|
.iter()
|
||||||
|
.map(|rect| rect.x + rect.width)
|
||||||
|
.fold(f32::NEG_INFINITY, f32::max);
|
||||||
|
let bottom = fragments
|
||||||
|
.iter()
|
||||||
|
.map(|rect| rect.y + rect.height)
|
||||||
|
.fold(f32::NEG_INFINITY, f32::max);
|
||||||
|
assert_eq!(
|
||||||
|
laid.rects[&token],
|
||||||
|
Rect { x: left, y: top, width: right - left, height: bottom - top }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn inline_provenance_maps_repeated_multibyte_text_across_hard_breaks() {
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"<style>
|
||||||
|
html,body,p { margin:0 }
|
||||||
|
p { width:200px; font:18px/24px sans-serif }
|
||||||
|
</style>
|
||||||
|
<p><span id="token">é界<br>é界</span></p>"#,
|
||||||
|
);
|
||||||
|
let laid = layout_dom(&tree, (300.0, 100.0));
|
||||||
|
let token = tree.get_element_by_id("token").unwrap();
|
||||||
|
let fragments = &laid.inline_fragments[&token];
|
||||||
|
|
||||||
|
assert_eq!(fragments.len(), 2, "{fragments:?}");
|
||||||
|
assert!((fragments[0].width - fragments[1].width).abs() < 0.01, "{fragments:?}");
|
||||||
|
assert!((fragments[1].y - fragments[0].y - 24.0).abs() < 0.01, "{fragments:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relative_inline_offsets_move_canonical_fragments_in_pixels_and_percentages() {
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"<style>
|
||||||
|
html,body,p { margin:0 }
|
||||||
|
p { width:200px; height:20px; font:16px/20px monospace }
|
||||||
|
#pixels { position:relative; left:7px; top:5px }
|
||||||
|
#percent { position:relative; left:10%; top:10% }
|
||||||
|
</style>
|
||||||
|
<p id="base-host"><span id="base">token</span></p>
|
||||||
|
<p id="pixel-host"><span id="pixels">token</span></p>
|
||||||
|
<p id="percent-host"><span id="percent">token</span></p>"#,
|
||||||
|
);
|
||||||
|
let laid = layout_dom(&tree, (400.0, 120.0));
|
||||||
|
let rect = |id: &str| -> Rect { laid.rects[&tree.get_element_by_id(id).unwrap()] };
|
||||||
|
let local = |token: &str, host: &str| {
|
||||||
|
let token = rect(token);
|
||||||
|
let host = rect(host);
|
||||||
|
(token.x - host.x, token.y - host.y)
|
||||||
|
};
|
||||||
|
let base = local("base", "base-host");
|
||||||
|
let pixels = local("pixels", "pixel-host");
|
||||||
|
let percent = local("percent", "percent-host");
|
||||||
|
|
||||||
|
assert!((pixels.0 - base.0 - 7.0).abs() < 0.01, "{base:?} {pixels:?}");
|
||||||
|
assert!((pixels.1 - base.1 - 5.0).abs() < 0.01, "{base:?} {pixels:?}");
|
||||||
|
assert!((percent.0 - base.0 - 20.0).abs() < 0.01, "{base:?} {percent:?}");
|
||||||
|
assert!((percent.1 - base.1 - 2.0).abs() < 0.01, "{base:?} {percent:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn canonical_inline_fragments_shape_with_the_loaded_webfont() {
|
||||||
let tree = parse_html(
|
let tree = parse_html(
|
||||||
r#"<style>
|
r#"<style>
|
||||||
html,body,p { margin:0 }
|
html,body,p { margin:0 }
|
||||||
@@ -12043,7 +12389,6 @@ mod tests {
|
|||||||
<p><span id="token">WWWWiiii</span></p>"#,
|
<p><span id="token">WWWWiiii</span></p>"#,
|
||||||
);
|
);
|
||||||
let token = tree.get_element_by_id("token").unwrap();
|
let token = tree.get_element_by_id("token").unwrap();
|
||||||
let text = rendered_children(&tree, token)[0];
|
|
||||||
let fallback = layout_dom(&tree, (500.0, 150.0));
|
let fallback = layout_dom(&tree, (500.0, 150.0));
|
||||||
let loaded = layout_dom_with_web_fonts(
|
let loaded = layout_dom_with_web_fonts(
|
||||||
&tree,
|
&tree,
|
||||||
@@ -12057,10 +12402,7 @@ mod tests {
|
|||||||
}],
|
}],
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(
|
assert!(loaded.inline_fragments.contains_key(&token));
|
||||||
loaded.word_ifc_items.get(&text).is_some_and(|items| !items.is_empty()),
|
|
||||||
"a decorated/relative inline fallback must retain shaped word items"
|
|
||||||
);
|
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
fallback.rects[&token].width, loaded.rects[&token].width,
|
fallback.rects[&token].width, loaded.rects[&token].width,
|
||||||
"the loaded face's advances must drive fallback inline geometry"
|
"the loaded face's advances must drive fallback inline geometry"
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
use std::{collections::HashMap, sync::Arc};
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
use cosmic_text::{
|
use cosmic_text::{
|
||||||
Align, Attrs, Buffer, CacheKey, CacheKeyFlags, Color, CssLineBreak, CssOverflowWrap,
|
Align, Attrs, Buffer, CacheKey, CacheKeyFlags, Color, CssLineBreak, CssOverflowWrap, Cursor,
|
||||||
CssWordBreak, Family, FeatureTag, FontFeatures, FontSystem, FontVariations, Metrics, Shaping,
|
CssWordBreak, Family, FeatureTag, FontFeatures, FontSystem, FontVariations, Metrics, Shaping,
|
||||||
Style, SwashCache, SwashImage, VariationTag, Weight, Wrap,
|
Style, SwashCache, SwashImage, VariationTag, Weight, Wrap,
|
||||||
};
|
};
|
||||||
@@ -307,9 +307,9 @@ fn normal_line_height(font_size: f32, metrics: FaceMetrics) -> f32 {
|
|||||||
/// participation uses [`used_line_height_with_metrics`], while its painted
|
/// participation uses [`used_line_height_with_metrics`], while its painted
|
||||||
/// fragment/client rect uses this raw font box plus block-axis padding and
|
/// fragment/client rect uses this raw font box plus block-axis padding and
|
||||||
/// border. Chromium and Gecko both fit ascent and descent independently.
|
/// border. Chromium and Gecko both fit ascent and descent independently.
|
||||||
fn font_box_height(font_size: f32, metrics: FaceMetrics) -> f32 {
|
fn fitted_font_box_metrics(font_size: f32, metrics: FaceMetrics) -> (f32, f32) {
|
||||||
let scale = font_size / metrics.units_per_em.max(1.0);
|
let scale = font_size / metrics.units_per_em.max(1.0);
|
||||||
(metrics.ascent * scale).round() + (metrics.descent * scale).round()
|
((metrics.ascent * scale).round(), (metrics.descent * scale).round())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn font_metrics(
|
fn font_metrics(
|
||||||
@@ -428,6 +428,42 @@ pub struct InlineItem {
|
|||||||
/// this distinct avoids mutating DOM text or the natural content buffer.
|
/// this distinct avoids mutating DOM text or the natural content buffer.
|
||||||
marker_buffer: Option<Buffer>,
|
marker_buffer: Option<Buffer>,
|
||||||
marker: Option<MarkerPlacement>,
|
marker: Option<MarkerPlacement>,
|
||||||
|
/// Canonical collapsed/transformed text used to map shaped byte ranges back
|
||||||
|
/// to ordinary inline DOM owners. Absent for the overwhelmingly common IFC
|
||||||
|
/// with no nested inline owner, keeping that path allocation-free.
|
||||||
|
owner_text: Option<String>,
|
||||||
|
owner_ranges: Vec<OwnerTextRange>,
|
||||||
|
/// Nonzero used relative-position offsets, projected onto text ranges.
|
||||||
|
/// Empty for ordinary IFCs and for nested inlines that remain at their
|
||||||
|
/// normal-flow position, so paint pays no provenance cost on that path.
|
||||||
|
relative_owner_ranges: Vec<RelativeOwnerTextRange>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
struct OwnerTextRange {
|
||||||
|
owner: NodeId,
|
||||||
|
start: usize,
|
||||||
|
end: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
struct RelativeOwnerTextRange {
|
||||||
|
start: usize,
|
||||||
|
end: usize,
|
||||||
|
offset: (f32, f32),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One ordinary inline owner's horizontal continuation on a finalized visual
|
||||||
|
/// line. DOM layout supplies the owner's font box and decorations around this
|
||||||
|
/// baseline-relative shaped extent.
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub(crate) struct InlineOwnerLineFragment {
|
||||||
|
pub owner: NodeId,
|
||||||
|
pub item_index: usize,
|
||||||
|
pub line_index: usize,
|
||||||
|
pub x: f32,
|
||||||
|
pub baseline_y: f32,
|
||||||
|
pub width: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
@@ -890,13 +926,18 @@ impl TextEngine {
|
|||||||
/// deliberately not CSS `line-height`: leading belongs to the containing
|
/// deliberately not CSS `line-height`: leading belongs to the containing
|
||||||
/// line and must not enlarge backgrounds, borders, or DOM client rects.
|
/// line and must not enlarge backgrounds, borders, or DOM client rects.
|
||||||
pub(crate) fn inline_font_box_height(&self, style: &LayoutStyle) -> f32 {
|
pub(crate) fn inline_font_box_height(&self, style: &LayoutStyle) -> f32 {
|
||||||
|
let (ascent, descent) = self.inline_font_box_metrics(style);
|
||||||
|
ascent + descent
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn inline_font_box_metrics(&self, style: &LayoutStyle) -> (f32, f32) {
|
||||||
let font = resolve_loaded_font(
|
let font = resolve_loaded_font(
|
||||||
style.font_family.as_deref(),
|
style.font_family.as_deref(),
|
||||||
crate::style::used_font_weight(style),
|
crate::style::used_font_weight(style),
|
||||||
style.font_style_italic.unwrap_or(false),
|
style.font_style_italic.unwrap_or(false),
|
||||||
&self.loaded_families,
|
&self.loaded_families,
|
||||||
);
|
);
|
||||||
font_box_height(style.font_size.unwrap_or(16.0), font.metrics)
|
fitted_font_box_metrics(style.font_size.unwrap_or(16.0), font.metrics)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Used line-height for the same selected face. Kept beside
|
/// Used line-height for the same selected face. Kept beside
|
||||||
@@ -935,7 +976,7 @@ impl TextEngine {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let base = styles.get(&id)?;
|
let base = styles.get(&id)?;
|
||||||
let mut collector = Collector { last_was_space: true, clip_fills: Vec::new() };
|
let mut collector = Collector::new();
|
||||||
let font = resolve_loaded_font(
|
let font = resolve_loaded_font(
|
||||||
base.font_family.as_deref(),
|
base.font_family.as_deref(),
|
||||||
crate::style::used_font_weight(base),
|
crate::style::used_font_weight(base),
|
||||||
@@ -954,7 +995,13 @@ impl TextEngine {
|
|||||||
&mut collector,
|
&mut collector,
|
||||||
&self.loaded_families,
|
&self.loaded_families,
|
||||||
);
|
);
|
||||||
self.push_shaped_item(base, line_height, spans, collector.clip_fills)
|
self.push_shaped_item(
|
||||||
|
base,
|
||||||
|
line_height,
|
||||||
|
spans,
|
||||||
|
collector.clip_fills,
|
||||||
|
collector.owner_ranges,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build an inline formatting context from a *run* of consecutive
|
/// Build an inline formatting context from a *run* of consecutive
|
||||||
@@ -981,7 +1028,7 @@ impl TextEngine {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let base = styles.get(&parent)?;
|
let base = styles.get(&parent)?;
|
||||||
let mut collector = Collector { last_was_space: true, clip_fills: Vec::new() };
|
let mut collector = Collector::new();
|
||||||
let font = resolve_loaded_font(
|
let font = resolve_loaded_font(
|
||||||
base.font_family.as_deref(),
|
base.font_family.as_deref(),
|
||||||
crate::style::used_font_weight(base),
|
crate::style::used_font_weight(base),
|
||||||
@@ -1002,7 +1049,13 @@ impl TextEngine {
|
|||||||
&self.loaded_families,
|
&self.loaded_families,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
self.push_shaped_item(base, line_height, spans, collector.clip_fills)
|
self.push_shaped_item(
|
||||||
|
base,
|
||||||
|
line_height,
|
||||||
|
spans,
|
||||||
|
collector.clip_fills,
|
||||||
|
collector.owner_ranges,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shape generated text that owns a positioned pseudo box.
|
/// Shape generated text that owns a positioned pseudo box.
|
||||||
@@ -1017,10 +1070,7 @@ impl TextEngine {
|
|||||||
text: &str,
|
text: &str,
|
||||||
style: &LayoutStyle,
|
style: &LayoutStyle,
|
||||||
) -> Option<usize> {
|
) -> Option<usize> {
|
||||||
let mut collector = Collector {
|
let mut collector = Collector::new();
|
||||||
last_was_space: true,
|
|
||||||
clip_fills: Vec::new(),
|
|
||||||
};
|
|
||||||
let font = resolve_loaded_font(
|
let font = resolve_loaded_font(
|
||||||
style.font_family.as_deref(),
|
style.font_family.as_deref(),
|
||||||
crate::style::used_font_weight(style),
|
crate::style::used_font_weight(style),
|
||||||
@@ -1057,7 +1107,13 @@ impl TextEngine {
|
|||||||
&mut spans,
|
&mut spans,
|
||||||
&mut collector,
|
&mut collector,
|
||||||
);
|
);
|
||||||
self.push_shaped_item(style, line_height, spans, collector.clip_fills)
|
self.push_shaped_item(
|
||||||
|
style,
|
||||||
|
line_height,
|
||||||
|
spans,
|
||||||
|
collector.clip_fills,
|
||||||
|
collector.owner_ranges,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shared tail of [`try_build`] / [`try_build_run`]: shape the collected
|
/// Shared tail of [`try_build`] / [`try_build_run`]: shape the collected
|
||||||
@@ -1069,6 +1125,7 @@ impl TextEngine {
|
|||||||
line_h: f32,
|
line_h: f32,
|
||||||
mut spans: Vec<(String, SpanAttrs)>,
|
mut spans: Vec<(String, SpanAttrs)>,
|
||||||
clip_fills: Vec<ClipTextFill>,
|
clip_fills: Vec<ClipTextFill>,
|
||||||
|
mut owner_ranges: Vec<OwnerTextRange>,
|
||||||
) -> Option<usize> {
|
) -> Option<usize> {
|
||||||
let white_space = base.white_space.unwrap_or_default();
|
let white_space = base.white_space.unwrap_or_default();
|
||||||
let layout_wrap = if spans.iter().any(|(_, attrs)| attrs.has_layout_emergency_breaks()) {
|
let layout_wrap = if spans.iter().any(|(_, attrs)| attrs.has_layout_emergency_breaks()) {
|
||||||
@@ -1109,6 +1166,19 @@ impl TextEngine {
|
|||||||
}) {
|
}) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
let text_len = spans.iter().map(|(text, _)| text.len()).sum::<usize>();
|
||||||
|
owner_ranges.retain_mut(|range| {
|
||||||
|
range.start = range.start.min(text_len);
|
||||||
|
range.end = range.end.min(text_len);
|
||||||
|
range.start < range.end
|
||||||
|
});
|
||||||
|
let owner_text = (!owner_ranges.is_empty()).then(|| {
|
||||||
|
let mut text = String::with_capacity(text_len);
|
||||||
|
for (span, _) in &spans {
|
||||||
|
text.push_str(span);
|
||||||
|
}
|
||||||
|
text
|
||||||
|
});
|
||||||
|
|
||||||
let base_size = base.font_size.unwrap_or(16.0);
|
let base_size = base.font_size.unwrap_or(16.0);
|
||||||
// Explicit line-height stays fractional. `normal` is derived from the
|
// Explicit line-height stays fractional. `normal` is derived from the
|
||||||
@@ -1248,6 +1318,9 @@ impl TextEngine {
|
|||||||
ellipsis_overflow,
|
ellipsis_overflow,
|
||||||
marker_buffer,
|
marker_buffer,
|
||||||
marker: None,
|
marker: None,
|
||||||
|
owner_text,
|
||||||
|
owner_ranges,
|
||||||
|
relative_owner_ranges: Vec::new(),
|
||||||
});
|
});
|
||||||
Some(idx)
|
Some(idx)
|
||||||
}
|
}
|
||||||
@@ -1457,6 +1530,101 @@ impl TextEngine {
|
|||||||
item.origin = (content_origin.0 + alignment_inset, content_origin.1);
|
item.origin = (content_origin.0 + alignment_inset, content_origin.1);
|
||||||
item.clip = clip;
|
item.clip = clip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Replace only the finalized clip without reshaping. Used when canonical
|
||||||
|
/// inline fragment rects make a second clip/transform tree walk necessary.
|
||||||
|
pub(crate) fn set_clip(&mut self, idx: usize, clip: Option<Rect>) {
|
||||||
|
if let Some(item) = self.items.get_mut(idx) {
|
||||||
|
item.clip = clip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn has_inline_owners(&self) -> bool {
|
||||||
|
self.items.iter().any(|item| !item.owner_ranges.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Install cumulative used offsets for relative ordinary-inline owners.
|
||||||
|
/// DOM resolves percentages against the real containing block. Selecting
|
||||||
|
/// the deepest matching range at paint time is therefore sufficient even
|
||||||
|
/// for nested relative inlines: its offset already includes its ancestors.
|
||||||
|
pub(crate) fn set_inline_owner_offsets(
|
||||||
|
&mut self,
|
||||||
|
offsets: &HashMap<NodeId, (f32, f32)>,
|
||||||
|
) {
|
||||||
|
for item in &mut self.items {
|
||||||
|
item.relative_owner_ranges = item
|
||||||
|
.owner_ranges
|
||||||
|
.iter()
|
||||||
|
.filter_map(|range| {
|
||||||
|
let offset = offsets.get(&range.owner).copied()?;
|
||||||
|
(offset != (0.0, 0.0)).then_some(RelativeOwnerTextRange {
|
||||||
|
start: range.start,
|
||||||
|
end: range.end,
|
||||||
|
offset,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Derive ordinary inline continuation extents from finalized shaping.
|
||||||
|
/// Provenance is a sidecar rather than glyph metadata, so changing DOM
|
||||||
|
/// owners never creates a font-shaping boundary or disables ligatures.
|
||||||
|
pub(crate) fn inline_owner_line_fragments(&self) -> Vec<InlineOwnerLineFragment> {
|
||||||
|
let mut out: Vec<InlineOwnerLineFragment> = Vec::new();
|
||||||
|
for (item_index, item) in self.items.iter().enumerate() {
|
||||||
|
let Some(source) = item.owner_text.as_deref() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let line_starts = source_line_starts(&item.buffer, source);
|
||||||
|
|
||||||
|
for (line_index, run) in item.buffer.layout_runs().enumerate() {
|
||||||
|
let Some(&line_start) = line_starts.get(run.line_i) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let line_end = line_start.saturating_add(run.text.len());
|
||||||
|
let first_line_offset = if line_index == 0 {
|
||||||
|
item.first_line_offset
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
for range in &item.owner_ranges {
|
||||||
|
let start = range.start.max(line_start);
|
||||||
|
let end = range.end.min(line_end);
|
||||||
|
if start >= end {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let start = Cursor::new(run.line_i, start - line_start);
|
||||||
|
let end = Cursor::new(run.line_i, end - line_start);
|
||||||
|
let Some((x, width)) = run.highlight(start, end) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let x = item.origin.0 + first_line_offset + x;
|
||||||
|
let baseline_y = item.origin.1 + run.line_y;
|
||||||
|
if let Some(existing) = out.iter_mut().find(|fragment| {
|
||||||
|
fragment.owner == range.owner
|
||||||
|
&& fragment.item_index == item_index
|
||||||
|
&& fragment.line_index == line_index
|
||||||
|
}) {
|
||||||
|
let left = existing.x.min(x);
|
||||||
|
let right = (existing.x + existing.width).max(x + width);
|
||||||
|
existing.x = left;
|
||||||
|
existing.width = (right - left).max(0.0);
|
||||||
|
} else {
|
||||||
|
out.push(InlineOwnerLineFragment {
|
||||||
|
owner: range.owner,
|
||||||
|
item_index,
|
||||||
|
line_index,
|
||||||
|
x,
|
||||||
|
baseline_y,
|
||||||
|
width: width.max(0.0),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A run of same-styled inline text.
|
/// A run of same-styled inline text.
|
||||||
@@ -1602,6 +1770,33 @@ struct SpanCtx {
|
|||||||
struct Collector {
|
struct Collector {
|
||||||
last_was_space: bool,
|
last_was_space: bool,
|
||||||
clip_fills: Vec<ClipTextFill>,
|
clip_fills: Vec<ClipTextFill>,
|
||||||
|
owners: Vec<NodeId>,
|
||||||
|
owner_ranges: Vec<OwnerTextRange>,
|
||||||
|
text_len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Collector {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
last_was_space: true,
|
||||||
|
clip_fills: Vec::new(),
|
||||||
|
owners: Vec::new(),
|
||||||
|
owner_ranges: Vec::new(),
|
||||||
|
text_len: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_text(&mut self, byte_len: usize) {
|
||||||
|
let start = self.text_len;
|
||||||
|
self.text_len = self.text_len.saturating_add(byte_len);
|
||||||
|
for &owner in &self.owners {
|
||||||
|
self.owner_ranges.push(OwnerTextRange {
|
||||||
|
owner,
|
||||||
|
start,
|
||||||
|
end: self.text_len,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// DFS the inline subtree, appending whitespace-collapsed text runs. Adjacent
|
/// DFS the inline subtree, appending whitespace-collapsed text runs. Adjacent
|
||||||
@@ -1763,9 +1958,16 @@ fn collect_node_spans(
|
|||||||
word_break: ctx.word_break,
|
word_break: ctx.word_break,
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
|
c.text_len = c.text_len.saturating_add(1);
|
||||||
c.last_was_space = true;
|
c.last_was_space = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let owns_inline_fragment = style.is_some_and(|style| {
|
||||||
|
style.ignores_used_box_sizes() && !style.display_contents
|
||||||
|
});
|
||||||
|
if owns_inline_fragment {
|
||||||
|
c.owners.push(cid);
|
||||||
|
}
|
||||||
let own_clip_fill = style.and_then(clip_text_fill).map(|fill| {
|
let own_clip_fill = style.and_then(clip_text_fill).map(|fill| {
|
||||||
let index = c.clip_fills.len();
|
let index = c.clip_fills.len();
|
||||||
c.clip_fills.push(fill);
|
c.clip_fills.push(fill);
|
||||||
@@ -1848,6 +2050,10 @@ fn collect_node_spans(
|
|||||||
c,
|
c,
|
||||||
loaded_families,
|
loaded_families,
|
||||||
);
|
);
|
||||||
|
if owns_inline_fragment {
|
||||||
|
let popped = c.owners.pop();
|
||||||
|
debug_assert_eq!(popped, Some(cid));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1893,6 +2099,7 @@ fn push_text(
|
|||||||
if buf.is_empty() {
|
if buf.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
c.record_text(buf.len());
|
||||||
if let Some((last_text, last_attrs)) = out.last_mut() {
|
if let Some((last_text, last_attrs)) = out.last_mut() {
|
||||||
if last_attrs == attrs {
|
if last_attrs == attrs {
|
||||||
last_text.push_str(&buf);
|
last_text.push_str(&buf);
|
||||||
@@ -1935,6 +2142,52 @@ fn buffer_size(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Map each cosmic-text BufferLine back to its byte start in the canonical
|
||||||
|
/// collapsed source. Buffer lines normally correspond to authored hard
|
||||||
|
/// breaks; text-indent can also split the first line synthetically.
|
||||||
|
fn source_line_starts(buffer: &Buffer, source: &str) -> Vec<usize> {
|
||||||
|
let mut starts = Vec::with_capacity(buffer.lines.len());
|
||||||
|
let mut source_offset = 0usize;
|
||||||
|
for line in &buffer.lines {
|
||||||
|
while source.as_bytes().get(source_offset) == Some(&b'\n') {
|
||||||
|
source_offset += 1;
|
||||||
|
}
|
||||||
|
let text = line.text();
|
||||||
|
let start = if source
|
||||||
|
.get(source_offset..)
|
||||||
|
.is_some_and(|tail| tail.starts_with(text))
|
||||||
|
{
|
||||||
|
source_offset
|
||||||
|
} else {
|
||||||
|
source
|
||||||
|
.get(source_offset..)
|
||||||
|
.and_then(|tail| tail.find(text))
|
||||||
|
.map_or(source_offset, |relative| source_offset + relative)
|
||||||
|
};
|
||||||
|
starts.push(start);
|
||||||
|
source_offset = start.saturating_add(text.len()).min(source.len());
|
||||||
|
}
|
||||||
|
starts
|
||||||
|
}
|
||||||
|
|
||||||
|
fn glyph_relative_offset(
|
||||||
|
ranges: &[RelativeOwnerTextRange],
|
||||||
|
line_start: usize,
|
||||||
|
glyph_start: usize,
|
||||||
|
glyph_end: usize,
|
||||||
|
) -> (f32, f32) {
|
||||||
|
if ranges.is_empty() {
|
||||||
|
return (0.0, 0.0);
|
||||||
|
}
|
||||||
|
let start = line_start.saturating_add(glyph_start);
|
||||||
|
let end = line_start.saturating_add(glyph_end);
|
||||||
|
ranges
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.find(|range| range.start < end && range.end > start)
|
||||||
|
.map_or((0.0, 0.0), |range| range.offset)
|
||||||
|
}
|
||||||
|
|
||||||
fn used_text_indent(value: Dimension, width: Option<f32>) -> f32 {
|
fn used_text_indent(value: Dimension, width: Option<f32>) -> f32 {
|
||||||
let indent = match value {
|
let indent = match value {
|
||||||
Dimension::Px(pixels) => pixels,
|
Dimension::Px(pixels) => pixels,
|
||||||
@@ -2190,7 +2443,7 @@ fn inline_child_ok(tree: &DomTree, cid: NodeId, styles: &std::collections::HashM
|
|||||||
// general path; so does an element with generated ::before/::after
|
// general path; so does an element with generated ::before/::after
|
||||||
// content (lost if folded) or an overflow clip of its own.
|
// content (lost if folded) or an overflow clip of its own.
|
||||||
let foldable_inline = style.display == Display::Inline
|
let foldable_inline = style.display == Display::Inline
|
||||||
&& style.position.is_none()
|
&& !matches!(style.position, Some(taffy::Position::Absolute))
|
||||||
&& style.float.is_none()
|
&& style.float.is_none()
|
||||||
&& !style.overflow_hidden
|
&& !style.overflow_hidden
|
||||||
&& style.before_pseudo.is_none()
|
&& style.before_pseudo.is_none()
|
||||||
@@ -2316,6 +2569,12 @@ impl TextEngine {
|
|||||||
let pw = pixmap.width() as i32;
|
let pw = pixmap.width() as i32;
|
||||||
let ph = pixmap.height() 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));
|
let clip_bounds = clip.map(|c| (c.x, c.y, c.x + c.width, c.y + c.height));
|
||||||
|
let line_source_starts = item
|
||||||
|
.owner_text
|
||||||
|
.as_deref()
|
||||||
|
.filter(|_| !item.relative_owner_ranges.is_empty())
|
||||||
|
.map(|source| source_line_starts(&item.buffer, source))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
// Collect underline segments before drawing glyphs (both borrow the
|
// Collect underline segments before drawing glyphs (both borrow the
|
||||||
// buffer). Underline is carried per glyph via metadata; group runs of
|
// buffer). Underline is carried per glyph via metadata; group runs of
|
||||||
@@ -2330,8 +2589,9 @@ impl TextEngine {
|
|||||||
} else {
|
} else {
|
||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
|
let line_source_start = line_source_starts.get(run.line_i).copied().unwrap_or(0);
|
||||||
let base_y = run.line_y;
|
let base_y = run.line_y;
|
||||||
let mut seg: Option<(f32, f32, f32, [u8; 4])> = None; // x0, x1, font_size, color
|
let mut seg: Option<(f32, f32, f32, [u8; 4], (f32, f32))> = None;
|
||||||
for g in run.glyphs {
|
for g in run.glyphs {
|
||||||
// Keep decoration and background-clip bounds in lockstep with
|
// Keep decoration and background-clip bounds in lockstep with
|
||||||
// glyph painting. A truncated glyph must not leave an
|
// glyph painting. A truncated glyph must not leave an
|
||||||
@@ -2343,14 +2603,20 @@ impl TextEngine {
|
|||||||
}) {
|
}) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
let relative = glyph_relative_offset(
|
||||||
|
&item.relative_owner_ranges,
|
||||||
|
line_source_start,
|
||||||
|
g.start,
|
||||||
|
g.end,
|
||||||
|
);
|
||||||
let underlined = g.metadata & META_UNDERLINE != 0;
|
let underlined = g.metadata & META_UNDERLINE != 0;
|
||||||
if let Some(fill_index) = metadata_fill(g.metadata) {
|
if let Some(fill_index) = metadata_fill(g.metadata) {
|
||||||
if let Some(bounds) = fill_bounds.get_mut(fill_index) {
|
if let Some(bounds) = fill_bounds.get_mut(fill_index) {
|
||||||
let glyph_bounds = (
|
let glyph_bounds = (
|
||||||
g.x + line_offset,
|
g.x + line_offset + relative.0,
|
||||||
run.line_top,
|
run.line_top + relative.1,
|
||||||
g.x + line_offset + g.w,
|
g.x + line_offset + g.w + relative.0,
|
||||||
run.line_top + run.line_height,
|
run.line_top + run.line_height + relative.1,
|
||||||
);
|
);
|
||||||
*bounds = Some(match *bounds {
|
*bounds = Some(match *bounds {
|
||||||
Some((x0, y0, x1, y1)) => (
|
Some((x0, y0, x1, y1)) => (
|
||||||
@@ -2366,28 +2632,31 @@ impl TextEngine {
|
|||||||
let col = g.color_opt.map(|c| [c.r(), c.g(), c.b(), c.a()]).unwrap_or([0, 0, 0, 255]);
|
let col = g.color_opt.map(|c| [c.r(), c.g(), c.b(), c.a()]).unwrap_or([0, 0, 0, 255]);
|
||||||
if underlined {
|
if underlined {
|
||||||
match &mut seg {
|
match &mut seg {
|
||||||
Some((_, x1, fs, c)) if *c == col => {
|
Some((_, x1, fs, c, prior_relative))
|
||||||
*x1 = g.x + line_offset + g.w;
|
if *c == col && *prior_relative == relative =>
|
||||||
|
{
|
||||||
|
*x1 = g.x + line_offset + g.w + relative.0;
|
||||||
*fs = fs.max(g.font_size);
|
*fs = fs.max(g.font_size);
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
if let Some((x0, x1, fs, c)) = seg.take() {
|
if let Some((x0, x1, fs, c, prior_relative)) = seg.take() {
|
||||||
underlines.push((x0, x1, base_y + (fs * 0.12).max(1.0), (fs / 14.0).max(1.0), c));
|
underlines.push((x0, x1, base_y + prior_relative.1 + (fs * 0.12).max(1.0), (fs / 14.0).max(1.0), c));
|
||||||
}
|
}
|
||||||
seg = Some((
|
seg = Some((
|
||||||
g.x + line_offset,
|
g.x + line_offset + relative.0,
|
||||||
g.x + line_offset + g.w,
|
g.x + line_offset + g.w + relative.0,
|
||||||
g.font_size,
|
g.font_size,
|
||||||
col,
|
col,
|
||||||
|
relative,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if let Some((x0, x1, fs, c)) = seg.take() {
|
} else if let Some((x0, x1, fs, c, relative)) = seg.take() {
|
||||||
underlines.push((x0, x1, base_y + (fs * 0.12).max(1.0), (fs / 14.0).max(1.0), c));
|
underlines.push((x0, x1, base_y + relative.1 + (fs * 0.12).max(1.0), (fs / 14.0).max(1.0), c));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some((x0, x1, fs, c)) = seg.take() {
|
if let Some((x0, x1, fs, c, relative)) = seg.take() {
|
||||||
underlines.push((x0, x1, base_y + (fs * 0.12).max(1.0), (fs / 14.0).max(1.0), c));
|
underlines.push((x0, x1, base_y + relative.1 + (fs * 0.12).max(1.0), (fs / 14.0).max(1.0), c));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2407,6 +2676,7 @@ impl TextEngine {
|
|||||||
} else {
|
} else {
|
||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
|
let line_source_start = line_source_starts.get(run.line_i).copied().unwrap_or(0);
|
||||||
for glyph in run.glyphs {
|
for glyph in run.glyphs {
|
||||||
if item.marker.is_some_and(|marker| {
|
if item.marker.is_some_and(|marker| {
|
||||||
marker.line_index == line_index
|
marker.line_index == line_index
|
||||||
@@ -2414,7 +2684,16 @@ impl TextEngine {
|
|||||||
}) {
|
}) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let physical = glyph.physical((line_offset, 0.0), 1.0);
|
let relative = glyph_relative_offset(
|
||||||
|
&item.relative_owner_ranges,
|
||||||
|
line_source_start,
|
||||||
|
glyph.start,
|
||||||
|
glyph.end,
|
||||||
|
);
|
||||||
|
let physical = glyph.physical(
|
||||||
|
(line_offset + relative.0, relative.1),
|
||||||
|
1.0,
|
||||||
|
);
|
||||||
let glyph_color = glyph.color_opt.unwrap_or(default);
|
let glyph_color = glyph.color_opt.unwrap_or(default);
|
||||||
let fill_index = metadata_fill(glyph.metadata);
|
let fill_index = metadata_fill(glyph.metadata);
|
||||||
let mut draw_pixel = |x, y, color: Color| {
|
let mut draw_pixel = |x, y, color: Color| {
|
||||||
@@ -3056,10 +3335,7 @@ mod tests {
|
|||||||
font_weight: Some("725".to_string()),
|
font_weight: Some("725".to_string()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let mut collector = Collector {
|
let mut collector = Collector::new();
|
||||||
last_was_space: true,
|
|
||||||
clip_fills: Vec::new(),
|
|
||||||
};
|
|
||||||
let context = base_span_ctx(&style, resolved, &mut collector);
|
let context = base_span_ctx(&style, resolved, &mut collector);
|
||||||
assert_eq!(context.weight, 725);
|
assert_eq!(context.weight, 725);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user