diff --git a/library/src/layout/par.rs b/library/src/layout/par.rs index 2d7c5d625..244a61a95 100644 --- a/library/src/layout/par.rs +++ b/library/src/layout/par.rs @@ -20,7 +20,7 @@ use crate::text::{ /// /// ## Example /// ```example -/// #set par(indent: 1em, justify: true) +/// #set par(first-line-indent: 1em, justify: true) /// #show par: set block(spacing: 0.65em) /// /// We proceed by contradiction. @@ -39,17 +39,6 @@ use crate::text::{ /// Category: layout #[node(Construct)] pub struct ParNode { - /// The indent the first line of a consecutive paragraph should have. - /// - /// The first paragraph on a page will never be indented. - /// - /// By typographic convention, paragraph breaks are indicated by either some - /// space between paragraphs or indented first lines. Consider turning the - /// [paragraph spacing]($func/block.spacing) off when using this property - /// (e.g. using `[#show par: set block(spacing: 0pt)]`). - #[resolve] - pub indent: Length, - /// The spacing between lines. /// /// The default value is `{0.65em}`. @@ -95,6 +84,21 @@ pub struct ParNode { #[default] pub linebreaks: Smart, + /// The indent the first line of a consecutive paragraph should have. + /// + /// The first paragraph on a page will never be indented. + /// + /// By typographic convention, paragraph breaks are indicated by either some + /// space between paragraphs or indented first lines. Consider turning the + /// [paragraph spacing]($func/block.spacing) off when using this property + /// (e.g. using `[#show par: set block(spacing: 0pt)]`). + #[resolve] + pub first_line_indent: Length, + + /// The indent all but the first line of a paragraph should have. + #[resolve] + pub hanging_indent: Length, + /// The contents of the paragraph. #[external] pub body: Content, @@ -153,7 +157,7 @@ impl ParNode { let p = prepare(&mut vt, &children, &text, segments, spans, styles, region)?; // Break the paragraph into lines. - let lines = linebreak(&vt, &p, region.x); + let lines = linebreak(&vt, &p, region.x - p.hang); // Stack the lines into one frame per region. finalize(&mut vt, &p, &lines, region, expand) @@ -261,6 +265,8 @@ struct Preparation<'a> { align: Align, /// Whether to justify the paragraph. justify: bool, + /// The paragraph's hanging indent. + hang: Abs, } impl<'a> Preparation<'a> { @@ -509,8 +515,8 @@ fn collect<'a>( let mut iter = children.iter().peekable(); if consecutive { - let indent = ParNode::indent_in(*styles); - if !indent.is_zero() + let first_line_indent = ParNode::first_line_indent_in(*styles); + if !first_line_indent.is_zero() && children .iter() .find_map(|child| { @@ -527,10 +533,16 @@ fn collect<'a>( .unwrap_or_default() { full.push(SPACING_REPLACE); - segments.push((Segment::Spacing(indent.into()), *styles)); + segments.push((Segment::Spacing(first_line_indent.into()), *styles)); } } + let hang = ParNode::hanging_indent_in(*styles); + if !hang.is_zero() { + full.push(SPACING_REPLACE); + segments.push((Segment::Spacing((-hang).into()), *styles)); + } + while let Some(mut child) = iter.next() { let outer = styles; let mut styles = *styles; @@ -681,6 +693,7 @@ fn prepare<'a>( lang: shared_get(styles, children, TextNode::lang_in), align: AlignNode::alignment_in(styles).x.resolve(styles), justify: ParNode::justify_in(styles), + hang: ParNode::hanging_indent_in(styles), }) } @@ -1158,7 +1171,7 @@ fn finalize( let width = if !region.x.is_finite() || (!expand && lines.iter().all(|line| line.fr().is_zero())) { - lines.iter().map(|line| line.width).max().unwrap_or_default() + p.hang + lines.iter().map(|line| line.width).max().unwrap_or_default() } else { region.x }; @@ -1204,11 +1217,14 @@ fn commit( width: Abs, full: Abs, ) -> SourceResult { - let mut remaining = width - line.width; + let mut remaining = width - line.width - p.hang; let mut offset = Abs::zero(); // Reorder the line from logical to visual order. - let reordered = reorder(line); + let (reordered, starts_rtl) = reorder(line); + if !starts_rtl { + offset += p.hang; + } // Handle hanging punctuation to the left. if let Some(Item::Text(text)) = reordered.first() { @@ -1308,12 +1324,12 @@ fn commit( } /// Return a line's items in visual order. -fn reorder<'a>(line: &'a Line<'a>) -> Vec<&Item<'a>> { +fn reorder<'a>(line: &'a Line<'a>) -> (Vec<&Item<'a>>, bool) { let mut reordered = vec![]; // The bidi crate doesn't like empty lines. if line.trimmed.is_empty() { - return line.slice(line.trimmed.clone()).collect(); + return (line.slice(line.trimmed.clone()).collect(), false); } // Find the paragraph that contains the line. @@ -1326,6 +1342,7 @@ fn reorder<'a>(line: &'a Line<'a>) -> Vec<&Item<'a>> { // Compute the reordered ranges in visual order (left to right). let (levels, runs) = line.bidi.visual_runs(para, line.trimmed.clone()); + let starts_rtl = levels.first().map_or(false, |level| level.is_rtl()); // Collect the reordered items. for run in runs { @@ -1343,7 +1360,7 @@ fn reorder<'a>(line: &'a Line<'a>) -> Vec<&Item<'a>> { } } - reordered + (reordered, starts_rtl) } /// How much a character should hang into the end margin. diff --git a/library/src/layout/terms.rs b/library/src/layout/terms.rs index b2f45446f..853dd32dc 100644 --- a/library/src/layout/terms.rs +++ b/library/src/layout/terms.rs @@ -1,4 +1,5 @@ -use crate::layout::{BlockNode, GridLayouter, HNode, ParNode, Sizing, Spacing}; +use super::{HNode, VNode}; +use crate::layout::{BlockNode, ParNode, Spacing}; use crate::prelude::*; use crate::text::{SpaceNode, TextNode}; @@ -42,7 +43,6 @@ pub struct TermsNode { pub tight: bool, /// The indentation of each item's term. - #[resolve] pub indent: Length, /// The hanging indent of the description. @@ -52,7 +52,6 @@ pub struct TermsNode { /// / Term: This term list does not /// make use of hanging indents. /// ``` - #[resolve] #[default(Em::new(1.0).into())] pub hanging_indent: Length, @@ -85,7 +84,7 @@ impl Layout for TermsNode { regions: Regions, ) -> SourceResult { let indent = self.indent(styles); - let body_indent = self.hanging_indent(styles); + let hanging_indent = self.hanging_indent(styles); let gutter = if self.tight(styles) { ParNode::leading_in(styles).into() } else { @@ -93,29 +92,22 @@ impl Layout for TermsNode { .unwrap_or_else(|| BlockNode::below_in(styles).amount()) }; - let mut cells = vec![]; - for child in self.children() { - let body = Content::sequence(vec![ - HNode::new((-body_indent).into()).pack(), - (child.term() + TextNode::packed(':')).strong(), - SpaceNode::new().pack(), - child.description(), - ]); - - cells.push(Content::empty()); - cells.push(body); + let mut seq = vec![]; + for (i, child) in self.children().into_iter().enumerate() { + if i > 0 { + seq.push(VNode::new(gutter).with_weakness(1).pack()); + } + if indent.is_zero() { + seq.push(HNode::new(indent.into()).pack()); + } + seq.push((child.term() + TextNode::packed(':')).strong()); + seq.push(SpaceNode::new().pack()); + seq.push(child.description()); } - let layouter = GridLayouter::new( - vt, - Axes::with_x(&[Sizing::Rel((indent + body_indent).into()), Sizing::Auto]), - Axes::with_y(&[gutter.into()]), - &cells, - regions, - styles, - ); - - Ok(layouter.layout()?.fragment) + Content::sequence(seq) + .styled(ParNode::set_hanging_indent(hanging_indent + indent)) + .layout(vt, styles, regions) } } diff --git a/tests/ref/layout/par-indent.png b/tests/ref/layout/par-indent.png index 269c00248..6e5e25b66 100644 Binary files a/tests/ref/layout/par-indent.png and b/tests/ref/layout/par-indent.png differ diff --git a/tests/typ/layout/par-indent.typ b/tests/typ/layout/par-indent.typ index 0cb937bf6..4890e5dcf 100644 --- a/tests/typ/layout/par-indent.typ +++ b/tests/typ/layout/par-indent.typ @@ -1,7 +1,7 @@ // Test paragraph indent. --- -#set par(indent: 12pt, leading: 5pt) +#set par(first-line-indent: 12pt, leading: 5pt) #set block(spacing: 5pt) #show heading: set text(size: 10pt) @@ -31,7 +31,22 @@ starts a paragraph without indent. --- // This is madness. -#set par(indent: 12pt) +#set par(first-line-indent: 12pt) Why would anybody ever ... ... want spacing and indent? + +--- +// Test hanging indent. +#set par(hanging-indent: 15pt, justify: true) +#lorem(10) + +--- +#set par(hanging-indent: 1em) +Welcome \ here. Does this work well? + +--- +#set par(hanging-indent: 2em) +#set text(dir: rtl) +لآن وقد أظلم الليل وبدأت النجوم +تنضخ وجه الطبيعة التي أعْيَتْ من طول ما انبعثت في النهار diff --git a/tests/typ/layout/par-justify.typ b/tests/typ/layout/par-justify.typ index 5a9012d1d..24d3ab38d 100644 --- a/tests/typ/layout/par-justify.typ +++ b/tests/typ/layout/par-justify.typ @@ -2,7 +2,7 @@ --- #set page(width: 180pt) #set block(spacing: 5pt) -#set par(justify: true, indent: 14pt, leading: 5pt) +#set par(justify: true, first-line-indent: 14pt, leading: 5pt) This text is justified, meaning that spaces are stretched so that the text forms a "block" with flush edges at both sides.