summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2023-03-14 22:30:21 +0100
committerLaurenz <laurmaedje@gmail.com>2023-03-14 22:30:21 +0100
commit2bacbaf2bdf93f5537040bdeecf8d73ec06f7eae (patch)
tree8cd688210530dedd6a42cce01629a67e255cdc46
parent724e9b140cc0a87208aa9c4914b1b8aeddf25c30 (diff)
Hanging indent
-rw-r--r--library/src/layout/par.rs61
-rw-r--r--library/src/layout/terms.rs42
-rw-r--r--tests/ref/layout/par-indent.pngbin47225 -> 63931 bytes
-rw-r--r--tests/typ/layout/par-indent.typ19
-rw-r--r--tests/typ/layout/par-justify.typ2
5 files changed, 74 insertions, 50 deletions
diff --git a/library/src/layout/par.rs b/library/src/layout/par.rs
index 2d7c5d62..244a61a9 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<Linebreaks>,
+ /// 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<Frame> {
- 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 b2f45446..853dd32d 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<Fragment> {
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 269c0024..6e5e25b6 100644
--- a/tests/ref/layout/par-indent.png
+++ b/tests/ref/layout/par-indent.png
Binary files differ
diff --git a/tests/typ/layout/par-indent.typ b/tests/typ/layout/par-indent.typ
index 0cb937bf..4890e5dc 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 5a9012d1..24d3ab38 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.