summaryrefslogtreecommitdiff
path: root/crates/typst-layout/src/inline/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/typst-layout/src/inline/mod.rs')
-rw-r--r--crates/typst-layout/src/inline/mod.rs191
1 files changed, 180 insertions, 11 deletions
diff --git a/crates/typst-layout/src/inline/mod.rs b/crates/typst-layout/src/inline/mod.rs
index f8a36368..5ef820d0 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<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,
expand: bool,
par: Option<ParSituation>,
+ base: &ConfigBase,
) -> 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.
- 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<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.
@@ -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<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)
+}