@ -26,9 +26,11 @@ fontdock = { path = "../fontdock", default-features = false }
|
||||
image = { version = "0.23", default-features = false, features = ["jpeg", "png"] }
|
||||
miniz_oxide = "0.3"
|
||||
pdf-writer = { path = "../pdf-writer" }
|
||||
rustybuzz = "0.3"
|
||||
ttf-parser = "0.9"
|
||||
rustybuzz = { git = "https://github.com/laurmaedje/rustybuzz" }
|
||||
ttf-parser = "0.12"
|
||||
unicode-bidi = "0.3.5"
|
||||
unicode-xid = "0.2"
|
||||
xi-unicode = "0.3"
|
||||
anyhow = { version = "1", optional = true }
|
||||
serde = { version = "1", features = ["derive"], optional = true }
|
||||
|
||||
|
BIN
fonts/NotoSerifCJKsc-Regular.otf
Normal file
BIN
fonts/NotoSerifHebrew-Bold.ttf
Normal file
BIN
fonts/NotoSerifHebrew-Regular.ttf
Normal file
@ -6,9 +6,8 @@ use crate::env::Env;
|
||||
use crate::eval::TemplateValue;
|
||||
use crate::geom::{Align, Dir, Gen, GenAxis, Length, Linear, Sides, Size};
|
||||
use crate::layout::{
|
||||
AnyNode, PadNode, PageRun, ParChild, ParNode, StackChild, StackNode, TextNode, Tree,
|
||||
AnyNode, PadNode, PageRun, ParChild, ParNode, StackChild, StackNode, Tree,
|
||||
};
|
||||
use crate::parse::{is_newline, Scanner};
|
||||
use crate::syntax::Span;
|
||||
|
||||
/// The context for execution.
|
||||
@ -73,28 +72,14 @@ impl<'a> ExecContext<'a> {
|
||||
|
||||
/// Push a word space into the active paragraph.
|
||||
pub fn push_word_space(&mut self) {
|
||||
let em = self.state.font.resolve_size();
|
||||
let amount = self.state.par.word_spacing.resolve(em);
|
||||
self.stack.par.push_soft(ParChild::Spacing(amount));
|
||||
self.stack.par.push_soft(self.make_text_node(" "));
|
||||
}
|
||||
|
||||
/// Push text into the active paragraph.
|
||||
///
|
||||
/// The text is split into lines at newlines.
|
||||
pub fn push_text(&mut self, text: &str) {
|
||||
let mut scanner = Scanner::new(text);
|
||||
let mut text = String::new();
|
||||
|
||||
while let Some(c) = scanner.eat_merging_crlf() {
|
||||
if is_newline(c) {
|
||||
self.stack.par.push_text(mem::take(&mut text), &self.state);
|
||||
self.linebreak();
|
||||
} else {
|
||||
text.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
self.stack.par.push_text(text, &self.state);
|
||||
pub fn push_text(&mut self, text: impl Into<String>) {
|
||||
self.stack.par.push(self.make_text_node(text));
|
||||
}
|
||||
|
||||
/// Push spacing into paragraph or stack depending on `axis`.
|
||||
@ -112,7 +97,7 @@ impl<'a> ExecContext<'a> {
|
||||
|
||||
/// Apply a forced line break.
|
||||
pub fn linebreak(&mut self) {
|
||||
self.stack.par.push_hard(ParChild::Linebreak);
|
||||
self.stack.par.push_hard(self.make_text_node("\n"));
|
||||
}
|
||||
|
||||
/// Apply a forced paragraph break.
|
||||
@ -140,6 +125,12 @@ impl<'a> ExecContext<'a> {
|
||||
self.pagebreak(true, false, Span::default());
|
||||
Pass::new(self.tree, self.diags)
|
||||
}
|
||||
|
||||
fn make_text_node(&self, text: impl Into<String>) -> ParChild {
|
||||
let align = self.state.aligns.cross;
|
||||
let props = self.state.font.resolve_props();
|
||||
ParChild::Text(text.into(), props, align)
|
||||
}
|
||||
}
|
||||
|
||||
struct PageBuilder {
|
||||
@ -231,24 +222,10 @@ impl ParBuilder {
|
||||
}
|
||||
|
||||
fn push(&mut self, child: ParChild) {
|
||||
self.children.extend(self.last.any());
|
||||
self.children.push(child);
|
||||
}
|
||||
|
||||
fn push_text(&mut self, text: String, state: &State) {
|
||||
self.children.extend(self.last.any());
|
||||
|
||||
let align = state.aligns.cross;
|
||||
let props = state.font.resolve_props();
|
||||
|
||||
if let Some(ParChild::Text(prev, prev_align)) = self.children.last_mut() {
|
||||
if *prev_align == align && prev.props == props {
|
||||
prev.text.push_str(&text);
|
||||
return;
|
||||
}
|
||||
if let Some(soft) = self.last.any() {
|
||||
self.push_inner(soft);
|
||||
}
|
||||
|
||||
self.children.push(ParChild::Text(TextNode { text, props }, align));
|
||||
self.push_inner(child);
|
||||
}
|
||||
|
||||
fn push_soft(&mut self, child: ParChild) {
|
||||
@ -257,6 +234,21 @@ impl ParBuilder {
|
||||
|
||||
fn push_hard(&mut self, child: ParChild) {
|
||||
self.last.hard();
|
||||
self.push_inner(child);
|
||||
}
|
||||
|
||||
fn push_inner(&mut self, child: ParChild) {
|
||||
if let ParChild::Text(curr_text, curr_props, curr_align) = &child {
|
||||
if let Some(ParChild::Text(prev_text, prev_props, prev_align)) =
|
||||
self.children.last_mut()
|
||||
{
|
||||
if prev_align == curr_align && prev_props == curr_props {
|
||||
prev_text.push_str(&curr_text);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.children.push(child);
|
||||
}
|
||||
|
||||
|
@ -64,7 +64,7 @@ impl ExecWithMap for Tree {
|
||||
impl ExecWithMap for Node {
|
||||
fn exec_with_map(&self, ctx: &mut ExecContext, map: &NodeMap) {
|
||||
match self {
|
||||
Node::Text(text) => ctx.push_text(text),
|
||||
Node::Text(text) => ctx.push_text(text.clone()),
|
||||
Node::Space => ctx.push_word_space(),
|
||||
_ => map[&(self as *const _)].exec(ctx),
|
||||
}
|
||||
@ -75,9 +75,9 @@ impl Exec for Value {
|
||||
fn exec(&self, ctx: &mut ExecContext) {
|
||||
match self {
|
||||
Value::None => {}
|
||||
Value::Int(v) => ctx.push_text(&pretty(v)),
|
||||
Value::Float(v) => ctx.push_text(&pretty(v)),
|
||||
Value::Str(v) => ctx.push_text(v),
|
||||
Value::Int(v) => ctx.push_text(pretty(v)),
|
||||
Value::Float(v) => ctx.push_text(pretty(v)),
|
||||
Value::Str(v) => ctx.push_text(v.clone()),
|
||||
Value::Template(v) => v.exec(ctx),
|
||||
Value::Error => {}
|
||||
other => {
|
||||
@ -85,7 +85,7 @@ impl Exec for Value {
|
||||
// the representation in monospace.
|
||||
let prev = Rc::clone(&ctx.state.font.families);
|
||||
ctx.set_monospace();
|
||||
ctx.push_text(&pretty(other));
|
||||
ctx.push_text(pretty(other));
|
||||
ctx.state.font.families = prev;
|
||||
}
|
||||
}
|
||||
@ -104,7 +104,7 @@ impl Exec for TemplateNode {
|
||||
fn exec(&self, ctx: &mut ExecContext) {
|
||||
match self {
|
||||
Self::Tree { tree, map } => tree.exec_with_map(ctx, &map),
|
||||
Self::Str(v) => ctx.push_text(v),
|
||||
Self::Str(v) => ctx.push_text(v.clone()),
|
||||
Self::Func(v) => v.exec(ctx),
|
||||
}
|
||||
}
|
||||
|
@ -97,6 +97,7 @@ pub struct ParState {
|
||||
/// The spacing between lines (dependent on scaled font size).
|
||||
pub leading: Linear,
|
||||
/// The spacing between words (dependent on scaled font size).
|
||||
// TODO: Don't ignore this.
|
||||
pub word_spacing: Linear,
|
||||
}
|
||||
|
||||
|
100
src/font.rs
@ -4,12 +4,18 @@ use std::fmt::{self, Display, Formatter};
|
||||
|
||||
use fontdock::FaceFromVec;
|
||||
|
||||
use crate::geom::Length;
|
||||
|
||||
/// An owned font face.
|
||||
pub struct FaceBuf {
|
||||
data: Box<[u8]>,
|
||||
index: u32,
|
||||
ttf: ttf_parser::Face<'static>,
|
||||
buzz: rustybuzz::Face<'static>,
|
||||
inner: rustybuzz::Face<'static>,
|
||||
units_per_em: f64,
|
||||
ascender: f64,
|
||||
cap_height: f64,
|
||||
x_height: f64,
|
||||
descender: f64,
|
||||
}
|
||||
|
||||
impl FaceBuf {
|
||||
@ -23,18 +29,27 @@ impl FaceBuf {
|
||||
self.index
|
||||
}
|
||||
|
||||
/// Get a reference to the underlying ttf-parser face.
|
||||
pub fn ttf(&self) -> &ttf_parser::Face<'_> {
|
||||
/// Get a reference to the underlying ttf-parser/rustybuzz face.
|
||||
pub fn ttf(&self) -> &rustybuzz::Face<'_> {
|
||||
// We can't implement Deref because that would leak the internal 'static
|
||||
// lifetime.
|
||||
&self.ttf
|
||||
&self.inner
|
||||
}
|
||||
|
||||
/// Get a reference to the underlying rustybuzz face.
|
||||
pub fn buzz(&self) -> &rustybuzz::Face<'_> {
|
||||
// We can't implement Deref because that would leak the internal 'static
|
||||
// lifetime.
|
||||
&self.buzz
|
||||
/// Look up a vertical metric.
|
||||
pub fn vertical_metric(&self, metric: VerticalFontMetric) -> EmLength {
|
||||
self.convert(match metric {
|
||||
VerticalFontMetric::Ascender => self.ascender,
|
||||
VerticalFontMetric::CapHeight => self.cap_height,
|
||||
VerticalFontMetric::XHeight => self.x_height,
|
||||
VerticalFontMetric::Baseline => 0.0,
|
||||
VerticalFontMetric::Descender => self.descender,
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert from font units to an em length length.
|
||||
pub fn convert(&self, units: impl Into<f64>) -> EmLength {
|
||||
EmLength(units.into() / self.units_per_em)
|
||||
}
|
||||
}
|
||||
|
||||
@ -47,15 +62,44 @@ impl FaceFromVec for FaceBuf {
|
||||
let slice: &'static [u8] =
|
||||
unsafe { std::slice::from_raw_parts(data.as_ptr(), data.len()) };
|
||||
|
||||
let inner = rustybuzz::Face::from_slice(slice, index)?;
|
||||
|
||||
// Look up some metrics we may need often.
|
||||
let units_per_em = inner.units_per_em();
|
||||
let ascender = inner.typographic_ascender().unwrap_or(inner.ascender());
|
||||
let cap_height = inner.capital_height().filter(|&h| h > 0).unwrap_or(ascender);
|
||||
let x_height = inner.x_height().filter(|&h| h > 0).unwrap_or(ascender);
|
||||
let descender = inner.typographic_descender().unwrap_or(inner.descender());
|
||||
|
||||
Some(Self {
|
||||
data,
|
||||
index,
|
||||
ttf: ttf_parser::Face::from_slice(slice, index).ok()?,
|
||||
buzz: rustybuzz::Face::from_slice(slice, index)?,
|
||||
inner,
|
||||
units_per_em: f64::from(units_per_em),
|
||||
ascender: f64::from(ascender),
|
||||
cap_height: f64::from(cap_height),
|
||||
x_height: f64::from(x_height),
|
||||
descender: f64::from(descender),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A length in resolved em units.
|
||||
#[derive(Default, Debug, Copy, Clone, PartialEq, PartialOrd)]
|
||||
pub struct EmLength(f64);
|
||||
|
||||
impl EmLength {
|
||||
/// Convert to a length at the given font size.
|
||||
pub fn scale(self, size: Length) -> Length {
|
||||
self.0 * size
|
||||
}
|
||||
|
||||
/// Get the number of em units.
|
||||
pub fn get(self) -> f64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Identifies a vertical metric of a font.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub enum VerticalFontMetric {
|
||||
@ -77,38 +121,6 @@ pub enum VerticalFontMetric {
|
||||
Descender,
|
||||
}
|
||||
|
||||
impl VerticalFontMetric {
|
||||
/// Look up the metric in the given font face.
|
||||
pub fn lookup(self, face: &ttf_parser::Face) -> i16 {
|
||||
match self {
|
||||
VerticalFontMetric::Ascender => lookup_ascender(face),
|
||||
VerticalFontMetric::CapHeight => face
|
||||
.capital_height()
|
||||
.filter(|&h| h > 0)
|
||||
.unwrap_or_else(|| lookup_ascender(face)),
|
||||
VerticalFontMetric::XHeight => face
|
||||
.x_height()
|
||||
.filter(|&h| h > 0)
|
||||
.unwrap_or_else(|| lookup_ascender(face)),
|
||||
VerticalFontMetric::Baseline => 0,
|
||||
VerticalFontMetric::Descender => lookup_descender(face),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The ascender of the face.
|
||||
fn lookup_ascender(face: &ttf_parser::Face) -> i16 {
|
||||
// We prefer the typographic ascender over the Windows ascender because
|
||||
// it can be overly large if the font has large glyphs.
|
||||
face.typographic_ascender().unwrap_or_else(|| face.ascender())
|
||||
}
|
||||
|
||||
/// The descender of the face.
|
||||
fn lookup_descender(face: &ttf_parser::Face) -> i16 {
|
||||
// See `lookup_ascender` for reason.
|
||||
face.typographic_descender().unwrap_or_else(|| face.descender())
|
||||
}
|
||||
|
||||
impl Display for VerticalFontMetric {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.pad(match self {
|
||||
|
@ -13,8 +13,8 @@ pub enum Align {
|
||||
|
||||
impl Align {
|
||||
/// Returns the position of this alignment in the given range.
|
||||
pub fn resolve(self, range: Range<Length>) -> Length {
|
||||
match self {
|
||||
pub fn resolve(self, dir: Dir, range: Range<Length>) -> Length {
|
||||
match if dir.is_positive() { self } else { self.inv() } {
|
||||
Self::Start => range.start,
|
||||
Self::Center => (range.start + range.end) / 2.0,
|
||||
Self::End => range.end,
|
||||
|
@ -81,6 +81,11 @@ impl Length {
|
||||
Self { raw: self.raw.max(other.raw) }
|
||||
}
|
||||
|
||||
/// Whether the other length fits into this one (i.e. is smaller).
|
||||
pub fn fits(self, other: Self) -> bool {
|
||||
self.raw + 1e-6 >= other.raw
|
||||
}
|
||||
|
||||
/// Whether the length is zero.
|
||||
pub fn is_zero(self) -> bool {
|
||||
self.raw == 0.0
|
||||
|
@ -28,8 +28,7 @@ impl Size {
|
||||
|
||||
/// Whether the other size fits into this one (smaller width and height).
|
||||
pub fn fits(self, other: Self) -> bool {
|
||||
const EPS: Length = Length::raw(1e-6);
|
||||
self.width + EPS >= other.width && self.height + EPS >= other.height
|
||||
self.width.fits(other.width) && self.height.fits(other.height)
|
||||
}
|
||||
|
||||
/// Whether both components are finite.
|
||||
|
@ -10,14 +10,16 @@ use crate::geom::{Length, Path, Point, Size};
|
||||
pub struct Frame {
|
||||
/// The size of the frame.
|
||||
pub size: Size,
|
||||
/// The baseline of the frame measured from the top.
|
||||
pub baseline: Length,
|
||||
/// The elements composing this layout.
|
||||
pub elements: Vec<(Point, Element)>,
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
/// Create a new, empty frame.
|
||||
pub fn new(size: Size) -> Self {
|
||||
Self { size, elements: vec![] }
|
||||
pub fn new(size: Size, baseline: Length) -> Self {
|
||||
Self { size, baseline, elements: vec![] }
|
||||
}
|
||||
|
||||
/// Add an element at a position.
|
||||
@ -38,62 +40,45 @@ impl Frame {
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Element {
|
||||
/// Shaped text.
|
||||
Text(ShapedText),
|
||||
Text(Text),
|
||||
/// A geometric shape.
|
||||
Geometry(Geometry),
|
||||
/// A raster image.
|
||||
Image(Image),
|
||||
}
|
||||
|
||||
/// A shaped run of text.
|
||||
/// A run of shaped text.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ShapedText {
|
||||
/// The font face the text was shaped with.
|
||||
pub face: FaceId,
|
||||
pub struct Text {
|
||||
/// The font face the glyphs are contained in.
|
||||
pub face_id: FaceId,
|
||||
/// The font size.
|
||||
pub size: Length,
|
||||
/// The width.
|
||||
pub width: Length,
|
||||
/// The extent to the top.
|
||||
pub top: Length,
|
||||
/// The extent to the bottom.
|
||||
pub bottom: Length,
|
||||
/// The glyph fill color / texture.
|
||||
pub color: Fill,
|
||||
/// The shaped glyphs.
|
||||
pub glyphs: Vec<GlyphId>,
|
||||
/// The horizontal offsets of the glyphs. This is indexed parallel to
|
||||
/// `glyphs`. Vertical offsets are not yet supported.
|
||||
pub offsets: Vec<Length>,
|
||||
/// The glyphs.
|
||||
pub glyphs: Vec<Glyph>,
|
||||
}
|
||||
|
||||
impl ShapedText {
|
||||
/// Create a new shape run with `width` zero and empty `glyphs` and `offsets`.
|
||||
pub fn new(
|
||||
face: FaceId,
|
||||
size: Length,
|
||||
top: Length,
|
||||
bottom: Length,
|
||||
color: Fill,
|
||||
) -> Self {
|
||||
Self {
|
||||
face,
|
||||
size,
|
||||
width: Length::ZERO,
|
||||
top,
|
||||
bottom,
|
||||
glyphs: vec![],
|
||||
offsets: vec![],
|
||||
color,
|
||||
}
|
||||
}
|
||||
/// A glyph in a run of shaped text.
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub struct Glyph {
|
||||
/// The glyph's ID in the face.
|
||||
pub id: GlyphId,
|
||||
/// The advance width of the glyph.
|
||||
pub x_advance: Length,
|
||||
/// The horizontal offset of the glyph.
|
||||
pub x_offset: Length,
|
||||
}
|
||||
|
||||
impl Text {
|
||||
/// Encode the glyph ids into a big-endian byte buffer.
|
||||
pub fn encode_glyphs_be(&self) -> Vec<u8> {
|
||||
let mut bytes = Vec::with_capacity(2 * self.glyphs.len());
|
||||
for &GlyphId(g) in &self.glyphs {
|
||||
bytes.push((g >> 8) as u8);
|
||||
bytes.push((g & 0xff) as u8);
|
||||
for glyph in &self.glyphs {
|
||||
let id = glyph.id.0;
|
||||
bytes.push((id >> 8) as u8);
|
||||
bytes.push((id & 0xff) as u8);
|
||||
}
|
||||
bytes
|
||||
}
|
||||
|
@ -38,6 +38,8 @@ fn pad(frame: &mut Frame, padding: Sides<Linear>) {
|
||||
let origin = Point::new(padding.left, padding.top);
|
||||
|
||||
frame.size = padded;
|
||||
frame.baseline += origin.y;
|
||||
|
||||
for (point, _) in &mut frame.elements {
|
||||
*point += origin;
|
||||
}
|
||||
|
@ -1,7 +1,14 @@
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
use std::mem;
|
||||
|
||||
use unicode_bidi::{BidiInfo, Level};
|
||||
use xi_unicode::LineBreakIterator;
|
||||
|
||||
use super::*;
|
||||
use crate::exec::FontProps;
|
||||
use crate::util::{RangeExt, SliceExt};
|
||||
|
||||
type Range = std::ops::Range<usize>;
|
||||
|
||||
/// A node that arranges its children into a paragraph.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@ -15,52 +22,63 @@ pub struct ParNode {
|
||||
}
|
||||
|
||||
/// A child of a paragraph node.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum ParChild {
|
||||
/// Spacing between other nodes.
|
||||
Spacing(Length),
|
||||
/// A run of text and how to align it in its line.
|
||||
Text(TextNode, Align),
|
||||
Text(String, FontProps, Align),
|
||||
/// Any child node and how to align it in its line.
|
||||
Any(AnyNode, Align),
|
||||
/// A forced linebreak.
|
||||
Linebreak,
|
||||
}
|
||||
|
||||
/// A consecutive, styled run of text.
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct TextNode {
|
||||
/// The text.
|
||||
pub text: String,
|
||||
/// Properties used for font selection and layout.
|
||||
pub props: FontProps,
|
||||
}
|
||||
|
||||
impl Debug for TextNode {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "Text({})", self.text)
|
||||
}
|
||||
}
|
||||
|
||||
impl Layout for ParNode {
|
||||
fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Vec<Frame> {
|
||||
let mut layouter = ParLayouter::new(self.dir, self.line_spacing, areas.clone());
|
||||
for child in &self.children {
|
||||
match *child {
|
||||
ParChild::Spacing(amount) => layouter.push_spacing(amount),
|
||||
ParChild::Text(ref node, align) => {
|
||||
let frame = shape(&node.text, &mut ctx.env.fonts, &node.props);
|
||||
layouter.push_frame(frame, align);
|
||||
}
|
||||
ParChild::Any(ref node, align) => {
|
||||
for frame in node.layout(ctx, &layouter.areas) {
|
||||
layouter.push_frame(frame, align);
|
||||
}
|
||||
}
|
||||
ParChild::Linebreak => layouter.finish_line(),
|
||||
}
|
||||
// Collect all text into one string used for BiDi analysis.
|
||||
let text = self.collect_text();
|
||||
|
||||
// Find out the BiDi embedding levels.
|
||||
let bidi = BidiInfo::new(&text, Level::from_dir(self.dir));
|
||||
|
||||
// Build a representation of the paragraph on which we can do
|
||||
// linebreaking without layouting each and every line from scratch.
|
||||
let layout = ParLayout::new(ctx, areas, self, bidi);
|
||||
|
||||
// Find suitable linebreaks.
|
||||
layout.build(ctx, areas.clone(), self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ParNode {
|
||||
/// Concatenate all text in the paragraph into one string, replacing spacing
|
||||
/// with a space character and other non-text nodes with the object
|
||||
/// replacement character. Returns the full text alongside the range each
|
||||
/// child spans in the text.
|
||||
fn collect_text(&self) -> String {
|
||||
let mut text = String::new();
|
||||
for string in self.strings() {
|
||||
text.push_str(string);
|
||||
}
|
||||
layouter.finish()
|
||||
text
|
||||
}
|
||||
|
||||
/// The range of each item in the collected text.
|
||||
fn ranges(&self) -> impl Iterator<Item = Range> + '_ {
|
||||
let mut cursor = 0;
|
||||
self.strings().map(move |string| {
|
||||
let start = cursor;
|
||||
cursor += string.len();
|
||||
start .. cursor
|
||||
})
|
||||
}
|
||||
|
||||
/// The string representation of each child.
|
||||
fn strings(&self) -> impl Iterator<Item = &str> {
|
||||
self.children.iter().map(|child| match child {
|
||||
ParChild::Spacing(_) => " ",
|
||||
ParChild::Text(ref piece, _, _) => piece,
|
||||
ParChild::Any(_, _) => "\u{FFFC}",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,174 +88,468 @@ impl From<ParNode> for AnyNode {
|
||||
}
|
||||
}
|
||||
|
||||
struct ParLayouter {
|
||||
dirs: Gen<Dir>,
|
||||
main: SpecAxis,
|
||||
cross: SpecAxis,
|
||||
line_spacing: Length,
|
||||
areas: Areas,
|
||||
finished: Vec<Frame>,
|
||||
stack: Vec<(Length, Frame, Align)>,
|
||||
stack_size: Gen<Length>,
|
||||
line: Vec<(Length, Frame, Align)>,
|
||||
line_size: Gen<Length>,
|
||||
line_ruler: Align,
|
||||
impl Debug for ParChild {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Self::Spacing(amount) => write!(f, "Spacing({:?})", amount),
|
||||
Self::Text(text, _, align) => write!(f, "Text({:?}, {:?})", text, align),
|
||||
Self::Any(any, align) => {
|
||||
f.debug_tuple("Any").field(any).field(align).finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ParLayouter {
|
||||
fn new(dir: Dir, line_spacing: Length, areas: Areas) -> Self {
|
||||
Self {
|
||||
dirs: Gen::new(Dir::TTB, dir),
|
||||
main: SpecAxis::Vertical,
|
||||
cross: SpecAxis::Horizontal,
|
||||
line_spacing,
|
||||
areas,
|
||||
finished: vec![],
|
||||
stack: vec![],
|
||||
stack_size: Gen::ZERO,
|
||||
line: vec![],
|
||||
line_size: Gen::ZERO,
|
||||
line_ruler: Align::Start,
|
||||
}
|
||||
}
|
||||
/// A paragraph representation in which children are already layouted and text
|
||||
/// is separated into shapable runs.
|
||||
struct ParLayout<'a> {
|
||||
/// The top-level direction.
|
||||
dir: Dir,
|
||||
/// Bidirectional text embedding levels for the paragraph.
|
||||
bidi: BidiInfo<'a>,
|
||||
/// Layouted children and separated text runs.
|
||||
items: Vec<ParItem<'a>>,
|
||||
/// The ranges of the items in `bidi.text`.
|
||||
ranges: Vec<Range>,
|
||||
}
|
||||
|
||||
fn push_spacing(&mut self, amount: Length) {
|
||||
let cross_max = self.areas.current.get(self.cross);
|
||||
self.line_size.cross = (self.line_size.cross + amount).min(cross_max);
|
||||
}
|
||||
impl<'a> ParLayout<'a> {
|
||||
/// Build a paragraph layout for the given node.
|
||||
fn new(
|
||||
ctx: &mut LayoutContext,
|
||||
areas: &Areas,
|
||||
par: &'a ParNode,
|
||||
bidi: BidiInfo<'a>,
|
||||
) -> Self {
|
||||
// Prepare an iterator over each child an the range it spans.
|
||||
let mut items = vec![];
|
||||
let mut ranges = vec![];
|
||||
|
||||
fn push_frame(&mut self, frame: Frame, align: Align) {
|
||||
// When the alignment of the last pushed frame (stored in the "ruler")
|
||||
// is further to the end than the new `frame`, we need a line break.
|
||||
//
|
||||
// For example
|
||||
// ```
|
||||
// #align(right)[First] #align(center)[Second]
|
||||
// ```
|
||||
// would be laid out as:
|
||||
// +----------------------------+
|
||||
// | First |
|
||||
// | Second |
|
||||
// +----------------------------+
|
||||
if self.line_ruler > align {
|
||||
self.finish_line();
|
||||
}
|
||||
// Layout the children and collect them into items.
|
||||
for (range, child) in par.ranges().zip(&par.children) {
|
||||
match *child {
|
||||
ParChild::Spacing(amount) => {
|
||||
items.push(ParItem::Spacing(amount));
|
||||
ranges.push(range);
|
||||
}
|
||||
ParChild::Text(_, ref props, align) => {
|
||||
// TODO: Also split by language and script.
|
||||
for (subrange, dir) in split_runs(&bidi, range) {
|
||||
let text = &bidi.text[subrange.clone()];
|
||||
let shaped = shape(ctx, text, dir, props);
|
||||
items.push(ParItem::Text(shaped, align));
|
||||
ranges.push(subrange);
|
||||
}
|
||||
}
|
||||
ParChild::Any(ref node, align) => {
|
||||
let frames = node.layout(ctx, areas);
|
||||
assert_eq!(frames.len(), 1);
|
||||
|
||||
// Find out whether the area still has enough space for this frame.
|
||||
// Space occupied by previous lines is already removed from
|
||||
// `areas.current`, but the cross-extent of the current line needs to be
|
||||
// subtracted to make sure the frame fits.
|
||||
let fits = {
|
||||
let mut usable = self.areas.current;
|
||||
*usable.get_mut(self.cross) -= self.line_size.cross;
|
||||
usable.fits(frame.size)
|
||||
};
|
||||
|
||||
if !fits {
|
||||
self.finish_line();
|
||||
|
||||
// Here, we can directly check whether the frame fits into
|
||||
// `areas.current` since we just called `finish_line`.
|
||||
while !self.areas.current.fits(frame.size) {
|
||||
if self.areas.in_full_last() {
|
||||
// The frame fits nowhere.
|
||||
// TODO: Should this be placed into the first area or the last?
|
||||
// TODO: Produce diagnostic once the necessary spans exist.
|
||||
break;
|
||||
} else {
|
||||
self.finish_area();
|
||||
let frame = frames.into_iter().next().unwrap();
|
||||
items.push(ParItem::Frame(frame, align));
|
||||
ranges.push(range);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A line can contain frames with different alignments. They exact
|
||||
// positions are calculated later depending on the alignments.
|
||||
let size = frame.size.switch(self.main);
|
||||
self.line.push((self.line_size.cross, frame, align));
|
||||
self.line_size.cross += size.cross;
|
||||
self.line_size.main = self.line_size.main.max(size.main);
|
||||
self.line_ruler = align;
|
||||
Self { dir: par.dir, bidi, items, ranges }
|
||||
}
|
||||
|
||||
fn finish_line(&mut self) {
|
||||
let full_size = {
|
||||
let expand = self.areas.expand.get(self.cross);
|
||||
let full = self.areas.full.get(self.cross);
|
||||
Gen::new(
|
||||
self.line_size.main,
|
||||
expand.resolve(self.line_size.cross, full),
|
||||
)
|
||||
};
|
||||
/// Find first-fit line breaks and build the paragraph.
|
||||
fn build(self, ctx: &mut LayoutContext, areas: Areas, par: &ParNode) -> Vec<Frame> {
|
||||
let mut stack = LineStack::new(par.line_spacing, areas);
|
||||
|
||||
let mut output = Frame::new(full_size.switch(self.main).to_size());
|
||||
// The current line attempt.
|
||||
// Invariant: Always fits into `stack.areas.current`.
|
||||
let mut last = None;
|
||||
|
||||
for (before, frame, align) in std::mem::take(&mut self.line) {
|
||||
let child_cross_size = frame.size.get(self.cross);
|
||||
// The start of the line in `last`.
|
||||
let mut start = 0;
|
||||
|
||||
// Position along the cross axis.
|
||||
let cross = align.resolve(if self.dirs.cross.is_positive() {
|
||||
let after_with_self = self.line_size.cross - before;
|
||||
before .. full_size.cross - after_with_self
|
||||
// Find suitable line breaks.
|
||||
// TODO: Provide line break opportunities on alignment changes.
|
||||
for (end, mandatory) in LineBreakIterator::new(self.bidi.text) {
|
||||
// Compute the line and its size.
|
||||
let mut line = LineLayout::new(ctx, &self, start .. end);
|
||||
|
||||
// If the line doesn't fit anymore, we push the last fitting attempt
|
||||
// into the stack and rebuild the line from its end. The resulting
|
||||
// line cannot be broken up further.
|
||||
if !stack.areas.current.fits(line.size) {
|
||||
if let Some((last_line, last_end)) = last.take() {
|
||||
stack.push(last_line);
|
||||
start = last_end;
|
||||
line = LineLayout::new(ctx, &self, start .. end);
|
||||
}
|
||||
}
|
||||
|
||||
// If the line does not fit vertically, we start a new area.
|
||||
if !stack.areas.current.height.fits(line.size.height)
|
||||
&& !stack.areas.in_full_last()
|
||||
{
|
||||
stack.finish_area(ctx);
|
||||
}
|
||||
|
||||
if mandatory || !stack.areas.current.width.fits(line.size.width) {
|
||||
// If the line does not fit horizontally or we have a mandatory
|
||||
// line break (i.e. due to "\n"), we push the line into the
|
||||
// stack.
|
||||
stack.push(line);
|
||||
start = end;
|
||||
last = None;
|
||||
|
||||
// If there is a trailing line break at the end of the
|
||||
// paragraph, we want to force an empty line.
|
||||
if mandatory && end == self.bidi.text.len() {
|
||||
stack.push(LineLayout::new(ctx, &self, end .. end));
|
||||
}
|
||||
} else {
|
||||
let before_with_self = before + child_cross_size;
|
||||
let after = self.line_size.cross - (before + child_cross_size);
|
||||
full_size.cross - before_with_self .. after
|
||||
});
|
||||
// Otherwise, the line fits both horizontally and vertically
|
||||
// and we remember it.
|
||||
last = Some((line, end));
|
||||
}
|
||||
}
|
||||
|
||||
let pos = Gen::new(Length::ZERO, cross).switch(self.main).to_point();
|
||||
if let Some((line, _)) = last {
|
||||
stack.push(line);
|
||||
}
|
||||
|
||||
stack.finish(ctx)
|
||||
}
|
||||
|
||||
/// Find the index of the item whose range contains the `text_offset`.
|
||||
fn find(&self, text_offset: usize) -> Option<usize> {
|
||||
self.ranges.binary_search_by(|r| r.locate(text_offset)).ok()
|
||||
}
|
||||
}
|
||||
|
||||
/// Split a range of text into runs of consistent direction.
|
||||
fn split_runs<'a>(
|
||||
bidi: &'a BidiInfo,
|
||||
range: Range,
|
||||
) -> impl Iterator<Item = (Range, Dir)> + 'a {
|
||||
let mut cursor = range.start;
|
||||
bidi.levels[range.clone()]
|
||||
.group_by_key(|&level| level)
|
||||
.map(move |(level, group)| {
|
||||
let start = cursor;
|
||||
cursor += group.len();
|
||||
(start .. cursor, level.dir())
|
||||
})
|
||||
}
|
||||
|
||||
/// A prepared item in a paragraph layout.
|
||||
enum ParItem<'a> {
|
||||
/// Spacing between other items.
|
||||
Spacing(Length),
|
||||
/// A shaped text run with consistent direction.
|
||||
Text(ShapedText<'a>, Align),
|
||||
/// A layouted child node.
|
||||
Frame(Frame, Align),
|
||||
}
|
||||
|
||||
impl ParItem<'_> {
|
||||
/// The size of the item.
|
||||
pub fn size(&self) -> Size {
|
||||
match self {
|
||||
Self::Spacing(amount) => Size::new(*amount, Length::ZERO),
|
||||
Self::Text(shaped, _) => shaped.size,
|
||||
Self::Frame(frame, _) => frame.size,
|
||||
}
|
||||
}
|
||||
|
||||
/// The baseline of the item.
|
||||
pub fn baseline(&self) -> Length {
|
||||
match self {
|
||||
Self::Spacing(_) => Length::ZERO,
|
||||
Self::Text(shaped, _) => shaped.baseline,
|
||||
Self::Frame(frame, _) => frame.baseline,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A simple layouter that stacks lines into areas.
|
||||
struct LineStack<'a> {
|
||||
line_spacing: Length,
|
||||
areas: Areas,
|
||||
finished: Vec<Frame>,
|
||||
lines: Vec<LineLayout<'a>>,
|
||||
size: Size,
|
||||
}
|
||||
|
||||
impl<'a> LineStack<'a> {
|
||||
fn new(line_spacing: Length, areas: Areas) -> Self {
|
||||
Self {
|
||||
line_spacing,
|
||||
areas,
|
||||
finished: vec![],
|
||||
lines: vec![],
|
||||
size: Size::ZERO,
|
||||
}
|
||||
}
|
||||
|
||||
fn push(&mut self, line: LineLayout<'a>) {
|
||||
self.areas.current.height -= line.size.height + self.line_spacing;
|
||||
|
||||
self.size.width = self.size.width.max(line.size.width);
|
||||
self.size.height += line.size.height;
|
||||
if !self.lines.is_empty() {
|
||||
self.size.height += self.line_spacing;
|
||||
}
|
||||
|
||||
self.lines.push(line);
|
||||
}
|
||||
|
||||
fn finish_area(&mut self, ctx: &mut LayoutContext) {
|
||||
let expand = self.areas.expand.horizontal;
|
||||
self.size.width = expand.resolve(self.size.width, self.areas.full.width);
|
||||
|
||||
let mut output = Frame::new(self.size, self.size.height);
|
||||
let mut first = true;
|
||||
let mut offset = Length::ZERO;
|
||||
|
||||
for line in mem::take(&mut self.lines) {
|
||||
let frame = line.build(ctx, self.size.width);
|
||||
let Frame { size, baseline, .. } = frame;
|
||||
|
||||
let pos = Point::new(Length::ZERO, offset);
|
||||
output.push_frame(pos, frame);
|
||||
}
|
||||
|
||||
// Add line spacing, but only between lines.
|
||||
if !self.stack.is_empty() {
|
||||
self.stack_size.main += self.line_spacing;
|
||||
*self.areas.current.get_mut(self.main) -= self.line_spacing;
|
||||
}
|
||||
if first {
|
||||
output.baseline = offset + baseline;
|
||||
first = false;
|
||||
}
|
||||
|
||||
// Update metrics of paragraph and reset for line.
|
||||
self.stack.push((self.stack_size.main, output, self.line_ruler));
|
||||
self.stack_size.main += full_size.main;
|
||||
self.stack_size.cross = self.stack_size.cross.max(full_size.cross);
|
||||
*self.areas.current.get_mut(self.main) -= full_size.main;
|
||||
self.line_size = Gen::ZERO;
|
||||
self.line_ruler = Align::Start;
|
||||
}
|
||||
|
||||
fn finish_area(&mut self) {
|
||||
let full_size = self.stack_size;
|
||||
let mut output = Frame::new(full_size.switch(self.main).to_size());
|
||||
|
||||
for (before, line, cross_align) in std::mem::take(&mut self.stack) {
|
||||
let child_size = line.size.switch(self.main);
|
||||
|
||||
// Position along the main axis.
|
||||
let main = if self.dirs.main.is_positive() {
|
||||
before
|
||||
} else {
|
||||
full_size.main - (before + child_size.main)
|
||||
};
|
||||
|
||||
// Align along the cross axis.
|
||||
let cross = cross_align.resolve(if self.dirs.cross.is_positive() {
|
||||
Length::ZERO .. full_size.cross - child_size.cross
|
||||
} else {
|
||||
full_size.cross - child_size.cross .. Length::ZERO
|
||||
});
|
||||
|
||||
let pos = Gen::new(main, cross).switch(self.main).to_point();
|
||||
output.push_frame(pos, line);
|
||||
offset += size.height + self.line_spacing;
|
||||
}
|
||||
|
||||
self.finished.push(output);
|
||||
self.areas.next();
|
||||
|
||||
// Reset metrics for the whole paragraph.
|
||||
self.stack_size = Gen::ZERO;
|
||||
self.size = Size::ZERO;
|
||||
}
|
||||
|
||||
fn finish(mut self) -> Vec<Frame> {
|
||||
self.finish_line();
|
||||
self.finish_area();
|
||||
fn finish(mut self, ctx: &mut LayoutContext) -> Vec<Frame> {
|
||||
self.finish_area(ctx);
|
||||
self.finished
|
||||
}
|
||||
}
|
||||
|
||||
/// A lightweight representation of a line that spans a specific range in a
|
||||
/// paragraph's text. This type enables you to cheaply measure the size of a
|
||||
/// line in a range before comitting to building the line's frame.
|
||||
struct LineLayout<'a> {
|
||||
/// The paragraph the line was created in.
|
||||
par: &'a ParLayout<'a>,
|
||||
/// The range the line spans in the paragraph.
|
||||
line: Range,
|
||||
/// A reshaped text item if the line sliced up a text item at the start.
|
||||
first: Option<ParItem<'a>>,
|
||||
/// Middle items which don't need to be reprocessed.
|
||||
items: &'a [ParItem<'a>],
|
||||
/// A reshaped text item if the line sliced up a text item at the end. If
|
||||
/// there is only one text item, this takes precedence over `first`.
|
||||
last: Option<ParItem<'a>>,
|
||||
/// The ranges, indexed as `[first, ..items, last]`. The ranges for `first`
|
||||
/// and `last` aren't trimmed to the line, but it doesn't matter because
|
||||
/// we're just checking which range an index falls into.
|
||||
ranges: &'a [Range],
|
||||
/// The size of the line.
|
||||
size: Size,
|
||||
/// The baseline of the line.
|
||||
baseline: Length,
|
||||
}
|
||||
|
||||
impl<'a> LineLayout<'a> {
|
||||
/// Create a line which spans the given range.
|
||||
fn new(ctx: &mut LayoutContext, par: &'a ParLayout<'a>, mut line: Range) -> Self {
|
||||
// Find the items which bound the text range.
|
||||
let last_idx = par.find(line.end.saturating_sub(1)).unwrap();
|
||||
let first_idx = if line.is_empty() {
|
||||
last_idx
|
||||
} else {
|
||||
par.find(line.start).unwrap()
|
||||
};
|
||||
|
||||
// Slice out the relevant items and ranges.
|
||||
let mut items = &par.items[first_idx ..= last_idx];
|
||||
let ranges = &par.ranges[first_idx ..= last_idx];
|
||||
|
||||
// Reshape the last item if it's split in half.
|
||||
let mut last = None;
|
||||
if let Some((ParItem::Text(shaped, align), rest)) = items.split_last() {
|
||||
// Compute the range we want to shape, trimming whitespace at the
|
||||
// end of the line.
|
||||
let base = par.ranges[last_idx].start;
|
||||
let start = line.start.max(base);
|
||||
let end = start + par.bidi.text[start .. line.end].trim_end().len();
|
||||
let range = start - base .. end - base;
|
||||
|
||||
// Reshape if necessary.
|
||||
if range.len() < shaped.text.len() {
|
||||
// If start == end and the rest is empty, then we have an empty
|
||||
// line. To make that line have the appropriate height, we shape the
|
||||
// empty string.
|
||||
if !range.is_empty() || rest.is_empty() {
|
||||
// Reshape that part.
|
||||
let reshaped = shaped.reshape(ctx, range);
|
||||
last = Some(ParItem::Text(reshaped, *align));
|
||||
}
|
||||
|
||||
items = rest;
|
||||
line.end = end;
|
||||
}
|
||||
}
|
||||
|
||||
// Reshape the start item if it's split in half.
|
||||
let mut first = None;
|
||||
if let Some((ParItem::Text(shaped, align), rest)) = items.split_first() {
|
||||
// Compute the range we want to shape.
|
||||
let Range { start: base, end: first_end } = par.ranges[first_idx];
|
||||
let start = line.start;
|
||||
let end = line.end.min(first_end);
|
||||
let range = start - base .. end - base;
|
||||
|
||||
// Reshape if necessary.
|
||||
if range.len() < shaped.text.len() {
|
||||
if !range.is_empty() {
|
||||
let reshaped = shaped.reshape(ctx, range);
|
||||
first = Some(ParItem::Text(reshaped, *align));
|
||||
}
|
||||
|
||||
items = rest;
|
||||
}
|
||||
}
|
||||
|
||||
let mut width = Length::ZERO;
|
||||
let mut top = Length::ZERO;
|
||||
let mut bottom = Length::ZERO;
|
||||
|
||||
// Measure the size of the line.
|
||||
for item in first.iter().chain(items).chain(&last) {
|
||||
let size = item.size();
|
||||
let baseline = item.baseline();
|
||||
width += size.width;
|
||||
top = top.max(baseline);
|
||||
bottom = bottom.max(size.height - baseline);
|
||||
}
|
||||
|
||||
Self {
|
||||
par,
|
||||
line,
|
||||
first,
|
||||
items,
|
||||
last,
|
||||
ranges,
|
||||
size: Size::new(width, top + bottom),
|
||||
baseline: top,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the line's frame.
|
||||
fn build(&self, ctx: &mut LayoutContext, width: Length) -> Frame {
|
||||
let full_width = self.size.width.max(width);
|
||||
let full_size = Size::new(full_width, self.size.height);
|
||||
let free_width = full_width - self.size.width;
|
||||
|
||||
let mut output = Frame::new(full_size, self.baseline);
|
||||
let mut ruler = Align::Start;
|
||||
let mut offset = Length::ZERO;
|
||||
|
||||
self.reordered(|item| {
|
||||
let frame = match *item {
|
||||
ParItem::Spacing(amount) => {
|
||||
offset += amount;
|
||||
return;
|
||||
}
|
||||
ParItem::Text(ref shaped, align) => {
|
||||
ruler = ruler.max(align);
|
||||
shaped.build(ctx)
|
||||
}
|
||||
ParItem::Frame(ref frame, align) => {
|
||||
ruler = ruler.max(align);
|
||||
frame.clone()
|
||||
}
|
||||
};
|
||||
|
||||
let Frame { size, baseline, .. } = frame;
|
||||
let pos = Point::new(
|
||||
ruler.resolve(self.par.dir, offset .. free_width + offset),
|
||||
self.baseline - baseline,
|
||||
);
|
||||
|
||||
output.push_frame(pos, frame);
|
||||
offset += size.width;
|
||||
});
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
/// Iterate through the line's items in visual order.
|
||||
fn reordered(&self, mut f: impl FnMut(&ParItem<'a>)) {
|
||||
// The bidi crate doesn't like empty lines.
|
||||
if self.line.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the paragraph that contains the line.
|
||||
let para = self
|
||||
.par
|
||||
.bidi
|
||||
.paragraphs
|
||||
.iter()
|
||||
.find(|para| para.range.contains(&self.line.start))
|
||||
.unwrap();
|
||||
|
||||
// Compute the reordered ranges in visual order (left to right).
|
||||
let (levels, runs) = self.par.bidi.visual_runs(para, self.line.clone());
|
||||
|
||||
// Find the items for each run.
|
||||
for run in runs {
|
||||
let first_idx = self.find(run.start).unwrap();
|
||||
let last_idx = self.find(run.end - 1).unwrap();
|
||||
let range = first_idx ..= last_idx;
|
||||
|
||||
// Provide the items forwards or backwards depending on the run's
|
||||
// direction.
|
||||
if levels[run.start].is_ltr() {
|
||||
for item in range {
|
||||
f(self.get(item).unwrap());
|
||||
}
|
||||
} else {
|
||||
for item in range.rev() {
|
||||
f(self.get(item).unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the index of the item whose range contains the `text_offset`.
|
||||
fn find(&self, text_offset: usize) -> Option<usize> {
|
||||
self.ranges.binary_search_by(|r| r.locate(text_offset)).ok()
|
||||
}
|
||||
|
||||
/// Get the item at the index.
|
||||
fn get(&self, index: usize) -> Option<&ParItem<'a>> {
|
||||
self.first.iter().chain(self.items).chain(&self.last).nth(index)
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper methods for BiDi levels.
|
||||
trait LevelExt: Sized {
|
||||
fn from_dir(dir: Dir) -> Option<Self>;
|
||||
fn dir(self) -> Dir;
|
||||
}
|
||||
|
||||
impl LevelExt for Level {
|
||||
fn from_dir(dir: Dir) -> Option<Self> {
|
||||
match dir {
|
||||
Dir::LTR => Some(Level::ltr()),
|
||||
Dir::RTL => Some(Level::rtl()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn dir(self) -> Dir {
|
||||
if self.is_ltr() { Dir::LTR } else { Dir::RTL }
|
||||
}
|
||||
}
|
||||
|
@ -1,30 +1,219 @@
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
use std::ops::Range;
|
||||
|
||||
use fontdock::FaceId;
|
||||
use rustybuzz::UnicodeBuffer;
|
||||
use ttf_parser::GlyphId;
|
||||
|
||||
use super::{Element, Frame, ShapedText};
|
||||
use super::{Element, Frame, Glyph, LayoutContext, Text};
|
||||
use crate::env::FontLoader;
|
||||
use crate::exec::FontProps;
|
||||
use crate::geom::{Point, Size};
|
||||
use crate::font::FaceBuf;
|
||||
use crate::geom::{Dir, Length, Point, Size};
|
||||
use crate::util::SliceExt;
|
||||
|
||||
/// Shape text into a frame containing [`ShapedText`] runs.
|
||||
pub fn shape(text: &str, loader: &mut FontLoader, props: &FontProps) -> Frame {
|
||||
let mut frame = Frame::new(Size::ZERO);
|
||||
shape_segment(&mut frame, text, loader, props, props.families.iter(), None);
|
||||
frame
|
||||
/// The result of shaping text.
|
||||
///
|
||||
/// This type contains owned or borrowed shaped text runs, which can be
|
||||
/// measured, used to reshape substrings more quickly and converted into a
|
||||
/// frame.
|
||||
pub struct ShapedText<'a> {
|
||||
/// The text that was shaped.
|
||||
pub text: &'a str,
|
||||
/// The text direction.
|
||||
pub dir: Dir,
|
||||
/// The properties used for font selection.
|
||||
pub props: &'a FontProps,
|
||||
/// The font size.
|
||||
pub size: Size,
|
||||
/// The baseline from the top of the frame.
|
||||
pub baseline: Length,
|
||||
/// The shaped glyphs.
|
||||
pub glyphs: Cow<'a, [ShapedGlyph]>,
|
||||
}
|
||||
|
||||
/// Shape text into a frame with font fallback using the `families` iterator.
|
||||
/// A single glyph resulting from shaping.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct ShapedGlyph {
|
||||
/// The font face the glyph is contained in.
|
||||
pub face_id: FaceId,
|
||||
/// The glyph's ID in the face.
|
||||
pub glyph_id: GlyphId,
|
||||
/// The advance width of the glyph.
|
||||
pub x_advance: i32,
|
||||
/// The horizontal offset of the glyph.
|
||||
pub x_offset: i32,
|
||||
/// The start index of the glyph in the source text.
|
||||
pub text_index: usize,
|
||||
/// Whether splitting the shaping result before this glyph would yield the
|
||||
/// same results as shaping the parts to both sides of `text_index`
|
||||
/// separately.
|
||||
pub safe_to_break: bool,
|
||||
}
|
||||
|
||||
/// A visual side.
|
||||
enum Side {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
impl<'a> ShapedText<'a> {
|
||||
/// Build the shaped text's frame.
|
||||
pub fn build(&self, ctx: &mut LayoutContext) -> Frame {
|
||||
let mut frame = Frame::new(self.size, self.baseline);
|
||||
let mut offset = Length::ZERO;
|
||||
|
||||
for (face_id, group) in self.glyphs.as_ref().group_by_key(|g| g.face_id) {
|
||||
let pos = Point::new(offset, self.baseline);
|
||||
let mut text = Text {
|
||||
face_id,
|
||||
size: self.props.size,
|
||||
color: self.props.color,
|
||||
glyphs: vec![],
|
||||
};
|
||||
|
||||
let face = ctx.env.fonts.face(face_id);
|
||||
for glyph in group {
|
||||
let x_advance = face.convert(glyph.x_advance).scale(self.props.size);
|
||||
let x_offset = face.convert(glyph.x_offset).scale(self.props.size);
|
||||
text.glyphs.push(Glyph { id: glyph.glyph_id, x_advance, x_offset });
|
||||
offset += x_advance;
|
||||
}
|
||||
|
||||
frame.push(pos, Element::Text(text));
|
||||
}
|
||||
|
||||
frame
|
||||
}
|
||||
|
||||
/// Reshape a range of the shaped text, reusing information from this
|
||||
/// shaping process if possible.
|
||||
pub fn reshape(
|
||||
&'a self,
|
||||
ctx: &mut LayoutContext,
|
||||
text_range: Range<usize>,
|
||||
) -> ShapedText<'a> {
|
||||
if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) {
|
||||
let (size, baseline) = measure(&mut ctx.env.fonts, glyphs, self.props);
|
||||
Self {
|
||||
text: &self.text[text_range],
|
||||
dir: self.dir,
|
||||
props: self.props,
|
||||
size,
|
||||
baseline,
|
||||
glyphs: Cow::Borrowed(glyphs),
|
||||
}
|
||||
} else {
|
||||
shape(ctx, &self.text[text_range], self.dir, self.props)
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the subslice of glyphs that represent the given text range if both
|
||||
/// sides are safe to break.
|
||||
fn slice_safe_to_break(&self, text_range: Range<usize>) -> Option<&[ShapedGlyph]> {
|
||||
let Range { mut start, mut end } = text_range;
|
||||
if !self.dir.is_positive() {
|
||||
std::mem::swap(&mut start, &mut end);
|
||||
}
|
||||
|
||||
let left = self.find_safe_to_break(start, Side::Left)?;
|
||||
let right = self.find_safe_to_break(end, Side::Right)?;
|
||||
Some(&self.glyphs[left .. right])
|
||||
}
|
||||
|
||||
/// Find the glyph offset matching the text index that is most towards the
|
||||
/// given side and safe-to-break.
|
||||
fn find_safe_to_break(&self, text_index: usize, towards: Side) -> Option<usize> {
|
||||
let ltr = self.dir.is_positive();
|
||||
|
||||
// Handle edge cases.
|
||||
let len = self.glyphs.len();
|
||||
if text_index == 0 {
|
||||
return Some(if ltr { 0 } else { len });
|
||||
} else if text_index == self.text.len() {
|
||||
return Some(if ltr { len } else { 0 });
|
||||
}
|
||||
|
||||
// Find any glyph with the text index.
|
||||
let mut idx = self
|
||||
.glyphs
|
||||
.binary_search_by(|g| {
|
||||
let ordering = g.text_index.cmp(&text_index);
|
||||
if ltr { ordering } else { ordering.reverse() }
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
let next = match towards {
|
||||
Side::Left => usize::checked_sub,
|
||||
Side::Right => usize::checked_add,
|
||||
};
|
||||
|
||||
// Search for the outermost glyph with the text index.
|
||||
while let Some(next) = next(idx, 1) {
|
||||
if self.glyphs.get(next).map_or(true, |g| g.text_index != text_index) {
|
||||
break;
|
||||
}
|
||||
idx = next;
|
||||
}
|
||||
|
||||
// RTL needs offset one because the left side of the range should be
|
||||
// exclusive and the right side inclusive, contrary to the normal
|
||||
// behaviour of ranges.
|
||||
if !ltr {
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
self.glyphs[idx].safe_to_break.then(|| idx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for ShapedText<'_> {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "Shaped({:?})", self.text)
|
||||
}
|
||||
}
|
||||
|
||||
/// Shape text into [`ShapedText`].
|
||||
pub fn shape<'a>(
|
||||
ctx: &mut LayoutContext,
|
||||
text: &'a str,
|
||||
dir: Dir,
|
||||
props: &'a FontProps,
|
||||
) -> ShapedText<'a> {
|
||||
let loader = &mut ctx.env.fonts;
|
||||
|
||||
let mut glyphs = vec![];
|
||||
let families = props.families.iter();
|
||||
if !text.is_empty() {
|
||||
shape_segment(loader, &mut glyphs, 0, text, dir, props, families, None);
|
||||
}
|
||||
|
||||
let (size, baseline) = measure(loader, &glyphs, props);
|
||||
|
||||
ShapedText {
|
||||
text,
|
||||
dir,
|
||||
props,
|
||||
size,
|
||||
baseline,
|
||||
glyphs: Cow::Owned(glyphs),
|
||||
}
|
||||
}
|
||||
|
||||
/// Shape text with font fallback using the `families` iterator.
|
||||
fn shape_segment<'a>(
|
||||
frame: &mut Frame,
|
||||
text: &str,
|
||||
loader: &mut FontLoader,
|
||||
glyphs: &mut Vec<ShapedGlyph>,
|
||||
base: usize,
|
||||
text: &str,
|
||||
dir: Dir,
|
||||
props: &FontProps,
|
||||
mut families: impl Iterator<Item = &'a str> + Clone,
|
||||
mut first: Option<FaceId>,
|
||||
mut first_face: Option<FaceId>,
|
||||
) {
|
||||
// Select the font family.
|
||||
let (id, fallback) = loop {
|
||||
let (face_id, fallback) = loop {
|
||||
// Try to load the next available font family.
|
||||
match families.next() {
|
||||
Some(family) => match loader.query(family, props.variant) {
|
||||
@ -33,97 +222,140 @@ fn shape_segment<'a>(
|
||||
},
|
||||
// We're out of families, so we don't do any more fallback and just
|
||||
// shape the tofus with the first face we originally used.
|
||||
None => match first {
|
||||
None => match first_face {
|
||||
Some(id) => break (id, false),
|
||||
None => return,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
// Register that this is the first available font.
|
||||
if first.is_none() {
|
||||
first = Some(id);
|
||||
}
|
||||
|
||||
// Find out some metrics and prepare the shaped text container.
|
||||
let face = loader.face(id);
|
||||
let ttf = face.ttf();
|
||||
let units_per_em = f64::from(ttf.units_per_em().unwrap_or(1000));
|
||||
let convert = |units| f64::from(units) / units_per_em * props.size;
|
||||
let top = convert(i32::from(props.top_edge.lookup(ttf)));
|
||||
let bottom = convert(i32::from(props.bottom_edge.lookup(ttf)));
|
||||
let mut shaped = ShapedText::new(id, props.size, top, bottom, props.color);
|
||||
// Remember the id if this the first available face since we use that one to
|
||||
// shape tofus.
|
||||
first_face.get_or_insert(face_id);
|
||||
|
||||
// Fill the buffer with our text.
|
||||
let mut buffer = UnicodeBuffer::new();
|
||||
buffer.push_str(text);
|
||||
buffer.guess_segment_properties();
|
||||
|
||||
// Find out the text direction.
|
||||
// TODO: Replace this once we do BiDi.
|
||||
let rtl = matches!(buffer.direction(), rustybuzz::Direction::RightToLeft);
|
||||
buffer.set_direction(match dir {
|
||||
Dir::LTR => rustybuzz::Direction::LeftToRight,
|
||||
Dir::RTL => rustybuzz::Direction::RightToLeft,
|
||||
_ => unimplemented!(),
|
||||
});
|
||||
|
||||
// Shape!
|
||||
let glyphs = rustybuzz::shape(face.buzz(), &[], buffer);
|
||||
let info = glyphs.glyph_infos();
|
||||
let pos = glyphs.glyph_positions();
|
||||
let mut iter = info.iter().zip(pos).peekable();
|
||||
let buffer = rustybuzz::shape(loader.face(face_id).ttf(), &[], buffer);
|
||||
let infos = buffer.glyph_infos();
|
||||
let pos = buffer.glyph_positions();
|
||||
|
||||
while let Some((info, pos)) = iter.next() {
|
||||
// Do font fallback if the glyph is a tofu.
|
||||
if info.codepoint == 0 && fallback {
|
||||
// Flush what we have so far.
|
||||
if !shaped.glyphs.is_empty() {
|
||||
place(frame, shaped);
|
||||
shaped = ShapedText::new(id, props.size, top, bottom, props.color);
|
||||
}
|
||||
// Collect the shaped glyphs, doing fallback and shaping parts again with
|
||||
// the next font if necessary.
|
||||
let mut i = 0;
|
||||
while i < infos.len() {
|
||||
let info = &infos[i];
|
||||
let cluster = info.cluster as usize;
|
||||
|
||||
// Determine the start and end cluster index of the tofu sequence.
|
||||
let mut start = info.cluster as usize;
|
||||
let mut end = info.cluster as usize;
|
||||
while let Some((info, _)) = iter.peek() {
|
||||
if info.codepoint != 0 {
|
||||
break;
|
||||
}
|
||||
end = info.cluster as usize;
|
||||
iter.next();
|
||||
}
|
||||
|
||||
// Because Harfbuzz outputs glyphs in visual order, the start
|
||||
// cluster actually corresponds to the last codepoint in
|
||||
// right-to-left text.
|
||||
if rtl {
|
||||
assert!(end <= start);
|
||||
std::mem::swap(&mut start, &mut end);
|
||||
}
|
||||
|
||||
// The end cluster index points right before the last character that
|
||||
// mapped to the tofu sequence. So we have to offset the end by one
|
||||
// char.
|
||||
let offset = text[end ..].chars().next().unwrap().len_utf8();
|
||||
let range = start .. end + offset;
|
||||
|
||||
// Recursively shape the tofu sequence with the next family.
|
||||
shape_segment(frame, &text[range], loader, props, families.clone(), first);
|
||||
} else {
|
||||
if info.codepoint != 0 || !fallback {
|
||||
// Add the glyph to the shaped output.
|
||||
// TODO: Don't ignore y_advance and y_offset.
|
||||
let glyph = GlyphId(info.codepoint as u16);
|
||||
shaped.glyphs.push(glyph);
|
||||
shaped.offsets.push(shaped.width + convert(pos.x_offset));
|
||||
shaped.width += convert(pos.x_advance);
|
||||
glyphs.push(ShapedGlyph {
|
||||
face_id,
|
||||
glyph_id: GlyphId(info.codepoint as u16),
|
||||
x_advance: pos[i].x_advance,
|
||||
x_offset: pos[i].x_offset,
|
||||
text_index: base + cluster,
|
||||
safe_to_break: !info.unsafe_to_break(),
|
||||
});
|
||||
} else {
|
||||
// Determine the source text range for the tofu sequence.
|
||||
let range = {
|
||||
// First, search for the end of the tofu sequence.
|
||||
let k = i;
|
||||
while infos.get(i + 1).map_or(false, |info| info.codepoint == 0) {
|
||||
i += 1;
|
||||
}
|
||||
|
||||
// Then, determine the start and end text index.
|
||||
//
|
||||
// Examples:
|
||||
// Everything is shown in visual order. Tofus are written as "_".
|
||||
// We want to find out that the tofus span the text `2..6`.
|
||||
// Note that the clusters are longer than 1 char.
|
||||
//
|
||||
// Left-to-right:
|
||||
// Text: h a l i h a l l o
|
||||
// Glyphs: A _ _ C E
|
||||
// Clusters: 0 2 4 6 8
|
||||
// k=1 i=2
|
||||
//
|
||||
// Right-to-left:
|
||||
// Text: O L L A H I L A H
|
||||
// Glyphs: E C _ _ A
|
||||
// Clusters: 8 6 4 2 0
|
||||
// k=2 i=3
|
||||
|
||||
let ltr = dir.is_positive();
|
||||
let first = if ltr { k } else { i };
|
||||
let start = infos[first].cluster as usize;
|
||||
|
||||
let last = if ltr { i.checked_add(1) } else { k.checked_sub(1) };
|
||||
let end = last
|
||||
.and_then(|last| infos.get(last))
|
||||
.map_or(text.len(), |info| info.cluster as usize);
|
||||
|
||||
start .. end
|
||||
};
|
||||
|
||||
// Recursively shape the tofu sequence with the next family.
|
||||
shape_segment(
|
||||
loader,
|
||||
glyphs,
|
||||
base + range.start,
|
||||
&text[range],
|
||||
dir,
|
||||
props,
|
||||
families.clone(),
|
||||
first_face,
|
||||
);
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Measure the size and baseline of a run of shaped glyphs with the given
|
||||
/// properties.
|
||||
fn measure(
|
||||
loader: &mut FontLoader,
|
||||
glyphs: &[ShapedGlyph],
|
||||
props: &FontProps,
|
||||
) -> (Size, Length) {
|
||||
let mut width = Length::ZERO;
|
||||
let mut top = Length::ZERO;
|
||||
let mut bottom = Length::ZERO;
|
||||
let mut expand_vertical = |face: &FaceBuf| {
|
||||
top = top.max(face.vertical_metric(props.top_edge).scale(props.size));
|
||||
bottom = bottom.max(-face.vertical_metric(props.bottom_edge).scale(props.size));
|
||||
};
|
||||
|
||||
if glyphs.is_empty() {
|
||||
// When there are no glyphs, we just use the vertical metrics of the
|
||||
// first available font.
|
||||
for family in props.families.iter() {
|
||||
if let Some(face_id) = loader.query(family, props.variant) {
|
||||
expand_vertical(loader.face(face_id));
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (face_id, group) in glyphs.group_by_key(|g| g.face_id) {
|
||||
let face = loader.face(face_id);
|
||||
expand_vertical(face);
|
||||
|
||||
for glyph in group {
|
||||
width += face.convert(glyph.x_advance).scale(props.size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !shaped.glyphs.is_empty() {
|
||||
place(frame, shaped)
|
||||
}
|
||||
}
|
||||
|
||||
/// Place shaped text into a frame.
|
||||
fn place(frame: &mut Frame, shaped: ShapedText) {
|
||||
let offset = frame.size.width;
|
||||
frame.size.width += shaped.width;
|
||||
frame.size.height = frame.size.height.max(shaped.top - shaped.bottom);
|
||||
frame.push(Point::new(offset, shaped.top), Element::Text(shaped));
|
||||
(Size::new(width, top + bottom), top)
|
||||
}
|
||||
|
@ -28,7 +28,13 @@ impl Layout for StackNode {
|
||||
match *child {
|
||||
StackChild::Spacing(amount) => layouter.push_spacing(amount),
|
||||
StackChild::Any(ref node, aligns) => {
|
||||
for frame in node.layout(ctx, &layouter.areas) {
|
||||
let mut frames = node.layout(ctx, &layouter.areas).into_iter();
|
||||
if let Some(frame) = frames.next() {
|
||||
layouter.push_frame(frame, aligns);
|
||||
}
|
||||
|
||||
for frame in frames {
|
||||
layouter.finish_area();
|
||||
layouter.push_frame(frame, aligns);
|
||||
}
|
||||
}
|
||||
@ -116,32 +122,39 @@ impl StackLayouter {
|
||||
size = Size::new(width, width / aspect);
|
||||
}
|
||||
|
||||
size.switch(self.main)
|
||||
size
|
||||
};
|
||||
|
||||
let mut output = Frame::new(full_size.switch(self.main).to_size());
|
||||
let mut output = Frame::new(full_size, full_size.height);
|
||||
let mut first = true;
|
||||
|
||||
let full_size = full_size.switch(self.main);
|
||||
for (before, frame, aligns) in std::mem::take(&mut self.frames) {
|
||||
let child_size = frame.size.switch(self.main);
|
||||
|
||||
// Align along the main axis.
|
||||
let main = aligns.main.resolve(if self.dirs.main.is_positive() {
|
||||
let after_with_self = self.size.main - before;
|
||||
before .. full_size.main - after_with_self
|
||||
} else {
|
||||
let before_with_self = before + child_size.main;
|
||||
let after = self.size.main - (before + child_size.main);
|
||||
full_size.main - before_with_self .. after
|
||||
});
|
||||
let main = aligns.main.resolve(
|
||||
self.dirs.main,
|
||||
if self.dirs.main.is_positive() {
|
||||
before .. before + full_size.main - self.size.main
|
||||
} else {
|
||||
self.size.main - (before + child_size.main)
|
||||
.. full_size.main - (before + child_size.main)
|
||||
},
|
||||
);
|
||||
|
||||
// Align along the cross axis.
|
||||
let cross = aligns.cross.resolve(if self.dirs.cross.is_positive() {
|
||||
Length::ZERO .. full_size.cross - child_size.cross
|
||||
} else {
|
||||
full_size.cross - child_size.cross .. Length::ZERO
|
||||
});
|
||||
let cross = aligns.cross.resolve(
|
||||
self.dirs.cross,
|
||||
Length::ZERO .. full_size.cross - child_size.cross,
|
||||
);
|
||||
|
||||
let pos = Gen::new(main, cross).switch(self.main).to_point();
|
||||
if first {
|
||||
output.baseline = pos.y + frame.baseline;
|
||||
first = false;
|
||||
}
|
||||
|
||||
output.push_frame(pos, frame);
|
||||
}
|
||||
|
||||
|
@ -44,6 +44,7 @@ pub mod parse;
|
||||
pub mod pdf;
|
||||
pub mod pretty;
|
||||
pub mod syntax;
|
||||
pub mod util;
|
||||
|
||||
use crate::diag::Pass;
|
||||
use crate::env::Env;
|
||||
|
@ -73,7 +73,7 @@ impl Layout for ImageNode {
|
||||
}
|
||||
};
|
||||
|
||||
let mut frame = Frame::new(size);
|
||||
let mut frame = Frame::new(size, size.height);
|
||||
frame.push(Point::ZERO, Element::Image(Image { res: self.res, size }));
|
||||
|
||||
vec![frame]
|
||||
|
@ -160,7 +160,7 @@ pub fn raw(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||
|
||||
let snapshot = ctx.state.clone();
|
||||
ctx.set_monospace();
|
||||
ctx.push_text(&text);
|
||||
ctx.push_text(text.clone());
|
||||
ctx.state = snapshot;
|
||||
|
||||
if block {
|
||||
|
@ -15,6 +15,7 @@ use ttf_parser::{name_id, GlyphId};
|
||||
|
||||
use crate::color::Color;
|
||||
use crate::env::{Env, ImageResource, ResourceId};
|
||||
use crate::font::{EmLength, VerticalFontMetric};
|
||||
use crate::geom::{self, Length, Size};
|
||||
use crate::layout::{Element, Fill, Frame, Image, Shape};
|
||||
|
||||
@ -50,7 +51,7 @@ impl<'a> PdfExporter<'a> {
|
||||
for frame in frames {
|
||||
for (_, element) in &frame.elements {
|
||||
match element {
|
||||
Element::Text(shaped) => fonts.insert(shaped.face),
|
||||
Element::Text(shaped) => fonts.insert(shaped.face_id),
|
||||
Element::Image(image) => {
|
||||
let img = env.resources.loaded::<ImageResource>(image.res);
|
||||
if img.buf.color().has_alpha() {
|
||||
@ -187,11 +188,11 @@ impl<'a> PdfExporter<'a> {
|
||||
|
||||
// Then, also check if we need to issue a font switching
|
||||
// action.
|
||||
if shaped.face != face || shaped.size != size {
|
||||
face = shaped.face;
|
||||
if shaped.face_id != face || shaped.size != size {
|
||||
face = shaped.face_id;
|
||||
size = shaped.size;
|
||||
|
||||
let name = format!("F{}", self.fonts.map(shaped.face));
|
||||
let name = format!("F{}", self.fonts.map(shaped.face_id));
|
||||
text.font(Name(name.as_bytes()), size.to_pt() as f32);
|
||||
}
|
||||
|
||||
@ -234,24 +235,18 @@ impl<'a> PdfExporter<'a> {
|
||||
flags.insert(FontFlags::SYMBOLIC);
|
||||
flags.insert(FontFlags::SMALL_CAP);
|
||||
|
||||
// Convert from OpenType font units to PDF glyph units.
|
||||
let em_per_unit = 1.0 / ttf.units_per_em().unwrap_or(1000) as f32;
|
||||
let convert = |font_unit: f32| (1000.0 * em_per_unit * font_unit).round();
|
||||
let convert_i16 = |font_unit: i16| convert(font_unit as f32);
|
||||
let convert_u16 = |font_unit: u16| convert(font_unit as f32);
|
||||
|
||||
let global_bbox = ttf.global_bounding_box();
|
||||
let bbox = Rect::new(
|
||||
convert_i16(global_bbox.x_min),
|
||||
convert_i16(global_bbox.y_min),
|
||||
convert_i16(global_bbox.x_max),
|
||||
convert_i16(global_bbox.y_max),
|
||||
face.convert(global_bbox.x_min).to_pdf(),
|
||||
face.convert(global_bbox.y_min).to_pdf(),
|
||||
face.convert(global_bbox.x_max).to_pdf(),
|
||||
face.convert(global_bbox.y_max).to_pdf(),
|
||||
);
|
||||
|
||||
let italic_angle = ttf.italic_angle().unwrap_or(0.0);
|
||||
let ascender = convert_i16(ttf.typographic_ascender().unwrap_or(0));
|
||||
let descender = convert_i16(ttf.typographic_descender().unwrap_or(0));
|
||||
let cap_height = ttf.capital_height().map(convert_i16);
|
||||
let ascender = face.vertical_metric(VerticalFontMetric::Ascender).to_pdf();
|
||||
let descender = face.vertical_metric(VerticalFontMetric::Descender).to_pdf();
|
||||
let cap_height = face.vertical_metric(VerticalFontMetric::CapHeight).to_pdf();
|
||||
let stem_v = 10.0 + 0.244 * (f32::from(ttf.weight().to_number()) - 50.0);
|
||||
|
||||
// Write the base font object referencing the CID font.
|
||||
@ -272,8 +267,8 @@ impl<'a> PdfExporter<'a> {
|
||||
.individual(0, {
|
||||
let num_glyphs = ttf.number_of_glyphs();
|
||||
(0 .. num_glyphs).map(|g| {
|
||||
let advance = ttf.glyph_hor_advance(GlyphId(g));
|
||||
convert_u16(advance.unwrap_or(0))
|
||||
let x = ttf.glyph_hor_advance(GlyphId(g)).unwrap_or(0);
|
||||
face.convert(x).to_pdf()
|
||||
})
|
||||
});
|
||||
|
||||
@ -286,7 +281,7 @@ impl<'a> PdfExporter<'a> {
|
||||
.italic_angle(italic_angle)
|
||||
.ascent(ascender)
|
||||
.descent(descender)
|
||||
.cap_height(cap_height.unwrap_or(ascender))
|
||||
.cap_height(cap_height)
|
||||
.stem_v(stem_v)
|
||||
.font_file2(refs.data);
|
||||
|
||||
@ -571,3 +566,15 @@ where
|
||||
self.to_layout.iter().copied()
|
||||
}
|
||||
}
|
||||
|
||||
/// Additional methods for [`EmLength`].
|
||||
trait EmLengthExt {
|
||||
/// Convert an em length to a number of PDF font units.
|
||||
fn to_pdf(self) -> f32;
|
||||
}
|
||||
|
||||
impl EmLengthExt for EmLength {
|
||||
fn to_pdf(self) -> f32 {
|
||||
1000.0 * self.get() as f32
|
||||
}
|
||||
}
|
||||
|
81
src/util.rs
Normal file
@ -0,0 +1,81 @@
|
||||
//! Utilities.
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::ops::Range;
|
||||
|
||||
/// Additional methods for slices.
|
||||
pub trait SliceExt<T> {
|
||||
/// Split a slice into consecutive groups with the same key.
|
||||
///
|
||||
/// Returns an iterator of pairs of a key and the group with that key.
|
||||
fn group_by_key<K, F>(&self, f: F) -> GroupByKey<'_, T, F>
|
||||
where
|
||||
F: FnMut(&T) -> K,
|
||||
K: PartialEq;
|
||||
}
|
||||
|
||||
impl<T> SliceExt<T> for [T] {
|
||||
fn group_by_key<K, F>(&self, f: F) -> GroupByKey<'_, T, F>
|
||||
where
|
||||
F: FnMut(&T) -> K,
|
||||
K: PartialEq,
|
||||
{
|
||||
GroupByKey { slice: self, f }
|
||||
}
|
||||
}
|
||||
|
||||
/// This struct is produced by [`SliceExt::group_by_key`].
|
||||
pub struct GroupByKey<'a, T, F> {
|
||||
slice: &'a [T],
|
||||
f: F,
|
||||
}
|
||||
|
||||
impl<'a, T, K, F> Iterator for GroupByKey<'a, T, F>
|
||||
where
|
||||
F: FnMut(&T) -> K,
|
||||
K: PartialEq,
|
||||
{
|
||||
type Item = (K, &'a [T]);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let first = self.slice.first()?;
|
||||
let key = (self.f)(first);
|
||||
|
||||
let mut i = 1;
|
||||
while self.slice.get(i).map_or(false, |t| (self.f)(t) == key) {
|
||||
i += 1;
|
||||
}
|
||||
|
||||
let (head, tail) = self.slice.split_at(i);
|
||||
self.slice = tail;
|
||||
Some((key, head))
|
||||
}
|
||||
}
|
||||
|
||||
/// Additional methods for [`Range<usize>`].
|
||||
pub trait RangeExt {
|
||||
/// Locate a position relative to a range.
|
||||
///
|
||||
/// This can be used for binary searching the range that contains the
|
||||
/// position as follows:
|
||||
/// ```
|
||||
/// # use typst::util::RangeExt;
|
||||
/// assert_eq!(
|
||||
/// [1..2, 2..7, 7..10].binary_search_by(|r| r.locate(5)),
|
||||
/// Ok(1),
|
||||
/// );
|
||||
/// ```
|
||||
fn locate(&self, pos: usize) -> Ordering;
|
||||
}
|
||||
|
||||
impl RangeExt for Range<usize> {
|
||||
fn locate(&self, pos: usize) -> Ordering {
|
||||
if pos < self.start {
|
||||
Ordering::Greater
|
||||
} else if pos < self.end {
|
||||
Ordering::Equal
|
||||
} else {
|
||||
Ordering::Less
|
||||
}
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 706 B After Width: | Height: | Size: 708 B |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 880 B After Width: | Height: | Size: 879 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 770 B After Width: | Height: | Size: 782 B |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 9.6 KiB |
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.0 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
BIN
tests/ref/text/align.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
BIN
tests/ref/text/bidi.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
tests/ref/text/chinese.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
tests/ref/text/linebreaks.png
Normal file
After Width: | Height: | Size: 9.9 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 13 KiB |
BIN
tests/ref/text/whitespace.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
@ -3,6 +3,7 @@
|
||||
---
|
||||
// Test configuring paragraph properties.
|
||||
|
||||
// FIXME: Word spacing doesn't work due to new shaping process.
|
||||
#par(spacing: 10pt, leading: 25%, word-spacing: 1pt)
|
||||
|
||||
But, soft! what light through yonder window breaks? It is the east, and Juliet
|
||||
|
@ -19,7 +19,7 @@
|
||||
|
||||
// Not visible, but creates a gap between the boxes above and below
|
||||
// due to line spacing.
|
||||
#rect(width: 2in, fill: #ff0000)
|
||||
#rect(width: 1in, fill: #ff0000)
|
||||
|
||||
// These are in a row!
|
||||
#rect(width: 0.5in, height: 10pt, fill: #D6CD67)
|
||||
|
@ -3,7 +3,7 @@
|
||||
---
|
||||
#let linebreak() = [
|
||||
// Inside the old line break definition is still active.
|
||||
#circle(radius: 2pt, fill: #000) \
|
||||
#square(length: 3pt, fill: #000) \
|
||||
]
|
||||
|
||||
A \ B \ C
|
||||
|
28
tests/typ/text/align.typ
Normal file
@ -0,0 +1,28 @@
|
||||
// Test text alignment.
|
||||
|
||||
---
|
||||
// Test that alignment depends on the paragraph's full width.
|
||||
#rect[
|
||||
Hello World \
|
||||
#align(right)[World]
|
||||
]
|
||||
|
||||
---
|
||||
// Test that a line with multiple alignments respects the paragraph's full
|
||||
// width.
|
||||
#rect[
|
||||
Hello #align(center)[World] \
|
||||
Hello from the World
|
||||
]
|
||||
|
||||
---
|
||||
// Test that `start` alignment after `end` alignment doesn't do anything until
|
||||
// the next line break ...
|
||||
L #align(right)[R] R
|
||||
|
||||
// ... but make sure it resets to left after the line break.
|
||||
L #align(right)[R] \ L
|
||||
|
||||
---
|
||||
// FIXME: There should be a line break opportunity on alignment change.
|
||||
LLLLLLLLLLLLL#align(center)[CCCC]
|
@ -1,6 +1,7 @@
|
||||
// Test simple text.
|
||||
|
||||
#page(width: 250pt)
|
||||
---
|
||||
#page(width: 250pt, height: 110pt)
|
||||
|
||||
But, soft! what light through yonder window breaks? It is the east, and Juliet
|
||||
is the sun. Arise, fair sun, and kill the envious moon, Who is already sick and
|
||||
|
49
tests/typ/text/bidi.typ
Normal file
@ -0,0 +1,49 @@
|
||||
// Test bidirectional text.
|
||||
|
||||
---
|
||||
// Test reordering with different top-level paragraph directions.
|
||||
#let text = [Text טֶקסט]
|
||||
#font("EB Garamond", "Noto Serif Hebrew")
|
||||
#lang("de") {text}
|
||||
#lang("he") {text}
|
||||
|
||||
---
|
||||
// Test that consecutiv, embedded LTR runs stay LTR.
|
||||
// Here, we have two runs: "A" and italic "B".
|
||||
#let text = [أنت A_B_مطرC]
|
||||
#font("EB Garamond", "Noto Sans Arabic")
|
||||
#lang("de") {text}
|
||||
#lang("ar") {text}
|
||||
|
||||
---
|
||||
// Test that consecutive, embedded RTL runs stay RTL.
|
||||
// Here, we have three runs: "גֶ", bold "שֶׁ", and "ם".
|
||||
#let text = [Aגֶ*שֶׁ*םB]
|
||||
#font("EB Garamond", "Noto Serif Hebrew")
|
||||
#lang("de") {text}
|
||||
#lang("he") {text}
|
||||
|
||||
---
|
||||
// Test embedding up to level 4 with isolates.
|
||||
#font("EB Garamond", "Noto Serif Hebrew", "Twitter Color Emoji")
|
||||
#lang(dir: rtl)
|
||||
א\u{2066}A\u{2067}Bב\u{2069}?
|
||||
|
||||
---
|
||||
// Test hard line break (leads to two paragraphs in unicode-bidi).
|
||||
#font("Noto Sans Arabic", "EB Garamond")
|
||||
#lang("ar")
|
||||
Life المطر هو الحياة \
|
||||
الحياة تمطر is rain.
|
||||
|
||||
---
|
||||
// Test spacing.
|
||||
#font("EB Garamond", "Noto Serif Hebrew")
|
||||
L #h(1cm) ריווחR \
|
||||
Lריווח #h(1cm) R
|
||||
|
||||
---
|
||||
// Test inline object.
|
||||
#font("Noto Serif Hebrew", "EB Garamond")
|
||||
#lang("he")
|
||||
קרנפיםRh#image("res/rhino.png", height: 11pt)inoחיים
|
9
tests/typ/text/chinese.typ
Normal file
@ -0,0 +1,9 @@
|
||||
// Test chinese text from Wikipedia.
|
||||
|
||||
---
|
||||
#font("Noto Serif CJK SC")
|
||||
|
||||
是美国广播公司电视剧《迷失》第3季的第22和23集,也是全剧的第71集和72集
|
||||
由执行制作人戴蒙·林道夫和卡尔顿·库斯编剧,导演则是另一名执行制作人杰克·本德
|
||||
节目于2007年5月23日在美国和加拿大首播,共计吸引了1400万美国观众收看
|
||||
本集加上插播广告一共也持续有两个小时
|
28
tests/typ/text/linebreaks.typ
Normal file
@ -0,0 +1,28 @@
|
||||
// Test line breaking special cases.
|
||||
|
||||
---
|
||||
// Test overlong word that is not directly after a hard break.
|
||||
This is a spaceexceedinglylongishy.
|
||||
|
||||
---
|
||||
// Test two overlong words in a row.
|
||||
Supercalifragilisticexpialidocious Expialigoricmetrioxidation.
|
||||
|
||||
---
|
||||
// Test that there are no unwanted line break opportunities on run change.
|
||||
This is partly emph_as_ized.
|
||||
|
||||
---
|
||||
Hard \ break.
|
||||
|
||||
---
|
||||
// Test hard break directly after normal break.
|
||||
Hard break directly after \ normal break.
|
||||
|
||||
---
|
||||
// Test consecutive breaks.
|
||||
Two consecutive \ \ breaks and three \ \ \ more.
|
||||
|
||||
---
|
||||
// Test trailing newline.
|
||||
Trailing break \
|
@ -8,7 +8,7 @@ Le fira
|
||||
|
||||
// This should just shape nicely.
|
||||
#font("Noto Sans Arabic")
|
||||
منش إلا بسم الله
|
||||
دع النص يمطر عليك
|
||||
|
||||
// This should form a three-member family.
|
||||
#font("Twitter Color Emoji")
|
||||
@ -26,7 +26,7 @@ Le fira
|
||||
A😀B
|
||||
|
||||
// Font fallback for entire text.
|
||||
منش إلا بسم الله
|
||||
دع النص يمطر عليك
|
||||
|
||||
// Font fallback in right-to-left text.
|
||||
ب🐈😀سم
|
||||
@ -36,3 +36,10 @@ Aب😀🏞سمB
|
||||
|
||||
// Tofus are rendered with the first font.
|
||||
A🐈中文B
|
||||
|
||||
---
|
||||
// Test reshaping.
|
||||
|
||||
#font("Noto Serif Hebrew")
|
||||
#lang("he")
|
||||
ס \ טֶ
|
||||
|
17
tests/typ/text/whitespace.typ
Normal file
@ -0,0 +1,17 @@
|
||||
// Test whitespace handling.
|
||||
|
||||
---
|
||||
// Test that a run consisting only of whitespace isn't trimmed.
|
||||
A#font("PT Sans")[ ]B
|
||||
|
||||
---
|
||||
// Test font change after space.
|
||||
Left #font("PT Sans")[Right].
|
||||
|
||||
---
|
||||
// Test that space at start of line is not trimmed.
|
||||
A{"\n"} B
|
||||
|
||||
---
|
||||
// Test that trailing space does not force a line break.
|
||||
LLLLLLLLLLLLLL R _L_
|
@ -20,7 +20,7 @@ use typst::env::{Env, FsIndexExt, ImageResource, ResourceLoader};
|
||||
use typst::eval::{EvalContext, FuncArgs, FuncValue, Scope, Value};
|
||||
use typst::exec::State;
|
||||
use typst::geom::{self, Length, Point, Sides, Size};
|
||||
use typst::layout::{Element, Fill, Frame, Geometry, Image, Shape, ShapedText};
|
||||
use typst::layout::{Element, Fill, Frame, Geometry, Image, Shape, Text};
|
||||
use typst::library;
|
||||
use typst::parse::{LineMap, Scanner};
|
||||
use typst::pdf;
|
||||
@ -413,19 +413,19 @@ fn draw(env: &Env, frames: &[Frame], pixel_per_pt: f32) -> Pixmap {
|
||||
canvas
|
||||
}
|
||||
|
||||
fn draw_text(canvas: &mut Pixmap, env: &Env, ts: Transform, shaped: &ShapedText) {
|
||||
let ttf = env.fonts.face(shaped.face).ttf();
|
||||
fn draw_text(canvas: &mut Pixmap, env: &Env, ts: Transform, shaped: &Text) {
|
||||
let ttf = env.fonts.face(shaped.face_id).ttf();
|
||||
let mut x = 0.0;
|
||||
|
||||
for (&glyph, &offset) in shaped.glyphs.iter().zip(&shaped.offsets) {
|
||||
let units_per_em = ttf.units_per_em().unwrap_or(1000);
|
||||
|
||||
let x = offset.to_pt() as f32;
|
||||
let s = (shaped.size / units_per_em as f64).to_pt() as f32;
|
||||
let ts = ts.pre_translate(x, 0.0);
|
||||
for glyph in &shaped.glyphs {
|
||||
let units_per_em = ttf.units_per_em();
|
||||
let s = shaped.size.to_pt() as f32 / units_per_em as f32;
|
||||
let dx = glyph.x_offset.to_pt() as f32;
|
||||
let ts = ts.pre_translate(x + dx, 0.0);
|
||||
|
||||
// Try drawing SVG if present.
|
||||
if let Some(tree) = ttf
|
||||
.glyph_svg_image(glyph)
|
||||
.glyph_svg_image(glyph.id)
|
||||
.and_then(|data| std::str::from_utf8(data).ok())
|
||||
.map(|svg| {
|
||||
let viewbox = format!("viewBox=\"0 0 {0} {0}\" xmlns", units_per_em);
|
||||
@ -445,19 +445,19 @@ fn draw_text(canvas: &mut Pixmap, env: &Env, ts: Transform, shaped: &ShapedText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
} else {
|
||||
// Otherwise, draw normal outline.
|
||||
let mut builder = WrappedPathBuilder(tiny_skia::PathBuilder::new());
|
||||
if ttf.outline_glyph(glyph.id, &mut builder).is_some() {
|
||||
let path = builder.0.finish().unwrap();
|
||||
let ts = ts.pre_scale(s, -s);
|
||||
let mut paint = convert_typst_fill(shaped.color);
|
||||
paint.anti_alias = true;
|
||||
canvas.fill_path(&path, &paint, FillRule::default(), ts, None);
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, draw normal outline.
|
||||
let mut builder = WrappedPathBuilder(tiny_skia::PathBuilder::new());
|
||||
if ttf.outline_glyph(glyph, &mut builder).is_some() {
|
||||
let path = builder.0.finish().unwrap();
|
||||
let ts = ts.pre_scale(s, -s);
|
||||
let mut paint = convert_typst_fill(shaped.color);
|
||||
paint.anti_alias = true;
|
||||
canvas.fill_path(&path, &paint, FillRule::default(), ts, None);
|
||||
}
|
||||
x += glyph.x_advance.to_pt() as f32;
|
||||
}
|
||||
}
|
||||
|
||||
|