Devise text layouter 📑

This commit is contained in:
Laurenz 2019-05-26 22:03:55 +02:00
parent c38e17d91f
commit 0274e93810
4 changed files with 278 additions and 15 deletions

View File

@ -2,20 +2,42 @@
use crate::doc::{Document, Page, TextAction};
use crate::font::{Font, FontLoader, FontFamily, FontError};
use crate::syntax::SyntaxTree;
use crate::syntax::{SyntaxTree, Node};
mod size;
mod text;
pub use size::Size;
pub use text::TextLayouter;
/// Layout a syntax tree in a given context.
#[allow(unused_variables)]
pub fn layout(tree: &SyntaxTree, ctx: &LayoutContext) -> LayoutResult<Layout> {
Ok(Layout {
extent: LayoutDimensions { width: Size::zero(), height: Size::zero() },
actions: vec![],
})
let mut layouter = TextLayouter::new(ctx);
let mut italic = false;
let mut bold = false;
for node in &tree.nodes {
match node {
Node::Text(text) => layouter.add_text(text)?,
Node::Space => layouter.add_space()?,
Node::Newline => layouter.add_paragraph()?,
Node::ToggleItalics => {
italic = !italic;
layouter.set_italic(italic);
},
Node::ToggleBold => {
bold = !bold;
layouter.set_bold(bold);
}
Node::Func(_) => unimplemented!(),
}
}
layouter.finish()
}
/// A collection of layouted content.

248
src/layout/text.rs Normal file
View File

@ -0,0 +1,248 @@
//! Layouting of text.
use std::cell::Ref;
use std::mem;
use smallvec::SmallVec;
use crate::doc::TextAction;
use crate::font::{Font, FontQuery};
use super::{Layouter, Layout, LayoutError, LayoutContext, Size, LayoutResult};
/// Layouts text within the constraints of a layouting context.
#[derive(Debug)]
pub struct TextLayouter<'a, 'p> {
ctx: &'a LayoutContext<'a, 'p>,
units: Vec<Unit>,
italic: bool,
bold: bool,
}
/// A units that is arranged by the text layouter.
#[derive(Debug, Clone)]
enum Unit {
/// A paragraph.
Paragraph,
/// A space with its font index and width.
Space(usize, Size),
/// One logical tex unit.
Text(TextUnit),
}
/// A logical unit of text (a word, syllable or a similar construct).
#[derive(Debug, Clone)]
struct TextUnit {
/// Contains pairs of (characters, font_index, char_width) for each character of the text.
chars_with_widths: SmallVec<[(char, usize, Size); 12]>,
/// The total width of the unit.
width: Size,
}
impl<'a, 'p> TextLayouter<'a, 'p> {
/// Create a new text layouter.
pub fn new(ctx: &'a LayoutContext<'a, 'p>) -> TextLayouter<'a, 'p> {
TextLayouter {
ctx,
italic: false,
bold: false,
units: vec![],
}
}
/// Add more text to the layout.
pub fn add_text(&mut self, text: &str) -> LayoutResult<()> {
let mut chars_with_widths = SmallVec::<[(char, usize, Size); 12]>::new();
// Find out which font to use for each character in the text and meanwhile calculate the
// width of the text.
let mut text_width = Size::zero();
for c in text.chars() {
// Find out the width and add it to the total width.
let (index, font) = self.get_font_for(c)?;
let char_width = self.width_of(c, &font);
text_width += char_width;
chars_with_widths.push((c, index, char_width));
}
self.units.push(Unit::Text(TextUnit {
chars_with_widths,
width: text_width,
}));
Ok(())
}
/// Add a single space character.
pub fn add_space(&mut self) -> LayoutResult<()> {
let (index, font) = self.get_font_for(' ')?;
let width = self.width_of(' ', &font);
drop(font);
Ok(self.units.push(Unit::Space(index, width)))
}
/// Start a new paragraph.
pub fn add_paragraph(&mut self) -> LayoutResult<()> {
Ok(self.units.push(Unit::Paragraph))
}
/// Enable or disable italics.
pub fn set_italic(&mut self, italic: bool) {
self.italic = italic;
}
/// Enable or disable boldface.
pub fn set_bold(&mut self, bold: bool) {
self.bold = bold;
}
/// Load a font that has the character we need.
fn get_font_for(&self, character: char) -> LayoutResult<(usize, Ref<Font>)> {
self.ctx.loader.get(FontQuery {
families: self.ctx.text_style.font_families.clone(),
italic: self.italic,
bold: self.bold,
character,
}).ok_or_else(|| LayoutError::NoSuitableFont)
}
/// The width of a char in a specific font.
fn width_of(&self, character: char, font: &Font) -> Size {
font.widths[font.map(character) as usize] * self.ctx.text_style.font_size
}
}
impl Layouter for TextLayouter<'_, '_> {
fn finish(self) -> LayoutResult<Layout> {
TextFinisher {
actions: vec![],
buffered_text: String::new(),
current_width: Size::zero(),
active_font: std::usize::MAX,
max_width: self.ctx.max_extent.width,
layouter: self,
}.finish()
}
}
/// Finishes a text layout by converting the text units into a stream of text actions.
#[derive(Debug)]
struct TextFinisher<'a, 'p> {
layouter: TextLayouter<'a, 'p>,
actions: Vec<TextAction>,
buffered_text: String,
current_width: Size,
active_font: usize,
max_width: Size,
}
impl<'a, 'p> TextFinisher<'a, 'p> {
/// Finish the layout.
fn finish(mut self) -> LayoutResult<Layout> {
// Move the units out of the layouter leaving an empty vector in place. This is needed to
// move the units out into the for loop while keeping the borrow checker happy.
let mut units = Vec::new();
mem::swap(&mut self.layouter.units, &mut units);
// Move to the top-left corner of the layout space.
self.move_start();
for unit in units {
match unit {
Unit::Paragraph => self.write_paragraph(),
Unit::Space(index, width) => self.write_space(index, width),
Unit::Text(text) => self.write_text_unit(text),
}
}
self.write_buffered_text();
Ok(Layout {
extent: self.layouter.ctx.max_extent.clone(),
actions: self.actions,
})
}
/// Add a paragraph to the output.
fn write_paragraph(&mut self) {
self.write_buffered_text();
self.move_newline(self.layouter.ctx.text_style.paragraph_spacing);
}
/// Add a single space to the output if it is not eaten by a line break.
fn write_space(&mut self, font: usize, width: Size) {
if self.would_overflow(width) {
self.write_buffered_text();
self.move_newline(1.0);
} else if self.current_width > Size::zero() {
if font != self.active_font {
self.write_buffered_text();
self.set_font(font);
}
self.buffered_text.push(' ');
self.current_width += width;
}
}
/// Add a single unit of text without breaking it apart.
fn write_text_unit(&mut self, text: TextUnit) {
if self.would_overflow(text.width) {
self.write_buffered_text();
self.move_newline(1.0);
}
// Finally write the word.
for (c, font, width) in text.chars_with_widths {
if font != self.active_font {
// If we will change the font, first write the remaining things.
self.write_buffered_text();
self.set_font(font);
}
self.buffered_text.push(c);
self.current_width += width;
}
}
/// Move to the top-left corner of the layout space.
fn move_start(&mut self) {
self.actions.push(TextAction::MoveNewline(
Size::zero(), self.layouter.ctx.max_extent.height
- Size::from_points(self.layouter.ctx.text_style.font_size)
));
}
/// Move to the next line. A factor of 1.0 uses the default line spacing.
fn move_newline(&mut self, factor: f32) {
if self.active_font != std::usize::MAX {
let vertical = Size::from_points(self.layouter.ctx.text_style.font_size)
* self.layouter.ctx.text_style.line_spacing
* factor;
self.actions.push(TextAction::MoveNewline(Size::zero(), -vertical));
self.current_width = Size::zero();
}
}
/// Output a text action containing the buffered text and reset the buffer.
fn write_buffered_text(&mut self) {
if !self.buffered_text.is_empty() {
let mut buffered = String::new();
mem::swap(&mut self.buffered_text, &mut buffered);
self.actions.push(TextAction::WriteText(buffered));
}
}
/// Output an action setting a new font and update the active font.
fn set_font(&mut self, index: usize) {
self.active_font = index;
self.actions.push(TextAction::SetFont(index, self.layouter.ctx.text_style.font_size));
}
/// Check whether additional text with the given width would overflow the current line.
fn would_overflow(&self, width: Size) -> bool {
self.current_width + width > self.max_width
}
}

