Respect par constructor arguments (#5842)

This commit is contained in:
Laurenz 2025-02-10 15:37:19 +01:00
parent ab5e356d81
commit 9c3ecf43a0
14 changed files with 314 additions and 249 deletions

View File

@ -124,7 +124,6 @@ impl<'a> Collector<'a, '_, '_> {
styles, styles,
self.base, self.base,
self.expand, self.expand,
None,
)? )?
.into_frames(); .into_frames();
@ -133,7 +132,8 @@ impl<'a> Collector<'a, '_, '_> {
self.output.push(Child::Tag(&elem.tag)); self.output.push(Child::Tag(&elem.tag));
} }
self.lines(lines, styles); let leading = ParElem::leading_in(styles);
self.lines(lines, leading, styles);
for (c, _) in &self.children[end..] { for (c, _) in &self.children[end..] {
let elem = c.to_packed::<TagElem>().unwrap(); let elem = c.to_packed::<TagElem>().unwrap();
@ -169,10 +169,12 @@ impl<'a> Collector<'a, '_, '_> {
)? )?
.into_frames(); .into_frames();
let spacing = ParElem::spacing_in(styles); let spacing = elem.spacing(styles);
let leading = elem.leading(styles);
self.output.push(Child::Rel(spacing.into(), 4)); self.output.push(Child::Rel(spacing.into(), 4));
self.lines(lines, styles); self.lines(lines, leading, styles);
self.output.push(Child::Rel(spacing.into(), 4)); self.output.push(Child::Rel(spacing.into(), 4));
self.par_situation = ParSituation::Consecutive; self.par_situation = ParSituation::Consecutive;
@ -181,9 +183,8 @@ impl<'a> Collector<'a, '_, '_> {
} }
/// Collect laid-out lines. /// Collect laid-out lines.
fn lines(&mut self, lines: Vec<Frame>, styles: StyleChain<'a>) { fn lines(&mut self, lines: Vec<Frame>, leading: Abs, styles: StyleChain<'a>) {
let align = AlignElem::alignment_in(styles).resolve(styles); let align = AlignElem::alignment_in(styles).resolve(styles);
let leading = ParElem::leading_in(styles);
let costs = TextElem::costs_in(styles); let costs = TextElem::costs_in(styles);
// Determine whether to prevent widow and orphans. // Determine whether to prevent widow and orphans.

View File

@ -197,7 +197,50 @@ pub fn layout_flow<'a>(
mode: FlowMode, mode: FlowMode,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
// Prepare configuration that is shared across the whole flow. // Prepare configuration that is shared across the whole flow.
let config = Config { let config = configuration(shared, regions, columns, column_gutter, mode);
// Collect the elements into pre-processed children. These are much easier
// to handle than the raw elements.
let bump = Bump::new();
let children = collect(
engine,
&bump,
children,
locator.next(&()),
Size::new(config.columns.width, regions.full),
regions.expand.x,
mode,
)?;
let mut work = Work::new(&children);
let mut finished = vec![];
// This loop runs once per region produced by the flow layout.
loop {
let frame = compose(engine, &mut work, &config, locator.next(&()), regions)?;
finished.push(frame);
// Terminate the loop when everything is processed, though draining the
// backlog if necessary.
if work.done() && (!regions.expand.y || regions.backlog.is_empty()) {
break;
}
regions.next();
}
Ok(Fragment::frames(finished))
}
/// Determine the flow's configuration.
fn configuration<'x>(
shared: StyleChain<'x>,
regions: Regions,
columns: NonZeroUsize,
column_gutter: Rel<Abs>,
mode: FlowMode,
) -> Config<'x> {
Config {
mode, mode,
shared, shared,
columns: { columns: {
@ -235,39 +278,7 @@ pub fn layout_flow<'a>(
) )
}, },
}), }),
};
// Collect the elements into pre-processed children. These are much easier
// to handle than the raw elements.
let bump = Bump::new();
let children = collect(
engine,
&bump,
children,
locator.next(&()),
Size::new(config.columns.width, regions.full),
regions.expand.x,
mode,
)?;
let mut work = Work::new(&children);
let mut finished = vec![];
// This loop runs once per region produced by the flow layout.
loop {
let frame = compose(engine, &mut work, &config, locator.next(&()), regions)?;
finished.push(frame);
// Terminate the loop when everything is processed, though draining the
// backlog if necessary.
if work.done() && (!regions.expand.y || regions.backlog.is_empty()) {
break;
} }
regions.next();
}
Ok(Fragment::frames(finished))
} }
/// The work that is left to do by flow layout. /// The work that is left to do by flow layout.

View File

