#[path = "box.rs"] mod box_; mod collect; mod deco; mod finalize; mod line; mod linebreak; mod prepare; mod shaping; 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, Resolve, Smart, StyleChain}; use typst_library::introspection::{Introspector, Locator, LocatorLink, SplitLocator}; 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; use self::finalize::finalize; use self::line::{apply_baseline_shift, commit, line, Line}; use self::linebreak::{linebreak, Breakpoint}; use self::prepare::{prepare, Preparation}; use self::shaping::{ cjk_punct_style, is_of_cj_script, shape_range, ShapedGlyph, ShapedText, BEGIN_PUNCT_PAT, END_PUNCT_PAT, }; /// Range of a substring of text. type Range = std::ops::Range; /// Layouts the paragraph. pub fn layout_par( elem: &Packed, engine: &mut Engine, locator: Locator, styles: StyleChain, region: Size, expand: bool, situation: ParSituation, ) -> SourceResult { layout_par_impl( elem, engine.routines, engine.world, engine.introspector, engine.traced, TrackedMut::reborrow_mut(&mut engine.sink), engine.route.track(), locator.track(), styles, region, expand, situation, ) } /// The internal, memoized implementation of `layout_par`. #[comemo::memoize] #[allow(clippy::too_many_arguments)] fn layout_par_impl( elem: &Packed, routines: &Routines, world: Tracked, introspector: Tracked, traced: Tracked, sink: TrackedMut, route: Tracked, locator: Tracked, styles: StyleChain, region: Size, expand: bool, situation: ParSituation, ) -> SourceResult { let link = LocatorLink::new(locator); let mut locator = Locator::link(&link).split(); let mut engine = Engine { routines, world, introspector, traced, sink, route: Route::extend(route), }; let arenas = Arenas::default(); let children = (engine.routines.realize)( RealizationKind::LayoutPar, &mut engine, &mut locator, &arenas, &elem.body, styles, )?; layout_inline_impl( &mut engine, &children, &mut locator, styles, 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. pub fn layout_inline<'a>( engine: &mut Engine, children: &[Pair<'a>], locator: &mut SplitLocator<'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, &config, region)?; // Perform BiDi analysis and performs some preparation steps before we // proceed to line breaking. let p = prepare(engine, &config, &text, segments, spans)?; // Break the text into lines. let lines = linebreak(engine, &p, region.x - config.hanging_indent); // Turn the selected lines into frames. 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. /// /// In the form `Option`, `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, } /// 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) }