summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2019-05-26 22:03:55 +0200
committerLaurenz <laurmaedje@gmail.com>2019-05-26 22:03:55 +0200
commit0274e9381064fa5942aa15b24a0a092832433859 (patch)
tree6e3db1ab1bf0a4105b0becb9ca0db64c8bd8eddf /src
parentc38e17d91f81632422171b8103ce90baacfdbe22 (diff)
Devise text layouter 📑
Diffstat (limited to 'src')
-rw-r--r--src/layout/mod.rs34
-rw-r--r--src/layout/text.rs248
-rw-r--r--src/parsing.rs7
-rw-r--r--src/syntax.rs4
4 files changed, 278 insertions, 15 deletions
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index f2ca3029..1af3156d 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<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.
diff --git a/src/layout/text.rs b/src/layout/text.rs
new file mode 100644
index 00000000..15da8373
--- /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<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
+ }
+}
diff --git a/src/parsing.rs b/src/parsing.rs
index 5ee8b382..727a06d8 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<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]);
diff --git a/src/syntax.rs b/src/syntax.rs
index cfd41719..c469686b 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.