@ -2,10 +2,8 @@ use typst_library::diag::warning;
use typst_library::foundations::{Packed, Resolve}; use typst_library::foundations::{Packed, Resolve};
use typst_library::introspection::{SplitLocator, Tag, TagElem}; use typst_library::introspection::{SplitLocator, Tag, TagElem};
use typst_library::layout::{ use typst_library::layout::{
Abs, AlignElem, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing, Abs, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing, Spacing,
Spacing,
}; };
use typst_library::model::{EnumElem, ListElem, TermsElem};
use typst_library::routines::Pair; use typst_library::routines::Pair;
use typst_library::text::{ use typst_library::text::{
is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes, is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes,
@ -123,41 +121,21 @@ pub fn collect<'a>(
children: &[Pair<'a>], children: &[Pair<'a>],
engine: &mut Engine<'_>, engine: &mut Engine<'_>,
locator: &mut SplitLocator<'a>, locator: &mut SplitLocator<'a>,
styles: StyleChain<'a>, config: &Config,
region: Size, region: Size,
situation: Option<ParSituation>,
) -> SourceResult<(String, Vec<Segment<'a>>, SpanMapper)> { ) -> SourceResult<(String, Vec<Segment<'a>>, SpanMapper)> {
let mut collector = Collector::new(2 + children.len()); let mut collector = Collector::new(2 + children.len());
let mut quoter = SmartQuoter::new(); let mut quoter = SmartQuoter::new();
let outer_dir = TextElem::dir_in(styles); if !config.first_line_indent.is_zero() {
collector.push_item(Item::Absolute(config.first_line_indent, false));
if let Some(situation) = situation {
let first_line_indent = ParElem::first_line_indent_in(styles);
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.amount.resolve(styles),
false,
));
collector.spans.push(1, Span::detached()); collector.spans.push(1, Span::detached());
} }
let hang = ParElem::hanging_indent_in(styles); if !config.hanging_indent.is_zero() {
if !hang.is_zero() { collector.push_item(Item::Absolute(-config.hanging_indent, false));
collector.push_item(Item::Absolute(-hang, false));
collector.spans.push(1, Span::detached()); collector.spans.push(1, Span::detached());
} }
}
for &(child, styles) in children { for &(child, styles) in children {
let prev_len = collector.full.len(); let prev_len = collector.full.len();
@ -167,7 +145,7 @@ pub fn collect<'a>(
} else if let Some(elem) = child.to_packed::<TextElem>() { } else if let Some(elem) = child.to_packed::<TextElem>() {
collector.build_text(styles, |full| { collector.build_text(styles, |full| {
let dir = TextElem::dir_in(styles); let dir = TextElem::dir_in(styles);
if dir != outer_dir { if dir != config.dir {
// Insert "Explicit Directional Embedding". // Insert "Explicit Directional Embedding".
match dir { match dir {
Dir::LTR => full.push_str(LTR_EMBEDDING), Dir::LTR => full.push_str(LTR_EMBEDDING),
@ -182,7 +160,7 @@ pub fn collect<'a>(
full.push_str(&elem.text); full.push_str(&elem.text);
} }
if dir != outer_dir { if dir != config.dir {
// Insert "Pop Directional Formatting". // Insert "Pop Directional Formatting".
full.push_str(POP_EMBEDDING); full.push_str(POP_EMBEDDING);
} }
@ -265,16 +243,6 @@ pub fn collect<'a>(
Ok((collector.full, collector.segments, collector.spans)) 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. /// Collects segments.
struct Collector<'a> { struct Collector<'a> {
full: String, full: String,

View File

@ -9,7 +9,6 @@ pub fn finalize(
engine: &mut Engine, engine: &mut Engine,
p: &Preparation, p: &Preparation,
lines: &[Line], lines: &[Line],
styles: StyleChain,
region: Size, region: Size,
expand: bool, expand: bool,
locator: &mut SplitLocator<'_>, locator: &mut SplitLocator<'_>,
@ -19,9 +18,10 @@ pub fn finalize(
let width = if !region.x.is_finite() let width = if !region.x.is_finite()
|| (!expand && lines.iter().all(|line| line.fr().is_zero())) || (!expand && lines.iter().all(|line| line.fr().is_zero()))
{ {
region region.x.min(
.x p.config.hanging_indent
.min(p.hang + lines.iter().map(|line| line.width).max().unwrap_or_default()) + lines.iter().map(|line| line.width).max().unwrap_or_default(),
)
} else { } else {
region.x region.x
}; };
@ -29,7 +29,7 @@ pub fn finalize(
// Stack the lines into one frame per region. // Stack the lines into one frame per region.
lines lines
.iter() .iter()
.map(|line| commit(engine, p, line, width, region.y, locator, styles)) .map(|line| commit(engine, p, line, width, region.y, locator))
.collect::<SourceResult<_>>() .collect::<SourceResult<_>>()
.map(Fragment::frames) .map(Fragment::frames)
} }

View File

@ -2,10 +2,9 @@ use std::fmt::{self, Debug, Formatter};
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use typst_library::engine::Engine; use typst_library::engine::Engine;
use typst_library::foundations::NativeElement;
use typst_library::introspection::{SplitLocator, Tag}; use typst_library::introspection::{SplitLocator, Tag};
use typst_library::layout::{Abs, Dir, Em, Fr, Frame, FrameItem, Point}; use typst_library::layout::{Abs, Dir, Em, Fr, Frame, FrameItem, Point};
use typst_library::model::{ParLine, ParLineMarker}; use typst_library::model::ParLineMarker;
use typst_library::text::{Lang, TextElem}; use typst_library::text::{Lang, TextElem};
use typst_utils::Numeric; use typst_utils::Numeric;
@ -135,7 +134,7 @@ pub fn line<'a>(
// Whether the line is justified. // Whether the line is justified.
let justify = full.ends_with(LINE_SEPARATOR) let justify = full.ends_with(LINE_SEPARATOR)
|| (p.justify && breakpoint != Breakpoint::Mandatory); || (p.config.justify && breakpoint != Breakpoint::Mandatory);
// Process dashes. // Process dashes.
let dash = if breakpoint.is_hyphen() || full.ends_with(SHY) { let dash = if breakpoint.is_hyphen() || full.ends_with(SHY) {
@ -157,14 +156,14 @@ pub fn line<'a>(
// Add a hyphen at the line start, if a previous dash should be repeated. // Add a hyphen at the line start, if a previous dash should be repeated.
if pred.map_or(false, |pred| should_repeat_hyphen(pred, full)) { if pred.map_or(false, |pred| should_repeat_hyphen(pred, full)) {
if let Some(shaped) = items.first_text_mut() { if let Some(shaped) = items.first_text_mut() {
shaped.prepend_hyphen(engine, p.fallback); shaped.prepend_hyphen(engine, p.config.fallback);
} }
} }
// Add a hyphen at the line end, if we ended on a soft hyphen. // Add a hyphen at the line end, if we ended on a soft hyphen.
if dash == Some(Dash::Soft) { if dash == Some(Dash::Soft) {
if let Some(shaped) = items.last_text_mut() { if let Some(shaped) = items.last_text_mut() {
shaped.push_hyphen(engine, p.fallback); shaped.push_hyphen(engine, p.config.fallback);
} }
} }
@ -234,13 +233,13 @@ where
{ {
// If there is nothing bidirectional going on, skip reordering. // If there is nothing bidirectional going on, skip reordering.
let Some(bidi) = &p.bidi else { let Some(bidi) = &p.bidi else {
f(range, p.dir == Dir::RTL); f(range, p.config.dir == Dir::RTL);
return; return;
}; };
// The bidi crate panics for empty lines. // The bidi crate panics for empty lines.
if range.is_empty() { if range.is_empty() {
f(range, p.dir == Dir::RTL); f(range, p.config.dir == Dir::RTL);
return; return;
} }
@ -308,13 +307,13 @@ fn collect_range<'a>(
/// punctuation marks at line start or line end. /// punctuation marks at line start or line end.
fn adjust_cj_at_line_boundaries(p: &Preparation, text: &str, items: &mut Items) { fn adjust_cj_at_line_boundaries(p: &Preparation, text: &str, items: &mut Items) {
if text.starts_with(BEGIN_PUNCT_PAT) if text.starts_with(BEGIN_PUNCT_PAT)
|| (p.cjk_latin_spacing && text.starts_with(is_of_cj_script)) || (p.config.cjk_latin_spacing && text.starts_with(is_of_cj_script))
{ {
adjust_cj_at_line_start(p, items); adjust_cj_at_line_start(p, items);
} }
if text.ends_with(END_PUNCT_PAT) if text.ends_with(END_PUNCT_PAT)
|| (p.cjk_latin_spacing && text.ends_with(is_of_cj_script)) || (p.config.cjk_latin_spacing && text.ends_with(is_of_cj_script))
{ {
adjust_cj_at_line_end(p, items); adjust_cj_at_line_end(p, items);
} }
@ -332,7 +331,10 @@ fn adjust_cj_at_line_start(p: &Preparation, items: &mut Items) {
let shrink = glyph.shrinkability().0; let shrink = glyph.shrinkability().0;
glyph.shrink_left(shrink); glyph.shrink_left(shrink);
shaped.width -= shrink.at(shaped.size); shaped.width -= shrink.at(shaped.size);
} else if p.cjk_latin_spacing && glyph.is_cj_script() && glyph.x_offset > Em::zero() { } else if p.config.cjk_latin_spacing
&& glyph.is_cj_script()
&& glyph.x_offset > Em::zero()
{
// If the first glyph is a CJK character adjusted by // If the first glyph is a CJK character adjusted by
// [`add_cjk_latin_spacing`], restore the original width. // [`add_cjk_latin_spacing`], restore the original width.
let glyph = shaped.glyphs.to_mut().first_mut().unwrap(); let glyph = shaped.glyphs.to_mut().first_mut().unwrap();
@ -359,7 +361,7 @@ fn adjust_cj_at_line_end(p: &Preparation, items: &mut Items) {
let punct = shaped.glyphs.to_mut().last_mut().unwrap(); let punct = shaped.glyphs.to_mut().last_mut().unwrap();
punct.shrink_right(shrink); punct.shrink_right(shrink);
shaped.width -= shrink.at(shaped.size); shaped.width -= shrink.at(shaped.size);
} else if p.cjk_latin_spacing } else if p.config.cjk_latin_spacing
&& glyph.is_cj_script() && glyph.is_cj_script()
&& (glyph.x_advance - glyph.x_offset) > Em::one() && (glyph.x_advance - glyph.x_offset) > Em::one()
{ {
@ -424,16 +426,15 @@ pub fn commit(
width: Abs, width: Abs,
full: Abs, full: Abs,
locator: &mut SplitLocator<'_>, locator: &mut SplitLocator<'_>,
styles: StyleChain,
) -> SourceResult<Frame> { ) -> SourceResult<Frame> {
let mut remaining = width - line.width - p.hang; let mut remaining = width - line.width - p.config.hanging_indent;
let mut offset = Abs::zero(); let mut offset = Abs::zero();
// We always build the line from left to right. In an LTR paragraph, we must // We always build the line from left to right. In an LTR paragraph, we must
// thus add the hanging indent to the offset. In an RTL paragraph, the // thus add the hanging indent to the offset. In an RTL paragraph, the
// hanging indent arises naturally due to the line width. // hanging indent arises naturally due to the line width.
if p.dir == Dir::LTR { if p.config.dir == Dir::LTR {
offset += p.hang; offset += p.config.hanging_indent;
} }
// Handle hanging punctuation to the left. // Handle hanging punctuation to the left.
@ -554,11 +555,13 @@ pub fn commit(
let mut output = Frame::soft(size); let mut output = Frame::soft(size);
output.set_baseline(top); output.set_baseline(top);
add_par_line_marker(&mut output, styles, engine, locator, top); if let Some(marker) = &p.config.numbering_marker {
add_par_line_marker(&mut output, marker, engine, locator, top);
}
// Construct the line's frame. // Construct the line's frame.
for (offset, frame) in frames { for (offset, frame) in frames {
let x = offset + p.align.position(remaining); let x = offset + p.config.align.position(remaining);
let y = top - frame.baseline(); let y = top - frame.baseline();
output.push_frame(Point::new(x, y), frame); output.push_frame(Point::new(x, y), frame);
} }
@ -575,26 +578,18 @@ pub fn commit(
/// number in the margin, is aligned to the line's baseline. /// number in the margin, is aligned to the line's baseline.
fn add_par_line_marker( fn add_par_line_marker(
output: &mut Frame, output: &mut Frame,
styles: StyleChain, marker: &Packed<ParLineMarker>,
engine: &mut Engine, engine: &mut Engine,
locator: &mut SplitLocator, locator: &mut SplitLocator,
top: Abs, top: Abs,
) { ) {
let Some(numbering) = ParLine::numbering_in(styles) else { return };
let margin = ParLine::number_margin_in(styles);
let align = ParLine::number_align_in(styles);
// Delay resolving the number clearance until line numbers are laid out to
// avoid inconsistent spacing depending on varying font size.
let clearance = ParLine::number_clearance_in(styles);
// Elements in tags must have a location for introspection to work. We do // Elements in tags must have a location for introspection to work. We do
// the work here instead of going through all of the realization process // the work here instead of going through all of the realization process
// just for this, given we don't need to actually place the marker as we // just for this, given we don't need to actually place the marker as we
// manually search for it in the frame later (when building a root flow, // manually search for it in the frame later (when building a root flow,
// where line numbers can be displayed), so we just need it to be in a tag // where line numbers can be displayed), so we just need it to be in a tag
// and to be valid (to have a location). // and to be valid (to have a location).
let mut marker = ParLineMarker::new(numbering, align, margin, clearance).pack(); let mut marker = marker.clone();
let key = typst_utils::hash128(&marker); let key = typst_utils::hash128(&marker);
let loc = locator.next_location(engine.introspector, key); let loc = locator.next_location(engine.introspector, key);
marker.set_location(loc); marker.set_location(loc);
@ -606,7 +601,7 @@ fn add_par_line_marker(
// line's general baseline. However, the line number will still need to // line's general baseline. However, the line number will still need to
// manually adjust its own 'y' position based on its own baseline. // manually adjust its own 'y' position based on its own baseline.
let pos = Point::with_y(top); let pos = Point::with_y(top);
output.push(pos, FrameItem::Tag(Tag::Start(marker))); output.push(pos, FrameItem::Tag(Tag::Start(marker.pack())));
output.push(pos, FrameItem::Tag(Tag::End(loc, key))); output.push(pos, FrameItem::Tag(Tag::End(loc, key)));
} }

View File

@ -110,15 +110,7 @@ pub fn linebreak<'a>(
p: &'a Preparation<'a>, p: &'a Preparation<'a>,
width: Abs, width: Abs,
) -> Vec<Line<'a>> { ) -> Vec<Line<'a>> {
let linebreaks = p.linebreaks.unwrap_or_else(|| { match p.config.linebreaks {
if p.justify {
Linebreaks::Optimized
} else {
Linebreaks::Simple
}
});
match linebreaks {
Linebreaks::Simple => linebreak_simple(engine, p, width), Linebreaks::Simple => linebreak_simple(engine, p, width),
Linebreaks::Optimized => linebreak_optimized(engine, p, width), Linebreaks::Optimized => linebreak_optimized(engine, p, width),
} }
@ -384,7 +376,7 @@ fn linebreak_optimized_approximate(
// Whether the line is justified. This is not 100% accurate w.r.t // Whether the line is justified. This is not 100% accurate w.r.t
// to line()'s behaviour, but good enough. // to line()'s behaviour, but good enough.
let justify = p.justify && breakpoint != Breakpoint::Mandatory; let justify = p.config.justify && breakpoint != Breakpoint::Mandatory;
// We don't really know whether the line naturally ends with a dash // We don't really know whether the line naturally ends with a dash
// here, so we can miss that case, but it's ok, since all of this // here, so we can miss that case, but it's ok, since all of this
@ -573,7 +565,7 @@ fn raw_ratio(
// calculate the extra amount. Also, don't divide by zero. // calculate the extra amount. Also, don't divide by zero.
let extra_stretch = (delta - adjustability) / justifiables.max(1) as f64; let extra_stretch = (delta - adjustability) / justifiables.max(1) as f64;
// Normalize the amount by half the em size. // Normalize the amount by half the em size.
ratio = 1.0 + extra_stretch / (p.size / 2.0); ratio = 1.0 + extra_stretch / (p.config.font_size / 2.0);
} }
// The min value must be < MIN_RATIO, but how much smaller doesn't matter // The min value must be < MIN_RATIO, but how much smaller doesn't matter
@ -663,9 +655,9 @@ fn breakpoints(p: &Preparation, mut f: impl FnMut(usize, Breakpoint)) {
return; return;
} }
let hyphenate = p.hyphenate != Some(false); let hyphenate = p.config.hyphenate != Some(false);
let lb = LINEBREAK_DATA.as_borrowed(); let lb = LINEBREAK_DATA.as_borrowed();
let segmenter = match p.lang { let segmenter = match p.config.lang {
Some(Lang::CHINESE | Lang::JAPANESE) => &CJ_SEGMENTER, Some(Lang::CHINESE | Lang::JAPANESE) => &CJ_SEGMENTER,
_ => &SEGMENTER, _ => &SEGMENTER,
}; };
@ -830,18 +822,18 @@ fn linebreak_link(link: &str, mut f: impl FnMut(usize)) {
/// Whether hyphenation is enabled at the given offset. /// Whether hyphenation is enabled at the given offset.
fn hyphenate_at(p: &Preparation, offset: usize) -> bool { fn hyphenate_at(p: &Preparation, offset: usize) -> bool {
p.hyphenate p.config.hyphenate.unwrap_or_else(|| {
.or_else(|| {
let (_, item) = p.get(offset); let (_, item) = p.get(offset);
let styles = item.text()?.styles; match item.text() {
Some(TextElem::hyphenate_in(styles)) Some(text) => TextElem::hyphenate_in(text.styles).unwrap_or(p.config.justify),
None => false,
}
}) })
.unwrap_or(false)
} }
/// The text language at the given offset. /// The text language at the given offset.
fn lang_at(p: &Preparation, offset: usize) -> Option<hypher::Lang> { fn lang_at(p: &Preparation, offset: usize) -> Option<hypher::Lang> {
let lang = p.lang.or_else(|| { let lang = p.config.lang.or_else(|| {
let (_, item) = p.get(offset); let (_, item) = p.get(offset);
let styles = item.text()?.styles; let styles = item.text()?.styles;
Some(TextElem::lang_in(styles)) Some(TextElem::lang_in(styles))
@ -865,13 +857,13 @@ impl CostMetrics {
fn compute(p: &Preparation) -> Self { fn compute(p: &Preparation) -> Self {
Self { Self {
// When justifying, we may stretch spaces below their natural width. // When justifying, we may stretch spaces below their natural width.
min_ratio: if p.justify { MIN_RATIO } else { 0.0 }, min_ratio: if p.config.justify { MIN_RATIO } else { 0.0 },
min_approx_ratio: if p.justify { MIN_APPROX_RATIO } else { 0.0 }, min_approx_ratio: if p.config.justify { MIN_APPROX_RATIO } else { 0.0 },
// Approximate hyphen width for estimates. // Approximate hyphen width for estimates.
approx_hyphen_width: Em::new(0.33).at(p.size), approx_hyphen_width: Em::new(0.33).at(p.config.font_size),
// Costs. // Costs.
hyph_cost: DEFAULT_HYPH_COST * p.costs.hyphenation().get(), hyph_cost: DEFAULT_HYPH_COST * p.config.costs.hyphenation().get(),
runt_cost: DEFAULT_RUNT_COST * p.costs.runt().get(), runt_cost: DEFAULT_RUNT_COST * p.config.costs.runt().get(),
} }
} }

View File

@ -13,12 +13,17 @@ pub use self::box_::layout_box;
use comemo::{Track, Tracked, TrackedMut}; use comemo::{Track, Tracked, TrackedMut};
use typst_library::diag::SourceResult; use typst_library::diag::SourceResult;
use typst_library::engine::{Engine, Route, Sink, Traced}; use typst_library::engine::{Engine, Route, Sink, Traced};
use typst_library::foundations::{Packed, StyleChain}; use typst_library::foundations::{Packed, Resolve, Smart, StyleChain};
use typst_library::introspection::{Introspector, Locator, LocatorLink, SplitLocator}; use typst_library::introspection::{Introspector, Locator, LocatorLink, SplitLocator};
use typst_library::layout::{Fragment, Size}; use typst_library::layout::{Abs, AlignElem, Dir, FixedAlignment, Fragment, Size};
use typst_library::model::ParElem; use typst_library::model::{
EnumElem, FirstLineIndent, Linebreaks, ListElem, ParElem, ParLine, ParLineMarker,
TermsElem,
};
use typst_library::routines::{Arenas, Pair, RealizationKind, Routines}; use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
use typst_library::text::{Costs, Lang, TextElem};
use typst_library::World; use typst_library::World;
use typst_utils::{Numeric, SliceExt};
use self::collect::{collect, Item, Segment, SpanMapper}; use self::collect::{collect, Item, Segment, SpanMapper};
use self::deco::decorate; use self::deco::decorate;
@ -98,7 +103,7 @@ fn layout_par_impl(
styles, styles,
)?; )?;
layout_inline( layout_inline_impl(
&mut engine, &mut engine,
&children, &children,
&mut locator, &mut locator,
@ -106,33 +111,134 @@ fn layout_par_impl(
region, region,
expand, expand,
Some(situation), Some(situation),
&ConfigBase {
justify: elem.justify(styles),
linebreaks: elem.linebreaks(styles),
first_line_indent: elem.first_line_indent(styles),
hanging_indent: elem.hanging_indent(styles),
},
) )
} }
/// Lays out realized content with inline layout. /// Lays out realized content with inline layout.
#[allow(clippy::too_many_arguments)]
pub fn layout_inline<'a>( pub fn layout_inline<'a>(
engine: &mut Engine, engine: &mut Engine,
children: &[Pair<'a>], children: &[Pair<'a>],
locator: &mut SplitLocator<'a>, locator: &mut SplitLocator<'a>,
styles: StyleChain<'a>, shared: StyleChain<'a>,
region: Size,
expand: bool,
) -> SourceResult<Fragment> {
layout_inline_impl(
engine,
children,
locator,
shared,
region,
expand,
None,
&ConfigBase {
justify: ParElem::justify_in(shared),
linebreaks: ParElem::linebreaks_in(shared),
first_line_indent: ParElem::first_line_indent_in(shared),
hanging_indent: ParElem::hanging_indent_in(shared),
},
)
}
/// The internal implementation of [`layout_inline`].
#[allow(clippy::too_many_arguments)]
fn layout_inline_impl<'a>(
engine: &mut Engine,
children: &[Pair<'a>],
locator: &mut SplitLocator<'a>,
shared: StyleChain<'a>,
region: Size, region: Size,
expand: bool, expand: bool,
par: Option<ParSituation>, par: Option<ParSituation>,
base: &ConfigBase,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
// Prepare configuration that is shared across the whole inline layout.
let config = configuration(base, children, shared, par);
// Collect all text into one string for BiDi analysis. // Collect all text into one string for BiDi analysis.
let (text, segments, spans) = let (text, segments, spans) = collect(children, engine, locator, &config, region)?;
collect(children, engine, locator, styles, region, par)?;
// Perform BiDi analysis and performs some preparation steps before we // Perform BiDi analysis and performs some preparation steps before we
// proceed to line breaking. // proceed to line breaking.
let p = prepare(engine, children, &text, segments, spans, styles, par)?; let p = prepare(engine, &config, &text, segments, spans)?;
// Break the text into lines. // Break the text into lines.
let lines = linebreak(engine, &p, region.x - p.hang); let lines = linebreak(engine, &p, region.x - config.hanging_indent);
// Turn the selected lines into frames. // Turn the selected lines into frames.
finalize(engine, &p, &lines, styles, region, expand, locator) finalize(engine, &p, &lines, region, expand, locator)
}
/// Determine the inline layout's configuration.
fn configuration(
base: &ConfigBase,
children: &[Pair],
shared: StyleChain,
situation: Option<ParSituation>,
) -> Config {
let justify = base.justify;
let font_size = TextElem::size_in(shared);
let dir = TextElem::dir_in(shared);
Config {
justify,
linebreaks: base.linebreaks.unwrap_or_else(|| {
if justify {
Linebreaks::Optimized
} else {
Linebreaks::Simple
}
}),
first_line_indent: {
let FirstLineIndent { amount, all } = base.first_line_indent;
if !amount.is_zero()
&& match situation {
// First-line indent for the first paragraph after a list
// bullet just looks bad.
Some(ParSituation::First) => all && !in_list(shared),
Some(ParSituation::Consecutive) => true,
Some(ParSituation::Other) => all,
None => false,
}
&& AlignElem::alignment_in(shared).resolve(shared).x == dir.start().into()
{
amount.at(font_size)
} else {
Abs::zero()
}
},
hanging_indent: if situation.is_some() {
base.hanging_indent
} else {
Abs::zero()
},
numbering_marker: ParLine::numbering_in(shared).map(|numbering| {
Packed::new(ParLineMarker::new(
numbering,
ParLine::number_align_in(shared),
ParLine::number_margin_in(shared),
// Delay resolving the number clearance until line numbers are
// laid out to avoid inconsistent spacing depending on varying
// font size.
ParLine::number_clearance_in(shared),
))
}),
align: AlignElem::alignment_in(shared).fix(dir).x,
font_size,
dir,
hyphenate: shared_get(children, shared, TextElem::hyphenate_in)
.map(|uniform| uniform.unwrap_or(justify)),
lang: shared_get(children, shared, TextElem::lang_in),
fallback: TextElem::fallback_in(shared),
cjk_latin_spacing: TextElem::cjk_latin_spacing_in(shared).is_auto(),
costs: TextElem::costs_in(shared),
}
} }
/// Distinguishes between a few different kinds of paragraphs. /// Distinguishes between a few different kinds of paragraphs.
@ -148,3 +254,66 @@ pub enum ParSituation {
/// Any other kind of paragraph. /// Any other kind of paragraph.
Other, Other,
} }
/// Raw values from a `ParElem` or style chain. Used to initialize a [`Config`].
struct ConfigBase {
justify: bool,
linebreaks: Smart<Linebreaks>,
first_line_indent: FirstLineIndent,
hanging_indent: Abs,
}
/// Shared configuration for the whole inline layout.
struct Config {
/// Whether to justify text.
justify: bool,
/// How to determine line breaks.
linebreaks: Linebreaks,
/// The indent the first line of a paragraph should have.
first_line_indent: Abs,
/// The indent that all but the first line of a paragraph should have.
hanging_indent: Abs,
/// Configuration for line numbering.
numbering_marker: Option<Packed<ParLineMarker>>,
/// The resolved horizontal alignment.
align: FixedAlignment,
/// The text size.
font_size: Abs,
/// The dominant direction.
dir: Dir,
/// A uniform hyphenation setting (only `Some(_)` if it's the same for all
/// children, otherwise `None`).
hyphenate: Option<bool>,
/// The text language (only `Some(_)` if it's the same for all
/// children, otherwise `None`).
lang: Option<Lang>,
/// Whether font fallback is enabled.
fallback: bool,
/// Whether to add spacing between CJK and Latin characters.
cjk_latin_spacing: bool,
/// Costs for various layout decisions.
costs: Costs,
}
/// Get a style property, but only if it is the same for all of the children.
fn shared_get<T: PartialEq>(
children: &[Pair],
styles: StyleChain<'_>,
getter: fn(StyleChain) -> T,
) -> Option<T> {
let value = getter(styles);
children
.group_by_key(|&(_, s)| s)
.all(|(s, _)| getter(s) == value)
.then_some(value)
}
/// 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)
}

View File

@ -1,9 +1,4 @@
use typst_library::foundations::{Resolve, Smart}; use typst_library::layout::{Dir, Em};
use typst_library::layout::{Abs, AlignElem, Dir, Em, FixedAlignment};
use typst_library::model::Linebreaks;
use typst_library::routines::Pair;
use typst_library::text::{Costs, Lang, TextElem};
use typst_utils::SliceExt;
use unicode_bidi::{BidiInfo, Level as BidiLevel}; use unicode_bidi::{BidiInfo, Level as BidiLevel};
use super::*; use super::*;
@ -17,6 +12,8 @@ use super::*;
pub struct Preparation<'a> { pub struct Preparation<'a> {
/// The full text. /// The full text.
pub text: &'a str, pub text: &'a str,
/// Configuration for inline layout.
pub config: &'a Config,
/// Bidirectional text embedding levels. /// Bidirectional text embedding levels.
/// ///
/// This is `None` if all text directions are uniform (all the base /// This is `None` if all text directions are uniform (all the base
@ -28,28 +25,6 @@ pub struct Preparation<'a> {
pub indices: Vec<usize>, pub indices: Vec<usize>,
/// The span mapper. /// The span mapper.
pub spans: SpanMapper, pub spans: SpanMapper,
/// Whether to hyphenate if it's the same for all children.
pub hyphenate: Option<bool>,
/// Costs for various layout decisions.
pub costs: Costs,
/// The dominant direction.
pub dir: Dir,
/// The text language if it's the same for all children.
pub lang: Option<Lang>,
/// The resolved horizontal alignment.
pub align: FixedAlignment,
/// Whether to justify text.
pub justify: bool,
/// Hanging indent to apply.
pub hang: Abs,
/// Whether to add spacing between CJK and Latin characters.
pub cjk_latin_spacing: bool,
/// Whether font fallback is enabled.
pub fallback: bool,
/// How to determine line breaks.
pub linebreaks: Smart<Linebreaks>,
/// The text size.
pub size: Abs,
} }
impl<'a> Preparation<'a> { impl<'a> Preparation<'a> {
@ -80,15 +55,12 @@ impl<'a> Preparation<'a> {
#[typst_macros::time] #[typst_macros::time]
pub fn prepare<'a>( pub fn prepare<'a>(
engine: &mut Engine, engine: &mut Engine,
children: &[Pair<'a>], config: &'a Config,
text: &'a str, text: &'a str,
segments: Vec<Segment<'a>>, segments: Vec<Segment<'a>>,
spans: SpanMapper, spans: SpanMapper,
styles: StyleChain<'a>,
situation: Option<ParSituation>,
) -> SourceResult<Preparation<'a>> { ) -> SourceResult<Preparation<'a>> {
let dir = TextElem::dir_in(styles); let default_level = match config.dir {
let default_level = match dir {
Dir::RTL => BidiLevel::rtl(), Dir::RTL => BidiLevel::rtl(),
_ => BidiLevel::ltr(), _ => BidiLevel::ltr(),
}; };
@ -124,51 +96,20 @@ pub fn prepare<'a>(
indices.extend(range.clone().map(|_| i)); indices.extend(range.clone().map(|_| i));
} }
let cjk_latin_spacing = TextElem::cjk_latin_spacing_in(styles).is_auto(); if config.cjk_latin_spacing {
if cjk_latin_spacing {
add_cjk_latin_spacing(&mut items); add_cjk_latin_spacing(&mut items);
} }
// Only apply hanging indent to real paragraphs.
let hang = if situation.is_some() {
ParElem::hanging_indent_in(styles)
} else {
Abs::zero()
};
Ok(Preparation { Ok(Preparation {
config,
text, text,
bidi: is_bidi.then_some(bidi), bidi: is_bidi.then_some(bidi),
items, items,
indices, indices,
spans, spans,
hyphenate: shared_get(children, styles, TextElem::hyphenate_in),
costs: TextElem::costs_in(styles),
dir,
lang: shared_get(children, styles, TextElem::lang_in),
align: AlignElem::alignment_in(styles).resolve(styles).x,
justify: ParElem::justify_in(styles),
hang,
cjk_latin_spacing,
fallback: TextElem::fallback_in(styles),
linebreaks: ParElem::linebreaks_in(styles),
size: TextElem::size_in(styles),
}) })
} }
/// Get a style property, but only if it is the same for all of the children.
fn shared_get<T: PartialEq>(
children: &[Pair],
styles: StyleChain<'_>,
getter: fn(StyleChain) -> T,
) -> Option<T> {
let value = getter(styles);
children
.group_by_key(|&(_, s)| s)
.all(|(s, _)| getter(s) == value)
.then_some(value)
}
/// Add some spacing between Han characters and western characters. See /// Add some spacing between Han characters and western characters. See
/// Requirements for Chinese Text Layout, Section 3.2.2 Mixed Text Composition /// Requirements for Chinese Text Layout, Section 3.2.2 Mixed Text Composition
/// in Horizontal Written Mode /// in Horizontal Written Mode

View File

@ -107,7 +107,6 @@ fn layout_inline_text(
styles, styles,
Size::splat(Abs::inf()), Size::splat(Abs::inf()),
false, false,
None,
)? )?
.into_frame(); .into_frame();

View File

@ -11,7 +11,7 @@ use crate::foundations::{
use crate::html::{attr, tag, HtmlElem}; use crate::html::{attr, tag, HtmlElem};
use crate::introspection::Location; use crate::introspection::Location;
use crate::layout::Position; use crate::layout::Position;
use crate::text::{Hyphenate, TextElem}; use crate::text::TextElem;
/// Links to a URL or a location in the document. /// Links to a URL or a location in the document.
/// ///
@ -138,7 +138,7 @@ impl Show for Packed<LinkElem> {
impl ShowSet for Packed<LinkElem> { impl ShowSet for Packed<LinkElem> {
fn show_set(&self, _: StyleChain) -> Styles { fn show_set(&self, _: StyleChain) -> Styles {
let mut out = Styles::new(); let mut out = Styles::new();
out.set(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false)))); out.set(TextElem::set_hyphenate(Smart::Custom(false)));
out out
} }
} }

View File

@ -51,7 +51,6 @@ use crate::foundations::{
}; };
use crate::layout::{Abs, Axis, Dir, Em, Length, Ratio, Rel}; use crate::layout::{Abs, Axis, Dir, Em, Length, Ratio, Rel};
use crate::math::{EquationElem, MathSize}; use crate::math::{EquationElem, MathSize};
use crate::model::ParElem;
use crate::visualize::{Color, Paint, RelativeTo, Stroke}; use crate::visualize::{Color, Paint, RelativeTo, Stroke};
use crate::World; use crate::World;
@ -504,9 +503,8 @@ pub struct TextElem {
/// enabling hyphenation can /// enabling hyphenation can
/// improve justification. /// improve justification.
/// ``` /// ```
#[resolve]
#[ghost] #[ghost]
pub hyphenate: Hyphenate, pub hyphenate: Smart<bool>,
/// The "cost" of various choices when laying out text. A higher cost means /// The "cost" of various choices when laying out text. A higher cost means
/// the layout engine will make the choice less often. Costs are specified /// the layout engine will make the choice less often. Costs are specified
@ -1110,27 +1108,6 @@ impl Resolve for TextDir {
} }
} }
/// Whether to hyphenate text.
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Hyphenate(pub Smart<bool>);
cast! {
Hyphenate,
self => self.0.into_value(),
v: Smart<bool> => Self(v),
}
impl Resolve for Hyphenate {
type Output = bool;
fn resolve(self, styles: StyleChain) -> Self::Output {
match self.0 {
Smart::Auto => ParElem::justify_in(styles),
Smart::Custom(v) => v,
}
}
}
/// A set of stylistic sets to enable. /// A set of stylistic sets to enable.
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash)]
pub struct StylisticSets(u32); pub struct StylisticSets(u32);

View File

@ -21,9 +21,7 @@ use crate::html::{tag, HtmlElem};
use crate::layout::{BlockBody, BlockElem, Em, HAlignment}; use crate::layout::{BlockBody, BlockElem, Em, HAlignment};
use crate::loading::{DataSource, Load}; use crate::loading::{DataSource, Load};
use crate::model::{Figurable, ParElem}; use crate::model::{Figurable, ParElem};
use crate::text::{ use crate::text::{FontFamily, FontList, LinebreakElem, LocalName, TextElem, TextSize};
FontFamily, FontList, Hyphenate, LinebreakElem, LocalName, TextElem, TextSize,
};
use crate::visualize::Color; use crate::visualize::Color;
use crate::World; use crate::World;
@ -472,7 +470,7 @@ impl ShowSet for Packed<RawElem> {
let mut out = Styles::new(); let mut out = Styles::new();
out.set(TextElem::set_overhang(false)); out.set(TextElem::set_overhang(false));
out.set(TextElem::set_lang(Lang::ENGLISH)); out.set(TextElem::set_lang(Lang::ENGLISH));
out.set(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false)))); out.set(TextElem::set_hyphenate(Smart::Custom(false)));
out.set(TextElem::set_size(TextSize(Em::new(0.8).into()))); out.set(TextElem::set_size(TextSize(Em::new(0.8).into())));
out.set(TextElem::set_font(FontList(vec![FontFamily::new("DejaVu Sans Mono")]))); out.set(TextElem::set_font(FontList(vec![FontFamily::new("DejaVu Sans Mono")])));
out.set(TextElem::set_cjk_latin_spacing(Smart::Custom(None))); out.set(TextElem::set_cjk_latin_spacing(Smart::Custom(None)));

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -322,6 +322,20 @@ A
#context test(query(<a>).len(), 1) #context test(query(<a>).len(), 1)
--- issue-5831-par-constructor-args ---
// Make sure that all arguments are also respected in the constructor.
A
#par(
leading: 2pt,
spacing: 20pt,
justify: true,
linebreaks: "simple",
first-line-indent: (amount: 1em, all: true),
hanging-indent: 5pt,
)[
The par function has a constructor and justification.
]
--- show-par-set-block-hint --- --- show-par-set-block-hint ---
// Warning: 2-36 `show par: set block(spacing: ..)` has no effect anymore // Warning: 2-36 `show par: set block(spacing: ..)` has no effect anymore
// Hint: 2-36 this is specific to paragraphs as they are not considered blocks anymore // Hint: 2-36 this is specific to paragraphs as they are not considered blocks anymore