summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2022-04-10 23:23:50 +0200
committerLaurenz <laurmaedje@gmail.com>2022-04-10 23:36:36 +0200
commit34fa8df044f1491069c9ae69f1c1e73d635c8955 (patch)
tree30b9144690f55f0343a1fdbfcac6c9238c617077 /src
parent29eb13ca6214461a4b0deb63d589cd39ad6d41c2 (diff)
Move language-related properties from `par` to `text`
Closes #67
Diffstat (limited to 'src')
-rw-r--r--src/eval/raw.rs4
-rw-r--r--src/eval/styles.rs19
-rw-r--r--src/library/graphics/shape.rs1
-rw-r--r--src/library/layout/align.rs4
-rw-r--r--src/library/layout/columns.rs4
-rw-r--r--src/library/layout/page.rs7
-rw-r--r--src/library/structure/heading.rs3
-rw-r--r--src/library/structure/list.rs4
-rw-r--r--src/library/structure/table.rs1
-rw-r--r--src/library/text/deco.rs2
-rw-r--r--src/library/text/mod.rs86
-rw-r--r--src/library/text/par.rs323
-rw-r--r--src/library/text/raw.rs6
13 files changed, 324 insertions, 140 deletions
diff --git a/src/eval/raw.rs b/src/eval/raw.rs
index b0f46fc9..a83c363f 100644
--- a/src/eval/raw.rs
+++ b/src/eval/raw.rs
@@ -6,7 +6,7 @@ use super::{Fold, Resolve, Smart, StyleChain, Value};
use crate::geom::{
Align, Em, Get, Length, Numeric, Paint, Relative, Spec, SpecAxis, Stroke,
};
-use crate::library::text::{ParNode, TextNode};
+use crate::library::text::TextNode;
/// The unresolved alignment representation.
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
@@ -23,7 +23,7 @@ impl Resolve for RawAlign {
type Output = Align;
fn resolve(self, styles: StyleChain) -> Self::Output {
- let dir = styles.get(ParNode::DIR);
+ let dir = styles.get(TextNode::DIR);
match self {
Self::Start => dir.start().into(),
Self::End => dir.end().into(),
diff --git a/src/eval/styles.rs b/src/eval/styles.rs
index 71293f40..f147d8cf 100644
--- a/src/eval/styles.rs
+++ b/src/eval/styles.rs
@@ -66,6 +66,14 @@ impl StyleMap {
self.0.push(Entry::Recipe(Recipe::new::<T>(func, span)));
}
+ /// Whether the map contains a style property for the given key.
+ pub fn contains<'a, K: Key<'a>>(&self, _: K) -> bool {
+ self.0
+ .iter()
+ .filter_map(|entry| entry.property())
+ .any(|property| property.key == TypeId::of::<K>())
+ }
+
/// Make `self` the first link of the `tail` chain.
///
/// The resulting style chain contains styles from `self` as well as
@@ -261,7 +269,7 @@ where
///
/// This trait is not intended to be implemented manually, but rather through
/// the `#[node]` proc-macro.
-pub trait Key<'a>: 'static {
+pub trait Key<'a>: Copy + 'static {
/// The unfolded type which this property is stored as in a style map. For
/// example, this is [`Toggle`](crate::geom::Length) for the
/// [`STRONG`](TextNode::STRONG) property.
@@ -680,6 +688,15 @@ impl<T> StyleVec<T> {
self.items.len()
}
+ /// Iterate over the contained maps. Note that zipping this with `items()`
+ /// does not yield the same result as calling `iter()` because this method
+ /// only returns maps once that are shared by consecutive items. This method
+ /// is designed for use cases where you want to check, for example, whether
+ /// any of the maps fulfills a specific property.
+ pub fn maps(&self) -> impl Iterator<Item = &StyleMap> {
+ self.maps.iter().map(|(map, _)| map)
+ }
+
/// Iterate over the contained items.
pub fn items(&self) -> std::slice::Iter<'_, T> {
self.items.iter()
diff --git a/src/library/graphics/shape.rs b/src/library/graphics/shape.rs
index a159a3af..236406c0 100644
--- a/src/library/graphics/shape.rs
+++ b/src/library/graphics/shape.rs
@@ -26,6 +26,7 @@ impl<const S: ShapeKind> ShapeNode<S> {
/// How to stroke the shape.
#[property(resolve, fold)]
pub const STROKE: Smart<Option<RawStroke>> = Smart::Auto;
+
/// How much to pad the shape's content.
pub const PADDING: Relative<RawLength> = Relative::zero();
diff --git a/src/library/layout/align.rs b/src/library/layout/align.rs
index 699a908c..2a4d039e 100644
--- a/src/library/layout/align.rs
+++ b/src/library/layout/align.rs
@@ -1,5 +1,5 @@
use crate::library::prelude::*;
-use crate::library::text::ParNode;
+use crate::library::text::{HorizontalAlign, ParNode};
/// Align a node along the layouting axes.
#[derive(Debug, Hash)]
@@ -33,7 +33,7 @@ impl Layout for AlignNode {
// Align paragraphs inside the child.
let mut passed = StyleMap::new();
if let Some(align) = self.aligns.x {
- passed.set(ParNode::ALIGN, align);
+ passed.set(ParNode::ALIGN, HorizontalAlign(align));
}
// Layout the child.
diff --git a/src/library/layout/columns.rs b/src/library/layout/columns.rs
index 3ef66b40..4963043e 100644
--- a/src/library/layout/columns.rs
+++ b/src/library/layout/columns.rs
@@ -1,5 +1,5 @@
use crate::library::prelude::*;
-use crate::library::text::ParNode;
+use crate::library::text::TextNode;
/// Separate a region into multiple equally sized columns.
#[derive(Debug, Hash)]
@@ -59,7 +59,7 @@ impl Layout for ColumnsNode {
// Layout the children.
let mut frames = self.child.layout(ctx, &pod, styles)?.into_iter();
- let dir = styles.get(ParNode::DIR);
+ let dir = styles.get(TextNode::DIR);
let total_regions = (frames.len() as f32 / columns as f32).ceil() as usize;
let mut finished = vec![];
diff --git a/src/library/layout/page.rs b/src/library/layout/page.rs
index 7aa53b23..13583f09 100644
--- a/src/library/layout/page.rs
+++ b/src/library/layout/page.rs
@@ -17,6 +17,7 @@ impl PageNode {
pub const HEIGHT: Smart<RawLength> = Smart::Custom(Paper::A4.height().into());
/// Whether the page is flipped into landscape orientation.
pub const FLIPPED: bool = false;
+
/// The left margin.
pub const LEFT: Smart<Relative<RawLength>> = Smart::Auto;
/// The right margin.
@@ -25,10 +26,12 @@ impl PageNode {
pub const TOP: Smart<Relative<RawLength>> = Smart::Auto;
/// The bottom margin.
pub const BOTTOM: Smart<Relative<RawLength>> = Smart::Auto;
- /// The page's background color.
- pub const FILL: Option<Paint> = None;
+
/// How many columns the page has.
pub const COLUMNS: NonZeroUsize = NonZeroUsize::new(1).unwrap();
+ /// The page's background color.
+ pub const FILL: Option<Paint> = None;
+
/// The page's header.
#[property(referenced)]
pub const HEADER: Marginal = Marginal::None;
diff --git a/src/library/structure/heading.rs b/src/library/structure/heading.rs
index dcf87f90..07e5e662 100644
--- a/src/library/structure/heading.rs
+++ b/src/library/structure/heading.rs
@@ -25,6 +25,7 @@ impl HeadingNode {
let upscale = (1.6 - 0.1 * level as f64).max(0.75);
TextSize(Em::new(upscale).into())
});
+
/// Whether text in the heading is strengthend.
#[property(referenced)]
pub const STRONG: Leveled<bool> = Leveled::Value(true);
@@ -34,12 +35,14 @@ impl HeadingNode {
/// Whether the heading is underlined.
#[property(referenced)]
pub const UNDERLINE: Leveled<bool> = Leveled::Value(false);
+
/// The extra padding above the heading.
#[property(referenced)]
pub const ABOVE: Leveled<RawLength> = Leveled::Value(Length::zero().into());
/// The extra padding below the heading.
#[property(referenced)]
pub const BELOW: Leveled<RawLength> = Leveled::Value(Length::zero().into());
+
/// Whether the heading is block-level.
#[property(referenced)]
pub const BLOCK: Leveled<bool> = Leveled::Value(true);
diff --git a/src/library/structure/list.rs b/src/library/structure/list.rs
index c3eae1af..02a7cd38 100644
--- a/src/library/structure/list.rs
+++ b/src/library/structure/list.rs
@@ -33,6 +33,7 @@ impl<const L: ListKind> ListNode<L> {
/// How the list is labelled.
#[property(referenced)]
pub const LABEL: Label = Label::Default;
+
/// The spacing between the list items of a non-wide list.
#[property(resolve)]
pub const SPACING: RawLength = RawLength::zero();
@@ -42,6 +43,7 @@ impl<const L: ListKind> ListNode<L> {
/// The space between the label and the body of each item.
#[property(resolve)]
pub const BODY_INDENT: RawLength = Em::new(0.5).into();
+
/// The extra padding above the list.
#[property(resolve)]
pub const ABOVE: RawLength = RawLength::zero();
@@ -137,7 +139,7 @@ pub const UNORDERED: ListKind = 0;
/// Ordered list labelling style.
pub const ORDERED: ListKind = 1;
-/// Either content or a closure mapping to content.
+/// How to label a list or enumeration.
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum Label {
/// The default labelling.
diff --git a/src/library/structure/table.rs b/src/library/structure/table.rs
index 40f25749..96d3bd5b 100644
--- a/src/library/structure/table.rs
+++ b/src/library/structure/table.rs
@@ -21,6 +21,7 @@ impl TableNode {
/// How to stroke the cells.
#[property(resolve, fold)]
pub const STROKE: Option<RawStroke> = Some(RawStroke::default());
+
/// How much to pad the cells's content.
pub const PADDING: Relative<RawLength> = Length::pt(5.0).into();
diff --git a/src/library/text/deco.rs b/src/library/text/deco.rs
index b8a0b3cb..7481b836 100644
--- a/src/library/text/deco.rs
+++ b/src/library/text/deco.rs
@@ -24,6 +24,7 @@ impl<const L: DecoLine> DecoNode<L> {
/// tables if `auto`.
#[property(shorthand, resolve, fold)]
pub const STROKE: Smart<RawStroke> = Smart::Auto;
+
/// Position of the line relative to the baseline, read from the font tables
/// if `auto`.
#[property(resolve)]
@@ -31,6 +32,7 @@ impl<const L: DecoLine> DecoNode<L> {
/// Amount that the line will be longer or shorter than its associated text.
#[property(resolve)]
pub const EXTENT: RawLength = RawLength::zero();
+
/// Whether the line skips sections in which it would collide
/// with the glyphs. Does not apply to strikethrough.
pub const EVADE: bool = true;
diff --git a/src/library/text/mod.rs b/src/library/text/mod.rs
index b5ccc636..1d750689 100644
--- a/src/library/text/mod.rs
+++ b/src/library/text/mod.rs
@@ -61,6 +61,18 @@ impl TextNode {
/// The bottom end of the text bounding box.
pub const BOTTOM_EDGE: TextEdge = TextEdge::Metric(VerticalFontMetric::Baseline);
+ /// An ISO 639-1 language code.
+ #[property(referenced)]
+ pub const LANG: Option<Lang> = None;
+ /// The direction for text and inline objects. When `auto`, the direction is
+ /// automatically inferred from the language.
+ #[property(resolve)]
+ pub const DIR: Smart<HorizontalDir> = Smart::Auto;
+ /// Whether to hyphenate text to improve line breaking. When `auto`, words
+ /// will will be hyphenated if and only if justification is enabled.
+ #[property(resolve)]
+ pub const HYPHENATE: Smart<Hyphenate> = Smart::Auto;
+
/// Whether to apply kerning ("kern").
pub const KERNING: bool = true;
/// Whether small capital glyphs should be used. ("smcp")
@@ -241,6 +253,80 @@ castable! {
}),
}
+/// A natural language.
+#[derive(Debug, Clone, Eq, PartialEq, Hash)]
+pub struct Lang(EcoString);
+
+impl Lang {
+ /// The default direction for the language.
+ pub fn dir(&self) -> Dir {
+ match self.0.as_str() {
+ "ar" | "dv" | "fa" | "he" | "ks" | "pa" | "ps" | "sd" | "ug" | "ur"
+ | "yi" => Dir::RTL,
+ _ => Dir::LTR,
+ }
+ }
+
+ /// Return the language code as a string slice.
+ pub fn as_str(&self) -> &str {
+ &self.0
+ }
+}
+
+castable! {
+ Lang,
+ Expected: "string",
+ Value::Str(string) => Self(string.to_lowercase()),
+}
+
+/// The direction of text and inline objects in their line.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct HorizontalDir(pub Dir);
+
+castable! {
+ HorizontalDir,
+ Expected: "direction",
+ @dir: Dir => match dir.axis() {
+ SpecAxis::Horizontal => Self(*dir),
+ SpecAxis::Vertical => Err("must be horizontal")?,
+ },
+}
+
+impl Resolve for Smart<HorizontalDir> {
+ type Output = Dir;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ match self {
+ Smart::Auto => match styles.get(TextNode::LANG) {
+ Some(lang) => lang.dir(),
+ None => Dir::LTR,
+ },
+ Smart::Custom(dir) => dir.0,
+ }
+ }
+}
+
+/// Whether to hyphenate text.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct Hyphenate(pub bool);
+
+castable! {
+ Hyphenate,
+ Expected: "boolean",
+ Value::Bool(v) => Self(v),
+}
+
+impl Resolve for Smart<Hyphenate> {
+ type Output = bool;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ match self {
+ Smart::Auto => styles.get(ParNode::JUSTIFY),
+ Smart::Custom(v) => v.0,
+ }
+ }
+}
+
/// A stylistic set in a font face.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct StylisticSet(u8);
diff --git a/src/library/text/par.rs b/src/library/text/par.rs
index 57e2b45d..765c3bf5 100644
--- a/src/library/text/par.rs
+++ b/src/library/text/par.rs
@@ -1,10 +1,9 @@
use std::sync::Arc;
-use either::Either;
use unicode_bidi::{BidiInfo, Level};
use xi_unicode::LineBreakIterator;
-use super::{shape, ShapedText, TextNode};
+use super::{shape, Lang, ShapedText, TextNode};
use crate::font::FontStore;
use crate::library::layout::Spacing;
use crate::library::prelude::*;
@@ -27,21 +26,6 @@ pub enum ParChild {
#[node]
impl ParNode {
- /// An ISO 639-1 language code.
- #[property(referenced)]
- pub const LANG: Option<EcoString> = None;
- /// The direction for text and inline objects.
- pub const DIR: Dir = Dir::LTR;
- /// How to align text and inline objects in their line.
- #[property(resolve)]
- pub const ALIGN: RawAlign = RawAlign::Start;
- /// Whether to justify text in its line.
- pub const JUSTIFY: bool = false;
- /// How to determine line breaks.
- pub const LINEBREAKS: Smart<Linebreaks> = Smart::Auto;
- /// Whether to hyphenate text to improve line breaking. When `auto`, words
- /// will will be hyphenated if and only if justification is enabled.
- pub const HYPHENATE: Smart<bool> = Smart::Auto;
/// The spacing between lines.
#[property(resolve)]
pub const LEADING: RawLength = Em::new(0.65).into();
@@ -52,6 +36,15 @@ impl ParNode {
#[property(resolve)]
pub const INDENT: RawLength = RawLength::zero();
+ /// How to align text and inline objects in their line.
+ #[property(resolve)]
+ pub const ALIGN: HorizontalAlign = HorizontalAlign(RawAlign::Start);
+ /// Whether to justify text in its line.
+ pub const JUSTIFY: bool = false;
+ /// How to determine line breaks.
+ #[property(resolve)]
+ pub const LINEBREAKS: Smart<Linebreaks> = Smart::Auto;
+
fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
// The paragraph constructor is special: It doesn't create a paragraph
// since that happens automatically through markup. Instead, it just
@@ -59,45 +52,6 @@ impl ParNode {
// adjacent stuff and it styles the contained paragraphs.
Ok(Content::Block(args.expect("body")?))
}
-
- fn set(args: &mut Args) -> TypResult<StyleMap> {
- let mut styles = StyleMap::new();
-
- let lang = args.named::<Option<EcoString>>("lang")?;
- let mut dir =
- lang.clone().flatten().map(|iso| match iso.to_lowercase().as_str() {
- "ar" | "dv" | "fa" | "he" | "ks" | "pa" | "ps" | "sd" | "ug" | "ur"
- | "yi" => Dir::RTL,
- _ => Dir::LTR,
- });
-
- if let Some(Spanned { v, span }) = args.named::<Spanned<Dir>>("dir")? {
- if v.axis() != SpecAxis::Horizontal {
- bail!(span, "must be horizontal");
- }
- dir = Some(v);
- }
-
- let mut align = None;
- if let Some(Spanned { v, span }) = args.named::<Spanned<RawAlign>>("align")? {
- if v.axis() != SpecAxis::Horizontal {
- bail!(span, "must be horizontal");
- }
- align = Some(v);
- };
-
- styles.set_opt(Self::LANG, lang);
- styles.set_opt(Self::DIR, dir);
- styles.set_opt(Self::ALIGN, align);
- styles.set_opt(Self::JUSTIFY, args.named("justify")?);
- styles.set_opt(Self::LINEBREAKS, args.named("linebreaks")?);
- styles.set_opt(Self::HYPHENATE, args.named("hyphenate")?);
- styles.set_opt(Self::LEADING, args.named("leading")?);
- styles.set_opt(Self::SPACING, args.named("spacing")?);
- styles.set_opt(Self::INDENT, args.named("indent")?);
-
- Ok(styles)
- }
}
impl ParNode {
@@ -147,7 +101,7 @@ impl Layout for ParNode {
let p = prepare(ctx, self, &text, regions, &styles)?;
// Break the paragraph into lines.
- let lines = linebreak(&p, &mut ctx.fonts, regions.first.x, styles);
+ let lines = linebreak(&p, &mut ctx.fonts, regions.first.x);
// Stack the lines into one frame per region.
Ok(stack(&lines, &ctx.fonts, regions, styles))
@@ -182,6 +136,27 @@ impl Merge for ParChild {
}
}
+/// A horizontal alignment.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct HorizontalAlign(pub RawAlign);
+
+castable! {
+ HorizontalAlign,
+ Expected: "alignment",
+ @align: RawAlign => match align.axis() {
+ SpecAxis::Horizontal => Self(*align),
+ SpecAxis::Vertical => Err("must be horizontal")?,
+ },
+}
+
+impl Resolve for HorizontalAlign {
+ type Output = Align;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ self.0.resolve(styles)
+ }
+}
+
/// How to determine line breaks in a paragraph.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum Linebreaks {
@@ -201,6 +176,20 @@ castable! {
},
}
+impl Resolve for Smart<Linebreaks> {
+ type Output = Linebreaks;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ self.unwrap_or_else(|| {
+ if styles.get(ParNode::JUSTIFY) {
+ Linebreaks::Optimized
+ } else {
+ Linebreaks::Simple
+ }
+ })
+ }
+}
+
/// A paragraph break.
pub struct ParbreakNode;
@@ -233,17 +222,35 @@ type Range = std::ops::Range<usize>;
struct Preparation<'a> {
/// Bidirectional text embedding levels for the paragraph.
bidi: BidiInfo<'a>,
+ /// The paragraph's children.
+ children: &'a StyleVec<ParChild>,
/// Spacing, separated text runs and layouted nodes.
items: Vec<ParItem<'a>>,
/// The ranges of the items in `bidi.text`.
ranges: Vec<Range>,
+ /// The shared styles.
+ styles: StyleChain<'a>,
}
-impl Preparation<'_> {
+impl<'a> Preparation<'a> {
+ /// Find the item whose range contains the `text_offset`.
+ fn find(&self, text_offset: usize) -> Option<&ParItem<'a>> {
+ self.find_idx(text_offset).map(|idx| &self.items[idx])
+ }
+
/// Find the index of the item whose range contains the `text_offset`.
- fn find(&self, text_offset: usize) -> Option<usize> {
+ fn find_idx(&self, text_offset: usize) -> Option<usize> {
self.ranges.binary_search_by(|r| r.locate(text_offset)).ok()
}
+
+ /// Get a style property, but only if it is the same for all children of the
+ /// paragraph.
+ fn get_shared<K: Key<'a>>(&self, key: K) -> Option<K::Output> {
+ self.children
+ .maps()
+ .all(|map| !map.contains(key))
+ .then(|| self.styles.get(key))
+ }
}
/// A prepared item in a paragraph layout.
@@ -258,6 +265,16 @@ enum ParItem<'a> {
Frame(Frame),
}
+impl<'a> ParItem<'a> {
+ /// If this a text item, return it.
+ fn text(&self) -> Option<&ShapedText<'a>> {
+ match self {
+ Self::Text(shaped) => Some(shaped),
+ _ => None,
+ }
+ }
+}
+
/// A layouted line, consisting of a sequence of layouted paragraph items that
/// are mostly borrowed from the preparation phase. This type enables you to
/// measure the size of a line in a range before comitting to building the
@@ -315,10 +332,8 @@ impl<'a> Line<'a> {
// How many justifiable glyphs the line contains.
fn justifiables(&self) -> usize {
let mut count = 0;
- for item in self.items() {
- if let ParItem::Text(shaped) = item {
- count += shaped.justifiables();
- }
+ for shaped in self.items().filter_map(ParItem::text) {
+ count += shaped.justifiables();
}
count
}
@@ -326,10 +341,8 @@ impl<'a> Line<'a> {
/// How much of the line is stretchable spaces.
fn stretch(&self) -> Length {
let mut stretch = Length::zero();
- for item in self.items() {
- if let ParItem::Text(shaped) = item {
- stretch += shaped.stretch();
- }
+ for shaped in self.items().filter_map(ParItem::text) {
+ stretch += shaped.stretch();
}
stretch
}
@@ -344,7 +357,7 @@ fn prepare<'a>(
regions: &Regions,
styles: &'a StyleChain,
) -> TypResult<Preparation<'a>> {
- let bidi = BidiInfo::new(&text, match styles.get(ParNode::DIR) {
+ let bidi = BidiInfo::new(&text, match styles.get(TextNode::DIR) {
Dir::LTR => Some(Level::ltr()),
Dir::RTL => Some(Level::rtl()),
_ => None,
@@ -358,7 +371,7 @@ fn prepare<'a>(
let styles = map.chain(styles);
match child {
ParChild::Text(_) => {
- // TODO: Also split by language and script.
+ // TODO: Also split by language.
let mut cursor = range.start;
for (level, count) in bidi.levels[range].group() {
let start = cursor;
@@ -402,7 +415,13 @@ fn prepare<'a>(
}
}
- Ok(Preparation { bidi, items, ranges })
+ Ok(Preparation {
+ bidi,
+ children: &par.0,
+ items,
+ ranges,
+ styles: *styles,
+ })
}
/// Find suitable linebreaks.
@@ -410,22 +429,13 @@ fn linebreak<'a>(
p: &'a Preparation<'a>,
fonts: &mut FontStore,
width: Length,
- styles: StyleChain,
) -> Vec<Line<'a>> {
- let breaks = styles.get(ParNode::LINEBREAKS).unwrap_or_else(|| {
- if styles.get(ParNode::JUSTIFY) {
- Linebreaks::Optimized
- } else {
- Linebreaks::Simple
- }
- });
-
- let breaker = match breaks {
+ let breaker = match p.styles.get(ParNode::LINEBREAKS) {
Linebreaks::Simple => linebreak_simple,
Linebreaks::Optimized => linebreak_optimized,
};
- breaker(p, fonts, width, styles)
+ breaker(p, fonts, width)
}
/// Perform line breaking in simple first-fit style. This means that we build
@@ -435,13 +445,12 @@ fn linebreak_simple<'a>(
p: &'a Preparation<'a>,
fonts: &mut FontStore,
width: Length,
- styles: StyleChain,
) -> Vec<Line<'a>> {
let mut lines = vec![];
let mut start = 0;
let mut last = None;
- for (end, mandatory, hyphen) in breakpoints(&p.bidi.text, styles) {
+ for (end, mandatory, hyphen) in breakpoints(p) {
// Compute the line and its size.
let mut attempt = line(p, fonts, start .. end, mandatory, hyphen);
@@ -496,7 +505,6 @@ fn linebreak_optimized<'a>(
p: &'a Preparation<'a>,
fonts: &mut FontStore,
width: Length,
- styles: StyleChain,
) -> Vec<Line<'a>> {
/// The cost of a line or paragraph layout.
type Cost = f64;
@@ -515,8 +523,8 @@ fn linebreak_optimized<'a>(
const MIN_COST: Cost = -MAX_COST;
const MIN_RATIO: f64 = -0.15;
- let em = styles.get(TextNode::SIZE);
- let justify = styles.get(ParNode::JUSTIFY);
+ let em = p.styles.get(TextNode::SIZE);
+ let justify = p.styles.get(ParNode::JUSTIFY);
// Dynamic programming table.
let mut active = 0;
@@ -526,7 +534,7 @@ fn linebreak_optimized<'a>(
line: line(p, fonts, 0 .. 0, false, false),
}];
- for (end, mandatory, hyphen) in breakpoints(&p.bidi.text, styles) {
+ for (end, mandatory, hyphen) in breakpoints(p) {
let k = table.len();
let eof = end == p.bidi.text.len();
let mut best: Option<Entry> = None;
@@ -611,47 +619,104 @@ fn linebreak_optimized<'a>(
/// Returns for each breakpoint the text index, whether the break is mandatory
/// (after `\n`) and whether a hyphen is required (when breaking inside of a
/// word).
-fn breakpoints<'a>(
- text: &'a str,
- styles: StyleChain,
-) -> impl Iterator<Item = (usize, bool, bool)> + 'a {
- let mut lang = None;
- if styles.get(ParNode::HYPHENATE).unwrap_or(styles.get(ParNode::JUSTIFY)) {
- lang = styles
- .get(ParNode::LANG)
- .as_ref()
- .and_then(|iso| iso.as_bytes().try_into().ok())
- .and_then(hypher::Lang::from_iso);
- }
-
- let breaks = LineBreakIterator::new(text);
- let mut last = 0;
-
- if let Some(lang) = lang {
- Either::Left(breaks.flat_map(move |(end, mandatory)| {
- // We don't want to confuse the hyphenator with trailing
- // punctuation, so we trim it. And if that makes the word empty, we
- // need to return the single breakpoint manually because hypher
- // would eat it.
- let word = &text[last .. end];
- let trimmed = word.trim_end_matches(|c: char| !c.is_alphabetic());
- let suffix = last + trimmed.len();
- let mut start = std::mem::replace(&mut last, end);
- if trimmed.is_empty() {
- Either::Left([(end, mandatory, false)].into_iter())
- } else {
- Either::Right(hypher::hyphenate(trimmed, lang).map(move |syllable| {
- start += syllable.len();
- if start == suffix {
- start = end;
- }
- let hyphen = start < end;
- (start, mandatory && !hyphen, hyphen)
- }))
+fn breakpoints<'a>(p: &'a Preparation) -> impl Iterator<Item = (usize, bool, bool)> + 'a {
+ Breakpoints {
+ p,
+ linebreaks: LineBreakIterator::new(p.bidi.text),
+ syllables: None,
+ offset: 0,
+ suffix: 0,
+ end: 0,
+ mandatory: false,
+ hyphenate: p.get_shared(TextNode::HYPHENATE),
+ lang: p.get_shared(TextNode::LANG).map(Option::as_ref),
+ }
+}
+
+/// An iterator over the line break opportunities in a text.
+struct Breakpoints<'a> {
+ /// The paragraph's items.
+ p: &'a Preparation<'a>,
+ /// The inner iterator over the unicode line break opportunities.
+ linebreaks: LineBreakIterator<'a>,
+ /// Iterator over syllables of the current word.
+ syllables: Option<hypher::Syllables<'a>>,
+ /// The current text offset.
+ offset: usize,
+ /// The trimmed end of the current word.
+ suffix: usize,
+ /// The untrimmed end of the current word.
+ end: usize,
+ /// Whether the break after the current word is mandatory.
+ mandatory: bool,
+ /// Whether to hyphenate if it's the same for all children.
+ hyphenate: Option<bool>,
+ /// The text language if it's the same for all children.
+ lang: Option<Option<&'a Lang>>,
+}
+
+impl Iterator for Breakpoints<'_> {
+ type Item = (usize, bool, bool);
+
+ fn next(&mut self) -> Option<Self::Item> {
+ // If we're currently in a hyphenated "word", process the next syllable.
+ if let Some(syllable) = self.syllables.as_mut().and_then(Iterator::next) {
+ self.offset += syllable.len();
+ if self.offset == self.suffix {
+ self.offset = self.end;
}
- }))
- } else {
- Either::Right(breaks.map(|(e, m)| (e, m, false)))
+
+ // Filter out hyphenation opportunities where hyphenation was
+ // actually disabled.
+ let hyphen = self.offset < self.end;
+ if hyphen && !self.hyphenate_at(self.offset) {
+ return self.next();
+ }
+
+ return Some((self.offset, self.mandatory && !hyphen, hyphen));
+ }
+
+ // Get the next "word".
+ (self.end, self.mandatory) = self.linebreaks.next()?;
+
+ // Hyphenate the next word.
+ if self.hyphenate != Some(false) {
+ if let Some(lang) = self.lang_at(self.offset) {
+ let word = &self.p.bidi.text[self.offset .. self.end];
+ let trimmed = word.trim_end_matches(|c: char| !c.is_alphabetic());
+ if !trimmed.is_empty() {
+ self.suffix = self.offset + trimmed.len();
+ self.syllables = Some(hypher::hyphenate(trimmed, lang));
+ return self.next();
+ }
+ }
+ }
+
+ self.offset = self.end;
+ Some((self.end, self.mandatory, false))
+ }
+}
+
+impl Breakpoints<'_> {
+ /// Whether hyphenation is enabled at the given offset.
+ fn hyphenate_at(&self, offset: usize) -> bool {
+ self.hyphenate
+ .or_else(|| {
+ let shaped = self.p.find(offset)?.text()?;
+ Some(shaped.styles.get(TextNode::HYPHENATE))
+ })
+ .unwrap_or(false)
+ }
+
+ /// The text language at the given offset.
+ fn lang_at(&self, offset: usize) -> Option<hypher::Lang> {
+ let lang = self.lang.unwrap_or_else(|| {
+ let shaped = self.p.find(offset)?.text()?;
+ shaped.styles.get(TextNode::LANG).as_ref()
+ })?;
+
+ let bytes = lang.as_str().as_bytes().try_into().ok()?;
+ hypher::Lang::from_iso(bytes)
}
}
@@ -664,11 +729,11 @@ fn line<'a>(
hyphen: bool,
) -> Line<'a> {
// Find the items which bound the text range.
- let last_idx = p.find(range.end.saturating_sub(1)).unwrap();
+ let last_idx = p.find_idx(range.end.saturating_sub(1)).unwrap();
let first_idx = if range.is_empty() {
last_idx
} else {
- p.find(range.start).unwrap()
+ p.find_idx(range.start).unwrap()
};
// Slice out the relevant items.
diff --git a/src/library/text/raw.rs b/src/library/text/raw.rs
index f09bc1d0..d96100af 100644
--- a/src/library/text/raw.rs
+++ b/src/library/text/raw.rs
@@ -3,7 +3,7 @@ use syntect::easy::HighlightLines;
use syntect::highlighting::{FontStyle, Highlighter, Style, Theme, ThemeSet};
use syntect::parsing::SyntaxSet;
-use super::{FontFamily, TextNode, Toggle};
+use super::{FontFamily, Hyphenate, TextNode, Toggle};
use crate::library::prelude::*;
use crate::source::SourceId;
use crate::syntax::{self, RedNode};
@@ -29,6 +29,7 @@ impl RawNode {
/// The raw text's font family. Just the normal text family if `none`.
#[property(referenced)]
pub const FAMILY: Smart<FontFamily> = Smart::Custom(FontFamily::new("IBM Plex Mono"));
+
/// The language to syntax-highlight in.
#[property(referenced)]
pub const LANG: Option<EcoString> = None;
@@ -97,6 +98,9 @@ impl Show for RawNode {
};
let mut map = StyleMap::new();
+ map.set(TextNode::OVERHANG, false);
+ map.set(TextNode::HYPHENATE, Smart::Custom(Hyphenate(false)));
+
if let Smart::Custom(family) = styles.get(Self::FAMILY) {
map.set_family(family.clone(), styles);
}