diff --git a/src/layout/mod.rs b/src/layout/mod.rs index f2ca30296..1af3156d1 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -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 { - 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. diff --git a/src/layout/text.rs b/src/layout/text.rs new file mode 100644 index 000000000..15da8373c --- /dev/null +++ b/src/layout/text.rs @@ -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, + 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)> { + 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 { + 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, + 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 { + // 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 + } +} diff --git a/src/parsing.rs b/src/parsing.rs index 5ee8b382d..727a06d8b 100644 --- a/src/parsing.rs +++ b/src/parsing.rs @@ -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) { @@ -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]); diff --git a/src/syntax.rs b/src/syntax.rs index cfd417196..c469686b3 100644 --- a/src/syntax.rs +++ b/src/syntax.rs @@ -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.