diff --git a/crates/typst-layout/src/flow/collect.rs b/crates/typst-layout/src/flow/collect.rs index 34362a6c5..2c14f7a37 100644 --- a/crates/typst-layout/src/flow/collect.rs +++ b/crates/typst-layout/src/flow/collect.rs @@ -124,7 +124,6 @@ impl<'a> Collector<'a, '_, '_> { styles, self.base, self.expand, - None, )? .into_frames(); @@ -133,7 +132,8 @@ impl<'a> Collector<'a, '_, '_> { 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..] { let elem = c.to_packed::().unwrap(); @@ -169,10 +169,12 @@ impl<'a> Collector<'a, '_, '_> { )? .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.lines(lines, styles); + self.lines(lines, leading, styles); self.output.push(Child::Rel(spacing.into(), 4)); self.par_situation = ParSituation::Consecutive; @@ -181,9 +183,8 @@ impl<'a> Collector<'a, '_, '_> { } /// Collect laid-out lines. - fn lines(&mut self, lines: Vec, styles: StyleChain<'a>) { + fn lines(&mut self, lines: Vec, leading: Abs, styles: StyleChain<'a>) { let align = AlignElem::alignment_in(styles).resolve(styles); - let leading = ParElem::leading_in(styles); let costs = TextElem::costs_in(styles); // Determine whether to prevent widow and orphans. diff --git a/crates/typst-layout/src/flow/mod.rs b/crates/typst-layout/src/flow/mod.rs index 2acbbcef3..cba228bcd 100644 --- a/crates/typst-layout/src/flow/mod.rs +++ b/crates/typst-layout/src/flow/mod.rs @@ -197,7 +197,50 @@ pub fn layout_flow<'a>( mode: FlowMode, ) -> SourceResult { // 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, + mode: FlowMode, +) -> Config<'x> { + Config { mode, shared, 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. diff --git a/crates/typst-layout/src/inline/collect.rs b/crates/typst-layout/src/inline/collect.rs index 14cf2e3b8..5a1b7b4fc 100644 --- a/crates/typst-layout/src/inline/collect.rs +++ b/crates/typst-layout/src/inline/collect.rs @@ -2,10 +2,8 @@ use typst_library::diag::warning; use typst_library::foundations::{Packed, Resolve}; use typst_library::introspection::{SplitLocator, Tag, TagElem}; use typst_library::layout::{ - Abs, AlignElem, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing, - Spacing, + Abs, 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, @@ -123,40 +121,20 @@ pub fn collect<'a>( children: &[Pair<'a>], engine: &mut Engine<'_>, locator: &mut SplitLocator<'a>, - styles: StyleChain<'a>, + config: &Config, region: Size, - situation: Option, ) -> SourceResult<(String, Vec>, SpanMapper)> { let mut collector = Collector::new(2 + children.len()); 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)); + collector.spans.push(1, Span::detached()); + } - 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()); - } - - let hang = ParElem::hanging_indent_in(styles); - if !hang.is_zero() { - collector.push_item(Item::Absolute(-hang, false)); - collector.spans.push(1, Span::detached()); - } + if !config.hanging_indent.is_zero() { + collector.push_item(Item::Absolute(-config.hanging_indent, false)); + collector.spans.push(1, Span::detached()); } for &(child, styles) in children { @@ -167,7 +145,7 @@ pub fn collect<'a>( } else if let Some(elem) = child.to_packed::() { collector.build_text(styles, |full| { let dir = TextElem::dir_in(styles); - if dir != outer_dir { + if dir != config.dir { // Insert "Explicit Directional Embedding". match dir { Dir::LTR => full.push_str(LTR_EMBEDDING), @@ -182,7 +160,7 @@ pub fn collect<'a>( full.push_str(&elem.text); } - if dir != outer_dir { + if dir != config.dir { // Insert "Pop Directional Formatting". full.push_str(POP_EMBEDDING); } @@ -265,16 +243,6 @@ 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/finalize.rs b/crates/typst-layout/src/inline/finalize.rs index 7ad287c45..c9de0085e 100644 --- a/crates/typst-layout/src/inline/finalize.rs +++ b/crates/typst-layout/src/inline/finalize.rs @@ -9,7 +9,6 @@ pub fn finalize( engine: &mut Engine, p: &Preparation, lines: &[Line], - styles: StyleChain, region: Size, expand: bool, locator: &mut SplitLocator<'_>, @@ -19,9 +18,10 @@ pub fn finalize( let width = if !region.x.is_finite() || (!expand && lines.iter().all(|line| line.fr().is_zero())) { - region - .x - .min(p.hang + lines.iter().map(|line| line.width).max().unwrap_or_default()) + region.x.min( + p.config.hanging_indent + + lines.iter().map(|line| line.width).max().unwrap_or_default(), + ) } else { region.x }; @@ -29,7 +29,7 @@ pub fn finalize( // Stack the lines into one frame per region. lines .iter() - .map(|line| commit(engine, p, line, width, region.y, locator, styles)) + .map(|line| commit(engine, p, line, width, region.y, locator)) .collect::>() .map(Fragment::frames) } diff --git a/crates/typst-layout/src/inline/line.rs b/crates/typst-layout/src/inline/line.rs index 9f6973807..bd08f30ef 100644 --- a/crates/typst-layout/src/inline/line.rs +++ b/crates/typst-layout/src/inline/line.rs @@ -2,10 +2,9 @@ use std::fmt::{self, Debug, Formatter}; use std::ops::{Deref, DerefMut}; use typst_library::engine::Engine; -use typst_library::foundations::NativeElement; use typst_library::introspection::{SplitLocator, Tag}; 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_utils::Numeric; @@ -135,7 +134,7 @@ pub fn line<'a>( // Whether the line is justified. let justify = full.ends_with(LINE_SEPARATOR) - || (p.justify && breakpoint != Breakpoint::Mandatory); + || (p.config.justify && breakpoint != Breakpoint::Mandatory); // Process dashes. 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. if pred.map_or(false, |pred| should_repeat_hyphen(pred, full)) { 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. if dash == Some(Dash::Soft) { 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. let Some(bidi) = &p.bidi else { - f(range, p.dir == Dir::RTL); + f(range, p.config.dir == Dir::RTL); return; }; // The bidi crate panics for empty lines. if range.is_empty() { - f(range, p.dir == Dir::RTL); + f(range, p.config.dir == Dir::RTL); return; } @@ -308,13 +307,13 @@ fn collect_range<'a>( /// punctuation marks at line start or line end. fn adjust_cj_at_line_boundaries(p: &Preparation, text: &str, items: &mut Items) { 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); } 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); } @@ -332,7 +331,10 @@ fn adjust_cj_at_line_start(p: &Preparation, items: &mut Items) { let shrink = glyph.shrinkability().0; glyph.shrink_left(shrink); 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 // [`add_cjk_latin_spacing`], restore the original width. 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(); punct.shrink_right(shrink); shaped.width -= shrink.at(shaped.size); - } else if p.cjk_latin_spacing + } else if p.config.cjk_latin_spacing && glyph.is_cj_script() && (glyph.x_advance - glyph.x_offset) > Em::one() { @@ -424,16 +426,15 @@ pub fn commit( width: Abs, full: Abs, locator: &mut SplitLocator<'_>, - styles: StyleChain, ) -> SourceResult { - let mut remaining = width - line.width - p.hang; + let mut remaining = width - line.width - p.config.hanging_indent; let mut offset = Abs::zero(); // 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 // hanging indent arises naturally due to the line width. - if p.dir == Dir::LTR { - offset += p.hang; + if p.config.dir == Dir::LTR { + offset += p.config.hanging_indent; } // Handle hanging punctuation to the left. @@ -554,11 +555,13 @@ pub fn commit( let mut output = Frame::soft(size); 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. 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(); 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. fn add_par_line_marker( output: &mut Frame, - styles: StyleChain, + marker: &Packed, engine: &mut Engine, locator: &mut SplitLocator, 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 // 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 // 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 // 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 loc = locator.next_location(engine.introspector, key); 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 // manually adjust its own 'y' position based on its own baseline. 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))); } diff --git a/crates/typst-layout/src/inline/linebreak.rs b/crates/typst-layout/src/inline/linebreak.rs index 87113c689..a9f21188b 100644 --- a/crates/typst-layout/src/inline/linebreak.rs +++ b/crates/typst-layout/src/inline/linebreak.rs @@ -110,15 +110,7 @@ pub fn linebreak<'a>( p: &'a Preparation<'a>, width: Abs, ) -> Vec> { - let linebreaks = p.linebreaks.unwrap_or_else(|| { - if p.justify { - Linebreaks::Optimized - } else { - Linebreaks::Simple - } - }); - - match linebreaks { + match p.config.linebreaks { Linebreaks::Simple => linebreak_simple(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 // 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 // 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. let extra_stretch = (delta - adjustability) / justifiables.max(1) as f64; // 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 @@ -663,9 +655,9 @@ fn breakpoints(p: &Preparation, mut f: impl FnMut(usize, Breakpoint)) { return; } - let hyphenate = p.hyphenate != Some(false); + let hyphenate = p.config.hyphenate != Some(false); let lb = LINEBREAK_DATA.as_borrowed(); - let segmenter = match p.lang { + let segmenter = match p.config.lang { Some(Lang::CHINESE | Lang::JAPANESE) => &CJ_SEGMENTER, _ => &SEGMENTER, }; @@ -830,18 +822,18 @@ fn linebreak_link(link: &str, mut f: impl FnMut(usize)) { /// Whether hyphenation is enabled at the given offset. fn hyphenate_at(p: &Preparation, offset: usize) -> bool { - p.hyphenate - .or_else(|| { - let (_, item) = p.get(offset); - let styles = item.text()?.styles; - Some(TextElem::hyphenate_in(styles)) - }) - .unwrap_or(false) + p.config.hyphenate.unwrap_or_else(|| { + let (_, item) = p.get(offset); + match item.text() { + Some(text) => TextElem::hyphenate_in(text.styles).unwrap_or(p.config.justify), + None => false, + } + }) } /// The text language at the given offset. fn lang_at(p: &Preparation, offset: usize) -> Option { - let lang = p.lang.or_else(|| { + let lang = p.config.lang.or_else(|| { let (_, item) = p.get(offset); let styles = item.text()?.styles; Some(TextElem::lang_in(styles)) @@ -865,13 +857,13 @@ impl CostMetrics { fn compute(p: &Preparation) -> Self { Self { // When justifying, we may stretch spaces below their natural width. - min_ratio: if p.justify { MIN_RATIO } else { 0.0 }, - min_approx_ratio: if p.justify { MIN_APPROX_RATIO } else { 0.0 }, + min_ratio: if p.config.justify { MIN_RATIO } else { 0.0 }, + min_approx_ratio: if p.config.justify { MIN_APPROX_RATIO } else { 0.0 }, // 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. - hyph_cost: DEFAULT_HYPH_COST * p.costs.hyphenation().get(), - runt_cost: DEFAULT_RUNT_COST * p.costs.runt().get(), + hyph_cost: DEFAULT_HYPH_COST * p.config.costs.hyphenation().get(), + runt_cost: DEFAULT_RUNT_COST * p.config.costs.runt().get(), } } diff --git a/crates/typst-layout/src/inline/mod.rs b/crates/typst-layout/src/inline/mod.rs index f8a36368d..5ef820d07 100644 --- a/crates/typst-layout/src/inline/mod.rs +++ b/crates/typst-layout/src/inline/mod.rs @@ -13,12 +13,17 @@ pub use self::box_::layout_box; use comemo::{Track, Tracked, TrackedMut}; use typst_library::diag::SourceResult; 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::layout::{Fragment, Size}; -use typst_library::model::ParElem; +use typst_library::layout::{Abs, AlignElem, Dir, FixedAlignment, Fragment, Size}; +use typst_library::model::{ + EnumElem, FirstLineIndent, Linebreaks, ListElem, ParElem, ParLine, ParLineMarker, + TermsElem, +}; use typst_library::routines::{Arenas, Pair, RealizationKind, Routines}; +use typst_library::text::{Costs, Lang, TextElem}; use typst_library::World; +use typst_utils::{Numeric, SliceExt}; use self::collect::{collect, Item, Segment, SpanMapper}; use self::deco::decorate; @@ -98,7 +103,7 @@ fn layout_par_impl( styles, )?; - layout_inline( + layout_inline_impl( &mut engine, &children, &mut locator, @@ -106,33 +111,134 @@ fn layout_par_impl( region, expand, 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. -#[allow(clippy::too_many_arguments)] pub fn layout_inline<'a>( engine: &mut Engine, children: &[Pair<'a>], locator: &mut SplitLocator<'a>, - styles: StyleChain<'a>, + shared: StyleChain<'a>, + region: Size, + expand: bool, +) -> SourceResult { + 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, expand: bool, par: Option, + base: &ConfigBase, ) -> SourceResult { + // 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. - let (text, segments, spans) = - collect(children, engine, locator, styles, region, par)?; + let (text, segments, spans) = collect(children, engine, locator, &config, region)?; // Perform BiDi analysis and performs some preparation steps before we // 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. - 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. - 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, +) -> 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. @@ -148,3 +254,66 @@ pub enum ParSituation { /// Any other kind of paragraph. Other, } + +/// Raw values from a `ParElem` or style chain. Used to initialize a [`Config`]. +struct ConfigBase { + justify: bool, + linebreaks: Smart, + 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>, + /// 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, + /// The text language (only `Some(_)` if it's the same for all + /// children, otherwise `None`). + lang: Option, + /// 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( + children: &[Pair], + styles: StyleChain<'_>, + getter: fn(StyleChain) -> T, +) -> Option { + 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) +} diff --git a/crates/typst-layout/src/inline/prepare.rs b/crates/typst-layout/src/inline/prepare.rs index 0344d4331..5d7fcd7cb 100644 --- a/crates/typst-layout/src/inline/prepare.rs +++ b/crates/typst-layout/src/inline/prepare.rs @@ -1,9 +1,4 @@ -use typst_library::foundations::{Resolve, Smart}; -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 typst_library::layout::{Dir, Em}; use unicode_bidi::{BidiInfo, Level as BidiLevel}; use super::*; @@ -17,6 +12,8 @@ use super::*; pub struct Preparation<'a> { /// The full text. pub text: &'a str, + /// Configuration for inline layout. + pub config: &'a Config, /// Bidirectional text embedding levels. /// /// This is `None` if all text directions are uniform (all the base @@ -28,28 +25,6 @@ pub struct Preparation<'a> { pub indices: Vec, /// The span mapper. pub spans: SpanMapper, - /// Whether to hyphenate if it's the same for all children. - pub hyphenate: Option, - /// 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, - /// 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, - /// The text size. - pub size: Abs, } impl<'a> Preparation<'a> { @@ -80,15 +55,12 @@ impl<'a> Preparation<'a> { #[typst_macros::time] pub fn prepare<'a>( engine: &mut Engine, - children: &[Pair<'a>], + config: &'a Config, text: &'a str, segments: Vec>, spans: SpanMapper, - styles: StyleChain<'a>, - situation: Option, ) -> SourceResult> { - let dir = TextElem::dir_in(styles); - let default_level = match dir { + let default_level = match config.dir { Dir::RTL => BidiLevel::rtl(), _ => BidiLevel::ltr(), }; @@ -124,51 +96,20 @@ pub fn prepare<'a>( indices.extend(range.clone().map(|_| i)); } - let cjk_latin_spacing = TextElem::cjk_latin_spacing_in(styles).is_auto(); - if cjk_latin_spacing { + if config.cjk_latin_spacing { 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 { + config, text, bidi: is_bidi.then_some(bidi), items, indices, 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( - children: &[Pair], - styles: StyleChain<'_>, - getter: fn(StyleChain) -> T, -) -> Option { - 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 /// Requirements for Chinese Text Layout, Section 3.2.2 Mixed Text Composition /// in Horizontal Written Mode diff --git a/crates/typst-layout/src/math/text.rs b/crates/typst-layout/src/math/text.rs index 9a64992aa..59ac5b089 100644 --- a/crates/typst-layout/src/math/text.rs +++ b/crates/typst-layout/src/math/text.rs @@ -107,7 +107,6 @@ fn layout_inline_text( styles, Size::splat(Abs::inf()), false, - None, )? .into_frame(); diff --git a/crates/typst-library/src/model/link.rs b/crates/typst-library/src/model/link.rs index 24b746b7e..ea85aa945 100644 --- a/crates/typst-library/src/model/link.rs +++ b/crates/typst-library/src/model/link.rs @@ -11,7 +11,7 @@ use crate::foundations::{ use crate::html::{attr, tag, HtmlElem}; use crate::introspection::Location; use crate::layout::Position; -use crate::text::{Hyphenate, TextElem}; +use crate::text::TextElem; /// Links to a URL or a location in the document. /// @@ -138,7 +138,7 @@ impl Show for Packed { impl ShowSet for Packed { fn show_set(&self, _: StyleChain) -> Styles { let mut out = Styles::new(); - out.set(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false)))); + out.set(TextElem::set_hyphenate(Smart::Custom(false))); out } } diff --git a/crates/typst-library/src/text/mod.rs b/crates/typst-library/src/text/mod.rs index 12f4e4c59..30c2ea1d1 100644 --- a/crates/typst-library/src/text/mod.rs +++ b/crates/typst-library/src/text/mod.rs @@ -51,7 +51,6 @@ use crate::foundations::{ }; use crate::layout::{Abs, Axis, Dir, Em, Length, Ratio, Rel}; use crate::math::{EquationElem, MathSize}; -use crate::model::ParElem; use crate::visualize::{Color, Paint, RelativeTo, Stroke}; use crate::World; @@ -504,9 +503,8 @@ pub struct TextElem { /// enabling hyphenation can /// improve justification. /// ``` - #[resolve] #[ghost] - pub hyphenate: Hyphenate, + pub hyphenate: Smart, /// 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 @@ -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); - -cast! { - Hyphenate, - self => self.0.into_value(), - v: Smart => 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. #[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash)] pub struct StylisticSets(u32); diff --git a/crates/typst-library/src/text/raw.rs b/crates/typst-library/src/text/raw.rs index 5bb21e43a..b330c01ef 100644 --- a/crates/typst-library/src/text/raw.rs +++ b/crates/typst-library/src/text/raw.rs @@ -21,9 +21,7 @@ use crate::html::{tag, HtmlElem}; use crate::layout::{BlockBody, BlockElem, Em, HAlignment}; use crate::loading::{DataSource, Load}; use crate::model::{Figurable, ParElem}; -use crate::text::{ - FontFamily, FontList, Hyphenate, LinebreakElem, LocalName, TextElem, TextSize, -}; +use crate::text::{FontFamily, FontList, LinebreakElem, LocalName, TextElem, TextSize}; use crate::visualize::Color; use crate::World; @@ -472,7 +470,7 @@ impl ShowSet for Packed { let mut out = Styles::new(); out.set(TextElem::set_overhang(false)); 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_font(FontList(vec![FontFamily::new("DejaVu Sans Mono")]))); out.set(TextElem::set_cjk_latin_spacing(Smart::Custom(None))); diff --git a/tests/ref/issue-5831-par-constructor-args.png b/tests/ref/issue-5831-par-constructor-args.png new file mode 100644 index 000000000..440b612ba Binary files /dev/null and b/tests/ref/issue-5831-par-constructor-args.png differ diff --git a/tests/suite/model/par.typ b/tests/suite/model/par.typ index e76690064..d2c3416e0 100644 --- a/tests/suite/model/par.typ +++ b/tests/suite/model/par.typ @@ -322,6 +322,20 @@ A #context test(query().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 --- // 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