mirror of
https://github.com/lexmount/moli.git
synced 2026-10-07 08:01:00 +00:00
fix(layout): resolve paragraph and inline-object bidi directions
Provide the CSS paragraph direction to Parley, distinguish neutral objects from transparent inline boundaries, and use resolved item direction for inline static-position edges. Pin a minimal Parley 0.11.1 extension without upgrading the shaping pipeline. Add 34 Chromium-verified geometry cases and direction-restyle coverage.
This commit is contained in:
Generated
+4
-8
@@ -1347,8 +1347,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "fontique"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6688bc1294fe7117d788937b6c53480169b29c566954af490830d4c09da9516a"
|
||||
source = "git+https://github.com/ldm0/parley?rev=ad5b8d7a24c8827fae74ec430a8b2636381db8d6#ad5b8d7a24c8827fae74ec430a8b2636381db8d6"
|
||||
dependencies = [
|
||||
"hashbrown 0.17.1",
|
||||
"linebender_resource_handle",
|
||||
@@ -3722,14 +3721,12 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "parlance"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b6937eda350acc1a5d05872c3cbf99fe78619c269096e2be3d4a350058639d5"
|
||||
source = "git+https://github.com/ldm0/parley?rev=ad5b8d7a24c8827fae74ec430a8b2636381db8d6#ad5b8d7a24c8827fae74ec430a8b2636381db8d6"
|
||||
|
||||
[[package]]
|
||||
name = "parley"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22d2ff88bd3f7d68d1d9b09c7e6209f9a8e8c05088295140a2bcf2e9b17038c5"
|
||||
source = "git+https://github.com/ldm0/parley?rev=ad5b8d7a24c8827fae74ec430a8b2636381db8d6#ad5b8d7a24c8827fae74ec430a8b2636381db8d6"
|
||||
dependencies = [
|
||||
"fontique",
|
||||
"harfrust",
|
||||
@@ -3746,8 +3743,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "parley_data"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1567535334d6ba2d3cde19221ba9a7bd0fabb3cbd99046ddfb10ae061cfcc889"
|
||||
source = "git+https://github.com/ldm0/parley?rev=ad5b8d7a24c8827fae74ec430a8b2636381db8d6#ad5b8d7a24c8827fae74ec430a8b2636381db8d6"
|
||||
dependencies = [
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
@@ -87,6 +87,8 @@ exclude = ["vendor/xml5ever-0.39.0"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[patch.crates-io]
|
||||
# Keep the 0.11.1 shaping pipeline with explicit paragraph and inline-object bidi analysis.
|
||||
parley = { git = "https://github.com/ldm0/parley", rev = "ad5b8d7a24c8827fae74ec430a8b2636381db8d6" }
|
||||
xml5ever = { path = "vendor/xml5ever-0.39.0" }
|
||||
curl = { git = "https://github.com/ldm0/curl-rust", branch = "moli" }
|
||||
curl-sys = { git = "https://github.com/ldm0/curl-rust", branch = "moli" }
|
||||
|
||||
@@ -14,7 +14,10 @@ use std::{
|
||||
ops::Range,
|
||||
};
|
||||
|
||||
use parley::{BreakReason, InlineBox, InlineBoxKind, Layout, PositionedLayoutItem, TextStyle};
|
||||
use parley::{
|
||||
BaseDirection, BreakReason, InlineBox, InlineBoxBidi, InlineBoxKind, Layout,
|
||||
PositionedLayoutItem, TextStyle,
|
||||
};
|
||||
use taffy::{MaybeResolve as _, Point, Size};
|
||||
|
||||
use crate::{
|
||||
@@ -1751,6 +1754,19 @@ impl InlineBuildInput {
|
||||
1.0,
|
||||
quantize,
|
||||
);
|
||||
let root_style = &world.boxes[self.root_style.index()].style;
|
||||
// CSS supplies the paragraph direction except for plaintext, which
|
||||
// resolves its base direction from the first strong character.
|
||||
builder.set_base_direction(
|
||||
if root_style.unicode_bidi() == InlineUnicodeBidi::Plaintext {
|
||||
BaseDirection::Auto
|
||||
} else {
|
||||
match root_style.direction() {
|
||||
InlineDirection::Ltr => BaseDirection::Ltr,
|
||||
InlineDirection::Rtl => BaseDirection::Rtl,
|
||||
}
|
||||
},
|
||||
);
|
||||
let style_indices = styles
|
||||
.iter()
|
||||
.map(|style| builder.push_style(style.clone()))
|
||||
@@ -1763,14 +1779,24 @@ impl InlineBuildInput {
|
||||
builder.push_style_run(style_indices[*style_slot], range.clone());
|
||||
}
|
||||
}
|
||||
for (object_id, (byte_index, _, kind)) in self.objects.iter().enumerate() {
|
||||
builder.push_inline_box(InlineBox {
|
||||
id: u64::try_from(object_id).expect("one IFC exceeded the u64 object limit"),
|
||||
kind: *kind,
|
||||
index: *byte_index,
|
||||
width: 0.0,
|
||||
height: 0.0,
|
||||
});
|
||||
for (object_id, (byte_index, object, kind)) in self.objects.iter().enumerate() {
|
||||
let bidi = match object.role {
|
||||
InlineObjectRole::StartEdge => InlineBoxBidi::StartBoundary,
|
||||
InlineObjectRole::EndEdge => InlineBoxBidi::EndBoundary,
|
||||
InlineObjectRole::Atomic
|
||||
| InlineObjectRole::Float
|
||||
| InlineObjectRole::OutOfFlow(_) => InlineBoxBidi::Neutral,
|
||||
};
|
||||
builder.push_inline_box_with_bidi(
|
||||
InlineBox {
|
||||
id: u64::try_from(object_id).expect("one IFC exceeded the u64 object limit"),
|
||||
kind: *kind,
|
||||
index: *byte_index,
|
||||
width: 0.0,
|
||||
height: 0.0,
|
||||
},
|
||||
bidi,
|
||||
);
|
||||
}
|
||||
let layout = builder.build(&self.text);
|
||||
let font_metrics = styles
|
||||
|
||||
@@ -2771,6 +2771,15 @@ where
|
||||
.map(|placement| placement.item_offset(item_index))
|
||||
.unwrap_or_default();
|
||||
if let InlineObjectRole::OutOfFlow(display) = object.role {
|
||||
// Inline-level placeholders use the resolved item's bidi
|
||||
// direction, which can differ from the paragraph direction.
|
||||
// A block-level placeholder remains attached to the IFC's
|
||||
// inline-start edge (Blink LogicalLineBuilder).
|
||||
let direction = match display {
|
||||
OutOfFlowDisplay::Inline if positioned.is_rtl() => InlineDirection::Rtl,
|
||||
OutOfFlowDisplay::Inline => InlineDirection::Ltr,
|
||||
OutOfFlowDisplay::Block => container_direction,
|
||||
};
|
||||
let inline_offset = match display {
|
||||
OutOfFlowDisplay::Inline => positioned.x,
|
||||
OutOfFlowDisplay::Block => match container_direction {
|
||||
@@ -2787,7 +2796,7 @@ where
|
||||
x: content_offset.x + inline_offset,
|
||||
y: content_offset.y + positioned.y + vertical_offset,
|
||||
},
|
||||
direction: container_direction,
|
||||
direction,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -192,8 +192,9 @@ pub enum LayoutCapabilityDiagnostic {
|
||||
pub(crate) struct InlineStaticPosition {
|
||||
pub(crate) owner: LayoutBoxId,
|
||||
pub(crate) point: Point<f32>,
|
||||
/// Direction of the formatting context that produced the static point,
|
||||
/// which need not be the direction of the actual containing block.
|
||||
/// Resolved bidi direction for an inline-level placeholder, or the IFC's
|
||||
/// direction for a block-level placeholder. Neither is necessarily the
|
||||
/// direction of the actual containing block.
|
||||
pub(crate) direction: InlineDirection,
|
||||
}
|
||||
|
||||
|
||||
@@ -1053,6 +1053,77 @@ JSON.stringify(Array.from(document.querySelectorAll('.slot'), slot => {
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_renderer_resolves_css_paragraph_and_inline_object_directions() {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("current-thread runtime should build");
|
||||
runtime.block_on(tokio::task::LocalSet::new().run_until(async move {
|
||||
let mut page = parse_phase_one_html_into_page_vm_for_test_with_env(
|
||||
include_str!("../../../tests/fixtures/inline-bidi-objects.html"),
|
||||
default_test_page_vm_env_config_with(|env| {
|
||||
env.layout_policy = moli_page_types::LayoutPolicy::OnDemand;
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
// Exercise the initial build and rebuilding an existing IFC in
|
||||
// both directions. Only explicit captures publish new geometry.
|
||||
for direction in ["rtl", "ltr", "rtl"] {
|
||||
page.vm_mut()
|
||||
.eval(&format!(
|
||||
"{{ const slot = document.getElementById('dynamic-direction'); \
|
||||
slot.style.direction = '{direction}'; \
|
||||
slot.dataset.expected = '{}'; }}",
|
||||
if direction == "rtl" {
|
||||
"150,0,20,10"
|
||||
} else {
|
||||
"30,0,20,10"
|
||||
},
|
||||
))
|
||||
.expect("update paragraph direction");
|
||||
page.vm_mut().sync_live_document_style_sources();
|
||||
page.vm_mut()
|
||||
.screenshot_layout_snapshot(moli_layout::PaintViewport::new(800, 600, 1.0))
|
||||
.expect("bidi fixture layout should succeed")
|
||||
.expect("fixture should have a document element");
|
||||
let geometry = page
|
||||
.vm_mut()
|
||||
.eval(
|
||||
r#"
|
||||
JSON.stringify(Array.from(document.querySelectorAll('.slot'), slot => {
|
||||
const origin = slot.getBoundingClientRect();
|
||||
const item = slot.querySelector('.item').getBoundingClientRect();
|
||||
return {
|
||||
id: slot.id,
|
||||
actual: [item.x - origin.x, item.y - origin.y, item.width, item.height],
|
||||
expected: slot.dataset.expected.split(',').map(Number)
|
||||
};
|
||||
}))
|
||||
"#,
|
||||
)
|
||||
.expect("read published bidi geometry");
|
||||
let cases: serde_json::Value =
|
||||
serde_json::from_str(&geometry).expect("geometry JSON");
|
||||
let cases = cases.as_array().expect("geometry cases");
|
||||
assert_eq!(cases.len(), 34);
|
||||
for case in cases {
|
||||
for axis in 0..4 {
|
||||
let actual = case["actual"][axis].as_f64().expect("actual coordinate");
|
||||
let expected = case["expected"][axis]
|
||||
.as_f64()
|
||||
.expect("expected coordinate");
|
||||
assert!(
|
||||
(actual - expected).abs() <= 1.0 / 64.0,
|
||||
"{}[{axis}] after {direction}: expected {expected}, got {actual}",
|
||||
case["id"],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_renderer_preserves_calc_min_width_in_float_intrinsic_contribution() {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>CSS paragraph direction and inline object bidi ordering</title>
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; }
|
||||
/* Zero-sized glyphs retain bidi classes while making geometry independent
|
||||
of installed fonts. Atomic boxes give each nonempty line a fixed size. */
|
||||
.slot { position: relative; display: flow-root; width: 200px; height: 90px;
|
||||
direction: rtl; font-size: 0; line-height: 30px; }
|
||||
.ltr { direction: ltr; }
|
||||
.atom { display: inline-block; width: 30px; height: 10px; }
|
||||
.wide { width: 50px; }
|
||||
.item { position: absolute; display: inline; width: 20px; height: 10px; }
|
||||
.measured { position: static; display: inline-block; width: 20px; height: 10px; }
|
||||
.embed { direction: ltr; unicode-bidi: embed; }
|
||||
.isolate { direction: ltr; unicode-bidi: isolate; }
|
||||
.override { direction: ltr; unicode-bidi: bidi-override; }
|
||||
</style>
|
||||
<div class="slot" id="empty" data-expected="180,0,20,10"><span class="item"></span></div>
|
||||
<div class="slot" id="neutral-before" data-expected="180,0,20,10"><span class="item"></span><span class="atom"></span></div>
|
||||
<div class="slot" id="neutral-after" data-expected="150,0,20,10"><span class="atom"></span><span class="item"></span></div>
|
||||
<div class="slot" id="leading-object-before" data-expected="180,0,20,10"><span class="item"></span><span class="atom"></span>א</div>
|
||||
<div class="slot" id="leading-object-after" data-expected="150,0,20,10"><span class="atom"></span><span class="item"></span>א</div>
|
||||
<div class="slot" id="latin-first" data-expected="150,0,20,10">a<span class="atom"></span><span class="item"></span></div>
|
||||
<div class="slot" id="numeric-first" data-expected="150,0,20,10">12<span class="atom"></span><span class="item"></span></div>
|
||||
<div class="slot" id="neutral-first" data-expected="150,0,20,10">...<span class="atom"></span><span class="item"></span></div>
|
||||
<div class="slot ltr" id="hebrew-ltr-before" data-expected="0,0,20,10"><span class="item"></span><span class="atom"></span>א</div>
|
||||
<div class="slot ltr" id="hebrew-ltr-after" data-expected="30,0,20,10">א<span class="atom"></span><span class="item"></span></div>
|
||||
<div class="slot" id="latin-surrounded" data-expected="200,0,20,10">a<span class="atom"></span><span class="item"></span>b</div>
|
||||
<div class="slot ltr" id="hebrew-surrounded" data-expected="-20,0,20,10">א<span class="atom"></span><span class="item"></span>ב</div>
|
||||
<div class="slot" id="paragraph-edge-after-latin" data-expected="150,0,20,10">a<span class="atom"></span>b<span class="item"></span></div>
|
||||
<div class="slot" id="two-atoms" data-expected="100,0,20,10"><span class="atom"></span><span class="atom wide"></span><span class="item"></span></div>
|
||||
<div class="slot" id="middle-of-atoms" data-expected="150,0,20,10"><span class="atom"></span><span class="item"></span><span class="atom wide"></span></div>
|
||||
<div class="slot" id="measured-neutral" data-expected="150,5,20,10"><span class="atom"></span><span class="item measured"></span></div>
|
||||
<div class="slot" id="measured-latin" data-expected="180,5,20,10">a<span class="atom"></span><span class="item measured"></span>b</div>
|
||||
<div class="slot ltr" id="measured-hebrew" data-expected="0,5,20,10">א<span class="atom"></span><span class="item measured"></span>ב</div>
|
||||
<div class="slot" id="span-boundaries" data-expected="150,0,20,10"><span><span class="atom"></span></span><span class="item"></span></div>
|
||||
<div class="slot" id="empty-span-boundaries" data-expected="150,0,20,10"><span></span><span class="atom"></span><span></span><span class="item"></span></div>
|
||||
<div class="slot" id="embed-before" data-expected="170,0,20,10"><span class="embed"><span class="item"></span><span class="atom"></span></span></div>
|
||||
<div class="slot" id="embed-after" data-expected="200,0,20,10"><span class="embed"><span class="atom"></span><span class="item"></span></span></div>
|
||||
<div class="slot" id="isolate-before" data-expected="170,0,20,10"><span class="isolate"><span class="item"></span><span class="atom"></span></span></div>
|
||||
<div class="slot" id="isolate-after" data-expected="200,0,20,10"><span class="isolate"><span class="atom"></span><span class="item"></span></span></div>
|
||||
<div class="slot" id="override-after" data-expected="200,0,20,10"><span class="override">א<span class="atom"></span><span class="item"></span>ב</span></div>
|
||||
<div class="slot" id="opposite-placeholder-style" data-expected="150,0,20,10"><span class="atom"></span><span class="item" style="direction:ltr"></span></div>
|
||||
<div class="slot" id="block-placeholder" data-expected="180,0,20,10"><span class="embed"><span class="atom"></span><span class="item" style="display:block"></span></span></div>
|
||||
<div class="slot" id="after-hard-break" data-expected="150,30,20,10"><span class="atom"></span><br><span class="atom"></span><span class="item"></span></div>
|
||||
<div class="slot" id="wrap" style="width:50px" data-expected="0,30,20,10"><span class="atom"></span><span class="atom"></span><span class="item"></span></div>
|
||||
<div class="slot" id="text-align-left" style="text-align:left" data-expected="-20,0,20,10"><span class="atom"></span><span class="item"></span></div>
|
||||
<div class="slot" id="text-align-center" style="text-align:center" data-expected="65,0,20,10"><span class="atom"></span><span class="item"></span></div>
|
||||
<div class="slot" id="plaintext-latin" style="unicode-bidi:plaintext" data-expected="30,0,20,10">a<span class="atom"></span><span class="item"></span></div>
|
||||
<div class="slot ltr" id="plaintext-hebrew" style="unicode-bidi:plaintext" data-expected="150,0,20,10">א<span class="atom"></span><span class="item"></span></div>
|
||||
<div class="slot" id="dynamic-direction" data-expected="150,0,20,10"><span class="atom"></span><span class="item"></span></div>
|
||||
Reference in New Issue
Block a user