summaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
authorMatt Fellenz <matt@felle.nz>2024-04-30 05:18:19 -0700
committerGitHub <noreply@github.com>2024-04-30 12:18:19 +0000
commit97de0a0595d28e29d944112ab6e06700d9c9d73d (patch)
tree220ffef576722553b311526258e23070b8a8ce95 /crates
parent0bb45b335fb8b34af9ce33e1684e58c046ee5e45 (diff)
Various text layout config improvements (#3787)
Diffstat (limited to 'crates')
-rw-r--r--crates/typst/src/layout/inline/mod.rs43
-rw-r--r--crates/typst/src/text/mod.rs123
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
+ },
+}