Match inline font fragment geometry
This commit is contained in:
1 parent
61d61feb50
commit
3e377cef67
9 files changed
+560
-19
No files matched your search
Vendored
+9
@@ -3044,6 +3044,14 @@ class Element extends Node {
|
|||||||
const geometry = this._renderBoxGeometry();
|
const geometry = this._renderBoxGeometry();
|
||||||
if (geometry === null) return new DOMRectList([]);
|
if (geometry === null) return new DOMRectList([]);
|
||||||
if (geometry !== undefined) {
|
if (geometry !== undefined) {
|
||||||
|
if (Array.isArray(geometry.clientRects)) {
|
||||||
|
return new DOMRectList(geometry.clientRects.map(
|
||||||
|
rect => this._rectFromRenderGeometry({
|
||||||
|
...rect,
|
||||||
|
viewportFixed: geometry.viewportFixed,
|
||||||
|
})
|
||||||
|
));
|
||||||
|
}
|
||||||
return new DOMRectList([this._rectFromRenderGeometry(geometry)]);
|
return new DOMRectList([this._rectFromRenderGeometry(geometry)]);
|
||||||
}
|
}
|
||||||
return new DOMRectList([this.getBoundingClientRect()]);
|
return new DOMRectList([this.getBoundingClientRect()]);
|
||||||
@@ -5945,6 +5953,7 @@ globalThis.getComputedStyle = (el) => {
|
|||||||
'margin-top': '0px', 'margin-right': '0px', 'margin-bottom': '0px', 'margin-left': '0px',
|
'margin-top': '0px', 'margin-right': '0px', 'margin-bottom': '0px', 'margin-left': '0px',
|
||||||
'padding-top': '0px', 'padding-right': '0px', 'padding-bottom': '0px', 'padding-left': '0px',
|
'padding-top': '0px', 'padding-right': '0px', 'padding-bottom': '0px', 'padding-left': '0px',
|
||||||
'font-size': '16px', 'line-height': 'normal', 'font-weight': '400',
|
'font-size': '16px', 'line-height': 'normal', 'font-weight': '400',
|
||||||
|
'letter-spacing': 'normal',
|
||||||
'font-family': 'Times',
|
'font-family': 'Times',
|
||||||
color: 'rgb(0, 0, 0)', 'background-color': 'rgba(0, 0, 0, 0)',
|
color: 'rgb(0, 0, 0)', 'background-color': 'rgba(0, 0, 0, 0)',
|
||||||
'border-width': '0px', 'border-style': 'none', 'border-color': 'rgb(0, 0, 0)',
|
'border-width': '0px', 'border-style': 'none', 'border-color': 'rgb(0, 0, 0)',
|
||||||
|
|||||||
@@ -2427,10 +2427,11 @@ pub(crate) fn clamp_scroll_offset(state: &mut ObscuraState, requested: (f32, f32
|
|||||||
/// cache. The cache is computed lazily on first read and cleared on navigation
|
/// cache. The cache is computed lazily on first read and cleared on navigation
|
||||||
/// (see `set_dom`). Coordinates are viewport-relative after the shared root
|
/// (see `set_dom`). Coordinates are viewport-relative after the shared root
|
||||||
/// scroll offset, except for viewport-fixed subtrees. Returns JSON
|
/// scroll offset, except for viewport-fixed subtrees. Returns JSON
|
||||||
/// `{"x","y","width","height","clientWidth","clientHeight"}` in CSS pixels,
|
/// `{"x","y","width","height","clientWidth","clientHeight","clientRects"}`
|
||||||
/// or an empty string when the node has no box. The client dimensions are the
|
/// in CSS pixels, or an empty string when the node has no box. The client
|
||||||
/// unscaled padding box used by CSSOM View, whereas the rect is the visual
|
/// dimensions are the unscaled padding box used by CSSOM View, `clientRects`
|
||||||
/// viewport-relative border box. Feature-gated.
|
/// retains every inline continuation, and the top-level rect is their visual
|
||||||
|
/// viewport-relative bounding union. Feature-gated.
|
||||||
#[cfg(feature = "render")]
|
#[cfg(feature = "render")]
|
||||||
#[op2]
|
#[op2]
|
||||||
#[string]
|
#[string]
|
||||||
@@ -2447,17 +2448,32 @@ fn op_layout_geometry(state: &OpState, #[string] nid_str: String) -> String {
|
|||||||
let Some((client_width, client_height)) = prepared.client_size(nid) else {
|
let Some((client_width, client_height)) = prepared.client_size(nid) else {
|
||||||
return String::new();
|
return String::new();
|
||||||
};
|
};
|
||||||
|
let Some(client_rects) = prepared.viewport_client_rects(nid, scroll) else {
|
||||||
|
return String::new();
|
||||||
|
};
|
||||||
|
let client_rects = client_rects
|
||||||
|
.into_iter()
|
||||||
|
.map(|rect| {
|
||||||
|
serde_json::json!({
|
||||||
|
"x": rect.x,
|
||||||
|
"y": rect.y,
|
||||||
|
"width": rect.width,
|
||||||
|
"height": rect.height,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
let viewport_fixed = prepared.viewport_fixed_nodes().contains(&nid);
|
let viewport_fixed = prepared.viewport_fixed_nodes().contains(&nid);
|
||||||
return format!(
|
return serde_json::json!({
|
||||||
"{{\"x\":{},\"y\":{},\"width\":{},\"height\":{},\"clientWidth\":{},\"clientHeight\":{},\"viewportFixed\":{}}}",
|
"x": rect.x,
|
||||||
rect.x,
|
"y": rect.y,
|
||||||
rect.y,
|
"width": rect.width,
|
||||||
rect.width,
|
"height": rect.height,
|
||||||
rect.height,
|
"clientWidth": client_width,
|
||||||
client_width,
|
"clientHeight": client_height,
|
||||||
client_height,
|
"clientRects": client_rects,
|
||||||
viewport_fixed
|
"viewportFixed": viewport_fixed,
|
||||||
);
|
})
|
||||||
|
.to_string();
|
||||||
}
|
}
|
||||||
String::new()
|
String::new()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2275,6 +2275,9 @@ mod tests {
|
|||||||
rect: [rect.x, rect.y, rect.width, rect.height],
|
rect: [rect.x, rect.y, rect.width, rect.height],
|
||||||
after: [afterRect.x, afterRect.y],
|
after: [afterRect.x, afterRect.y],
|
||||||
client: [token.clientWidth, token.clientHeight],
|
client: [token.clientWidth, token.clientHeight],
|
||||||
|
clientRects: Array.from(token.getClientRects(), r => [
|
||||||
|
r.x, r.y, r.width, r.height
|
||||||
|
]),
|
||||||
atomic: [atomic.width, atomic.height],
|
atomic: [atomic.width, atomic.height],
|
||||||
replaced: [replaced.width, replaced.height],
|
replaced: [replaced.width, replaced.height],
|
||||||
item: [item.width, item.height, getComputedStyle(item).display]
|
item: [item.width, item.height, getComputedStyle(item).display]
|
||||||
@@ -2298,6 +2301,19 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert!(token_height < 40.0, "ignored block size leaked into geometry");
|
assert!(token_height < 40.0, "ignored block size leaked into geometry");
|
||||||
assert_eq!(result["client"], serde_json::json!([0, 0]));
|
assert_eq!(result["client"], serde_json::json!([0, 0]));
|
||||||
|
let client_rects = result["clientRects"].as_array().unwrap();
|
||||||
|
assert_eq!(client_rects.len(), 1);
|
||||||
|
let client_rect = client_rects[0].as_array().unwrap();
|
||||||
|
for (actual, expected) in client_rect
|
||||||
|
.iter()
|
||||||
|
.map(|value| value.as_f64().unwrap())
|
||||||
|
.zip([token_x, token_y, token_width, token_height])
|
||||||
|
{
|
||||||
|
assert!(
|
||||||
|
(actual - expected).abs() < 0.001,
|
||||||
|
"getClientRects must expose the renderer's inline fragments"
|
||||||
|
);
|
||||||
|
}
|
||||||
let after = result["after"].as_array().unwrap();
|
let after = result["after"].as_array().unwrap();
|
||||||
assert!(after[0].as_f64().unwrap() >= token_x + token_width - 0.01);
|
assert!(after[0].as_f64().unwrap() >= token_x + token_width - 0.01);
|
||||||
assert!((after[1].as_f64().unwrap() - token_y).abs() < 0.01);
|
assert!((after[1].as_f64().unwrap() - token_y).abs() < 0.01);
|
||||||
@@ -2377,6 +2393,49 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "render")]
|
||||||
|
#[test]
|
||||||
|
fn computed_typography_uses_resolved_renderer_values() {
|
||||||
|
let dom = parse_html(
|
||||||
|
r#"<html><head><style>
|
||||||
|
#parent {
|
||||||
|
font-size:20px; line-height:1.5;
|
||||||
|
letter-spacing:-.05em; white-space:pre-wrap;
|
||||||
|
text-align:end
|
||||||
|
}
|
||||||
|
#child { font-size:10px }
|
||||||
|
#zero { letter-spacing:0px; white-space:break-spaces }
|
||||||
|
</style></head><body>
|
||||||
|
<div id="parent"><span id="child">child</span></div>
|
||||||
|
<div id="zero">zero</div>
|
||||||
|
</body></html>"#,
|
||||||
|
);
|
||||||
|
let mut rt = ObscuraJsRuntime::new();
|
||||||
|
rt.set_dom(dom);
|
||||||
|
rt.set_viewport(400.0, 200.0);
|
||||||
|
rt.run_page_init();
|
||||||
|
|
||||||
|
let result = rt
|
||||||
|
.evaluate(
|
||||||
|
r#"
|
||||||
|
const sample = id => {
|
||||||
|
const s = getComputedStyle(document.getElementById(id));
|
||||||
|
return [s.lineHeight, s.letterSpacing, s.whiteSpace, s.textAlign];
|
||||||
|
};
|
||||||
|
return [sample("parent"), sample("child"), sample("zero")];
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
serde_json::json!([
|
||||||
|
["30px", "-1px", "pre-wrap", "end"],
|
||||||
|
["15px", "-1px", "pre-wrap", "end"],
|
||||||
|
["normal", "normal", "break-spaces", "start"],
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "render")]
|
#[cfg(feature = "render")]
|
||||||
#[test]
|
#[test]
|
||||||
fn computed_style_exposes_cascaded_custom_properties_and_invalidates() {
|
fn computed_style_exposes_cascaded_custom_properties_and_invalidates() {
|
||||||
|
|||||||
@@ -224,6 +224,11 @@ impl OverflowClip {
|
|||||||
/// Per-element border boxes after layout, in viewport coordinates.
|
/// Per-element border boxes after layout, in viewport coordinates.
|
||||||
pub struct DomLayout {
|
pub struct DomLayout {
|
||||||
pub rects: HashMap<NodeId, Rect>,
|
pub rects: HashMap<NodeId, Rect>,
|
||||||
|
/// Per-line border-box fragments for ordinary non-replaced inline
|
||||||
|
/// elements. `rects` retains their union for `getBoundingClientRect()`;
|
||||||
|
/// this list is the source for background/border painting and
|
||||||
|
/// `getClientRects()` as wrapping support is extended.
|
||||||
|
pub inline_fragments: HashMap<NodeId, Vec<Rect>>,
|
||||||
pub styles: HashMap<NodeId, crate::LayoutStyle>,
|
pub styles: HashMap<NodeId, crate::LayoutStyle>,
|
||||||
/// Computed custom properties in force for each element. Values are
|
/// Computed custom properties in force for each element. Values are
|
||||||
/// reference-counted because the overwhelmingly common case is inheritance
|
/// reference-counted because the overwhelmingly common case is inheritance
|
||||||
@@ -262,6 +267,11 @@ pub struct DomLayout {
|
|||||||
/// have no DOM node of their own.
|
/// have no DOM node of their own.
|
||||||
#[cfg(feature = "paint")]
|
#[cfg(feature = "paint")]
|
||||||
pub run_ifc_items: HashMap<NodeId, Vec<usize>>,
|
pub run_ifc_items: HashMap<NodeId, Vec<usize>>,
|
||||||
|
/// Webfont-shaped items backing the general per-word fallback. Unlike
|
||||||
|
/// `run_ifc_items`, these remain keyed by their real DOM text node so they
|
||||||
|
/// can paint in exact tree order at each independently wrapped Taffy box.
|
||||||
|
#[cfg(feature = "paint")]
|
||||||
|
pub word_ifc_items: HashMap<NodeId, Vec<usize>>,
|
||||||
/// Anonymous in-flow boxes generated by `::before`/`::after`. These have
|
/// Anonymous in-flow boxes generated by `::before`/`::after`. These have
|
||||||
/// no DOM node of their own, but unlike the legacy text-only fast path
|
/// no DOM node of their own, but unlike the legacy text-only fast path
|
||||||
/// they participate in layout with their pseudo style's real box model.
|
/// they participate in layout with their pseudo style's real box model.
|
||||||
@@ -1019,6 +1029,7 @@ fn sync_positioned_pseudo_percentage_padding(
|
|||||||
struct IfcRegistry {
|
struct IfcRegistry {
|
||||||
whole: HashMap<NodeId, usize>,
|
whole: HashMap<NodeId, usize>,
|
||||||
runs: HashMap<NodeId, Vec<usize>>,
|
runs: HashMap<NodeId, Vec<usize>>,
|
||||||
|
word_items: HashMap<NodeId, Vec<usize>>,
|
||||||
generated: Vec<GeneratedBoxBuild>,
|
generated: Vec<GeneratedBoxBuild>,
|
||||||
/// Specified column widths per table grid node, from `<col>` elements and
|
/// Specified column widths per table grid node, from `<col>` elements and
|
||||||
/// colspan-1 cells: `(px, percent)` per column index. Consumed by the
|
/// colspan-1 cells: `(px, percent)` per column index. Consumed by the
|
||||||
@@ -2035,6 +2046,7 @@ fn layout_dom_once(
|
|||||||
.find(|id| tree.get_node(*id).map(|n| n.is_element()).unwrap_or(false));
|
.find(|id| tree.get_node(*id).map(|n| n.is_element()).unwrap_or(false));
|
||||||
|
|
||||||
let mut rects = HashMap::new();
|
let mut rects = HashMap::new();
|
||||||
|
let mut inline_fragments = HashMap::new();
|
||||||
let mut text_runs = HashMap::new();
|
let mut text_runs = HashMap::new();
|
||||||
// Final absolute rects of anonymous inline-run leaves, keyed by the
|
// Final absolute rects of anonymous inline-run leaves, keyed by the
|
||||||
// engine item index (they have no DOM id to key `rects` by).
|
// engine item index (they have no DOM id to key `rects` by).
|
||||||
@@ -3866,6 +3878,8 @@ fn layout_dom_once(
|
|||||||
&generated_nodes,
|
&generated_nodes,
|
||||||
&mut generated_rects,
|
&mut generated_rects,
|
||||||
);
|
);
|
||||||
|
inline_fragments =
|
||||||
|
synthesize_ordinary_inline_fragments(&mut rects, &styles, &engine);
|
||||||
synthesize_row_rects(tree, &mut rects);
|
synthesize_row_rects(tree, &mut rects);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3959,6 +3973,23 @@ fn layout_dom_once(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// General word-fallback items are shaped with the same loaded font system
|
||||||
|
// as full IFCs but retain one independently wrapped Taffy rect per token.
|
||||||
|
// Their source is a real text node, so clipping/translation follows that
|
||||||
|
// node while each item is pinned to its anonymous leaf.
|
||||||
|
#[cfg(feature = "paint")]
|
||||||
|
for (text_node, items) in &ifc_items.word_items {
|
||||||
|
let clip = clip_rects
|
||||||
|
.get(text_node)
|
||||||
|
.copied()
|
||||||
|
.flatten()
|
||||||
|
.map(|clip| clip.viewport_rect(viewport));
|
||||||
|
for &idx in items {
|
||||||
|
if let Some(rect) = anon_rects.get(&idx) {
|
||||||
|
engine.finalize(idx, (rect.x, rect.y), rect.width, clip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let generated_boxes = ifc_items
|
let generated_boxes = ifc_items
|
||||||
.generated
|
.generated
|
||||||
@@ -3976,6 +4007,7 @@ fn layout_dom_once(
|
|||||||
(
|
(
|
||||||
DomLayout {
|
DomLayout {
|
||||||
rects,
|
rects,
|
||||||
|
inline_fragments,
|
||||||
styles,
|
styles,
|
||||||
custom_properties,
|
custom_properties,
|
||||||
clip_rects,
|
clip_rects,
|
||||||
@@ -3987,6 +4019,8 @@ fn layout_dom_once(
|
|||||||
ifc_items: ifc_items.whole,
|
ifc_items: ifc_items.whole,
|
||||||
#[cfg(feature = "paint")]
|
#[cfg(feature = "paint")]
|
||||||
run_ifc_items: ifc_items.runs,
|
run_ifc_items: ifc_items.runs,
|
||||||
|
#[cfg(feature = "paint")]
|
||||||
|
word_ifc_items: ifc_items.word_items,
|
||||||
generated_boxes,
|
generated_boxes,
|
||||||
},
|
},
|
||||||
signature,
|
signature,
|
||||||
@@ -3995,6 +4029,54 @@ fn layout_dom_once(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert Taffy's ordinary-inline line surrogate into the element's actual
|
||||||
|
/// visual fragment box.
|
||||||
|
///
|
||||||
|
/// The surrogate remains at CSS line-height so it contributes the correct
|
||||||
|
/// line advance. The fragment uses the selected face's grid-fitted ascent plus
|
||||||
|
/// descent, expanded by block-axis padding and border. Those decorations
|
||||||
|
/// protrude into the leading and never feed back into the line height.
|
||||||
|
fn synthesize_ordinary_inline_fragments(
|
||||||
|
rects: &mut HashMap<NodeId, Rect>,
|
||||||
|
styles: &HashMap<NodeId, crate::LayoutStyle>,
|
||||||
|
engine: &crate::inline::TextEngine,
|
||||||
|
) -> HashMap<NodeId, Vec<Rect>> {
|
||||||
|
let mut fragments = HashMap::new();
|
||||||
|
let mut unions = Vec::new();
|
||||||
|
for (&id, style) in styles {
|
||||||
|
if !style.ignores_used_box_sizes() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(flow_rect) = rects.get(&id).copied() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let font_height = engine.inline_font_box_height(style).max(0.0);
|
||||||
|
let line_height = engine.selected_line_height(style).max(0.0);
|
||||||
|
// Taffy's surrogate may be taller than this element's own strut when
|
||||||
|
// a nested inline enlarges the line. Center the element's logical
|
||||||
|
// line-height inside that allocation before removing its leading.
|
||||||
|
let logical_top = flow_rect.y + (flow_rect.height - line_height) / 2.0;
|
||||||
|
let fragment = Rect {
|
||||||
|
x: flow_rect.x,
|
||||||
|
y: logical_top + (line_height - font_height) / 2.0
|
||||||
|
- style.padding.top
|
||||||
|
- style.border.top,
|
||||||
|
width: flow_rect.width,
|
||||||
|
height: font_height
|
||||||
|
+ style.padding.top
|
||||||
|
+ style.padding.bottom
|
||||||
|
+ style.border.top
|
||||||
|
+ style.border.bottom,
|
||||||
|
};
|
||||||
|
fragments.insert(id, vec![fragment]);
|
||||||
|
unions.push((id, fragment));
|
||||||
|
}
|
||||||
|
for (id, union) in unions {
|
||||||
|
rects.insert(id, union);
|
||||||
|
}
|
||||||
|
fragments
|
||||||
|
}
|
||||||
|
|
||||||
fn container_snapshot(
|
fn container_snapshot(
|
||||||
tree: &DomTree,
|
tree: &DomTree,
|
||||||
layout: &DomLayout,
|
layout: &DomLayout,
|
||||||
@@ -5736,7 +5818,15 @@ fn build_any(
|
|||||||
.map(|n| matches!(n.data, obscura_dom::tree::NodeData::Text { .. }))
|
.map(|n| matches!(n.data, obscura_dom::tree::NodeData::Text { .. }))
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
if is_text {
|
if is_text {
|
||||||
return build_text_words(tree, id, taffy_tree, styles, words);
|
return build_text_words(
|
||||||
|
tree,
|
||||||
|
id,
|
||||||
|
taffy_tree,
|
||||||
|
styles,
|
||||||
|
words,
|
||||||
|
engine,
|
||||||
|
ifc_items,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// `display: contents` removes the element's own box; its children lay out as
|
// `display: contents` removes the element's own box; its children lay out as
|
||||||
// if they were direct children of its parent (CSS Display 3). Splice them
|
// if they were direct children of its parent (CSS Display 3). Splice them
|
||||||
@@ -5975,6 +6065,8 @@ fn build_text_words(
|
|||||||
taffy_tree: &mut TaffyTree<usize>,
|
taffy_tree: &mut TaffyTree<usize>,
|
||||||
styles: &HashMap<NodeId, crate::LayoutStyle>,
|
styles: &HashMap<NodeId, crate::LayoutStyle>,
|
||||||
words: &mut HashMap<taffy::NodeId, (NodeId, String)>,
|
words: &mut HashMap<taffy::NodeId, (NodeId, String)>,
|
||||||
|
engine: &mut crate::inline::TextEngine,
|
||||||
|
ifc_items: &mut IfcRegistry,
|
||||||
) -> Vec<taffy::NodeId> {
|
) -> Vec<taffy::NodeId> {
|
||||||
let Some(node) = tree.get_node(id) else { return Vec::new() };
|
let Some(node) = tree.get_node(id) else { return Vec::new() };
|
||||||
let obscura_dom::tree::NodeData::Text { contents } = &node.data else { return Vec::new() };
|
let obscura_dom::tree::NodeData::Text { contents } = &node.data else { return Vec::new() };
|
||||||
@@ -6011,8 +6103,22 @@ fn build_text_words(
|
|||||||
letter_spacing = p_style.letter_spacing.unwrap_or(0.0);
|
letter_spacing = p_style.letter_spacing.unwrap_or(0.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
display_text = transform_word_leaf_text(&display_text, transform);
|
if let Some(style) = node.parent.and_then(|parent| styles.get(&parent)) {
|
||||||
|
let shaped = build_shaped_word_leaves(
|
||||||
|
id,
|
||||||
|
&display_text,
|
||||||
|
style,
|
||||||
|
taffy_tree,
|
||||||
|
words,
|
||||||
|
engine,
|
||||||
|
ifc_items,
|
||||||
|
);
|
||||||
|
if !shaped.is_empty() {
|
||||||
|
return shaped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
display_text = transform_word_leaf_text(&display_text, transform);
|
||||||
build_word_leaves(
|
build_word_leaves(
|
||||||
id,
|
id,
|
||||||
&display_text,
|
&display_text,
|
||||||
@@ -6026,6 +6132,70 @@ fn build_text_words(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Webfont-aware counterpart to [`build_word_leaves`].
|
||||||
|
///
|
||||||
|
/// The general flex-wrap fallback still needs one independently placeable
|
||||||
|
/// Taffy leaf per word. Shape each visible token through the render pass's
|
||||||
|
/// retained `TextEngine`, then reuse that same item at paint time. This keeps
|
||||||
|
/// loaded faces, variable axes, optical sizing, transforms, and glyph
|
||||||
|
/// rasterization identical to the full inline-formatting-context path.
|
||||||
|
fn build_shaped_word_leaves(
|
||||||
|
source_id: NodeId,
|
||||||
|
text: &str,
|
||||||
|
style: &crate::LayoutStyle,
|
||||||
|
taffy_tree: &mut TaffyTree<usize>,
|
||||||
|
words: &mut HashMap<taffy::NodeId, (NodeId, String)>,
|
||||||
|
engine: &mut crate::inline::TextEngine,
|
||||||
|
ifc_items: &mut IfcRegistry,
|
||||||
|
) -> Vec<taffy::NodeId> {
|
||||||
|
let mut leaves = Vec::new();
|
||||||
|
for token in tokenize_with_spaces(text) {
|
||||||
|
if token.trim().is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// A token normally retains one trailing collapsed space. Shape it as
|
||||||
|
// preformatted content so the item keeps that advance instead of the
|
||||||
|
// paragraph path trimming collapsible trailing whitespace.
|
||||||
|
let mut token_style = style.clone();
|
||||||
|
token_style.white_space = Some(crate::WhiteSpace::Pre);
|
||||||
|
let Some(item) = engine.push_generated_text(&token, &token_style) else {
|
||||||
|
// Returning no leaves makes the caller use the deterministic
|
||||||
|
// layout-only/static-font fallback for the whole text node.
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let (width, height) = engine.measure_word(item);
|
||||||
|
let taffy_style = taffy::Style {
|
||||||
|
size: taffy::Size {
|
||||||
|
width: taffy::Dimension::length(width.max(0.0)),
|
||||||
|
height: taffy::Dimension::length(height.max(0.0)),
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let Ok(leaf) = taffy_tree.new_leaf_with_context(taffy_style, item) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
words.insert(
|
||||||
|
leaf,
|
||||||
|
(
|
||||||
|
source_id,
|
||||||
|
transform_word_leaf_text(
|
||||||
|
&token,
|
||||||
|
style
|
||||||
|
.text_transform
|
||||||
|
.unwrap_or(crate::TextTransform::None),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
ifc_items
|
||||||
|
.word_items
|
||||||
|
.entry(source_id)
|
||||||
|
.or_default()
|
||||||
|
.push(item);
|
||||||
|
leaves.push(leaf);
|
||||||
|
}
|
||||||
|
leaves
|
||||||
|
}
|
||||||
|
|
||||||
fn transform_word_leaf_text(text: &str, transform: crate::TextTransform) -> String {
|
fn transform_word_leaf_text(text: &str, transform: crate::TextTransform) -> String {
|
||||||
match transform {
|
match transform {
|
||||||
crate::TextTransform::None => text.to_string(),
|
crate::TextTransform::None => text.to_string(),
|
||||||
@@ -10985,6 +11155,101 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ordinary_inline_fragment_uses_font_box_while_line_keeps_line_height() {
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"<style>
|
||||||
|
html,body,p { margin:0 }
|
||||||
|
p { height:auto; font:16px/40px monospace }
|
||||||
|
code {
|
||||||
|
position:relative;
|
||||||
|
font:16px/40px monospace;
|
||||||
|
padding-top:5px; padding-bottom:7px;
|
||||||
|
border-top:2px solid; border-bottom:3px solid;
|
||||||
|
margin-top:19px; margin-bottom:23px;
|
||||||
|
background:red
|
||||||
|
}
|
||||||
|
#control { margin-top:0; margin-bottom:0 }
|
||||||
|
</style>
|
||||||
|
<p id="with-margin"><code id="token">token</code></p>
|
||||||
|
<p id="without-margin"><code id="control">token</code></p>"#,
|
||||||
|
);
|
||||||
|
let laid = layout_dom(&tree, (400.0, 200.0));
|
||||||
|
let host = tree.get_element_by_id("with-margin").unwrap();
|
||||||
|
let control_host = tree.get_element_by_id("without-margin").unwrap();
|
||||||
|
let token = tree.get_element_by_id("token").unwrap();
|
||||||
|
let control = tree.get_element_by_id("control").unwrap();
|
||||||
|
let style = &laid.styles[&token];
|
||||||
|
let raw_font_height = laid.text_engine.inline_font_box_height(style);
|
||||||
|
let expected_fragment_height = raw_font_height + 5.0 + 7.0 + 2.0 + 3.0;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
laid.rects[&host].height, 40.0,
|
||||||
|
"block-axis inline padding/border must not increase line advance"
|
||||||
|
);
|
||||||
|
assert_eq!(laid.rects[&control_host].height, 40.0);
|
||||||
|
assert_eq!(laid.rects[&token].height, expected_fragment_height);
|
||||||
|
assert_eq!(laid.inline_fragments[&token], vec![laid.rects[&token]]);
|
||||||
|
assert_eq!(
|
||||||
|
laid.rects[&token].y - laid.rects[&host].y,
|
||||||
|
(40.0 - raw_font_height) / 2.0 - 5.0 - 2.0
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
laid.rects[&control].y - laid.rects[&control_host].y,
|
||||||
|
laid.rects[&token].y - laid.rects[&host].y,
|
||||||
|
"ordinary inline block-axis margins must not move its fragment"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fallback_word_boxes_shape_and_paint_with_the_loaded_webfont() {
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"<style>
|
||||||
|
html,body,p { margin:0 }
|
||||||
|
#token {
|
||||||
|
position:relative;
|
||||||
|
font-family:Fixture, sans-serif;
|
||||||
|
font-size:40px;
|
||||||
|
line-height:50px;
|
||||||
|
background:red
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<p><span id="token">WWWWiiii</span></p>"#,
|
||||||
|
);
|
||||||
|
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 loaded = layout_dom_with_web_fonts(
|
||||||
|
&tree,
|
||||||
|
(500.0, 150.0),
|
||||||
|
&HashMap::new(),
|
||||||
|
&[crate::inline::WebFont {
|
||||||
|
data: include_bytes!("../assets/liberation-serif.ttf").to_vec(),
|
||||||
|
family: Some("Fixture".to_string()),
|
||||||
|
weight: Some((400, 400)),
|
||||||
|
italic: Some(false),
|
||||||
|
}],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
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!(
|
||||||
|
fallback.rects[&token].width, loaded.rects[&token].width,
|
||||||
|
"the loaded face's advances must drive fallback inline geometry"
|
||||||
|
);
|
||||||
|
assert_ne!(
|
||||||
|
fallback.rects[&token].height, loaded.rects[&token].height,
|
||||||
|
"the loaded face's raw ascent/descent must drive the visual fragment"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
loaded.inline_fragments[&token],
|
||||||
|
vec![loaded.rects[&token]],
|
||||||
|
"the loaded-font fragment must be the geometry/paint source"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn positioned_pseudo_inherits_the_hosts_computed_font_metrics() {
|
fn positioned_pseudo_inherits_the_hosts_computed_font_metrics() {
|
||||||
let tree = parse_html(
|
let tree = parse_html(
|
||||||
|
|||||||
@@ -300,6 +300,18 @@ fn normal_line_height(font_size: f32, metrics: FaceMetrics) -> f32 {
|
|||||||
+ (metrics.line_gap * scale).round()
|
+ (metrics.line_gap * scale).round()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Grid-fitted font ascent plus descent, excluding both the face line gap and
|
||||||
|
/// authored CSS `line-height`.
|
||||||
|
///
|
||||||
|
/// A non-replaced inline has two different vertical boxes. Its line
|
||||||
|
/// participation uses [`used_line_height_with_metrics`], while its painted
|
||||||
|
/// fragment/client rect uses this raw font box plus block-axis padding and
|
||||||
|
/// border. Chromium and Gecko both fit ascent and descent independently.
|
||||||
|
fn font_box_height(font_size: f32, metrics: FaceMetrics) -> f32 {
|
||||||
|
let scale = font_size / metrics.units_per_em.max(1.0);
|
||||||
|
(metrics.ascent * scale).round() + (metrics.descent * scale).round()
|
||||||
|
}
|
||||||
|
|
||||||
fn font_metrics(
|
fn font_metrics(
|
||||||
db: &cosmic_text::fontdb::Database,
|
db: &cosmic_text::fontdb::Database,
|
||||||
id: cosmic_text::fontdb::ID,
|
id: cosmic_text::fontdb::ID,
|
||||||
@@ -848,6 +860,32 @@ impl TextEngine {
|
|||||||
pub fn len(&self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
self.items.len()
|
self.items.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Raw selected-font box used by ordinary inline fragments. This is
|
||||||
|
/// deliberately not CSS `line-height`: leading belongs to the containing
|
||||||
|
/// line and must not enlarge backgrounds, borders, or DOM client rects.
|
||||||
|
pub(crate) fn inline_font_box_height(&self, style: &LayoutStyle) -> f32 {
|
||||||
|
let font = resolve_loaded_font(
|
||||||
|
style.font_family.as_deref(),
|
||||||
|
crate::style::used_font_weight(style),
|
||||||
|
style.font_style_italic.unwrap_or(false),
|
||||||
|
&self.loaded_families,
|
||||||
|
);
|
||||||
|
font_box_height(style.font_size.unwrap_or(16.0), font.metrics)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used line-height for the same selected face. Kept beside
|
||||||
|
/// [`inline_font_box_height`](Self::inline_font_box_height) so layout can
|
||||||
|
/// distribute leading around the raw fragment using one font decision.
|
||||||
|
pub(crate) fn selected_line_height(&self, style: &LayoutStyle) -> f32 {
|
||||||
|
let font = resolve_loaded_font(
|
||||||
|
style.font_family.as_deref(),
|
||||||
|
crate::style::used_font_weight(style),
|
||||||
|
style.font_style_italic.unwrap_or(false),
|
||||||
|
&self.loaded_families,
|
||||||
|
);
|
||||||
|
used_line_height_for_font(style, &font)
|
||||||
|
}
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.items.is_empty()
|
self.items.is_empty()
|
||||||
}
|
}
|
||||||
@@ -1158,6 +1196,32 @@ impl TextEngine {
|
|||||||
(width, height.max(item.forced_min_height))
|
(width, height.max(item.forced_min_height))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Exact max-content size for one fallback word item. Paragraph IFCs keep
|
||||||
|
/// their historical integer-ceiled intrinsic width, but word boxes need
|
||||||
|
/// the selected webfont's fractional advance or every token accumulates a
|
||||||
|
/// pixel of horizontal drift against browser geometry.
|
||||||
|
pub(crate) fn measure_word(&mut self, idx: usize) -> (f32, f32) {
|
||||||
|
let TextEngine {
|
||||||
|
font_system, items, ..
|
||||||
|
} = self;
|
||||||
|
let Some(item) = items.get_mut(idx) else {
|
||||||
|
return (0.0, 0.0);
|
||||||
|
};
|
||||||
|
shape_with_text_indent(font_system, item, None);
|
||||||
|
let mut width = 0.0f32;
|
||||||
|
let mut height = 0.0f32;
|
||||||
|
for (line_index, run) in item.buffer.layout_runs().enumerate() {
|
||||||
|
let offset = if line_index == 0 {
|
||||||
|
item.first_line_offset
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
width = width.max((run.line_w + offset).max(0.0));
|
||||||
|
height = height.max(run.line_top + run.line_height);
|
||||||
|
}
|
||||||
|
(width, height.max(item.forced_min_height))
|
||||||
|
}
|
||||||
|
|
||||||
/// Register a replaced element's intrinsic size as a taffy measure
|
/// Register a replaced element's intrinsic size as a taffy measure
|
||||||
/// context. Percentage-sized image leaves still need their intrinsic
|
/// context. Percentage-sized image leaves still need their intrinsic
|
||||||
/// max-content contribution while an auto-sized ancestor is measured.
|
/// max-content contribution while an auto-sized ancestor is measured.
|
||||||
|
|||||||
@@ -127,6 +127,37 @@ pub mod inline {
|
|||||||
) -> Option<usize> {
|
) -> Option<usize> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Layout-only builds do not load page fonts. Preserve the same
|
||||||
|
/// line-vs-fragment contract with deterministic bundled metrics.
|
||||||
|
pub(crate) fn inline_font_box_height(&self, style: &crate::LayoutStyle) -> f32 {
|
||||||
|
style.font_size.unwrap_or(16.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn selected_line_height(&self, style: &crate::LayoutStyle) -> f32 {
|
||||||
|
match style.line_height.unwrap_or(crate::LineHeight::Normal) {
|
||||||
|
crate::LineHeight::Normal => style.font_size.unwrap_or(16.0) * 1.2,
|
||||||
|
crate::LineHeight::Ratio(value) => style.font_size.unwrap_or(16.0) * value,
|
||||||
|
crate::LineHeight::Px(value) => value,
|
||||||
|
crate::LineHeight::Relative(crate::Dimension::Percent(value)) => {
|
||||||
|
style.font_size.unwrap_or(16.0) * value
|
||||||
|
}
|
||||||
|
crate::LineHeight::Relative(crate::Dimension::Px(value)) => value,
|
||||||
|
crate::LineHeight::Relative(_) => style.font_size.unwrap_or(16.0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn push_generated_text(
|
||||||
|
&mut self,
|
||||||
|
_text: &str,
|
||||||
|
_style: &crate::LayoutStyle,
|
||||||
|
) -> Option<usize> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn measure_word(&mut self, _idx: usize) -> (f32, f32) {
|
||||||
|
(0.0, 0.0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1463,6 +1494,19 @@ pub(crate) fn to_taffy_style(style: &LayoutStyle) -> Style {
|
|||||||
s.margin = rect_auto(style.margin, style.margin_auto);
|
s.margin = rect_auto(style.margin, style.margin_auto);
|
||||||
s.padding = rect_lp_percent(style.padding, style.padding_percent);
|
s.padding = rect_lp_percent(style.padding, style.padding_percent);
|
||||||
s.border = rect_lp(style.border);
|
s.border = rect_lp(style.border);
|
||||||
|
if style.ignores_used_box_sizes() {
|
||||||
|
// Block-axis margins on an ordinary non-replaced inline neither move
|
||||||
|
// nor size its fragment. Padding and border do paint around the raw
|
||||||
|
// font box, but they protrude outside the CSS line-height rather than
|
||||||
|
// increasing line advance. Keep them in LayoutStyle for fragment
|
||||||
|
// synthesis/paint and remove them only from this Taffy line surrogate.
|
||||||
|
s.margin.top = taffy::LengthPercentageAuto::length(0.0);
|
||||||
|
s.margin.bottom = taffy::LengthPercentageAuto::length(0.0);
|
||||||
|
s.padding.top = taffy::LengthPercentage::length(0.0);
|
||||||
|
s.padding.bottom = taffy::LengthPercentage::length(0.0);
|
||||||
|
s.border.top = taffy::LengthPercentage::length(0.0);
|
||||||
|
s.border.bottom = taffy::LengthPercentage::length(0.0);
|
||||||
|
}
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -479,11 +479,37 @@ impl PreparedRender {
|
|||||||
"line-height",
|
"line-height",
|
||||||
match style.line_height.unwrap_or(crate::LineHeight::Normal) {
|
match style.line_height.unwrap_or(crate::LineHeight::Normal) {
|
||||||
crate::LineHeight::Normal => "normal".to_string(),
|
crate::LineHeight::Normal => "normal".to_string(),
|
||||||
crate::LineHeight::Ratio(value) => css_number(value),
|
_ => css_px(self.layout.text_engine.selected_line_height(style)),
|
||||||
crate::LineHeight::Px(value) => css_px(value),
|
|
||||||
crate::LineHeight::Relative(value) => dimension_css(value, "normal"),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
out.insert(
|
||||||
|
"letter-spacing",
|
||||||
|
match style.letter_spacing.unwrap_or(0.0) {
|
||||||
|
value if value == 0.0 => "normal".to_string(),
|
||||||
|
value => css_px(value),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
out.insert(
|
||||||
|
"white-space",
|
||||||
|
match style.white_space.unwrap_or_default() {
|
||||||
|
crate::WhiteSpace::Normal => "normal",
|
||||||
|
crate::WhiteSpace::NoWrap => "nowrap",
|
||||||
|
crate::WhiteSpace::Pre => "pre",
|
||||||
|
crate::WhiteSpace::PreWrap => "pre-wrap",
|
||||||
|
crate::WhiteSpace::PreLine => "pre-line",
|
||||||
|
crate::WhiteSpace::BreakSpaces => "break-spaces",
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
out.insert(
|
||||||
|
"text-align",
|
||||||
|
match style.text_align {
|
||||||
|
Some(taffy::AlignItems::CENTER) => "center",
|
||||||
|
Some(taffy::AlignItems::FLEX_END | taffy::AlignItems::END) => "end",
|
||||||
|
_ => "start",
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
if style.ignores_used_box_sizes() {
|
if style.ignores_used_box_sizes() {
|
||||||
out.insert("width", dimension_css(style.width, "auto"));
|
out.insert("width", dimension_css(style.width, "auto"));
|
||||||
@@ -677,6 +703,49 @@ impl PreparedRender {
|
|||||||
Some(rect)
|
Some(rect)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Every CSS border-box fragment in the current root viewport. Ordinary
|
||||||
|
/// inlines can have one fragment per line; all other boxes expose their
|
||||||
|
/// single border box. Keeping this separate from the bounding union is
|
||||||
|
/// required by CSSOM View's `getClientRects()`.
|
||||||
|
pub fn viewport_client_rects(
|
||||||
|
&self,
|
||||||
|
id: obscura_dom::tree::NodeId,
|
||||||
|
requested_scroll: (f32, f32),
|
||||||
|
) -> Option<Vec<crate::Rect>> {
|
||||||
|
let source = self
|
||||||
|
.layout
|
||||||
|
.inline_fragments
|
||||||
|
.get(&id)
|
||||||
|
.cloned()
|
||||||
|
.or_else(|| self.layout.rects.get(&id).copied().map(|rect| vec![rect]))?;
|
||||||
|
let transform = self
|
||||||
|
.layout
|
||||||
|
.translates
|
||||||
|
.get(&id)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or((0.0, 0.0));
|
||||||
|
let scroll = self.clamp_scroll(requested_scroll);
|
||||||
|
let movement = if self.viewport_fixed.contains(&id) {
|
||||||
|
transform
|
||||||
|
} else {
|
||||||
|
let sticky = self.sticky.translation_for(id, self.viewport, scroll);
|
||||||
|
(
|
||||||
|
transform.0 + sticky.0 - scroll.0,
|
||||||
|
transform.1 + sticky.1 - scroll.1,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
Some(
|
||||||
|
source
|
||||||
|
.into_iter()
|
||||||
|
.map(|rect| crate::Rect {
|
||||||
|
x: rect.x + movement.0,
|
||||||
|
y: rect.y + movement.1,
|
||||||
|
..rect
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn selected_image(
|
pub fn selected_image(
|
||||||
&self,
|
&self,
|
||||||
id: obscura_dom::tree::NodeId,
|
id: obscura_dom::tree::NodeId,
|
||||||
@@ -1183,7 +1252,16 @@ fn paint_laid_dom_scrolled(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if node.is_text() {
|
if node.is_text() {
|
||||||
|
if let Some(items) = laid.word_ifc_items.get(&nid) {
|
||||||
|
let offset = scroll_state.translation_for(laid, nid);
|
||||||
|
let clip = scroll_state.shaped_text_clip_for(laid, nid);
|
||||||
|
for &item in items {
|
||||||
|
laid.text_engine
|
||||||
|
.paint_item_with_clip(item, &mut pixmap, offset, clip);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
paint_text_node(tree, nid, laid, &scroll_state, &mut pixmap);
|
paint_text_node(tree, nid, laid, &scroll_state, &mut pixmap);
|
||||||
|
}
|
||||||
for generated in &generated_after_at[paint_index] {
|
for generated in &generated_after_at[paint_index] {
|
||||||
paint_in_flow_generated_box(
|
paint_in_flow_generated_box(
|
||||||
&mut pixmap,
|
&mut pixmap,
|
||||||
|
|||||||
@@ -96,6 +96,9 @@ def geometry_probe_javascript(selectors_expression):
|
|||||||
"margin_bottom:style.marginBottom,margin_left:style.marginLeft,"
|
"margin_bottom:style.marginBottom,margin_left:style.marginLeft,"
|
||||||
"padding_top:style.paddingTop,padding_right:style.paddingRight,"
|
"padding_top:style.paddingTop,padding_right:style.paddingRight,"
|
||||||
"padding_bottom:style.paddingBottom,padding_left:style.paddingLeft,"
|
"padding_bottom:style.paddingBottom,padding_left:style.paddingLeft,"
|
||||||
|
"font_family:style.fontFamily,font_size:style.fontSize,"
|
||||||
|
"font_weight:style.fontWeight,line_height:style.lineHeight,"
|
||||||
|
"letter_spacing:style.letterSpacing,white_space:style.whiteSpace,"
|
||||||
"flex_direction:style.flexDirection,"
|
"flex_direction:style.flexDirection,"
|
||||||
"align_items:style.alignItems,justify_content:style.justifyContent,"
|
"align_items:style.alignItems,justify_content:style.justifyContent,"
|
||||||
"text_align:style.textAlign,contain:style.contain,"
|
"text_align:style.textAlign,contain:style.contain,"
|
||||||
|
|||||||
@@ -344,6 +344,9 @@ class GeometryProbeTests(unittest.TestCase):
|
|||||||
self.assertEqual(args, (selectors,))
|
self.assertEqual(args, (selectors,))
|
||||||
self.assertIn("querySelectorAll(selector)", expression)
|
self.assertIn("querySelectorAll(selector)", expression)
|
||||||
self.assertIn("catch(error)", expression)
|
self.assertIn("catch(error)", expression)
|
||||||
|
self.assertIn("font_family:style.fontFamily", expression)
|
||||||
|
self.assertIn("line_height:style.lineHeight", expression)
|
||||||
|
self.assertIn("white_space:style.whiteSpace", expression)
|
||||||
self.assertIn("geometry_probes: geometryProbes", expression)
|
self.assertIn("geometry_probes: geometryProbes", expression)
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
f'sampled_phase: "{paired_corpus.CAPTURE_BOUNDARY_PHASE}"',
|
f'sampled_phase: "{paired_corpus.CAPTURE_BOUNDARY_PHASE}"',
|
||||||
|
|||||||
Reference in new issue
Block a user