summaryrefslogtreecommitdiff
path: root/crates/typst-layout
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2025-01-27 14:15:20 +0100
committerGitHub <noreply@github.com>2025-01-27 13:15:20 +0000
commit85d177897468165b93056947a80086b2f84d815d (patch)
treea86ccf46ad27ef1ea2c8f5d9010a44eb0e5efde8 /crates/typst-layout
parent176b070c779ef8aa4515c8ff062b17ca9114fd3f (diff)
Support first-line-indent for every paragraph (#5768)
Diffstat (limited to 'crates/typst-layout')
-rw-r--r--crates/typst-layout/src/flow/collect.rs14
-rw-r--r--crates/typst-layout/src/inline/collect.rs32
-rw-r--r--crates/typst-layout/src/inline/mod.rs30
-rw-r--r--crates/typst-layout/src/inline/prepare.rs8
-rw-r--r--crates/typst-layout/src/math/text.rs3
5 files changed, 60 insertions, 27 deletions
diff --git a/crates/typst-layout/src/flow/collect.rs b/crates/typst-layout/src/flow/collect.rs
index f2c7ebd1..34362a6c 100644
--- a/crates/typst-layout/src/flow/collect.rs
+++ b/crates/typst-layout/src/flow/collect.rs
@@ -23,6 +23,7 @@ use typst_library::World;
use typst_utils::SliceExt;
use super::{layout_multi_block, layout_single_block, FlowMode};
+use crate::inline::ParSituation;
use crate::modifiers::layout_and_modify;
/// Collects all elements of the flow into prepared children. These are much
@@ -46,7 +47,7 @@ pub fn collect<'a>(
base,
expand,
output: Vec::with_capacity(children.len()),
- last_was_par: false,
+ par_situation: ParSituation::First,
}
.run(mode)
}
@@ -60,7 +61,7 @@ struct Collector<'a, 'x, 'y> {
expand: bool,
locator: SplitLocator<'a>,
output: Vec<Child<'a>>,
- last_was_par: bool,
+ par_situation: ParSituation,
}
impl<'a> Collector<'a, '_, '_> {
@@ -123,8 +124,7 @@ impl<'a> Collector<'a, '_, '_> {
styles,
self.base,
self.expand,
- false,
- false,
+ None,
)?
.into_frames();
@@ -165,7 +165,7 @@ impl<'a> Collector<'a, '_, '_> {
styles,
self.base,
self.expand,
- self.last_was_par,
+ self.par_situation,
)?
.into_frames();
@@ -175,7 +175,7 @@ impl<'a> Collector<'a, '_, '_> {
self.lines(lines, styles);
self.output.push(Child::Rel(spacing.into(), 4));
- self.last_was_par = true;
+ self.par_situation = ParSituation::Consecutive;
Ok(())
}
@@ -272,7 +272,7 @@ impl<'a> Collector<'a, '_, '_> {
};
self.output.push(spacing(elem.below(styles)));
- self.last_was_par = false;
+ self.par_situation = ParSituation::Other;
}
/// Collects a placed element into a [`PlacedChild`].
diff --git a/crates/typst-layout/src/inline/collect.rs b/crates/typst-layout/src/inline/collect.rs
index cbc490ba..14cf2e3b 100644
--- a/crates/typst-layout/src/inline/collect.rs
+++ b/crates/typst-layout/src/inline/collect.rs
@@ -5,6 +5,7 @@ use typst_library::layout::{
Abs, AlignElem, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing,
Spacing,
};
+use typst_library::model::{EnumElem, ListElem, TermsElem};
use typst_library::routines::Pair;
use typst_library::text::{
is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes,
@@ -124,26 +125,33 @@ pub fn collect<'a>(
locator: &mut SplitLocator<'a>,
styles: StyleChain<'a>,
region: Size,
- consecutive: bool,
- paragraph: bool,
+ situation: Option<ParSituation>,
) -> SourceResult<(String, Vec<Segment<'a>>, SpanMapper)> {
let mut collector = Collector::new(2 + children.len());
let mut quoter = SmartQuoter::new();
let outer_dir = TextElem::dir_in(styles);
- if paragraph && consecutive {
+ if let Some(situation) = situation {
let first_line_indent = ParElem::first_line_indent_in(styles);
- if !first_line_indent.is_zero()
+ if !first_line_indent.amount.is_zero()
+ && match situation {
+ // First-line indent for the first paragraph after a list bullet
+ // just looks bad.
+ ParSituation::First => first_line_indent.all && !in_list(styles),
+ ParSituation::Consecutive => true,
+ ParSituation::Other => first_line_indent.all,
+ }
&& AlignElem::alignment_in(styles).resolve(styles).x
== outer_dir.start().into()
{
- collector.push_item(Item::Absolute(first_line_indent.resolve(styles), false));
+ collector.push_item(Item::Absolute(
+ first_line_indent.amount.resolve(styles),
+ false,
+ ));
collector.spans.push(1, Span::detached());
}
- }
- if paragraph {
let hang = ParElem::hanging_indent_in(styles);
if !hang.is_zero() {
collector.push_item(Item::Absolute(-hang, false));
@@ -257,6 +265,16 @@ pub fn collect<'a>(
Ok((collector.full, collector.segments, collector.spans))
}
+/// Whether we have a list ancestor.
+///
+/// When we support some kind of more general ancestry mechanism, this can
+/// become more elegant.
+fn in_list(styles: StyleChain) -> bool {
+ ListElem::depth_in(styles).0 > 0
+ || !EnumElem::parents_in(styles).is_empty()
+ || TermsElem::within_in(styles)
+}
+
/// Collects segments.
struct Collector<'a> {
full: String,
diff --git a/crates/typst-layout/src/inline/mod.rs b/crates/typst-layout/src/inline/mod.rs
index 83ca82bf..f8a36368 100644
--- a/crates/typst-layout/src/inline/mod.rs
+++ b/crates/typst-layout/src/inline/mod.rs
@@ -42,7 +42,7 @@ pub fn layout_par(
styles: StyleChain,
region: Size,
expand: bool,
- consecutive: bool,
+ situation: ParSituation,
) -> SourceResult<Fragment> {
layout_par_impl(
elem,
@@ -56,7 +56,7 @@ pub fn layout_par(
styles,
region,
expand,
- consecutive,
+ situation,
)
}
@@ -75,7 +75,7 @@ fn layout_par_impl(
styles: StyleChain,
region: Size,
expand: bool,
- consecutive: bool,
+ situation: ParSituation,
) -> SourceResult<Fragment> {
let link = LocatorLink::new(locator);
let mut locator = Locator::link(&link).split();
@@ -105,8 +105,7 @@ fn layout_par_impl(
styles,
region,
expand,
- true,
- consecutive,
+ Some(situation),
)
}
@@ -119,16 +118,15 @@ pub fn layout_inline<'a>(
styles: StyleChain<'a>,
region: Size,
expand: bool,
- paragraph: bool,
- consecutive: bool,
+ par: Option<ParSituation>,
) -> SourceResult<Fragment> {
// Collect all text into one string for BiDi analysis.
let (text, segments, spans) =
- collect(children, engine, locator, styles, region, consecutive, paragraph)?;
+ collect(children, engine, locator, styles, region, par)?;
// Perform BiDi analysis and performs some preparation steps before we
// proceed to line breaking.
- let p = prepare(engine, children, &text, segments, spans, styles, paragraph)?;
+ let p = prepare(engine, children, &text, segments, spans, styles, par)?;
// Break the text into lines.
let lines = linebreak(engine, &p, region.x - p.hang);
@@ -136,3 +134,17 @@ pub fn layout_inline<'a>(
// Turn the selected lines into frames.
finalize(engine, &p, &lines, styles, region, expand, locator)
}
+
+/// Distinguishes between a few different kinds of paragraphs.
+///
+/// In the form `Option<ParSituation>`, `None` implies that we are creating an
+/// inline layout that isn't a semantic paragraph.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum ParSituation {
+ /// The paragraph is the first thing in the flow.
+ First,
+ /// The paragraph follows another paragraph.
+ Consecutive,
+ /// Any other kind of paragraph.
+ Other,
+}
diff --git a/crates/typst-layout/src/inline/prepare.rs b/crates/typst-layout/src/inline/prepare.rs
index e26c9b14..0344d433 100644
--- a/crates/typst-layout/src/inline/prepare.rs
+++ b/crates/typst-layout/src/inline/prepare.rs
@@ -85,7 +85,7 @@ pub fn prepare<'a>(
segments: Vec<Segment<'a>>,
spans: SpanMapper,
styles: StyleChain<'a>,
- paragraph: bool,
+ situation: Option<ParSituation>,
) -> SourceResult<Preparation<'a>> {
let dir = TextElem::dir_in(styles);
let default_level = match dir {
@@ -130,7 +130,11 @@ pub fn prepare<'a>(
}
// Only apply hanging indent to real paragraphs.
- let hang = if paragraph { ParElem::hanging_indent_in(styles) } else { Abs::zero() };
+ let hang = if situation.is_some() {
+ ParElem::hanging_indent_in(styles)
+ } else {
+ Abs::zero()
+ };
Ok(Preparation {
text,
diff --git a/crates/typst-layout/src/math/text.rs b/crates/typst-layout/src/math/text.rs
index 5897c3c0..9a64992a 100644
--- a/crates/typst-layout/src/math/text.rs
+++ b/crates/typst-layout/src/math/text.rs
@@ -107,8 +107,7 @@ fn layout_inline_text(
styles,
Size::splat(Abs::inf()),
false,
- false,
- false,
+ None,
)?
.into_frame();