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;
|
let _fpSeed = 0;
|
||||||
// Dynamic script import queue — serializes concurrent import() calls
|
// Dynamic module/in-order script queue. Module evaluation remains serialized
|
||||||
// to prevent re-entrant RefCell panic in deno_core's futures_unordered_driver
|
// to prevent a re-entrant RefCell panic in deno_core's
|
||||||
// when SPAs dynamically insert multiple <script module> tags at once.
|
// 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 __dynScriptQueue = [];
|
||||||
let __dynScriptBusy = false;
|
let __dynScriptBusy = false;
|
||||||
|
let __dynClassicPending = 0;
|
||||||
Object.defineProperty(globalThis, '__obscura_hasPendingDynamicScripts', {
|
Object.defineProperty(globalThis, '__obscura_hasPendingDynamicScripts', {
|
||||||
value: function() { return __dynScriptBusy || __dynScriptQueue.length > 0; },
|
value: function() {
|
||||||
|
return __dynClassicPending > 0 || __dynScriptBusy || __dynScriptQueue.length > 0;
|
||||||
|
},
|
||||||
writable: false,
|
writable: false,
|
||||||
enumerable: false,
|
enumerable: false,
|
||||||
configurable: false,
|
configurable: false,
|
||||||
@@ -251,15 +258,7 @@ function _decodeDataScriptUrl(url) {
|
|||||||
globalThis.__markParserScripts = function(nids) {
|
globalThis.__markParserScripts = function(nids) {
|
||||||
for (const nid of nids || []) Deno.core.ops.op_script_mark_started(+nid);
|
for (const nid of nids || []) Deno.core.ops.op_script_mark_started(+nid);
|
||||||
};
|
};
|
||||||
async function __processDynScriptQueue() {
|
async function __runDynScriptTask(task) {
|
||||||
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();
|
|
||||||
try {
|
try {
|
||||||
if (task.isModule) {
|
if (task.isModule) {
|
||||||
await import(task.url);
|
await import(task.url);
|
||||||
@@ -268,34 +267,64 @@ async function __processDynScriptQueue() {
|
|||||||
if (task.url.startsWith('data:')) {
|
if (task.url.startsWith('data:')) {
|
||||||
body = _decodeDataScriptUrl(task.url);
|
body = _decodeDataScriptUrl(task.url);
|
||||||
} else {
|
} 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);
|
const parsed = JSON.parse(raw);
|
||||||
// The HTML script-fetch algorithm treats an unsuccessful HTTP
|
// The HTML script-fetch algorithm treats an unsuccessful HTTP
|
||||||
// response as a network error. Evaluating its response body is both
|
// response as a network error. Evaluating its response body is both
|
||||||
// observably unlike browsers and dangerous: JSON error payloads
|
// observably unlike browsers and dangerous: JSON error payloads and
|
||||||
// become JavaScript source, while servers can return diagnostic
|
// diagnostic HTML must never become script source.
|
||||||
// HTML that mutates global state before throwing.
|
|
||||||
if (!(parsed.status >= 200 && parsed.status <= 299)) {
|
if (!(parsed.status >= 200 && parsed.status <= 299)) {
|
||||||
throw new Error('HTTP ' + (parsed.status || 0));
|
throw new Error('HTTP ' + (parsed.status || 0));
|
||||||
}
|
}
|
||||||
body = parsed.body;
|
body = parsed.body;
|
||||||
}
|
}
|
||||||
if (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;
|
globalThis.__currentScriptNid = task.nid;
|
||||||
try { (0, eval)(body); }
|
try { (0, eval)(body); }
|
||||||
catch(e) { console.error('Dynamic script error (' + task.url + '):', e.message); }
|
catch(e) { console.error('Dynamic script error (' + task.url + '):', e.message); }
|
||||||
finally { globalThis.__currentScriptNid = task.prevNid || 0; }
|
finally { globalThis.__currentScriptNid = task.prevNid || 0; }
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
if (_scheduleAfter(0, execute) === undefined) execute();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Fire load via dispatchEvent only: it invokes the element's onload
|
// Fire load via dispatchEvent only: it invokes the element's onload
|
||||||
// property handler and any addEventListener('load') listeners, read
|
// property handler and any addEventListener('load') listeners, read live
|
||||||
// live off the element. Calling onload separately would double-fire it.
|
// off the element. Calling onload separately would double-fire it.
|
||||||
try { task.dispatchEvent(new Event('load')); } catch(e) {}
|
try { task.dispatchEvent(new Event('load')); } catch(e) {}
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error('Dynamic script fetch error:', e.message);
|
console.error('Dynamic script fetch error:', e.message);
|
||||||
try { task.dispatchEvent(new Event('error')); } catch(ex) {}
|
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 {
|
} finally {
|
||||||
__dynScriptBusy = false;
|
__dynScriptBusy = false;
|
||||||
}
|
}
|
||||||
@@ -1290,15 +1319,27 @@ function __prepareInsertedScript(script) {
|
|||||||
fullUrl = src;
|
fullUrl = src;
|
||||||
}
|
}
|
||||||
const pageOrigin = (function() { try { return new URL(baseUrl).origin; } catch(e) { return ""; } })();
|
const pageOrigin = (function() { try { return new URL(baseUrl).origin; } catch(e) { return ""; } })();
|
||||||
__dynScriptQueue.push({
|
const task = {
|
||||||
url: fullUrl,
|
url: fullUrl,
|
||||||
isModule,
|
isModule,
|
||||||
nid: script._nid,
|
nid: script._nid,
|
||||||
prevNid,
|
prevNid,
|
||||||
pageOrigin,
|
pageOrigin,
|
||||||
dispatchEvent: (ev) => { try { script.dispatchEvent(ev); } catch(e) {} },
|
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();
|
__processDynScriptQueue();
|
||||||
|
}
|
||||||
} else if (isModule) {
|
} else if (isModule) {
|
||||||
const dataUrl = 'data:text/javascript;base64,' + btoa(unescape(encodeURIComponent(code)));
|
const dataUrl = 'data:text/javascript;base64,' + btoa(unescape(encodeURIComponent(code)));
|
||||||
__dynScriptQueue.push({
|
__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")]
|
#[tokio::test(flavor = "current_thread")]
|
||||||
async fn test_response_array_buffer_preserves_typed_array_view() {
|
async fn test_response_array_buffer_preserves_typed_array_view() {
|
||||||
let mut rt = setup_runtime("<html><body></body></html>");
|
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
|
/// nodes real geometry; `apply_float_continuations` uses it to narrow the
|
||||||
/// later intersecting bands before the final layout.
|
/// later intersecting bands before the final layout.
|
||||||
float_continuations: Vec<FloatContinuation>,
|
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
|
/// CSS multi-column containers built as a row of anonymous block
|
||||||
/// fragmentainers. All in-flow boxes start in the first fragmentainer so
|
/// fragmentainers. All in-flow boxes start in the first fragmentainer so
|
||||||
/// the preliminary layout measures them at the final column width; the
|
/// the preliminary layout measures them at the final column width; the
|
||||||
@@ -5490,12 +5496,6 @@ fn layout_dom_once(
|
|||||||
crate::Dimension::Px(w) => w,
|
crate::Dimension::Px(w) => w,
|
||||||
_ => max_c,
|
_ => 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
|
let available_outer = available_widths
|
||||||
.get(&tnode)
|
.get(&tnode)
|
||||||
.copied()
|
.copied()
|
||||||
@@ -5508,10 +5508,14 @@ fn layout_dom_once(
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.unwrap_or(initial_cb_width);
|
.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 {
|
if table_style.box_sizing == crate::BoxSizing::ContentBox {
|
||||||
(used_outer - inline_outer_edges).max(0.0)
|
(used_outer - inline_outer_edges).max(0.0)
|
||||||
} else {
|
} else {
|
||||||
@@ -5627,8 +5631,6 @@ fn layout_dom_once(
|
|||||||
let (horizontal_spacing, _) = table_spacing(table_style);
|
let (horizontal_spacing, _) = table_spacing(table_style);
|
||||||
let interior_spacing =
|
let interior_spacing =
|
||||||
horizontal_spacing * ncols.saturating_sub(1) as f32;
|
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
|
// A specified length on a cell/column is a preferred
|
||||||
// contribution, not a min-content floor. Preserve the
|
// contribution, not a min-content floor. Preserve the
|
||||||
// content minimum and raise only the preferred width so
|
// 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]);
|
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(
|
let widths = distribute_auto_table_columns(
|
||||||
target,
|
target,
|
||||||
&col_min,
|
&col_min,
|
||||||
@@ -8802,7 +8821,11 @@ fn build_in_flow_pseudo(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let children = content
|
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))
|
.map(|text| build_pseudo_content(host, text, pseudo, taffy_tree, words))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let mut taffy_style = to_taffy_style(pseudo);
|
let mut taffy_style = to_taffy_style(pseudo);
|
||||||
@@ -9350,6 +9373,48 @@ fn table_inline_outer_edges(style: &crate::LayoutStyle) -> f32 {
|
|||||||
+ spacing * 2.0
|
+ 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.
|
/// Resolve auto-table column constraints into final track widths.
|
||||||
///
|
///
|
||||||
/// Gecko's auto-table strategy uses four monotonic guesses: all columns at
|
/// 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"
|
let is_closed_html_details = _name.ns.as_ref() == "http://www.w3.org/1999/xhtml"
|
||||||
&& _name.local.as_ref() == "details"
|
&& _name.local.as_ref() == "details"
|
||||||
&& node.get_attribute("open").is_none();
|
&& 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 let Some(item) = engine.try_build(tree, id, styles) {
|
||||||
if style.display == crate::Display::Block && style.width == crate::Dimension::Auto {
|
if style.display == crate::Display::Block && style.width == crate::Dimension::Auto {
|
||||||
// A pure-text block is still a fill-available block. Its shaped
|
// 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
|
/// Native taffy floats are currently sound for a deliberately small structural
|
||||||
/// subset: a flat block band made only of definite-width direct floats plus
|
/// subset: a flat block band made from boxed direct floats, boxed block flow,
|
||||||
/// empty generated/direct clearance boxes. Text flow, transparent wrappers,
|
/// and empty generated/direct clearance boxes. Text nodes directly in the
|
||||||
/// shrink-to-fit floats, independent formatting contexts, and tables retain
|
/// parent, transparent wrappers, independent formatting contexts, and tables
|
||||||
/// the synthetic float-zone path.
|
/// retain the synthetic float-zone path.
|
||||||
fn can_use_native_float_band(
|
fn can_use_native_float_band(
|
||||||
tree: &DomTree,
|
tree: &DomTree,
|
||||||
parent_style: &crate::LayoutStyle,
|
parent_style: &crate::LayoutStyle,
|
||||||
@@ -12005,6 +12070,7 @@ fn can_use_native_float_band(
|
|||||||
let mut has_right = false;
|
let mut has_right = false;
|
||||||
let mut saw_float = false;
|
let mut saw_float = false;
|
||||||
let mut saw_clear_after_floats = 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 let Some(before) = parent_style.before_pseudo.as_deref() {
|
||||||
if !is_structural_native_float_pseudo(before) {
|
if !is_structural_native_float_pseudo(before) {
|
||||||
@@ -12031,12 +12097,13 @@ fn can_use_native_float_band(
|
|||||||
|
|
||||||
if let Some(side) = style.float {
|
if let Some(side) = style.float {
|
||||||
if saw_clear_after_floats
|
if saw_clear_after_floats
|
||||||
|
|| saw_flow_after_floats
|
||||||
|| style.display_contents
|
|| style.display_contents
|
||||||
|| style.is_table_box
|
|| style.is_table_box
|
||||||
|| matches!(style.position, Some(taffy::Position::Absolute))
|
|| matches!(style.position, Some(taffy::Position::Absolute))
|
||||||
|| !matches!(
|
|| !matches!(
|
||||||
style.width,
|
style.width,
|
||||||
crate::Dimension::Px(_) | crate::Dimension::Percent(_)
|
crate::Dimension::Auto | crate::Dimension::Px(_) | crate::Dimension::Percent(_)
|
||||||
)
|
)
|
||||||
|| style.size_expressions[0].is_some()
|
|| style.size_expressions[0].is_some()
|
||||||
|| has_deferred_or_auto_margin(style)
|
|| has_deferred_or_auto_margin(style)
|
||||||
@@ -12055,6 +12122,22 @@ fn can_use_native_float_band(
|
|||||||
has_left,
|
has_left,
|
||||||
has_right,
|
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 {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -12075,7 +12158,7 @@ fn can_use_native_float_band(
|
|||||||
// distinct taffy-side representation, so they are not an escape signal.
|
// distinct taffy-side representation, so they are not an escape signal.
|
||||||
let parent_is_native_bfc =
|
let parent_is_native_bfc =
|
||||||
parent_style.overflow_scroll_container && !parent_style.overflow_propagated_to_viewport;
|
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(
|
fn set_native_float_clear(
|
||||||
@@ -12138,6 +12221,12 @@ fn build_children_with_native_float_band(
|
|||||||
let Some(style) = styles.get(&id) else {
|
let Some(style) = styles.get(&id) else {
|
||||||
continue;
|
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(
|
for node in build_any(
|
||||||
tree, id, taffy_tree, id_map, words, engine, ifc_items, styles,
|
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]
|
#[test]
|
||||||
fn ineligible_nearest_inline_container_does_not_fall_through() {
|
fn ineligible_nearest_inline_container_does_not_fall_through() {
|
||||||
let tree = parse_html(
|
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]
|
#[test]
|
||||||
fn auto_table_percent_guess_reserves_intrinsic_neighbor_columns() {
|
fn auto_table_percent_guess_reserves_intrinsic_neighbor_columns() {
|
||||||
let widths = distribute_auto_table_columns(
|
let widths = distribute_auto_table_columns(
|
||||||
@@ -16115,6 +16290,23 @@ mod tests {
|
|||||||
assert!((widths[2] - 70.0).abs() < 0.01, "{widths:?}");
|
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]
|
#[test]
|
||||||
fn white_space_inherits_into_a_block_descendant_inline_context() {
|
fn white_space_inherits_into_a_block_descendant_inline_context() {
|
||||||
let tree = parse_html(
|
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.
|
/// One computed CSS counter operation.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct CounterDirective {
|
pub struct CounterDirective {
|
||||||
@@ -887,6 +924,9 @@ pub struct LayoutStyle {
|
|||||||
/// and color stops. It is painted below the first linear layer, matching
|
/// and color stops. It is painted below the first linear layer, matching
|
||||||
/// the common `linear-gradient(...), radial-gradient(...)` hero pattern.
|
/// the common `linear-gradient(...), radial-gradient(...)` hero pattern.
|
||||||
pub background_radial_gradient: Option<((f32, f32), Vec<([u8; 4], Option<f32>)>)>,
|
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,
|
/// `conic-gradient(...)` background. The angle is the CSS `from` angle,
|
||||||
/// the center is a fraction of the border box, and stops are normalized
|
/// the center is a fraction of the border box, and stops are normalized
|
||||||
/// during paint. Conic gradients commonly provide the color source for a
|
/// 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
|
/// Every parsed gradient in authored background-layer order. The legacy
|
||||||
/// single-kind fields above remain populated for mask/text fast paths.
|
/// single-kind fields above remain populated for mask/text fast paths.
|
||||||
pub background_gradient_layers: Vec<BackgroundGradientLayer>,
|
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`
|
/// The first `url(...)` reference from `background`/`background-image`
|
||||||
/// (gradients and repeat keywords in the same shorthand are ignored: we
|
/// (gradients and repeat keywords in the same shorthand are ignored: we
|
||||||
/// paint the referenced image, not the gradient layer).
|
/// paint the referenced image, not the gradient layer).
|
||||||
|
|||||||
@@ -2486,6 +2486,43 @@ fn stacking_z_index(
|
|||||||
None
|
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 {
|
fn has_authored_transform(style: &crate::LayoutStyle) -> bool {
|
||||||
!style.transform_ops.is_empty()
|
!style.transform_ops.is_empty()
|
||||||
|| style.individual_translate.is_some()
|
|| 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
|
// zero, so skip the per-node ancestor walk entirely and keep the paint
|
||||||
// path free of any added cost.
|
// path free of any added cost.
|
||||||
|
|
||||||
// Paint order: tree order for the normal flow (later elements paint over
|
// Paint order follows CSS2's stacking bands: in-flow block backgrounds
|
||||||
// earlier ones), except that a positioned element or a flex/grid item
|
// 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
|
// 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
|
// unit: negative layers paint under the normal flow, non-negative ones
|
||||||
// above it, each sorted by z-index ascending (stable, so equal z keeps
|
// 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.
|
// leaking into different global paint phases.
|
||||||
let mut neg_layers: Vec<(i32, Vec<obscura_dom::tree::NodeId>)> = Vec::new();
|
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 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 normal: Vec<obscura_dom::tree::NodeId> = Vec::new();
|
||||||
let mut consumed: std::collections::HashSet<obscura_dom::tree::NodeId> =
|
let mut consumed: std::collections::HashSet<obscura_dom::tree::NodeId> =
|
||||||
std::collections::HashSet::new();
|
std::collections::HashSet::new();
|
||||||
@@ -2648,6 +2691,7 @@ fn paint_laid_dom_scrolled(
|
|||||||
let z = (suppress_stacking_for != Some(nid))
|
let z = (suppress_stacking_for != Some(nid))
|
||||||
.then(|| stacking_z_index(tree, laid, nid))
|
.then(|| stacking_z_index(tree, laid, nid))
|
||||||
.flatten();
|
.flatten();
|
||||||
|
let is_float_root = paint_root != Some(nid) && is_effective_float(tree, laid, nid);
|
||||||
if is_opacity_root || is_transform_root {
|
if is_opacity_root || is_transform_root {
|
||||||
let mut sub = vec![nid];
|
let mut sub = vec![nid];
|
||||||
sub.extend(tree.descendants(nid));
|
sub.extend(tree.descendants(nid));
|
||||||
@@ -2659,6 +2703,7 @@ fn paint_laid_dom_scrolled(
|
|||||||
match z {
|
match z {
|
||||||
Some(z) if z < 0 => neg_layers.push((z, vec![nid])),
|
Some(z) if z < 0 => neg_layers.push((z, vec![nid])),
|
||||||
Some(z) => pos_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),
|
None => normal.push(nid),
|
||||||
}
|
}
|
||||||
} else if let Some(z) = z {
|
} else if let Some(z) = z {
|
||||||
@@ -2672,6 +2717,10 @@ fn paint_laid_dom_scrolled(
|
|||||||
} else {
|
} else {
|
||||||
pos_layers.push((z, vec![nid]));
|
pos_layers.push((z, vec![nid]));
|
||||||
}
|
}
|
||||||
|
} else if is_float_root {
|
||||||
|
consumed.insert(nid);
|
||||||
|
consumed.extend(tree.descendants(nid));
|
||||||
|
float_layers.push(nid);
|
||||||
} else {
|
} else {
|
||||||
normal.push(nid);
|
normal.push(nid);
|
||||||
}
|
}
|
||||||
@@ -2682,6 +2731,7 @@ fn paint_laid_dom_scrolled(
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.flat_map(|(_, sub)| sub)
|
.flat_map(|(_, sub)| sub)
|
||||||
.chain(normal)
|
.chain(normal)
|
||||||
|
.chain(float_layers)
|
||||||
.chain(pos_layers.into_iter().flat_map(|(_, sub)| sub))
|
.chain(pos_layers.into_iter().flat_map(|(_, sub)| sub))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -2778,6 +2828,39 @@ fn paint_laid_dom_scrolled(
|
|||||||
)?;
|
)?;
|
||||||
continue;
|
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) {
|
let node = match tree.get_node(nid) {
|
||||||
Some(n) => n,
|
Some(n) => n,
|
||||||
None => continue,
|
None => continue,
|
||||||
@@ -3238,6 +3321,10 @@ fn paint_laid_dom_scrolled(
|
|||||||
&background.origin_rect,
|
&background.origin_rect,
|
||||||
*center,
|
*center,
|
||||||
stops,
|
stops,
|
||||||
|
style.background_radial_gradient_geometry,
|
||||||
|
style.font_size.unwrap_or(16.0),
|
||||||
|
root_font_size,
|
||||||
|
viewport,
|
||||||
background_mask,
|
background_mask,
|
||||||
raster_scale,
|
raster_scale,
|
||||||
);
|
);
|
||||||
@@ -3284,6 +3371,10 @@ fn paint_laid_dom_scrolled(
|
|||||||
radius,
|
radius,
|
||||||
fill,
|
fill,
|
||||||
style.background_radial_gradient.as_ref(),
|
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_gradient.as_ref(),
|
||||||
style.background_conic_gradient.as_ref(),
|
style.background_conic_gradient.as_ref(),
|
||||||
style.mask_size,
|
style.mask_size,
|
||||||
@@ -3990,6 +4081,10 @@ fn paint_inline_fragment_decorations(
|
|||||||
&background_origin,
|
&background_origin,
|
||||||
*center,
|
*center,
|
||||||
stops,
|
stops,
|
||||||
|
fragment_style.background_radial_gradient_geometry,
|
||||||
|
fragment_style.font_size.unwrap_or(16.0),
|
||||||
|
root_font_size,
|
||||||
|
viewport,
|
||||||
background_mask.as_ref(),
|
background_mask.as_ref(),
|
||||||
raster_scale,
|
raster_scale,
|
||||||
);
|
);
|
||||||
@@ -4035,6 +4130,10 @@ fn paint_inline_fragment_decorations(
|
|||||||
radius,
|
radius,
|
||||||
fill,
|
fill,
|
||||||
fragment_style.background_radial_gradient.as_ref(),
|
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_gradient.as_ref(),
|
||||||
fragment_style.background_conic_gradient.as_ref(),
|
fragment_style.background_conic_gradient.as_ref(),
|
||||||
fragment_style.mask_size,
|
fragment_style.mask_size,
|
||||||
@@ -6383,6 +6482,10 @@ fn paint_radial_gradient(
|
|||||||
rect: &crate::Rect,
|
rect: &crate::Rect,
|
||||||
center: (f32, f32),
|
center: (f32, f32),
|
||||||
stops: &[([u8; 4], Option<f32>)],
|
stops: &[([u8; 4], Option<f32>)],
|
||||||
|
geometry: Option<crate::RadialGradientGeometry>,
|
||||||
|
em: f32,
|
||||||
|
root_font_size: f32,
|
||||||
|
viewport: (f32, f32),
|
||||||
clip: Option<&tiny_skia::Mask>,
|
clip: Option<&tiny_skia::Mask>,
|
||||||
raster_scale: f32,
|
raster_scale: f32,
|
||||||
) {
|
) {
|
||||||
@@ -6393,14 +6496,11 @@ fn paint_radial_gradient(
|
|||||||
rect.x + rect.width * center.0,
|
rect.x + rect.width * center.0,
|
||||||
rect.y + rect.height * center.1,
|
rect.y + rect.height * center.1,
|
||||||
);
|
);
|
||||||
let radius = [
|
let Some((radius_x, radius_y)) =
|
||||||
(rect.x - center.x).hypot(rect.y - center.y),
|
resolve_radial_gradient_radii(rect, center, geometry, em, root_font_size, viewport)
|
||||||
(rect.x + rect.width - center.x).hypot(rect.y - center.y),
|
else {
|
||||||
(rect.x - center.x).hypot(rect.y + rect.height - center.y),
|
return;
|
||||||
(rect.x + rect.width - center.x).hypot(rect.y + rect.height - center.y),
|
};
|
||||||
]
|
|
||||||
.into_iter()
|
|
||||||
.fold(0.0, f32::max);
|
|
||||||
let normalized = normalized_stops(stops);
|
let normalized = normalized_stops(stops);
|
||||||
let gradient_stops = normalized
|
let gradient_stops = normalized
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -6411,14 +6511,26 @@ fn paint_radial_gradient(
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect();
|
.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(
|
if let Some(shader) = RadialGradient::new(
|
||||||
center,
|
center,
|
||||||
0.0,
|
0.0,
|
||||||
center,
|
center,
|
||||||
radius,
|
radius_x,
|
||||||
gradient_stops,
|
gradient_stops,
|
||||||
SpreadMode::Pad,
|
SpreadMode::Pad,
|
||||||
Transform::identity(),
|
gradient_transform,
|
||||||
) {
|
) {
|
||||||
let mut paint = Paint::default();
|
let mut paint = Paint::default();
|
||||||
paint.shader = shader;
|
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(
|
fn paint_background_gradient_layers(
|
||||||
pixmap: &mut Pixmap,
|
pixmap: &mut Pixmap,
|
||||||
path: &tiny_skia::Path,
|
path: &tiny_skia::Path,
|
||||||
@@ -6484,6 +6671,7 @@ fn paint_background_gradient_layers(
|
|||||||
&tile_rect,
|
&tile_rect,
|
||||||
crate::ResolvedBorderRadii::default(),
|
crate::ResolvedBorderRadii::default(),
|
||||||
layers,
|
layers,
|
||||||
|
&style.background_gradient_layer_radial_geometries,
|
||||||
em,
|
em,
|
||||||
root_font_size,
|
root_font_size,
|
||||||
viewport,
|
viewport,
|
||||||
@@ -6578,6 +6766,7 @@ fn paint_background_gradient_layers(
|
|||||||
clip_rect,
|
clip_rect,
|
||||||
clip_radii,
|
clip_radii,
|
||||||
layers,
|
layers,
|
||||||
|
&style.background_gradient_layer_radial_geometries,
|
||||||
em,
|
em,
|
||||||
root_font_size,
|
root_font_size,
|
||||||
viewport,
|
viewport,
|
||||||
@@ -6593,6 +6782,7 @@ fn paint_gradient_layer_stack(
|
|||||||
clip_rect: &crate::Rect,
|
clip_rect: &crate::Rect,
|
||||||
clip_radii: crate::ResolvedBorderRadii,
|
clip_radii: crate::ResolvedBorderRadii,
|
||||||
layers: &[crate::BackgroundGradientLayer],
|
layers: &[crate::BackgroundGradientLayer],
|
||||||
|
radial_geometries: &[Option<crate::RadialGradientGeometry>],
|
||||||
em: f32,
|
em: f32,
|
||||||
root_font_size: f32,
|
root_font_size: f32,
|
||||||
viewport: (f32, 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
|
// CSS lists the topmost background first. Paint back-to-front so every
|
||||||
// translucent layer composites over the layers authored after it.
|
// 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 {
|
match layer {
|
||||||
crate::BackgroundGradientLayer::Linear {
|
crate::BackgroundGradientLayer::Linear {
|
||||||
angle,
|
angle,
|
||||||
@@ -6631,6 +6821,10 @@ fn paint_gradient_layer_stack(
|
|||||||
sampling_rect,
|
sampling_rect,
|
||||||
*center,
|
*center,
|
||||||
stops,
|
stops,
|
||||||
|
radial_geometries.get(index).copied().flatten(),
|
||||||
|
em,
|
||||||
|
root_font_size,
|
||||||
|
viewport,
|
||||||
clip,
|
clip,
|
||||||
raster_scale,
|
raster_scale,
|
||||||
);
|
);
|
||||||
@@ -6876,24 +7070,26 @@ fn radial_color_at(
|
|||||||
rect: &crate::Rect,
|
rect: &crate::Rect,
|
||||||
center: (f32, f32),
|
center: (f32, f32),
|
||||||
stops: &[(f32, [u8; 4])],
|
stops: &[(f32, [u8; 4])],
|
||||||
|
geometry: Option<crate::RadialGradientGeometry>,
|
||||||
|
em: f32,
|
||||||
|
root_font_size: f32,
|
||||||
|
viewport: (f32, f32),
|
||||||
x: f32,
|
x: f32,
|
||||||
y: f32,
|
y: f32,
|
||||||
) -> [u8; 4] {
|
) -> [u8; 4] {
|
||||||
let center_x = rect.x + rect.width * center.0;
|
let center_x = rect.x + rect.width * center.0;
|
||||||
let center_y = rect.y + rect.height * center.1;
|
let center_y = rect.y + rect.height * center.1;
|
||||||
let radius = [
|
let radii = resolve_radial_gradient_radii(
|
||||||
(rect.x - center_x).hypot(rect.y - center_y),
|
rect,
|
||||||
(rect.x + rect.width - center_x).hypot(rect.y - center_y),
|
Point::from_xy(center_x, center_y),
|
||||||
(rect.x - center_x).hypot(rect.y + rect.height - center_y),
|
geometry,
|
||||||
(rect.x + rect.width - center_x).hypot(rect.y + rect.height - center_y),
|
em,
|
||||||
]
|
root_font_size,
|
||||||
.into_iter()
|
viewport,
|
||||||
.fold(0.0, f32::max);
|
);
|
||||||
let position = if radius <= f32::EPSILON {
|
let position = radii.map_or(0.0, |(radius_x, radius_y)| {
|
||||||
0.0
|
(((x - center_x) / radius_x).powi(2) + ((y - center_y) / radius_y).powi(2)).sqrt()
|
||||||
} else {
|
});
|
||||||
(x - center_x).hypot(y - center_y) / radius
|
|
||||||
};
|
|
||||||
sample_normalized_stops(stops, position)
|
sample_normalized_stops(stops, position)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7171,6 +7367,10 @@ fn paint_in_flow_generated_box(
|
|||||||
&background.origin_rect,
|
&background.origin_rect,
|
||||||
*center,
|
*center,
|
||||||
stops,
|
stops,
|
||||||
|
style.background_radial_gradient_geometry,
|
||||||
|
style.font_size.unwrap_or(16.0),
|
||||||
|
root_font_size,
|
||||||
|
viewport,
|
||||||
background_mask.as_ref(),
|
background_mask.as_ref(),
|
||||||
raster_scale,
|
raster_scale,
|
||||||
);
|
);
|
||||||
@@ -7215,6 +7415,10 @@ fn paint_in_flow_generated_box(
|
|||||||
radius,
|
radius,
|
||||||
fill,
|
fill,
|
||||||
style.background_radial_gradient.as_ref(),
|
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_gradient.as_ref(),
|
||||||
style.background_conic_gradient.as_ref(),
|
style.background_conic_gradient.as_ref(),
|
||||||
style.mask_size,
|
style.mask_size,
|
||||||
@@ -7405,6 +7609,10 @@ fn paint_positioned_pseudo(
|
|||||||
&background.origin_rect,
|
&background.origin_rect,
|
||||||
*center,
|
*center,
|
||||||
stops,
|
stops,
|
||||||
|
style.background_radial_gradient_geometry,
|
||||||
|
em,
|
||||||
|
root_font_size,
|
||||||
|
viewport,
|
||||||
background_mask.as_ref(),
|
background_mask.as_ref(),
|
||||||
raster_scale,
|
raster_scale,
|
||||||
);
|
);
|
||||||
@@ -7449,6 +7657,10 @@ fn paint_positioned_pseudo(
|
|||||||
radius,
|
radius,
|
||||||
fill,
|
fill,
|
||||||
style.background_radial_gradient.as_ref(),
|
style.background_radial_gradient.as_ref(),
|
||||||
|
style.background_radial_gradient_geometry,
|
||||||
|
em,
|
||||||
|
root_font_size,
|
||||||
|
viewport,
|
||||||
style.background_gradient.as_ref(),
|
style.background_gradient.as_ref(),
|
||||||
style.background_conic_gradient.as_ref(),
|
style.background_conic_gradient.as_ref(),
|
||||||
style.mask_size,
|
style.mask_size,
|
||||||
@@ -9202,6 +9414,10 @@ fn paint_mask(
|
|||||||
border_radius: crate::ResolvedBorderRadii,
|
border_radius: crate::ResolvedBorderRadii,
|
||||||
fill: [u8; 4],
|
fill: [u8; 4],
|
||||||
radial_gradient: Option<&((f32, f32), Vec<([u8; 4], Option<f32>)>)>,
|
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>)>)>,
|
linear_gradient: Option<&(f32, Vec<([u8; 4], Option<f32>)>)>,
|
||||||
conic_gradient: Option<&(f32, (f32, f32), Vec<([u8; 4], Option<f32>)>)>,
|
conic_gradient: Option<&(f32, (f32, f32), Vec<([u8; 4], Option<f32>)>)>,
|
||||||
mask_size: Option<(f32, f32)>,
|
mask_size: Option<(f32, f32)>,
|
||||||
@@ -9270,7 +9486,17 @@ fn paint_mask(
|
|||||||
} else if let (Some((center, _)), Some(stops)) =
|
} else if let (Some((center, _)), Some(stops)) =
|
||||||
(radial_gradient, normalized_radial.as_deref())
|
(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 {
|
} else {
|
||||||
fill
|
fill
|
||||||
};
|
};
|
||||||
@@ -10105,6 +10331,54 @@ mod tests {
|
|||||||
assert_eq!(outside.blue(), 255);
|
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
|
/// Chromium 150 oracle for CSS Backgrounds box geometry. Transparent
|
||||||
/// borders make each clip edge directly observable, asymmetric insets
|
/// borders make each clip edge directly observable, asymmetric insets
|
||||||
/// distinguish authored gradient coordinates from the visible clip, and
|
/// 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]
|
#[test]
|
||||||
fn effectful_region_capture_keeps_the_proven_bounded_resample_fallback() {
|
fn effectful_region_capture_keeps_the_proven_bounded_resample_fallback() {
|
||||||
let tree = parse_html(
|
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" => {
|
"background-image" | "mask-image" | "-webkit-mask-image" => {
|
||||||
lower == "none"
|
lower == "none"
|
||||||
|| parse_url(value).is_some()
|
|| 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" => {
|
"background-repeat" | "mask-repeat" | "-webkit-mask-repeat" => {
|
||||||
parse_image_repeat(value).is_some()
|
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) {
|
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 {
|
style.background_gradient = layers.iter().find_map(|layer| match layer {
|
||||||
crate::BackgroundGradientLayer::Linear { angle, stops, .. } => {
|
crate::BackgroundGradientLayer::Linear { angle, stops, .. } => {
|
||||||
Some((*angle, stops.clone()))
|
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())),
|
crate::BackgroundGradientLayer::Radial { center, stops } => Some((*center, stops.clone())),
|
||||||
_ => None,
|
_ => 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 {
|
style.background_conic_gradient = layers.iter().find_map(|layer| match layer {
|
||||||
crate::BackgroundGradientLayer::Conic {
|
crate::BackgroundGradientLayer::Conic {
|
||||||
angle,
|
angle,
|
||||||
@@ -6748,13 +6758,18 @@ fn set_background_gradients(style: &mut LayoutStyle, value: &str) {
|
|||||||
_ => None,
|
_ => None,
|
||||||
});
|
});
|
||||||
style.background_gradient_layers = layers;
|
style.background_gradient_layers = layers;
|
||||||
|
style.background_gradient_layer_radial_geometries = radial_geometries;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_background_gradient_layers(
|
fn parse_background_gradient_layers(
|
||||||
value: &str,
|
value: &str,
|
||||||
dark_scheme: bool,
|
dark_scheme: bool,
|
||||||
) -> Vec<crate::BackgroundGradientLayer> {
|
) -> (
|
||||||
|
Vec<crate::BackgroundGradientLayer>,
|
||||||
|
Vec<Option<crate::RadialGradientGeometry>>,
|
||||||
|
) {
|
||||||
let mut layers = Vec::new();
|
let mut layers = Vec::new();
|
||||||
|
let mut radial_geometries = Vec::new();
|
||||||
for authored_layer in split_top_level(value, ',') {
|
for authored_layer in split_top_level(value, ',') {
|
||||||
if let Some(linear) = parse_linear_gradient(authored_layer, dark_scheme) {
|
if let Some(linear) = parse_linear_gradient(authored_layer, dark_scheme) {
|
||||||
layers.push(crate::BackgroundGradientLayer::Linear {
|
layers.push(crate::BackgroundGradientLayer::Linear {
|
||||||
@@ -6763,8 +6778,13 @@ fn parse_background_gradient_layers(
|
|||||||
stop_positions: linear.stop_positions,
|
stop_positions: linear.stop_positions,
|
||||||
repeating: linear.repeating,
|
repeating: linear.repeating,
|
||||||
});
|
});
|
||||||
} else if let Some((center, stops)) = parse_radial_gradient(authored_layer, dark_scheme) {
|
radial_geometries.push(None);
|
||||||
layers.push(crate::BackgroundGradientLayer::Radial { center, stops });
|
} 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)) =
|
} else if let Some((angle, center, stops)) =
|
||||||
parse_conic_gradient(authored_layer, dark_scheme)
|
parse_conic_gradient(authored_layer, dark_scheme)
|
||||||
{
|
{
|
||||||
@@ -6773,9 +6793,10 @@ fn parse_background_gradient_layers(
|
|||||||
center,
|
center,
|
||||||
stops,
|
stops,
|
||||||
});
|
});
|
||||||
|
radial_geometries.push(None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
layers
|
(layers, radial_geometries)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ParsedLinearGradient {
|
struct ParsedLinearGradient {
|
||||||
@@ -6938,10 +6959,13 @@ fn gradient_position_is_valid(value: &str) -> bool {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_radial_gradient(
|
struct ParsedRadialGradient {
|
||||||
value: &str,
|
center: (f32, f32),
|
||||||
dark_scheme: bool,
|
stops: Vec<([u8; 4], Option<f32>)>,
|
||||||
) -> Option<((f32, f32), 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 lower = value.to_ascii_lowercase();
|
||||||
let start = lower.find("radial-gradient(")?;
|
let start = lower.find("radial-gradient(")?;
|
||||||
let open = start + "radial-gradient(".len();
|
let open = start + "radial-gradient(".len();
|
||||||
@@ -6951,17 +6975,19 @@ fn parse_radial_gradient(
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let mut center = (0.5, 0.5);
|
let mut center = (0.5, 0.5);
|
||||||
|
let mut geometry = crate::RadialGradientGeometry::default();
|
||||||
let mut stop_start = 0;
|
let mut stop_start = 0;
|
||||||
let prelude = parts[0].trim().to_ascii_lowercase();
|
let prelude = parts[0].trim().to_ascii_lowercase();
|
||||||
if prelude.contains(" at ") || prelude.starts_with("at ") {
|
if prelude.contains(" at ") || prelude.starts_with("at ") {
|
||||||
let coords = prelude
|
let (shape, coords) = prelude
|
||||||
.split_once(" at ")
|
.split_once(" at ")
|
||||||
.map(|(_, coords)| coords)
|
.map(|(shape, coords)| (shape, coords))
|
||||||
.or_else(|| prelude.strip_prefix("at "))
|
.or_else(|| prelude.strip_prefix("at ").map(|coords| ("", coords)))?;
|
||||||
.unwrap_or_default();
|
geometry = parse_radial_gradient_geometry(shape)?;
|
||||||
center = parse_gradient_center(coords);
|
center = parse_gradient_center(coords);
|
||||||
stop_start = 1;
|
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;
|
stop_start = 1;
|
||||||
}
|
}
|
||||||
let mut stops = Vec::new();
|
let mut stops = Vec::new();
|
||||||
@@ -6971,7 +6997,131 @@ fn parse_radial_gradient(
|
|||||||
stops.push((color, position));
|
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) {
|
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]
|
#[test]
|
||||||
fn repeating_linear_gradient_retains_length_stops_and_repeat_axes() {
|
fn repeating_linear_gradient_retains_length_stops_and_repeat_axes() {
|
||||||
let style = compute_style(
|
let style = compute_style(
|
||||||
|
|||||||
@@ -939,6 +939,11 @@ fn float_flow_zone_preserves_blocks_inline_runs_and_clearance() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn float_exclusion_continues_through_non_bfc_block_wrappers() {
|
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!(
|
let tree = parse_html(include_str!(
|
||||||
"../../../render-repros/float-bfc-continuation.html"
|
"../../../render-repros/float-bfc-continuation.html"
|
||||||
));
|
));
|
||||||
@@ -959,21 +964,21 @@ fn float_exclusion_continues_through_non_bfc_block_wrappers() {
|
|||||||
assert!(
|
assert!(
|
||||||
(lead.x - 0.0).abs() < 0.01
|
(lead.x - 0.0).abs() < 0.01
|
||||||
&& (lead.y - 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.height - 250.0).abs() < 0.01,
|
||||||
"lead: {lead:?}"
|
"lead: {lead:?}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
(heading.x - 0.0).abs() < 0.01
|
(heading.x - 0.0).abs() < 0.01
|
||||||
&& (heading.y - 250.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.height - 50.0).abs() < 0.01,
|
||||||
"heading: {heading:?}"
|
"heading: {heading:?}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
(beside.x - 0.0).abs() < 0.01
|
(beside.x - 0.0).abs() < 0.01
|
||||||
&& (beside.y - 300.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.height - 100.0).abs() < 0.01,
|
||||||
"beside: {beside:?}"
|
"beside: {beside:?}"
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in new issue
Block a user