Improve float, gradient, and script rendering parity
This commit is contained in:
1 parent
0562fb4921
commit
03f506b01a
7 files changed
+974
-86
No files matched your search
Vendored
+62
-21
@@ -189,13 +189,20 @@ if (_origStackDesc && _origStackDesc.get) {
|
||||
}
|
||||
|
||||
let _fpSeed = 0;
|
||||
// Dynamic script import queue — serializes concurrent import() calls
|
||||
// to prevent re-entrant RefCell panic in deno_core's futures_unordered_driver
|
||||
// when SPAs dynamically insert multiple <script module> tags at once.
|
||||
// Dynamic module/in-order script queue. Module evaluation remains serialized
|
||||
// to prevent a re-entrant RefCell panic in deno_core's
|
||||
// futures_unordered_driver when SPAs insert multiple <script type=module>
|
||||
// elements at once. Ordinary dynamically inserted classic scripts are async
|
||||
// by default, so their fetches run independently and execute when ready just
|
||||
// like browser ScriptRunner tasks; serializing those fetches made unrelated
|
||||
// analytics/widgets form one long load-blocking waterfall.
|
||||
let __dynScriptQueue = [];
|
||||
let __dynScriptBusy = false;
|
||||
let __dynClassicPending = 0;
|
||||
Object.defineProperty(globalThis, '__obscura_hasPendingDynamicScripts', {
|
||||
value: function() { return __dynScriptBusy || __dynScriptQueue.length > 0; },
|
||||
value: function() {
|
||||
return __dynClassicPending > 0 || __dynScriptBusy || __dynScriptQueue.length > 0;
|
||||
},
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
@@ -251,15 +258,7 @@ function _decodeDataScriptUrl(url) {
|
||||
globalThis.__markParserScripts = function(nids) {
|
||||
for (const nid of nids || []) Deno.core.ops.op_script_mark_started(+nid);
|
||||
};
|
||||
async function __processDynScriptQueue() {
|
||||
if (__dynScriptBusy) return;
|
||||
__dynScriptBusy = true;
|
||||
// try/finally so the busy flag is always cleared even if a task throws
|
||||
// outside its own guard; otherwise the queue would wedge and silently
|
||||
// block every later dynamic script on the page.
|
||||
try {
|
||||
while (__dynScriptQueue.length > 0) {
|
||||
const task = __dynScriptQueue.shift();
|
||||
async function __runDynScriptTask(task) {
|
||||
try {
|
||||
if (task.isModule) {
|
||||
await import(task.url);
|
||||
@@ -268,34 +267,64 @@ async function __processDynScriptQueue() {
|
||||
if (task.url.startsWith('data:')) {
|
||||
body = _decodeDataScriptUrl(task.url);
|
||||
} else {
|
||||
const raw = await Deno.core.ops.op_fetch_url(task.url, "GET", "{}", "", task.pageOrigin, "no-cors", "same-origin");
|
||||
const raw = await Deno.core.ops.op_fetch_url(
|
||||
task.url, "GET", "{}", "", task.pageOrigin, "no-cors", "same-origin"
|
||||
);
|
||||
const parsed = JSON.parse(raw);
|
||||
// The HTML script-fetch algorithm treats an unsuccessful HTTP
|
||||
// response as a network error. Evaluating its response body is both
|
||||
// observably unlike browsers and dangerous: JSON error payloads
|
||||
// become JavaScript source, while servers can return diagnostic
|
||||
// HTML that mutates global state before throwing.
|
||||
// observably unlike browsers and dangerous: JSON error payloads and
|
||||
// diagnostic HTML must never become script source.
|
||||
if (!(parsed.status >= 200 && parsed.status <= 299)) {
|
||||
throw new Error('HTTP ' + (parsed.status || 0));
|
||||
}
|
||||
body = parsed.body;
|
||||
}
|
||||
if (body) {
|
||||
// A fetched async script is executed by a ScriptRunner task, not by
|
||||
// the fetch promise's microtask continuation. Besides matching event
|
||||
// loop ordering, this prevents a batch of concurrently completed
|
||||
// third-party scripts from being charged to (and pinning) whichever
|
||||
// parser script happened to trigger the microtask checkpoint.
|
||||
await new Promise(resolve => {
|
||||
const execute = () => {
|
||||
globalThis.__currentScriptNid = task.nid;
|
||||
try { (0, eval)(body); }
|
||||
catch(e) { console.error('Dynamic script error (' + task.url + '):', e.message); }
|
||||
finally { globalThis.__currentScriptNid = task.prevNid || 0; }
|
||||
resolve();
|
||||
};
|
||||
if (_scheduleAfter(0, execute) === undefined) execute();
|
||||
});
|
||||
}
|
||||
}
|
||||
// Fire load via dispatchEvent only: it invokes the element's onload
|
||||
// property handler and any addEventListener('load') listeners, read
|
||||
// live off the element. Calling onload separately would double-fire it.
|
||||
// property handler and any addEventListener('load') listeners, read live
|
||||
// off the element. Calling onload separately would double-fire it.
|
||||
try { task.dispatchEvent(new Event('load')); } catch(e) {}
|
||||
} catch(e) {
|
||||
console.error('Dynamic script fetch error:', e.message);
|
||||
try { task.dispatchEvent(new Event('error')); } catch(ex) {}
|
||||
}
|
||||
}
|
||||
async function __runAsyncClassicScript(task) {
|
||||
__dynClassicPending++;
|
||||
try {
|
||||
await __runDynScriptTask(task);
|
||||
} finally {
|
||||
__dynClassicPending--;
|
||||
}
|
||||
}
|
||||
async function __processDynScriptQueue() {
|
||||
if (__dynScriptBusy) return;
|
||||
__dynScriptBusy = true;
|
||||
// try/finally so the busy flag is always cleared even if a task throws
|
||||
// outside its own guard; otherwise the queue would wedge and silently
|
||||
// block every later module or explicitly in-order script on the page.
|
||||
try {
|
||||
while (__dynScriptQueue.length > 0) {
|
||||
await __runDynScriptTask(__dynScriptQueue.shift());
|
||||
}
|
||||
} finally {
|
||||
__dynScriptBusy = false;
|
||||
}
|
||||
@@ -1290,15 +1319,27 @@ function __prepareInsertedScript(script) {
|
||||
fullUrl = src;
|
||||
}
|
||||
const pageOrigin = (function() { try { return new URL(baseUrl).origin; } catch(e) { return ""; } })();
|
||||
__dynScriptQueue.push({
|
||||
const task = {
|
||||
url: fullUrl,
|
||||
isModule,
|
||||
nid: script._nid,
|
||||
prevNid,
|
||||
pageOrigin,
|
||||
dispatchEvent: (ev) => { try { script.dispatchEvent(ev); } catch(e) {} },
|
||||
});
|
||||
};
|
||||
// A non-parser-inserted classic script is force-async unless script code
|
||||
// explicitly assigned `.async = false`. Keep that opt-out in insertion
|
||||
// order; default/async=true scripts fetch concurrently and execute as soon
|
||||
// as each response is ready.
|
||||
const explicitlyInOrder = !isModule
|
||||
&& Object.prototype.hasOwnProperty.call(script, 'async')
|
||||
&& script.async === false;
|
||||
if (!isModule && !explicitlyInOrder) {
|
||||
__runAsyncClassicScript(task);
|
||||
} else {
|
||||
__dynScriptQueue.push(task);
|
||||
__processDynScriptQueue();
|
||||
}
|
||||
} else if (isModule) {
|
||||
const dataUrl = 'data:text/javascript;base64,' + btoa(unescape(encodeURIComponent(code)));
|
||||
__dynScriptQueue.push({
|
||||
|
||||
@@ -10730,6 +10730,64 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn dynamic_classic_scripts_are_async_by_default_but_honor_async_false_order() {
|
||||
let mut rt = setup_runtime("<html><head></head><body></body></html>");
|
||||
let result = rt
|
||||
.call_function_on_for_cdp(
|
||||
r#"async () => {
|
||||
const originalFetchOp = Deno.core.ops.op_fetch_url;
|
||||
const runPair = async (explicitlyInOrder) => {
|
||||
globalThis.__dynamicOrder = [];
|
||||
Deno.core.ops.op_fetch_url = (url) => new Promise(resolve => {
|
||||
const slow = url.includes("slow");
|
||||
setTimeout(() => resolve(JSON.stringify({
|
||||
status: 200,
|
||||
headers: {"content-type": "text/javascript"},
|
||||
body: `globalThis.__dynamicOrder.push("${slow ? "slow" : "fast"}")`,
|
||||
url,
|
||||
})), slow ? 30 : 1);
|
||||
});
|
||||
const load = name => new Promise(resolve => {
|
||||
const script = document.createElement("script");
|
||||
if (explicitlyInOrder) script.async = false;
|
||||
script.src = `/${name}.js`;
|
||||
script.onload = resolve;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
await Promise.all([load("slow"), load("fast")]);
|
||||
return globalThis.__dynamicOrder.slice();
|
||||
};
|
||||
try {
|
||||
const asyncOrder = await runPair(false);
|
||||
const inOrder = await runPair(true);
|
||||
return {
|
||||
asyncOrder,
|
||||
inOrder,
|
||||
pending: globalThis.__obscura_hasPendingDynamicScripts(),
|
||||
};
|
||||
} finally {
|
||||
Deno.core.ops.op_fetch_url = originalFetchOp;
|
||||
}
|
||||
}"#,
|
||||
None,
|
||||
&[],
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
result.value.unwrap(),
|
||||
serde_json::json!({
|
||||
"asyncOrder": ["fast", "slow"],
|
||||
"inOrder": ["slow", "fast"],
|
||||
"pending": false,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn test_response_array_buffer_preserves_typed_array_view() {
|
||||
let mut rt = setup_runtime("<html><body></body></html>");
|
||||
|
||||
@@ -1654,6 +1654,12 @@ struct IfcRegistry {
|
||||
/// nodes real geometry; `apply_float_continuations` uses it to narrow the
|
||||
/// later intersecting bands before the final layout.
|
||||
float_continuations: Vec<FloatContinuation>,
|
||||
/// Ordinary blocks participating in a native float band must keep a
|
||||
/// distinct inner line box. Collapsing the block and all of its text into
|
||||
/// one measured leaf gives Taffy no descendant at which the shared BFC
|
||||
/// float context can shorten the line while retaining the block's full
|
||||
/// border-box width.
|
||||
float_aware_blocks: HashSet<NodeId>,
|
||||
/// CSS multi-column containers built as a row of anonymous block
|
||||
/// fragmentainers. All in-flow boxes start in the first fragmentainer so
|
||||
/// the preliminary layout measures them at the final column width; the
|
||||
@@ -5490,12 +5496,6 @@ fn layout_dom_once(
|
||||
crate::Dimension::Px(w) => w,
|
||||
_ => max_c,
|
||||
};
|
||||
let used_outer = match width_style {
|
||||
// A definite table width is not clamped to its
|
||||
// containing block. Like other fixed boxes it may
|
||||
// overflow, while min-content can still make it wider.
|
||||
crate::Dimension::Px(_) => preferred_outer.max(min_c),
|
||||
_ => {
|
||||
let available_outer = available_widths
|
||||
.get(&tnode)
|
||||
.copied()
|
||||
@@ -5508,10 +5508,14 @@ fn layout_dom_once(
|
||||
)
|
||||
})
|
||||
.unwrap_or(initial_cb_width);
|
||||
preferred_outer.max(min_c).min(available_outer.max(min_c))
|
||||
}
|
||||
let mut used_outer = match width_style {
|
||||
// A definite table width is not clamped to its
|
||||
// containing block. Like other fixed boxes it may
|
||||
// overflow, while min-content can still make it wider.
|
||||
crate::Dimension::Px(_) => preferred_outer.max(min_c),
|
||||
_ => preferred_outer.max(min_c).min(available_outer.max(min_c)),
|
||||
};
|
||||
let used_declaration =
|
||||
let mut used_declaration =
|
||||
if table_style.box_sizing == crate::BoxSizing::ContentBox {
|
||||
(used_outer - inline_outer_edges).max(0.0)
|
||||
} else {
|
||||
@@ -5627,8 +5631,6 @@ fn layout_dom_once(
|
||||
let (horizontal_spacing, _) = table_spacing(table_style);
|
||||
let interior_spacing =
|
||||
horizontal_spacing * ncols.saturating_sub(1) as f32;
|
||||
let target =
|
||||
(used_outer - inline_outer_edges - interior_spacing).max(0.0);
|
||||
// A specified length on a cell/column is a preferred
|
||||
// contribution, not a min-content floor. Preserve the
|
||||
// content minimum and raise only the preferred width so
|
||||
@@ -5655,6 +5657,23 @@ fn layout_dom_once(
|
||||
col_max[j] = width.max(col_min[j]);
|
||||
}
|
||||
}
|
||||
if !matches!(width_style, crate::Dimension::Px(_)) {
|
||||
let percentage_floor =
|
||||
auto_table_percentage_intrinsic_floor(&col_min, &percentages);
|
||||
let percentage_outer =
|
||||
percentage_floor + inline_outer_edges + interior_spacing;
|
||||
used_outer = used_outer
|
||||
.max(percentage_outer)
|
||||
.min(available_outer.max(min_c));
|
||||
used_declaration =
|
||||
if table_style.box_sizing == crate::BoxSizing::ContentBox {
|
||||
(used_outer - inline_outer_edges).max(0.0)
|
||||
} else {
|
||||
used_outer
|
||||
};
|
||||
}
|
||||
let target =
|
||||
(used_outer - inline_outer_edges - interior_spacing).max(0.0);
|
||||
let widths = distribute_auto_table_columns(
|
||||
target,
|
||||
&col_min,
|
||||
@@ -8802,7 +8821,11 @@ fn build_in_flow_pseudo(
|
||||
}
|
||||
|
||||
let children = content
|
||||
.filter(|text| !text.is_empty())
|
||||
// CSS table fixup discards whitespace-only text between table
|
||||
// structures. Clearfixes conventionally use `content:" "`, so
|
||||
// retaining that text as a measured leaf gives the otherwise-empty
|
||||
// generated table a spurious space glyph of intrinsic width.
|
||||
.filter(|text| !text.is_empty() && !(pseudo.is_table_box && text.trim().is_empty()))
|
||||
.map(|text| build_pseudo_content(host, text, pseudo, taffy_tree, words))
|
||||
.unwrap_or_default();
|
||||
let mut taffy_style = to_taffy_style(pseudo);
|
||||
@@ -9350,6 +9373,48 @@ fn table_inline_outer_edges(style: &crate::LayoutStyle) -> f32 {
|
||||
+ spacing * 2.0
|
||||
}
|
||||
|
||||
/// Minimum track width implied by percentage columns in an auto-width table.
|
||||
///
|
||||
/// Percentage table-cell widths participate in the table's intrinsic width.
|
||||
/// For example, if a 50% column has a 100px minimum next to 100px of auto
|
||||
/// columns, the table needs 200px before both constraints can hold. When the
|
||||
/// constraints cannot all fit (a 100% cell plus any non-empty neighbour), the
|
||||
/// intrinsic requirement is unbounded and the used width is capped by the
|
||||
/// available containing-block width. This is the behavior Bootstrap input
|
||||
/// groups rely on to make an otherwise auto-width `display:table` fill their
|
||||
/// form while reserving intrinsic space for narrow addon/button cells.
|
||||
fn auto_table_percentage_intrinsic_floor(minimums: &[f32], percentages: &[Option<f32>]) -> f32 {
|
||||
if minimums.len() != percentages.len() || minimums.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let mut remaining = 1.0f32;
|
||||
let mut auto_minimum = 0.0f32;
|
||||
let mut required = 0.0f32;
|
||||
for (minimum, percentage) in minimums.iter().zip(percentages) {
|
||||
let minimum = minimum.max(0.0);
|
||||
let Some(percentage) = percentage else {
|
||||
auto_minimum += minimum;
|
||||
continue;
|
||||
};
|
||||
let effective = percentage.max(0.0).min(remaining);
|
||||
remaining = (remaining - effective).max(0.0);
|
||||
if minimum > 0.0 {
|
||||
if effective <= f32::EPSILON {
|
||||
return f32::INFINITY;
|
||||
}
|
||||
required = required.max(minimum / effective);
|
||||
}
|
||||
}
|
||||
if auto_minimum > 0.0 {
|
||||
if remaining <= f32::EPSILON {
|
||||
return f32::INFINITY;
|
||||
}
|
||||
required = required.max(auto_minimum / remaining);
|
||||
}
|
||||
required
|
||||
}
|
||||
|
||||
/// Resolve auto-table column constraints into final track widths.
|
||||
///
|
||||
/// Gecko's auto-table strategy uses four monotonic guesses: all columns at
|
||||
@@ -10719,7 +10784,7 @@ fn build(
|
||||
let is_closed_html_details = _name.ns.as_ref() == "http://www.w3.org/1999/xhtml"
|
||||
&& _name.local.as_ref() == "details"
|
||||
&& node.get_attribute("open").is_none();
|
||||
if !is_closed_html_details {
|
||||
if !is_closed_html_details && !ifc_items.float_aware_blocks.contains(&id) {
|
||||
if let Some(item) = engine.try_build(tree, id, styles) {
|
||||
if style.display == crate::Display::Block && style.width == crate::Dimension::Auto {
|
||||
// A pure-text block is still a fill-available block. Its shaped
|
||||
@@ -11976,10 +12041,10 @@ fn is_structural_native_float_pseudo(style: &crate::LayoutStyle) -> bool {
|
||||
}
|
||||
|
||||
/// Native taffy floats are currently sound for a deliberately small structural
|
||||
/// subset: a flat block band made only of definite-width direct floats plus
|
||||
/// empty generated/direct clearance boxes. Text flow, transparent wrappers,
|
||||
/// shrink-to-fit floats, independent formatting contexts, and tables retain
|
||||
/// the synthetic float-zone path.
|
||||
/// subset: a flat block band made from boxed direct floats, boxed block flow,
|
||||
/// and empty generated/direct clearance boxes. Text nodes directly in the
|
||||
/// parent, transparent wrappers, independent formatting contexts, and tables
|
||||
/// retain the synthetic float-zone path.
|
||||
fn can_use_native_float_band(
|
||||
tree: &DomTree,
|
||||
parent_style: &crate::LayoutStyle,
|
||||
@@ -12005,6 +12070,7 @@ fn can_use_native_float_band(
|
||||
let mut has_right = false;
|
||||
let mut saw_float = false;
|
||||
let mut saw_clear_after_floats = false;
|
||||
let mut saw_flow_after_floats = false;
|
||||
|
||||
if let Some(before) = parent_style.before_pseudo.as_deref() {
|
||||
if !is_structural_native_float_pseudo(before) {
|
||||
@@ -12031,12 +12097,13 @@ fn can_use_native_float_band(
|
||||
|
||||
if let Some(side) = style.float {
|
||||
if saw_clear_after_floats
|
||||
|| saw_flow_after_floats
|
||||
|| style.display_contents
|
||||
|| style.is_table_box
|
||||
|| matches!(style.position, Some(taffy::Position::Absolute))
|
||||
|| !matches!(
|
||||
style.width,
|
||||
crate::Dimension::Px(_) | crate::Dimension::Percent(_)
|
||||
crate::Dimension::Auto | crate::Dimension::Px(_) | crate::Dimension::Percent(_)
|
||||
)
|
||||
|| style.size_expressions[0].is_some()
|
||||
|| has_deferred_or_auto_margin(style)
|
||||
@@ -12055,6 +12122,22 @@ fn can_use_native_float_band(
|
||||
has_left,
|
||||
has_right,
|
||||
);
|
||||
} else if saw_float
|
||||
&& style.display == crate::Display::Block
|
||||
&& !style.display_contents
|
||||
&& !style.is_inline_block
|
||||
&& !style.is_table_box
|
||||
&& !style.internal_flex_container
|
||||
&& !matches!(style.position, Some(taffy::Position::Absolute))
|
||||
{
|
||||
// Gecko keeps an ordinary block's border box at the BFC's full
|
||||
// inline size and narrows only its descendant line boxes. A block
|
||||
// that establishes its own BFC (overflow/flow-root) instead moves
|
||||
// as one float-avoiding box. Taffy's native block float context
|
||||
// implements both branches; the old synthetic flex row could
|
||||
// represent neither distinction because it shrank every sibling's
|
||||
// outer box beside the float.
|
||||
saw_flow_after_floats = true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
@@ -12075,7 +12158,7 @@ fn can_use_native_float_band(
|
||||
// distinct taffy-side representation, so they are not an escape signal.
|
||||
let parent_is_native_bfc =
|
||||
parent_style.overflow_scroll_container && !parent_style.overflow_propagated_to_viewport;
|
||||
saw_float && (saw_clear_after_floats || parent_is_native_bfc)
|
||||
saw_float && (saw_flow_after_floats || saw_clear_after_floats || parent_is_native_bfc)
|
||||
}
|
||||
|
||||
fn set_native_float_clear(
|
||||
@@ -12138,6 +12221,12 @@ fn build_children_with_native_float_band(
|
||||
let Some(style) = styles.get(&id) else {
|
||||
continue;
|
||||
};
|
||||
if style.float.is_none()
|
||||
&& style.display == crate::Display::Block
|
||||
&& !establishes_block_formatting_context(style)
|
||||
{
|
||||
ifc_items.float_aware_blocks.insert(id);
|
||||
}
|
||||
for node in build_any(
|
||||
tree, id, taffy_tree, id_map, words, engine, ifc_items, styles,
|
||||
) {
|
||||
@@ -13634,6 +13723,62 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_float_keeps_normal_block_outer_width_and_shortens_its_line_band() {
|
||||
// Chromium 145 geometry for a Bootstrap-style single header float,
|
||||
// an ordinary full-width collapse block, and an overflow BFC below it.
|
||||
let tree = parse_html(
|
||||
r#"<style>
|
||||
*{box-sizing:border-box} html,body{margin:0}
|
||||
#parent{width:780px}
|
||||
#parent::before,#parent::after{display:table;content:" "}
|
||||
#parent::after{clear:both}
|
||||
#float{float:left;height:100px}
|
||||
#float::after{display:table;clear:both;content:" "}
|
||||
#brand{float:left;width:250px;height:100px}
|
||||
#flow{height:50px}
|
||||
#line{display:inline-block;width:400px;height:20px}
|
||||
#bfc{overflow:hidden;height:30px}
|
||||
</style>
|
||||
<div id="parent">
|
||||
<div id="float"><div id="brand"></div></div>
|
||||
<div id="flow"><span id="line"></span></div>
|
||||
<div id="bfc"></div>
|
||||
</div>"#,
|
||||
);
|
||||
let laid = layout_dom(&tree, (900.0, 300.0));
|
||||
let rect = |id: &str| laid.rects[&tree.get_element_by_id(id).unwrap()];
|
||||
let parent = rect("parent");
|
||||
let float = rect("float");
|
||||
let flow = rect("flow");
|
||||
let line = rect("line");
|
||||
let bfc = rect("bfc");
|
||||
|
||||
assert_eq!(
|
||||
(parent.x, parent.y, parent.width, parent.height),
|
||||
(0.0, 0.0, 780.0, 100.0)
|
||||
);
|
||||
assert_eq!(
|
||||
(float.x, float.y, float.width, float.height),
|
||||
(0.0, 0.0, 250.0, 100.0)
|
||||
);
|
||||
assert_eq!(
|
||||
(flow.x, flow.y, flow.width, flow.height),
|
||||
(0.0, 0.0, 780.0, 50.0),
|
||||
"a normal block border box spans beneath the float"
|
||||
);
|
||||
assert_eq!(
|
||||
(line.x, line.y, line.width, line.height),
|
||||
(250.0, 0.0, 400.0, 20.0),
|
||||
"inline content starts at the float band's available edge"
|
||||
);
|
||||
assert_eq!(
|
||||
(bfc.x, bfc.y, bfc.width, bfc.height),
|
||||
(250.0, 50.0, 530.0, 30.0),
|
||||
"a float-avoiding BFC moves and shrinks as one box"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ineligible_nearest_inline_container_does_not_fall_through() {
|
||||
let tree = parse_html(
|
||||
@@ -16101,6 +16246,36 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_width_authored_table_honors_percentage_cell_intrinsic_constraints() {
|
||||
let tree = parse_html(
|
||||
r#"<style>
|
||||
* { box-sizing:border-box }
|
||||
html,body { margin:0 }
|
||||
#host { width:600px }
|
||||
#group { display:table }
|
||||
#field,#button { display:table-cell; height:34px }
|
||||
#field { width:100% }
|
||||
#button { width:1%; min-width:100px }
|
||||
</style>
|
||||
<div id="host"><div id="group"><div id="field"></div><div id="button"></div></div></div>"#,
|
||||
);
|
||||
let laid = layout_dom(&tree, (800.0, 200.0));
|
||||
let rect = |id: &str| laid.rects[&tree.get_element_by_id(id).unwrap()];
|
||||
let group = rect("group");
|
||||
let field = rect("field");
|
||||
let button = rect("button");
|
||||
|
||||
// Chromium 145: group=600, field=500, button=100. An auto-width
|
||||
// table cannot shrink-wrap to 100px here: the 100% field and the
|
||||
// non-empty 1% neighbor make their percentage constraints fill the
|
||||
// definite available width.
|
||||
assert!((group.width - 600.0).abs() < 0.1, "{group:?}");
|
||||
assert!((field.width - 500.0).abs() < 0.1, "{field:?}");
|
||||
assert!((button.width - 100.0).abs() < 0.1, "{button:?}");
|
||||
assert!((button.x - (field.x + field.width)).abs() < 0.1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_table_percent_guess_reserves_intrinsic_neighbor_columns() {
|
||||
let widths = distribute_auto_table_columns(
|
||||
@@ -16115,6 +16290,23 @@ mod tests {
|
||||
assert!((widths[2] - 70.0).abs() < 0.01, "{widths:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_table_percentage_intrinsic_floor_matches_chromium_cases() {
|
||||
let percentages = [Some(0.1), None];
|
||||
assert!(
|
||||
(auto_table_percentage_intrinsic_floor(&[16.0, 144.0], &percentages) - 160.0).abs()
|
||||
< 0.01
|
||||
);
|
||||
let percentages = [Some(0.5), None];
|
||||
assert!(
|
||||
(auto_table_percentage_intrinsic_floor(&[16.0, 106.71875], &percentages) - 213.4375)
|
||||
.abs()
|
||||
< 0.01
|
||||
);
|
||||
let percentages = [Some(0.9), None];
|
||||
assert!(auto_table_percentage_intrinsic_floor(&[16.0, 106.71875], &percentages) > 1000.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_space_inherits_into_a_block_descendant_inline_context() {
|
||||
let tree = parse_html(
|
||||
|
||||
@@ -689,6 +689,43 @@ pub enum BackgroundGradientLayer {
|
||||
},
|
||||
}
|
||||
|
||||
/// Parsed ending-shape geometry for one CSS radial gradient.
|
||||
///
|
||||
/// This is kept alongside `BackgroundGradientLayer` rather than adding fields
|
||||
/// to its public `Radial` variant, preserving the existing construction API for
|
||||
/// embedders while allowing authored circle/ellipse sizing to survive to paint.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub(crate) struct RadialGradientGeometry {
|
||||
pub shape: RadialGradientShape,
|
||||
pub size: RadialGradientSize,
|
||||
}
|
||||
|
||||
impl Default for RadialGradientGeometry {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
shape: RadialGradientShape::Ellipse,
|
||||
size: RadialGradientSize::FarthestCorner,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum RadialGradientShape {
|
||||
Circle,
|
||||
Ellipse,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub(crate) enum RadialGradientSize {
|
||||
ClosestSide,
|
||||
ClosestCorner,
|
||||
FarthestSide,
|
||||
FarthestCorner,
|
||||
/// Horizontal and vertical radii. Percentages resolve against the
|
||||
/// corresponding axis of the gradient box, per CSS Images.
|
||||
Explicit(Dimension, Dimension),
|
||||
}
|
||||
|
||||
/// One computed CSS counter operation.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CounterDirective {
|
||||
@@ -887,6 +924,9 @@ pub struct LayoutStyle {
|
||||
/// and color stops. It is painted below the first linear layer, matching
|
||||
/// the common `linear-gradient(...), radial-gradient(...)` hero pattern.
|
||||
pub background_radial_gradient: Option<((f32, f32), Vec<([u8; 4], Option<f32>)>)>,
|
||||
/// Geometry paired with `background_radial_gradient`. The legacy public
|
||||
/// tuple above remains unchanged for API compatibility.
|
||||
pub(crate) background_radial_gradient_geometry: Option<RadialGradientGeometry>,
|
||||
/// `conic-gradient(...)` background. The angle is the CSS `from` angle,
|
||||
/// the center is a fraction of the border box, and stops are normalized
|
||||
/// during paint. Conic gradients commonly provide the color source for a
|
||||
@@ -895,6 +935,9 @@ pub struct LayoutStyle {
|
||||
/// Every parsed gradient in authored background-layer order. The legacy
|
||||
/// single-kind fields above remain populated for mask/text fast paths.
|
||||
pub background_gradient_layers: Vec<BackgroundGradientLayer>,
|
||||
/// One entry per `background_gradient_layers` item. Radial entries carry
|
||||
/// their authored ending shape; non-radial entries are `None`.
|
||||
pub(crate) background_gradient_layer_radial_geometries: Vec<Option<RadialGradientGeometry>>,
|
||||
/// The first `url(...)` reference from `background`/`background-image`
|
||||
/// (gradients and repeat keywords in the same shorthand are ignored: we
|
||||
/// paint the referenced image, not the gradient layer).
|
||||
|
||||
@@ -2486,6 +2486,43 @@ fn stacking_z_index(
|
||||
None
|
||||
}
|
||||
|
||||
/// Whether this box participates in the float painting band of its current
|
||||
/// stacking context. A retained `float` declaration has no effect on an
|
||||
/// absolutely positioned box or on a flex/grid item, matching layout's
|
||||
/// blockification rules.
|
||||
fn is_effective_float(
|
||||
tree: &DomTree,
|
||||
laid: &crate::DomLayout,
|
||||
id: obscura_dom::tree::NodeId,
|
||||
) -> bool {
|
||||
let Some(style) = laid.styles.get(&id) else {
|
||||
return false;
|
||||
};
|
||||
if style.float.is_none()
|
||||
|| style.display == crate::Display::None
|
||||
|| style.display_contents
|
||||
|| matches!(style.position, Some(taffy::Position::Absolute))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut parent = tree.get_node(id).and_then(|node| node.parent);
|
||||
while let Some(parent_id) = parent {
|
||||
let Some(parent_style) = laid.styles.get(&parent_id) else {
|
||||
parent = tree.get_node(parent_id).and_then(|node| node.parent);
|
||||
continue;
|
||||
};
|
||||
if parent_style.display_contents {
|
||||
parent = tree.get_node(parent_id).and_then(|node| node.parent);
|
||||
continue;
|
||||
}
|
||||
return parent_style.display != crate::Display::Grid
|
||||
&& !(parent_style.display == crate::Display::Flex
|
||||
&& !parent_style.internal_flex_container);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn has_authored_transform(style: &crate::LayoutStyle) -> bool {
|
||||
!style.transform_ops.is_empty()
|
||||
|| style.individual_translate.is_some()
|
||||
@@ -2618,8 +2655,13 @@ fn paint_laid_dom_scrolled(
|
||||
// zero, so skip the per-node ancestor walk entirely and keep the paint
|
||||
// path free of any added cost.
|
||||
|
||||
// Paint order: tree order for the normal flow (later elements paint over
|
||||
// earlier ones), except that a positioned element or a flex/grid item
|
||||
// Paint order follows CSS2's stacking bands: in-flow block backgrounds
|
||||
// and borders first, then non-positioned floats, then inline/text content
|
||||
// (painted by the later text pass). A float is an atomic paint unit in
|
||||
// this band, so its background, replaced content, and descendants cannot
|
||||
// be covered by a later full-width normal block background.
|
||||
//
|
||||
// A positioned element or a flex/grid item
|
||||
// with a non-auto z-index lifts its whole subtree into an atomic stacking
|
||||
// unit: negative layers paint under the normal flow, non-negative ones
|
||||
// above it, each sorted by z-index ascending (stable, so equal z keeps
|
||||
@@ -2628,6 +2670,7 @@ fn paint_laid_dom_scrolled(
|
||||
// leaking into different global paint phases.
|
||||
let mut neg_layers: Vec<(i32, Vec<obscura_dom::tree::NodeId>)> = Vec::new();
|
||||
let mut pos_layers: Vec<(i32, Vec<obscura_dom::tree::NodeId>)> = Vec::new();
|
||||
let mut float_layers: Vec<obscura_dom::tree::NodeId> = Vec::new();
|
||||
let mut normal: Vec<obscura_dom::tree::NodeId> = Vec::new();
|
||||
let mut consumed: std::collections::HashSet<obscura_dom::tree::NodeId> =
|
||||
std::collections::HashSet::new();
|
||||
@@ -2648,6 +2691,7 @@ fn paint_laid_dom_scrolled(
|
||||
let z = (suppress_stacking_for != Some(nid))
|
||||
.then(|| stacking_z_index(tree, laid, nid))
|
||||
.flatten();
|
||||
let is_float_root = paint_root != Some(nid) && is_effective_float(tree, laid, nid);
|
||||
if is_opacity_root || is_transform_root {
|
||||
let mut sub = vec![nid];
|
||||
sub.extend(tree.descendants(nid));
|
||||
@@ -2659,6 +2703,7 @@ fn paint_laid_dom_scrolled(
|
||||
match z {
|
||||
Some(z) if z < 0 => neg_layers.push((z, vec![nid])),
|
||||
Some(z) => pos_layers.push((z, vec![nid])),
|
||||
None if is_float_root => float_layers.push(nid),
|
||||
None => normal.push(nid),
|
||||
}
|
||||
} else if let Some(z) = z {
|
||||
@@ -2672,6 +2717,10 @@ fn paint_laid_dom_scrolled(
|
||||
} else {
|
||||
pos_layers.push((z, vec![nid]));
|
||||
}
|
||||
} else if is_float_root {
|
||||
consumed.insert(nid);
|
||||
consumed.extend(tree.descendants(nid));
|
||||
float_layers.push(nid);
|
||||
} else {
|
||||
normal.push(nid);
|
||||
}
|
||||
@@ -2682,6 +2731,7 @@ fn paint_laid_dom_scrolled(
|
||||
.into_iter()
|
||||
.flat_map(|(_, sub)| sub)
|
||||
.chain(normal)
|
||||
.chain(float_layers)
|
||||
.chain(pos_layers.into_iter().flat_map(|(_, sub)| sub))
|
||||
.collect();
|
||||
|
||||
@@ -2778,6 +2828,39 @@ fn paint_laid_dom_scrolled(
|
||||
)?;
|
||||
continue;
|
||||
}
|
||||
if paint_root != Some(nid) && is_effective_float(tree, laid, nid) {
|
||||
opacity_subtree_skip.insert(nid);
|
||||
opacity_subtree_skip.extend(tree.descendants(nid));
|
||||
// CSS paints each float as an atomic unit in the float band. The
|
||||
// recursive display-list build preserves the float's own stacking,
|
||||
// opacity, transforms, generated boxes, replaced content, and
|
||||
// inline text while keeping outer in-flow inline content above it.
|
||||
pixmap = paint_laid_dom_scrolled(
|
||||
tree,
|
||||
viewport,
|
||||
base_url,
|
||||
scroll,
|
||||
resolved_scroll,
|
||||
pixmap,
|
||||
image_cache,
|
||||
selected_images,
|
||||
svg_fonts,
|
||||
content_size,
|
||||
viewport_fixed,
|
||||
sticky,
|
||||
laid,
|
||||
Some(nid),
|
||||
suppress_opacity_for,
|
||||
suppress_stacking_for,
|
||||
suppress_transform_for,
|
||||
clip_scope_root,
|
||||
surface_extent,
|
||||
surface_offset,
|
||||
raster_scale,
|
||||
print_economy,
|
||||
)?;
|
||||
continue;
|
||||
}
|
||||
let node = match tree.get_node(nid) {
|
||||
Some(n) => n,
|
||||
None => continue,
|
||||
@@ -3238,6 +3321,10 @@ fn paint_laid_dom_scrolled(
|
||||
&background.origin_rect,
|
||||
*center,
|
||||
stops,
|
||||
style.background_radial_gradient_geometry,
|
||||
style.font_size.unwrap_or(16.0),
|
||||
root_font_size,
|
||||
viewport,
|
||||
background_mask,
|
||||
raster_scale,
|
||||
);
|
||||
@@ -3284,6 +3371,10 @@ fn paint_laid_dom_scrolled(
|
||||
radius,
|
||||
fill,
|
||||
style.background_radial_gradient.as_ref(),
|
||||
style.background_radial_gradient_geometry,
|
||||
style.font_size.unwrap_or(16.0),
|
||||
root_font_size,
|
||||
viewport,
|
||||
style.background_gradient.as_ref(),
|
||||
style.background_conic_gradient.as_ref(),
|
||||
style.mask_size,
|
||||
@@ -3990,6 +4081,10 @@ fn paint_inline_fragment_decorations(
|
||||
&background_origin,
|
||||
*center,
|
||||
stops,
|
||||
fragment_style.background_radial_gradient_geometry,
|
||||
fragment_style.font_size.unwrap_or(16.0),
|
||||
root_font_size,
|
||||
viewport,
|
||||
background_mask.as_ref(),
|
||||
raster_scale,
|
||||
);
|
||||
@@ -4035,6 +4130,10 @@ fn paint_inline_fragment_decorations(
|
||||
radius,
|
||||
fill,
|
||||
fragment_style.background_radial_gradient.as_ref(),
|
||||
fragment_style.background_radial_gradient_geometry,
|
||||
fragment_style.font_size.unwrap_or(16.0),
|
||||
root_font_size,
|
||||
viewport,
|
||||
fragment_style.background_gradient.as_ref(),
|
||||
fragment_style.background_conic_gradient.as_ref(),
|
||||
fragment_style.mask_size,
|
||||
@@ -6383,6 +6482,10 @@ fn paint_radial_gradient(
|
||||
rect: &crate::Rect,
|
||||
center: (f32, f32),
|
||||
stops: &[([u8; 4], Option<f32>)],
|
||||
geometry: Option<crate::RadialGradientGeometry>,
|
||||
em: f32,
|
||||
root_font_size: f32,
|
||||
viewport: (f32, f32),
|
||||
clip: Option<&tiny_skia::Mask>,
|
||||
raster_scale: f32,
|
||||
) {
|
||||
@@ -6393,14 +6496,11 @@ fn paint_radial_gradient(
|
||||
rect.x + rect.width * center.0,
|
||||
rect.y + rect.height * center.1,
|
||||
);
|
||||
let radius = [
|
||||
(rect.x - center.x).hypot(rect.y - center.y),
|
||||
(rect.x + rect.width - center.x).hypot(rect.y - center.y),
|
||||
(rect.x - center.x).hypot(rect.y + rect.height - center.y),
|
||||
(rect.x + rect.width - center.x).hypot(rect.y + rect.height - center.y),
|
||||
]
|
||||
.into_iter()
|
||||
.fold(0.0, f32::max);
|
||||
let Some((radius_x, radius_y)) =
|
||||
resolve_radial_gradient_radii(rect, center, geometry, em, root_font_size, viewport)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let normalized = normalized_stops(stops);
|
||||
let gradient_stops = normalized
|
||||
.into_iter()
|
||||
@@ -6411,14 +6511,26 @@ fn paint_radial_gradient(
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
// tiny-skia's native radial shader is circular. Keep the established CSS
|
||||
// coordinate-space shader and transform that circle around its center into
|
||||
// the authored ellipse. The paint transform later handles device scale.
|
||||
let ellipse_scale = radius_y / radius_x;
|
||||
let gradient_transform = Transform::from_row(
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
ellipse_scale,
|
||||
0.0,
|
||||
center.y * (1.0 - ellipse_scale),
|
||||
);
|
||||
if let Some(shader) = RadialGradient::new(
|
||||
center,
|
||||
0.0,
|
||||
center,
|
||||
radius,
|
||||
radius_x,
|
||||
gradient_stops,
|
||||
SpreadMode::Pad,
|
||||
Transform::identity(),
|
||||
gradient_transform,
|
||||
) {
|
||||
let mut paint = Paint::default();
|
||||
paint.shader = shader;
|
||||
@@ -6433,6 +6545,81 @@ fn paint_radial_gradient(
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_radial_gradient_radii(
|
||||
rect: &crate::Rect,
|
||||
center: Point,
|
||||
geometry: Option<crate::RadialGradientGeometry>,
|
||||
em: f32,
|
||||
root_font_size: f32,
|
||||
viewport: (f32, f32),
|
||||
) -> Option<(f32, f32)> {
|
||||
use crate::{RadialGradientShape as Shape, RadialGradientSize as Size};
|
||||
|
||||
let left = (center.x - rect.x).abs();
|
||||
let right = (rect.x + rect.width - center.x).abs();
|
||||
let top = (center.y - rect.y).abs();
|
||||
let bottom = (rect.y + rect.height - center.y).abs();
|
||||
|
||||
// Programmatically constructed legacy `BackgroundGradientLayer::Radial`
|
||||
// values have no sidecar geometry. Preserve their former circular
|
||||
// farthest-corner behavior exactly.
|
||||
let Some(geometry) = geometry else {
|
||||
let radius = [
|
||||
left.hypot(top),
|
||||
right.hypot(top),
|
||||
left.hypot(bottom),
|
||||
right.hypot(bottom),
|
||||
]
|
||||
.into_iter()
|
||||
.fold(0.0, f32::max);
|
||||
return (radius > f32::EPSILON).then_some((radius, radius));
|
||||
};
|
||||
|
||||
let (mut radius_x, mut radius_y) = match geometry.size {
|
||||
Size::ClosestSide => (left.min(right), top.min(bottom)),
|
||||
Size::FarthestSide => (left.max(right), top.max(bottom)),
|
||||
Size::ClosestCorner => {
|
||||
let sqrt_two = 2.0f32.sqrt();
|
||||
(left.min(right) * sqrt_two, top.min(bottom) * sqrt_two)
|
||||
}
|
||||
Size::FarthestCorner => {
|
||||
let sqrt_two = 2.0f32.sqrt();
|
||||
(left.max(right) * sqrt_two, top.max(bottom) * sqrt_two)
|
||||
}
|
||||
Size::Explicit(x, y) => (
|
||||
resolve_radial_radius(x, rect.width, em, root_font_size, viewport)?,
|
||||
resolve_radial_radius(y, rect.height, em, root_font_size, viewport)?,
|
||||
),
|
||||
};
|
||||
if geometry.shape == Shape::Circle && !matches!(geometry.size, Size::Explicit(..)) {
|
||||
let radius = match geometry.size {
|
||||
Size::ClosestSide => radius_x.min(radius_y),
|
||||
Size::FarthestSide => radius_x.max(radius_y),
|
||||
Size::ClosestCorner => left.min(right).hypot(top.min(bottom)),
|
||||
Size::FarthestCorner => left.max(right).hypot(top.max(bottom)),
|
||||
Size::Explicit(..) => unreachable!(),
|
||||
};
|
||||
radius_x = radius;
|
||||
radius_y = radius;
|
||||
}
|
||||
(radius_x > f32::EPSILON && radius_y > f32::EPSILON).then_some((radius_x, radius_y))
|
||||
}
|
||||
|
||||
fn resolve_radial_radius(
|
||||
radius: crate::Dimension,
|
||||
percentage_basis: f32,
|
||||
em: f32,
|
||||
root_font_size: f32,
|
||||
viewport: (f32, f32),
|
||||
) -> Option<f32> {
|
||||
match radius.resolve(em, root_font_size, viewport.0 / 100.0, viewport.1 / 100.0) {
|
||||
crate::Dimension::Px(value) => Some(value),
|
||||
crate::Dimension::Percent(value) => Some(value * percentage_basis),
|
||||
_ => None,
|
||||
}
|
||||
.filter(|value| value.is_finite() && *value >= 0.0)
|
||||
}
|
||||
|
||||
fn paint_background_gradient_layers(
|
||||
pixmap: &mut Pixmap,
|
||||
path: &tiny_skia::Path,
|
||||
@@ -6484,6 +6671,7 @@ fn paint_background_gradient_layers(
|
||||
&tile_rect,
|
||||
crate::ResolvedBorderRadii::default(),
|
||||
layers,
|
||||
&style.background_gradient_layer_radial_geometries,
|
||||
em,
|
||||
root_font_size,
|
||||
viewport,
|
||||
@@ -6578,6 +6766,7 @@ fn paint_background_gradient_layers(
|
||||
clip_rect,
|
||||
clip_radii,
|
||||
layers,
|
||||
&style.background_gradient_layer_radial_geometries,
|
||||
em,
|
||||
root_font_size,
|
||||
viewport,
|
||||
@@ -6593,6 +6782,7 @@ fn paint_gradient_layer_stack(
|
||||
clip_rect: &crate::Rect,
|
||||
clip_radii: crate::ResolvedBorderRadii,
|
||||
layers: &[crate::BackgroundGradientLayer],
|
||||
radial_geometries: &[Option<crate::RadialGradientGeometry>],
|
||||
em: f32,
|
||||
root_font_size: f32,
|
||||
viewport: (f32, f32),
|
||||
@@ -6601,7 +6791,7 @@ fn paint_gradient_layer_stack(
|
||||
) {
|
||||
// CSS lists the topmost background first. Paint back-to-front so every
|
||||
// translucent layer composites over the layers authored after it.
|
||||
for layer in layers.iter().rev() {
|
||||
for (index, layer) in layers.iter().enumerate().rev() {
|
||||
match layer {
|
||||
crate::BackgroundGradientLayer::Linear {
|
||||
angle,
|
||||
@@ -6631,6 +6821,10 @@ fn paint_gradient_layer_stack(
|
||||
sampling_rect,
|
||||
*center,
|
||||
stops,
|
||||
radial_geometries.get(index).copied().flatten(),
|
||||
em,
|
||||
root_font_size,
|
||||
viewport,
|
||||
clip,
|
||||
raster_scale,
|
||||
);
|
||||
@@ -6876,24 +7070,26 @@ fn radial_color_at(
|
||||
rect: &crate::Rect,
|
||||
center: (f32, f32),
|
||||
stops: &[(f32, [u8; 4])],
|
||||
geometry: Option<crate::RadialGradientGeometry>,
|
||||
em: f32,
|
||||
root_font_size: f32,
|
||||
viewport: (f32, f32),
|
||||
x: f32,
|
||||
y: f32,
|
||||
) -> [u8; 4] {
|
||||
let center_x = rect.x + rect.width * center.0;
|
||||
let center_y = rect.y + rect.height * center.1;
|
||||
let radius = [
|
||||
(rect.x - center_x).hypot(rect.y - center_y),
|
||||
(rect.x + rect.width - center_x).hypot(rect.y - center_y),
|
||||
(rect.x - center_x).hypot(rect.y + rect.height - center_y),
|
||||
(rect.x + rect.width - center_x).hypot(rect.y + rect.height - center_y),
|
||||
]
|
||||
.into_iter()
|
||||
.fold(0.0, f32::max);
|
||||
let position = if radius <= f32::EPSILON {
|
||||
0.0
|
||||
} else {
|
||||
(x - center_x).hypot(y - center_y) / radius
|
||||
};
|
||||
let radii = resolve_radial_gradient_radii(
|
||||
rect,
|
||||
Point::from_xy(center_x, center_y),
|
||||
geometry,
|
||||
em,
|
||||
root_font_size,
|
||||
viewport,
|
||||
);
|
||||
let position = radii.map_or(0.0, |(radius_x, radius_y)| {
|
||||
(((x - center_x) / radius_x).powi(2) + ((y - center_y) / radius_y).powi(2)).sqrt()
|
||||
});
|
||||
sample_normalized_stops(stops, position)
|
||||
}
|
||||
|
||||
@@ -7171,6 +7367,10 @@ fn paint_in_flow_generated_box(
|
||||
&background.origin_rect,
|
||||
*center,
|
||||
stops,
|
||||
style.background_radial_gradient_geometry,
|
||||
style.font_size.unwrap_or(16.0),
|
||||
root_font_size,
|
||||
viewport,
|
||||
background_mask.as_ref(),
|
||||
raster_scale,
|
||||
);
|
||||
@@ -7215,6 +7415,10 @@ fn paint_in_flow_generated_box(
|
||||
radius,
|
||||
fill,
|
||||
style.background_radial_gradient.as_ref(),
|
||||
style.background_radial_gradient_geometry,
|
||||
style.font_size.unwrap_or(16.0),
|
||||
root_font_size,
|
||||
viewport,
|
||||
style.background_gradient.as_ref(),
|
||||
style.background_conic_gradient.as_ref(),
|
||||
style.mask_size,
|
||||
@@ -7405,6 +7609,10 @@ fn paint_positioned_pseudo(
|
||||
&background.origin_rect,
|
||||
*center,
|
||||
stops,
|
||||
style.background_radial_gradient_geometry,
|
||||
em,
|
||||
root_font_size,
|
||||
viewport,
|
||||
background_mask.as_ref(),
|
||||
raster_scale,
|
||||
);
|
||||
@@ -7449,6 +7657,10 @@ fn paint_positioned_pseudo(
|
||||
radius,
|
||||
fill,
|
||||
style.background_radial_gradient.as_ref(),
|
||||
style.background_radial_gradient_geometry,
|
||||
em,
|
||||
root_font_size,
|
||||
viewport,
|
||||
style.background_gradient.as_ref(),
|
||||
style.background_conic_gradient.as_ref(),
|
||||
style.mask_size,
|
||||
@@ -9202,6 +9414,10 @@ fn paint_mask(
|
||||
border_radius: crate::ResolvedBorderRadii,
|
||||
fill: [u8; 4],
|
||||
radial_gradient: Option<&((f32, f32), Vec<([u8; 4], Option<f32>)>)>,
|
||||
radial_geometry: Option<crate::RadialGradientGeometry>,
|
||||
em: f32,
|
||||
root_font_size: f32,
|
||||
viewport: (f32, f32),
|
||||
linear_gradient: Option<&(f32, Vec<([u8; 4], Option<f32>)>)>,
|
||||
conic_gradient: Option<&(f32, (f32, f32), Vec<([u8; 4], Option<f32>)>)>,
|
||||
mask_size: Option<(f32, f32)>,
|
||||
@@ -9270,7 +9486,17 @@ fn paint_mask(
|
||||
} else if let (Some((center, _)), Some(stops)) =
|
||||
(radial_gradient, normalized_radial.as_deref())
|
||||
{
|
||||
radial_color_at(rect, *center, stops, sample_x, sample_y)
|
||||
radial_color_at(
|
||||
rect,
|
||||
*center,
|
||||
stops,
|
||||
radial_geometry,
|
||||
em,
|
||||
root_font_size,
|
||||
viewport,
|
||||
sample_x,
|
||||
sample_y,
|
||||
)
|
||||
} else {
|
||||
fill
|
||||
};
|
||||
@@ -10105,6 +10331,54 @@ mod tests {
|
||||
assert_eq!(outside.blue(), 255);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn float_paints_above_normal_block_backgrounds_and_below_later_flow() {
|
||||
// Chromium 145: ordinary block border boxes remain full-width beneath
|
||||
// the float, but their backgrounds are in the lower block-background
|
||||
// paint band. The right float therefore stays visible through its
|
||||
// 80px height; the following block becomes visible across the full
|
||||
// width once it starts below the float.
|
||||
let tree = parse_html(
|
||||
r#"<style>
|
||||
html,body{margin:0}
|
||||
#bfc{display:flow-root;width:200px}
|
||||
#float{float:right;width:50px;height:80px;background:#1971c2}
|
||||
#lead{height:50px;background:#087f5b}
|
||||
#heading{height:10px;background:#e8590c}
|
||||
#beside{height:20px;background:#7048e8}
|
||||
#after{height:20px;background:#a61e4d}
|
||||
</style>
|
||||
<main id="bfc">
|
||||
<aside id="float"></aside>
|
||||
<div id="lead"></div>
|
||||
<div id="heading"></div>
|
||||
<div id="beside"></div>
|
||||
<div id="after"></div>
|
||||
</main>"#,
|
||||
);
|
||||
let pixmap = paint_dom(&tree, (200.0, 120.0), None).expect("float paint");
|
||||
let rgb = |x, y| {
|
||||
let pixel = pixmap.pixel(x, y).expect("sample pixel");
|
||||
(pixel.red(), pixel.green(), pixel.blue())
|
||||
};
|
||||
|
||||
assert_eq!(rgb(25, 25), (8, 127, 91), "left side keeps lead background");
|
||||
assert_eq!(rgb(25, 55), (232, 89, 12), "left side keeps heading background");
|
||||
assert_eq!(rgb(25, 70), (112, 72, 232), "left side keeps beside background");
|
||||
for y in [25, 55, 70] {
|
||||
assert_eq!(
|
||||
rgb(175, y),
|
||||
(25, 113, 194),
|
||||
"float must overlay the full-width block background at y={y}"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
rgb(175, 90),
|
||||
(166, 30, 77),
|
||||
"normal flow below the float paints across the full width"
|
||||
);
|
||||
}
|
||||
|
||||
/// Chromium 150 oracle for CSS Backgrounds box geometry. Transparent
|
||||
/// borders make each clip edge directly observable, asymmetric insets
|
||||
/// distinguish authored gradient coordinates from the visible clip, and
|
||||
@@ -13206,6 +13480,60 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_radial_ellipse_matches_chromium_axis_samples() {
|
||||
let tree = parse_html(
|
||||
r#"<html style="margin:0"><body style="margin:0">
|
||||
<div style="width:200px;height:100px;
|
||||
background:radial-gradient(50% 25% at 50% 50%,#fff 0%,#000 100%)">
|
||||
</div>
|
||||
</body></html>"#,
|
||||
);
|
||||
let pixmap = paint_dom(&tree, (200.0, 100.0), None).expect("pixmap");
|
||||
// Chromium 144 at DPR 1 yields R values 249, 126, 2, 127, and 5 at
|
||||
// these pixel centers. Allow a small raster-backend interpolation
|
||||
// tolerance while requiring both authored radii to control geometry.
|
||||
for ((x, y), chromium) in [
|
||||
((100, 50), 249i16),
|
||||
((150, 50), 126),
|
||||
((199, 50), 2),
|
||||
((100, 62), 127),
|
||||
((100, 74), 5),
|
||||
] {
|
||||
let pixel = pixmap.pixel(x, y).expect("sample pixel");
|
||||
let actual = pixel.red() as i16;
|
||||
assert!(
|
||||
(actual - chromium).abs() <= 5,
|
||||
"ellipse sample ({x},{y}) was {actual}, Chromium was {chromium}: {pixel:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn radial_extent_keywords_keep_circle_and_ellipse_geometry_distinct() {
|
||||
let tree = parse_html(
|
||||
r#"<html style="margin:0"><body style="margin:0;display:flex">
|
||||
<div style="width:200px;height:100px;
|
||||
background:radial-gradient(circle farthest-side at 25% 50%,#fff,#000)">
|
||||
</div>
|
||||
<div style="width:200px;height:100px;
|
||||
background:radial-gradient(ellipse farthest-side at 25% 50%,#fff,#000)">
|
||||
</div>
|
||||
</body></html>"#,
|
||||
);
|
||||
let pixmap = paint_dom(&tree, (400.0, 100.0), None).expect("pixmap");
|
||||
let circle_bottom = pixmap.pixel(50, 99).expect("circle sample").red();
|
||||
let ellipse_bottom = pixmap.pixel(250, 99).expect("ellipse sample").red();
|
||||
assert!(
|
||||
circle_bottom > 150,
|
||||
"farthest-side circle radius is 150px, so its bottom remains light: {circle_bottom}"
|
||||
);
|
||||
assert!(
|
||||
ellipse_bottom < 10,
|
||||
"farthest-side ellipse vertical radius is 50px: {ellipse_bottom}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effectful_region_capture_keeps_the_proven_bounded_resample_fallback() {
|
||||
let tree = parse_html(
|
||||
|
||||
@@ -2596,7 +2596,7 @@ fn supports_conservative_known_value(name: &str, value: &str) -> bool {
|
||||
"background-image" | "mask-image" | "-webkit-mask-image" => {
|
||||
lower == "none"
|
||||
|| parse_url(value).is_some()
|
||||
|| !parse_background_gradient_layers(value, false).is_empty()
|
||||
|| !parse_background_gradient_layers(value, false).0.is_empty()
|
||||
}
|
||||
"background-repeat" | "mask-repeat" | "-webkit-mask-repeat" => {
|
||||
parse_image_repeat(value).is_some()
|
||||
@@ -6728,7 +6728,8 @@ fn token(value: &str) -> Option<&str> {
|
||||
}
|
||||
|
||||
fn set_background_gradients(style: &mut LayoutStyle, value: &str) {
|
||||
let layers = parse_background_gradient_layers(value, style.color_scheme_dark);
|
||||
let (layers, radial_geometries) =
|
||||
parse_background_gradient_layers(value, style.color_scheme_dark);
|
||||
style.background_gradient = layers.iter().find_map(|layer| match layer {
|
||||
crate::BackgroundGradientLayer::Linear { angle, stops, .. } => {
|
||||
Some((*angle, stops.clone()))
|
||||
@@ -6739,6 +6740,15 @@ fn set_background_gradients(style: &mut LayoutStyle, value: &str) {
|
||||
crate::BackgroundGradientLayer::Radial { center, stops } => Some((*center, stops.clone())),
|
||||
_ => None,
|
||||
});
|
||||
style.background_radial_gradient_geometry =
|
||||
layers
|
||||
.iter()
|
||||
.zip(&radial_geometries)
|
||||
.find_map(|(layer, geometry)| {
|
||||
matches!(layer, crate::BackgroundGradientLayer::Radial { .. })
|
||||
.then_some(*geometry)
|
||||
.flatten()
|
||||
});
|
||||
style.background_conic_gradient = layers.iter().find_map(|layer| match layer {
|
||||
crate::BackgroundGradientLayer::Conic {
|
||||
angle,
|
||||
@@ -6748,13 +6758,18 @@ fn set_background_gradients(style: &mut LayoutStyle, value: &str) {
|
||||
_ => None,
|
||||
});
|
||||
style.background_gradient_layers = layers;
|
||||
style.background_gradient_layer_radial_geometries = radial_geometries;
|
||||
}
|
||||
|
||||
fn parse_background_gradient_layers(
|
||||
value: &str,
|
||||
dark_scheme: bool,
|
||||
) -> Vec<crate::BackgroundGradientLayer> {
|
||||
) -> (
|
||||
Vec<crate::BackgroundGradientLayer>,
|
||||
Vec<Option<crate::RadialGradientGeometry>>,
|
||||
) {
|
||||
let mut layers = Vec::new();
|
||||
let mut radial_geometries = Vec::new();
|
||||
for authored_layer in split_top_level(value, ',') {
|
||||
if let Some(linear) = parse_linear_gradient(authored_layer, dark_scheme) {
|
||||
layers.push(crate::BackgroundGradientLayer::Linear {
|
||||
@@ -6763,8 +6778,13 @@ fn parse_background_gradient_layers(
|
||||
stop_positions: linear.stop_positions,
|
||||
repeating: linear.repeating,
|
||||
});
|
||||
} else if let Some((center, stops)) = parse_radial_gradient(authored_layer, dark_scheme) {
|
||||
layers.push(crate::BackgroundGradientLayer::Radial { center, stops });
|
||||
radial_geometries.push(None);
|
||||
} else if let Some(radial) = parse_radial_gradient(authored_layer, dark_scheme) {
|
||||
layers.push(crate::BackgroundGradientLayer::Radial {
|
||||
center: radial.center,
|
||||
stops: radial.stops,
|
||||
});
|
||||
radial_geometries.push(Some(radial.geometry));
|
||||
} else if let Some((angle, center, stops)) =
|
||||
parse_conic_gradient(authored_layer, dark_scheme)
|
||||
{
|
||||
@@ -6773,9 +6793,10 @@ fn parse_background_gradient_layers(
|
||||
center,
|
||||
stops,
|
||||
});
|
||||
radial_geometries.push(None);
|
||||
}
|
||||
}
|
||||
layers
|
||||
(layers, radial_geometries)
|
||||
}
|
||||
|
||||
struct ParsedLinearGradient {
|
||||
@@ -6938,10 +6959,13 @@ fn gradient_position_is_valid(value: &str) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_radial_gradient(
|
||||
value: &str,
|
||||
dark_scheme: bool,
|
||||
) -> Option<((f32, f32), Vec<([u8; 4], Option<f32>)>)> {
|
||||
struct ParsedRadialGradient {
|
||||
center: (f32, f32),
|
||||
stops: Vec<([u8; 4], Option<f32>)>,
|
||||
geometry: crate::RadialGradientGeometry,
|
||||
}
|
||||
|
||||
fn parse_radial_gradient(value: &str, dark_scheme: bool) -> Option<ParsedRadialGradient> {
|
||||
let lower = value.to_ascii_lowercase();
|
||||
let start = lower.find("radial-gradient(")?;
|
||||
let open = start + "radial-gradient(".len();
|
||||
@@ -6951,17 +6975,19 @@ fn parse_radial_gradient(
|
||||
return None;
|
||||
}
|
||||
let mut center = (0.5, 0.5);
|
||||
let mut geometry = crate::RadialGradientGeometry::default();
|
||||
let mut stop_start = 0;
|
||||
let prelude = parts[0].trim().to_ascii_lowercase();
|
||||
if prelude.contains(" at ") || prelude.starts_with("at ") {
|
||||
let coords = prelude
|
||||
let (shape, coords) = prelude
|
||||
.split_once(" at ")
|
||||
.map(|(_, coords)| coords)
|
||||
.or_else(|| prelude.strip_prefix("at "))
|
||||
.unwrap_or_default();
|
||||
.map(|(shape, coords)| (shape, coords))
|
||||
.or_else(|| prelude.strip_prefix("at ").map(|coords| ("", coords)))?;
|
||||
geometry = parse_radial_gradient_geometry(shape)?;
|
||||
center = parse_gradient_center(coords);
|
||||
stop_start = 1;
|
||||
} else if parse_color_for_scheme(parts[0].trim(), dark_scheme).is_none() {
|
||||
} else if parse_color_for_scheme(split_color_stop(parts[0].trim()).0, dark_scheme).is_none() {
|
||||
geometry = parse_radial_gradient_geometry(&prelude)?;
|
||||
stop_start = 1;
|
||||
}
|
||||
let mut stops = Vec::new();
|
||||
@@ -6971,7 +6997,131 @@ fn parse_radial_gradient(
|
||||
stops.push((color, position));
|
||||
}
|
||||
}
|
||||
(stops.len() >= 2).then_some((center, stops))
|
||||
(stops.len() >= 2).then_some(ParsedRadialGradient {
|
||||
center,
|
||||
stops,
|
||||
geometry,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_radial_gradient_geometry(value: &str) -> Option<crate::RadialGradientGeometry> {
|
||||
use crate::{
|
||||
RadialGradientGeometry as Geometry, RadialGradientShape as Shape,
|
||||
RadialGradientSize as Size,
|
||||
};
|
||||
|
||||
let tokens = value
|
||||
.split_whitespace()
|
||||
.map(str::trim)
|
||||
.filter(|token| !token.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
if tokens.is_empty() {
|
||||
return Some(Geometry::default());
|
||||
}
|
||||
|
||||
let explicit_shape = tokens.iter().find_map(|token| match *token {
|
||||
"circle" => Some(Shape::Circle),
|
||||
"ellipse" => Some(Shape::Ellipse),
|
||||
_ => None,
|
||||
});
|
||||
if tokens
|
||||
.iter()
|
||||
.filter(|token| matches!(**token, "circle" | "ellipse"))
|
||||
.count()
|
||||
> 1
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let extent = tokens.iter().find_map(|token| match *token {
|
||||
"closest-side" | "contain" => Some(Size::ClosestSide),
|
||||
"closest-corner" => Some(Size::ClosestCorner),
|
||||
"farthest-side" => Some(Size::FarthestSide),
|
||||
"farthest-corner" | "cover" => Some(Size::FarthestCorner),
|
||||
_ => None,
|
||||
});
|
||||
if tokens
|
||||
.iter()
|
||||
.filter(|token| {
|
||||
matches!(
|
||||
**token,
|
||||
"closest-side"
|
||||
| "closest-corner"
|
||||
| "farthest-side"
|
||||
| "farthest-corner"
|
||||
| "contain"
|
||||
| "cover"
|
||||
)
|
||||
})
|
||||
.count()
|
||||
> 1
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let dimensions = tokens
|
||||
.iter()
|
||||
.filter(|token| {
|
||||
!matches!(
|
||||
**token,
|
||||
"circle"
|
||||
| "ellipse"
|
||||
| "closest-side"
|
||||
| "closest-corner"
|
||||
| "farthest-side"
|
||||
| "farthest-corner"
|
||||
| "contain"
|
||||
| "cover"
|
||||
)
|
||||
})
|
||||
.map(|token| dimension_value(token))
|
||||
.collect::<Vec<_>>();
|
||||
if dimensions
|
||||
.iter()
|
||||
.any(|dimension| !radial_radius_is_non_negative(*dimension))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
if extent.is_some() && !dimensions.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
match dimensions.as_slice() {
|
||||
[] => Some(Geometry {
|
||||
shape: explicit_shape.unwrap_or(Shape::Ellipse),
|
||||
size: extent.unwrap_or(Size::FarthestCorner),
|
||||
}),
|
||||
[radius] if explicit_shape != Some(Shape::Ellipse) => {
|
||||
// Circle radii are lengths, never percentages.
|
||||
if matches!(radius, crate::Dimension::Percent(_)) {
|
||||
return None;
|
||||
}
|
||||
Some(Geometry {
|
||||
shape: Shape::Circle,
|
||||
size: Size::Explicit(*radius, *radius),
|
||||
})
|
||||
}
|
||||
[x, y] if explicit_shape != Some(Shape::Circle) => Some(Geometry {
|
||||
shape: Shape::Ellipse,
|
||||
size: Size::Explicit(*x, *y),
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn radial_radius_is_non_negative(value: crate::Dimension) -> bool {
|
||||
match value {
|
||||
crate::Dimension::Auto => false,
|
||||
crate::Dimension::Px(value)
|
||||
| crate::Dimension::Percent(value)
|
||||
| crate::Dimension::Em(value)
|
||||
| crate::Dimension::Ex(value)
|
||||
| crate::Dimension::Rem(value)
|
||||
| crate::Dimension::Vw(value)
|
||||
| crate::Dimension::Vh(value)
|
||||
| crate::Dimension::Vmin(value)
|
||||
| crate::Dimension::Vmax(value) => value.is_finite() && value >= 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_gradient_center(value: &str) -> (f32, f32) {
|
||||
@@ -9321,6 +9471,77 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn radial_gradients_retain_explicit_ellipse_radii_and_extent_shape() {
|
||||
let explicit = compute_style(
|
||||
"div",
|
||||
Some(
|
||||
"background:radial-gradient(141.53% 114.68% at 87.46% 55.27%,\
|
||||
#9a7cff 36.75%,#0e0aa200 100%)",
|
||||
),
|
||||
);
|
||||
assert_eq!(
|
||||
explicit.background_radial_gradient_geometry,
|
||||
Some(crate::RadialGradientGeometry {
|
||||
shape: crate::RadialGradientShape::Ellipse,
|
||||
size: crate::RadialGradientSize::Explicit(
|
||||
crate::Dimension::Percent(1.4153),
|
||||
crate::Dimension::Percent(1.1468),
|
||||
),
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
explicit
|
||||
.background_radial_gradient
|
||||
.as_ref()
|
||||
.map(|value| value.0),
|
||||
Some((0.8746, 0.5527))
|
||||
);
|
||||
|
||||
let keyword = compute_style(
|
||||
"div",
|
||||
Some("background-image:radial-gradient(circle closest-side at 25% 75%,red,blue)"),
|
||||
);
|
||||
assert_eq!(
|
||||
keyword.background_radial_gradient_geometry,
|
||||
Some(crate::RadialGradientGeometry {
|
||||
shape: crate::RadialGradientShape::Circle,
|
||||
size: crate::RadialGradientSize::ClosestSide,
|
||||
})
|
||||
);
|
||||
|
||||
let lengths = compute_style(
|
||||
"div",
|
||||
Some("background-image:radial-gradient(80px 2em at center,#fff 0%,#000 100%)"),
|
||||
);
|
||||
assert_eq!(
|
||||
lengths.background_radial_gradient_geometry,
|
||||
Some(crate::RadialGradientGeometry {
|
||||
shape: crate::RadialGradientShape::Ellipse,
|
||||
size: crate::RadialGradientSize::Explicit(
|
||||
crate::Dimension::Px(80.0),
|
||||
crate::Dimension::Em(2.0),
|
||||
),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn radial_gradient_rejects_invalid_shape_radius_combinations() {
|
||||
for value in [
|
||||
"radial-gradient(circle 50%,red,blue)",
|
||||
"radial-gradient(ellipse 10px,red,blue)",
|
||||
"radial-gradient(circle 10px 20px,red,blue)",
|
||||
"radial-gradient(-10px 20px,red,blue)",
|
||||
] {
|
||||
let style = compute_style("div", Some(&format!("background-image:{value}")));
|
||||
assert!(
|
||||
style.background_gradient_layers.is_empty(),
|
||||
"invalid radial geometry must not become a painted layer: {value}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeating_linear_gradient_retains_length_stops_and_repeat_axes() {
|
||||
let style = compute_style(
|
||||
|
||||
@@ -939,6 +939,11 @@ fn float_flow_zone_preserves_blocks_inline_runs_and_clearance() {
|
||||
|
||||
#[test]
|
||||
fn float_exclusion_continues_through_non_bfc_block_wrappers() {
|
||||
// Chromium keeps each ordinary block's outer box at the containing
|
||||
// block's full width. Only line boxes inside those blocks query the
|
||||
// float-reduced available band; backgrounds and borders extend beneath
|
||||
// the float. This is deliberately distinct from a new BFC, which avoids
|
||||
// the float as one box.
|
||||
let tree = parse_html(include_str!(
|
||||
"../../../render-repros/float-bfc-continuation.html"
|
||||
));
|
||||
@@ -959,21 +964,21 @@ fn float_exclusion_continues_through_non_bfc_block_wrappers() {
|
||||
assert!(
|
||||
(lead.x - 0.0).abs() < 0.01
|
||||
&& (lead.y - 0.0).abs() < 0.01
|
||||
&& (lead.width - 650.0).abs() < 0.01
|
||||
&& (lead.width - 900.0).abs() < 0.01
|
||||
&& (lead.height - 250.0).abs() < 0.01,
|
||||
"lead: {lead:?}"
|
||||
);
|
||||
assert!(
|
||||
(heading.x - 0.0).abs() < 0.01
|
||||
&& (heading.y - 250.0).abs() < 0.01
|
||||
&& (heading.width - 650.0).abs() < 0.01
|
||||
&& (heading.width - 900.0).abs() < 0.01
|
||||
&& (heading.height - 50.0).abs() < 0.01,
|
||||
"heading: {heading:?}"
|
||||
);
|
||||
assert!(
|
||||
(beside.x - 0.0).abs() < 0.01
|
||||
&& (beside.y - 300.0).abs() < 0.01
|
||||
&& (beside.width - 650.0).abs() < 0.01
|
||||
&& (beside.width - 900.0).abs() < 0.01
|
||||
&& (beside.height - 100.0).abs() < 0.01,
|
||||
"beside: {beside:?}"
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user