View File

@ -172,12 +172,11 @@ impl<'s> Iterator for Tokens<'s> {
':' if self.state == TS::Function => Token::Colon,
'=' if self.state == TS::Function => Token::Equals,
// Double star/underscore and dollar in bodies
// Double star/underscore in bodies
'*' if self.state == TS::Body && afterwards == Some('*')
=> self.consumed(Token::DoubleStar),
'_' if self.state == TS::Body && afterwards == Some('_')
=> self.consumed(Token::DoubleUnderscore),
'$' if self.state == TS::Body => Token::Dollar,
// Escaping
'\\' => {
@ -393,7 +392,6 @@ impl<'s> Parser<'s> {
// Modifiers
Token::DoubleUnderscore => self.append_consumed(Node::ToggleItalics),
Token::DoubleStar => self.append_consumed(Node::ToggleBold),
Token::Dollar => self.append_consumed(Node::ToggleMath),
// Normal text
Token::Text(word) => self.append_consumed(Node::Text(word.to_owned())),
@ -678,7 +676,7 @@ mod token_tests {
use super::*;
use Token::{Space as S, Newline as N, LeftBracket as L, RightBracket as R,
Colon as C, Equals as E, DoubleUnderscore as DU, DoubleStar as DS,
Dollar as D, Text as T, LineComment as LC, BlockComment as BC, StarSlash as SS};
Text as T, LineComment as LC, BlockComment as BC, StarSlash as SS};
/// Test if the source code tokenizes to the tokens.
fn test(src: &str, tokens: Vec<Token>) {
@ -692,7 +690,6 @@ mod token_tests {
test("Hallo", vec![T("Hallo")]);
test("[", vec![L]);
test("]", vec![R]);
test("$", vec![D]);
test("**", vec![DS]);
test("__", vec![DU]);
test("\n", vec![N]);

View File

@ -28,8 +28,6 @@ pub enum Token<'s> {
DoubleUnderscore,
/// Two stars, indicating bold text.
DoubleStar,
/// A dollar sign, indicating mathematical content.
Dollar,
/// A line comment.
LineComment(&'s str),
/// A block comment.
@ -67,8 +65,6 @@ pub enum Node {
ToggleItalics,
/// Indicates that boldface was enabled / disabled.
ToggleBold,
/// Indicates that math mode was enabled / disabled.
ToggleMath,
/// Literal text.
Text(String),
/// A function invocation.