Merge pull request #1561 from Hamza0101/fix/rtl-first-line-indent

A right-to-left paragraph's first-line indent is at its right edge (#1448)
This commit is contained in:
Brandon Thomas
2026-10-23 17:29:17 -04:00
committed by GitHub
2 changed files with 49 additions and 1 deletions
+7 -1
View File
@@ -1676,10 +1676,16 @@ fn compose_with_db(doc: &Document, story: &Story, frames: &[FrameSpec], opts: &C
}
// A drop cap sits at the start-side indent; the first-line indent doesn't move it.
let beside = drop_cap.as_ref().filter(|dc| line_no < dc.lines && col_first_line == 0).map(DropCap::indent);
let first_line = if line_no == 0 { pp.first_line_indent } else { 0.0 };
let (ind_l, ind_r) = match beside {
Some(w) if rtl => (pp.left_indent, pp.right_indent + w),
Some(w) => (pp.left_indent + w, pp.right_indent),
None => (pp.left_indent + if line_no == 0 { pp.first_line_indent } else { 0.0 }, pp.right_indent),
// The first-line indent is at the paragraph's start side, which is the right
// one in a right-to-left paragraph: applied on the left it moved the line the
// wrong way, out of the frame for the hanging indent a numbered list uses
// (#1448).
None if rtl => (pp.left_indent, pp.right_indent + first_line),
None => (pp.left_indent + first_line, pp.right_indent),
};
let lx0 = x0 + ind_l;
let lx1 = x1 - ind_r;
+42
View File
@@ -7323,3 +7323,45 @@ fn parent_page_frames_know_which_side_of_the_spine_they_are_on() {
assert!(frame_specs(&d, left_story)[0].left_page);
assert!(!frame_specs(&d, right_story)[0].left_page);
}
/// A right-to-left paragraph's first-line indent belongs at its start side, the right. Applied on
/// the left it moved the first line the wrong way — out of the frame for the hanging indent a
/// numbered list uses (right indent 10 mm, first line −10 mm), while the wrapped lines sat where
/// the first line should have been (#1448).
#[test]
fn an_rtl_first_line_indent_is_at_the_right_edge() {
const MM: f64 = 72.0 / 25.4;
// Latin text in a right-to-left paragraph: the direction is the paragraph's, and it is the
// placement that is being measured, so no Arabic font has to be present.
let text = LOREM;
let rect = Rect::new(0.0, 0.0, 200.0, 300.0);
let para = |dir, align, first, left, right| ParaAttrs {
direction: Some(dir),
align: Some(align),
first_line_indent: Some(first),
left_indent: Some(left),
right_indent: Some(right),
..Default::default()
};
// The hanging indent of a numbered list, right to left.
let (d, sid, _) = doc_with(text, rect, para(designcraft_doc::TextDirection::RightToLeft, Align::Right, -10.0 * MM, 0.0, 10.0 * MM));
let cs = compose_story(&d, sid, &ComposeOptions::default());
let lines = all_lines(&cs);
assert!(lines.len() >= 2, "the text wraps: {}", lines.len());
let right_edge = |l: &Line| l.glyphs.iter().filter(|g| g.visible).map(|g| g.x + g.adv).fold(f64::MIN, f64::max);
let left_edge = |l: &Line| l.glyphs.iter().filter(|g| g.visible).map(|g| g.x).fold(f64::MAX, f64::min);
// The first line reaches the frame's right edge; the wrapped lines stop 10 mm short of it.
assert!((right_edge(lines[0]) - rect.x1).abs() < 1.0, "first line at the edge: {}", right_edge(lines[0]));
assert!((right_edge(lines[1]) - (rect.x1 - 10.0 * MM)).abs() < 1.0, "wrapped line indented: {}", right_edge(lines[1]));
// And nothing leaves the frame on the left, which is where the first line used to go.
for (i, l) in lines.iter().enumerate() {
assert!(left_edge(l) >= rect.x0 - 0.01, "line {i} starts inside the frame: {}", left_edge(l));
}
// The same paragraph left to right is unchanged: the first line hangs on the left.
let (d, sid, _) = doc_with(text, rect, para(designcraft_doc::TextDirection::LeftToRight, Align::Left, -10.0 * MM, 10.0 * MM, 0.0));
let cs = compose_story(&d, sid, &ComposeOptions::default());
let lines = all_lines(&cs);
assert!((left_edge(lines[0]) - rect.x0).abs() < 1.0, "left to right, first line at the edge: {}", left_edge(lines[0]));
assert!((left_edge(lines[1]) - (rect.x0 + 10.0 * MM)).abs() < 1.0, "left to right, wrapped line indented: {}", left_edge(lines[1]));
}