diff options
| author | Matt Fellenz <matt@felle.nz> | 2024-04-30 05:18:19 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-04-30 12:18:19 +0000 |
| commit | 97de0a0595d28e29d944112ab6e06700d9c9d73d (patch) | |
| tree | 220ffef576722553b311526258e23070b8a8ce95 /crates | |
| parent | 0bb45b335fb8b34af9ce33e1684e58c046ee5e45 (diff) | |
Various text layout config improvements (#3787)
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/typst/src/layout/inline/mod.rs | 43 | ||||
| -rw-r--r-- | crates/typst/src/text/mod.rs | 123 |
2 files changed, 145 insertions, 21 deletions
diff --git a/crates/typst/src/layout/inline/mod.rs b/crates/typst/src/layout/inline/mod.rs index fd1e60db..77408c7a 100644 --- a/crates/typst/src/layout/inline/mod.rs +++ b/crates/typst/src/layout/inline/mod.rs @@ -117,6 +117,7 @@ struct Preparation<'a> { spans: SpanMapper, /// Whether to hyphenate if it's the same for all children. hyphenate: Option<bool>, + costs: crate::text::Costs, /// The text language if it's the same for all children. lang: Option<Lang>, /// The paragraph's resolved horizontal alignment. @@ -630,11 +631,14 @@ fn prepare<'a>( add_cjk_latin_spacing(&mut items); } + let costs = TextElem::costs_in(styles); + Ok(Preparation { bidi, items, spans, hyphenate: shared_get(styles, children, TextElem::hyphenate_in), + costs, lang: shared_get(styles, children, TextElem::lang_in), align: AlignElem::alignment_in(styles).resolve(styles).x, justify: ParElem::justify_in(styles), @@ -876,12 +880,15 @@ fn linebreak_optimized<'a>( } // Cost parameters. - const HYPH_COST: Cost = 0.5; - const RUNT_COST: Cost = 0.5; + const DEFAULT_HYPH_COST: Cost = 0.5; + const DEFAULT_RUNT_COST: Cost = 0.5; const CONSECUTIVE_DASH_COST: Cost = 0.3; const MAX_COST: Cost = 1_000_000.0; const MIN_RATIO: f64 = -1.0; + let hyph_cost = DEFAULT_HYPH_COST * p.costs.hyphenation().get(); + let runt_cost = DEFAULT_RUNT_COST * p.costs.runt().get(); + // Dynamic programming table. let mut active = 0; let mut table = vec![Entry { @@ -965,12 +972,12 @@ fn linebreak_optimized<'a>( // Penalize runts. if k == i + 1 && is_end { - cost += RUNT_COST; + cost += runt_cost; } // Penalize hyphens. if breakpoint == Breakpoint::Hyphen { - cost += HYPH_COST; + cost += hyph_cost; } // In Knuth paper, cost = (1 + 100|r|^3 + p)^2 + a, @@ -1212,19 +1219,23 @@ fn finalize( .map(|line| commit(engine, p, line, width, region.y, shrink)) .collect::<SourceResult<_>>()?; - // Prevent orphans. - if frames.len() >= 2 && !frames[1].is_empty() { - let second = frames.remove(1); - let first = &mut frames[0]; - merge(first, second, p.leading); + // Positive ratios enable prevention, while zero and negative ratios disable it. + if p.costs.orphan().get() > 0.0 { + // Prevent orphans. + if frames.len() >= 2 && !frames[1].is_empty() { + let second = frames.remove(1); + let first = &mut frames[0]; + merge(first, second, p.leading); + } } - - // Prevent widows. - let len = frames.len(); - if len >= 2 && !frames[len - 2].is_empty() { - let second = frames.pop().unwrap(); - let first = frames.last_mut().unwrap(); - merge(first, second, p.leading); + if p.costs.widow().get() > 0.0 { + // Prevent widows. + let len = frames.len(); + if len >= 2 && !frames[len - 2].is_empty() { + let second = frames.pop().unwrap(); + let first = frames.last_mut().unwrap(); + merge(first, second, p.leading); + } } Ok(Fragment::frames(frames)) diff --git a/crates/typst/src/text/mod.rs b/crates/typst/src/text/mod.rs index ef621ee5..0b8818e8 100644 --- a/crates/typst/src/text/mod.rs +++ b/crates/typst/src/text/mod.rs @@ -37,13 +37,12 @@ use ttf_parser::Rect; use crate::diag::{bail, warning, SourceResult, StrResult}; use crate::engine::Engine; -use crate::foundations::Packed; use crate::foundations::{ - cast, category, elem, Args, Array, Cast, Category, Construct, Content, Dict, Fold, - NativeElement, Never, PlainText, Repr, Resolve, Scope, Set, Smart, StyleChain, + cast, category, dict, elem, Args, Array, Cast, Category, Construct, Content, Dict, + Fold, NativeElement, Never, Packed, PlainText, Repr, Resolve, Scope, Set, Smart, + StyleChain, }; -use crate::layout::Em; -use crate::layout::{Abs, Axis, Dir, Length, Rel}; +use crate::layout::{Abs, Axis, Dir, Em, Length, Ratio, Rel}; use crate::model::ParElem; use crate::syntax::Spanned; use crate::visualize::{Color, Paint, RelativeTo, Stroke}; @@ -482,6 +481,52 @@ pub struct TextElem { #[ghost] pub hyphenate: Hyphenate, + /// 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 + /// as a ratio of the default cost, so `50%` will make text layout twice as + /// eager to make a given choice, while `200%` will make it half as eager. + /// + /// Currently, the following costs can be customized: + /// - `hyphenation`: splitting a word across multiple lines + /// - `runt`: ending a paragraph with a line with a single word + /// - `widow`: leaving a single line of paragraph on the next page + /// - `orphan`: leaving single line of paragraph on the previous page + /// + /// Hyphenation is generally avoided by placing the whole word on the next + /// line, so a higher hyphenation cost can result in awkward justification + /// spacing. + /// + /// Runts are avoided by placing more or fewer words on previous lines, so a + /// higher runt cost can result in more awkward in justification spacing. + /// + /// Text layout prevents widows and orphans by default because they are + /// generally discouraged by style guides. However, in some contexts they + /// are allowed because the prevention method, which moves a line to the + /// next page, can result in an uneven number of lines between pages. + /// The `widow` and `orphan` costs allow disabling these modifications. + /// (Currently, 0% allows widows/orphans; anything else, including the + /// default of `auto`, prevents them. More nuanced cost specification for + /// these modifications is planned for the future.) + /// + /// The default costs are an acceptable balance, but some may find that it + /// hyphenates or avoids runs too eagerly, breaking the flow of dense prose. + /// A cost of 600% (six times the normal cost) may work better for such + /// contexts. + /// + /// ```example + /// #set text(hyphenate: true, size: 11.4pt) + /// #set par(justify: true) + /// + /// #lorem(10) + /// + /// // Set hyphenation to ten times the normal cost. + /// #set text(costs: (hyphenation: 1000%)) + /// + /// #lorem(10) + /// ``` + #[fold] + pub costs: Costs, + /// Whether to apply kerning. /// /// When enabled, specific letter pairings move closer together or further @@ -1184,3 +1229,71 @@ impl Fold for WeightDelta { Self(outer.0 + self.0) } } + +/// Costs that are updated (prioritizing the later value) when folded. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] +#[non_exhaustive] // We may add more costs in the future. +pub struct Costs { + pub hyphenation: Option<Ratio>, + pub runt: Option<Ratio>, + pub widow: Option<Ratio>, + pub orphan: Option<Ratio>, +} + +impl Costs { + #[inline] + #[must_use] + pub fn hyphenation(&self) -> Ratio { + self.hyphenation.unwrap_or(Ratio::one()) + } + + #[inline] + #[must_use] + pub fn runt(&self) -> Ratio { + self.runt.unwrap_or(Ratio::one()) + } + + #[inline] + #[must_use] + pub fn widow(&self) -> Ratio { + self.widow.unwrap_or(Ratio::one()) + } + + #[inline] + #[must_use] + pub fn orphan(&self) -> Ratio { + self.orphan.unwrap_or(Ratio::one()) + } +} + +impl Fold for Costs { + #[inline] + fn fold(self, outer: Self) -> Self { + Self { + hyphenation: self.hyphenation.or(outer.hyphenation), + runt: self.runt.or(outer.runt), + widow: self.widow.or(outer.widow), + orphan: self.orphan.or(outer.orphan), + } + } +} + +cast! { + Costs, + self => dict![ + "hyphenation" => self.hyphenation(), + "runt" => self.runt(), + "widow" => self.widow(), + "orphan" => self.orphan(), + ].into_value(), + mut v: Dict => { + let ret = Self { + hyphenation: v.take("hyphenation").ok().map(|v| v.cast()).transpose()?, + runt: v.take("runt").ok().map(|v| v.cast()).transpose()?, + widow: v.take("widow").ok().map(|v| v.cast()).transpose()?, + orphan: v.take("orphan").ok().map(|v| v.cast()).transpose()?, + }; + v.finish(&["hyphenation", "runt", "widow", "orphan"])?; + ret + }, +} |
