From ce0e7f4e095c0152ddf4908c7216cf2fe203f9a0 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sat, 16 May 2020 20:05:52 +0200 Subject: [PATCH] Refactor font layout: introduce y_max for each Line in a Galley --- emigui/src/font.rs | 236 ++++++++++++++++++++++-------------------- emigui/src/mesher.rs | 2 +- emigui/src/widgets.rs | 3 +- 3 files changed, 125 insertions(+), 116 deletions(-) diff --git a/emigui/src/font.rs b/emigui/src/font.rs index 2257ffb3..8999e8ad 100644 --- a/emigui/src/font.rs +++ b/emigui/src/font.rs @@ -8,40 +8,6 @@ use crate::{ texture_atlas::TextureAtlas, }; -/// A typeset piece of text on a single line. -#[derive(Clone, Debug)] -pub struct Line { - /// The start of each character, probably starting at zero. - /// The last element is the end of the last character. - /// x_offsets.len() == text.chars().count() + 1 - /// This is never empty. - /// Unit: points. - pub x_offsets: Vec, - - /// Top y offset of this line. 0.0 for the first line, n * line_spacing for the rest. - /// Unit: points. - pub y_offset: f32, -} - -impl Line { - pub fn sanity_check(&self) { - assert!(!self.x_offsets.is_empty()); - } - - pub fn char_count(&self) -> usize { - assert!(!self.x_offsets.is_empty()); - self.x_offsets.len() - 1 - } - - pub fn min_x(&self) -> f32 { - *self.x_offsets.first().unwrap() - } - - pub fn max_x(&self) -> f32 { - *self.x_offsets.last().unwrap() - } -} - /// A collection of text locked into place. #[derive(Clone, Debug, Default)] pub struct Galley { @@ -52,11 +18,29 @@ pub struct Galley { /// The number of chars in all lines sum up to text.chars().count() pub lines: Vec, - // We need size here to keep track of extra newline at the end. Hacky. Should fix. - // Newlines should probably be part of the start of the line? + // Optimization: calculate once and reuse. pub size: Vec2, } +/// A typeset piece of text on a single line. +#[derive(Clone, Debug)] +pub struct Line { + /// The start of each character, probably starting at zero. + /// The last element is the end of the last character. + /// x_offsets.len() == text.chars().count() + 1 + /// This is never empty. + /// Unit: points. + pub x_offsets: Vec, + + /// Top of the line, offset within the Galley. + /// Unit: points. + pub y_min: f32, + + /// Bottom of the line, offset within the Galley. + /// Unit: points. + pub y_max: f32, +} + impl Galley { pub fn sanity_check(&self) { let mut char_count = 0; @@ -75,18 +59,13 @@ impl Galley { let line_char_count = line.char_count(); if char_count <= char_idx && char_idx < char_count + line_char_count { let line_char_offset = char_idx - char_count; - return vec2(line.x_offsets[line_char_offset], line.y_offset); + return vec2(line.x_offsets[line_char_offset], line.y_min); } char_count += line_char_count; } if let Some(last) = self.lines.last() { - if self.text.ends_with('\n') { - // The position of the next character will be here: - vec2(0.0, 0.5 * (self.size.y + last.y_offset)) // TODO: fix this hack - } else { - vec2(last.max_x(), last.y_offset) - } + vec2(last.max_x(), last.y_min) } else { // Empty galley vec2(0.0, 0.0) @@ -94,6 +73,25 @@ impl Galley { } } +impl Line { + pub fn sanity_check(&self) { + assert!(!self.x_offsets.is_empty()); + } + + pub fn char_count(&self) -> usize { + assert!(!self.x_offsets.is_empty()); + self.x_offsets.len() - 1 + } + + pub fn min_x(&self) -> f32 { + *self.x_offsets.first().unwrap() + } + + pub fn max_x(&self) -> f32 { + *self.x_offsets.last().unwrap() + } +} + // ---------------------------------------------------------------------------- // const REPLACEMENT_CHAR: char = '\u{25A1}'; // □ white square Replaces a missing or unsupported Unicode character. @@ -255,6 +253,82 @@ impl Font { ); } + /// Typeset the given text onto one line. + /// Assumes there are no \n in the text. + /// Always returns exactly one frament. + pub fn layout_single_line(&self, text: &str) -> Galley { + let x_offsets = self.layout_single_line_fragment(text); + let line = Line { + x_offsets, + y_min: 0.0, + y_max: self.height(), + }; + let width = line.max_x(); + let size = vec2(width, self.height()); + let galley = Galley { + text: text.to_owned(), + lines: vec![line], + size, + }; + galley.sanity_check(); + galley + } + + pub fn layout_multiline(&self, text: &str, max_width_in_points: f32) -> Galley { + let line_spacing = self.line_spacing(); + let mut cursor_y = 0.0; + let mut lines = Vec::new(); + + let mut paragraph_start = 0; + + while paragraph_start < text.len() { + let next_newline = text[paragraph_start..].find('\n'); + let paragraph_end = next_newline + .map(|newline| paragraph_start + newline + 1) + .unwrap_or_else(|| text.len()); + + assert!(paragraph_start < paragraph_end); + let paragraph_text = &text[paragraph_start..paragraph_end]; + let mut paragraph_lines = + self.layout_paragraph_max_width(paragraph_text, max_width_in_points); + assert!(!paragraph_lines.is_empty()); + + for line in &mut paragraph_lines { + line.y_min += cursor_y; + line.y_max += cursor_y; + } + cursor_y = paragraph_lines.last().unwrap().y_max; + cursor_y += line_spacing * 0.4; // extra spacing between paragraphs. less hacky + + lines.append(&mut paragraph_lines); + + paragraph_start = paragraph_end; + } + + if text.is_empty() || text.ends_with('\n') { + // Add an empty last line for correct visuals etc: + lines.push(Line { + x_offsets: vec![0.0], + y_min: cursor_y, + y_max: cursor_y + line_spacing, + }); + } + + let mut widest_line = 0.0; + for line in &lines { + widest_line = line.max_x().max(widest_line); + } + let size = vec2(widest_line, lines.last().unwrap().y_max); + + let galley = Galley { + text: text.to_owned(), + lines, + size, + }; + galley.sanity_check(); + galley + } + /// Typeset the given text onto one line. /// Assumes there are no \n in the text. /// Return x_offsets, one longer than the number of characters in the text. @@ -284,26 +358,6 @@ impl Font { x_offsets } - /// Typeset the given text onto one line. - /// Assumes there are no \n in the text. - /// Always returns exactly one frament. - pub fn layout_single_line(&self, text: &str) -> Galley { - let x_offsets = self.layout_single_line_fragment(text); - let line = Line { - x_offsets, - y_offset: 0.0, - }; - let width = line.max_x(); - let size = vec2(width, self.height()); - let galley = Galley { - text: text.to_owned(), - lines: vec![line], - size, - }; - galley.sanity_check(); - galley - } - /// A paragraph is text with no line break character in it. /// The text will be linebreaked by the given `max_width_in_points`. pub fn layout_paragraph_max_width(&self, text: &str, max_width_in_points: f32) -> Vec { @@ -331,7 +385,8 @@ impl Font { .iter() .map(|x| x - line_start_x) .collect(), - y_offset: cursor_y, + y_min: cursor_y, + y_max: cursor_y + self.height(), } } else { Line { @@ -339,7 +394,8 @@ impl Font { .iter() .map(|x| x - line_start_x) .collect(), - y_offset: cursor_y, + y_min: cursor_y, + y_max: cursor_y + self.height(), } }; line.sanity_check(); @@ -365,7 +421,8 @@ impl Font { .iter() .map(|x| x - line_start_x) .collect(), - y_offset: cursor_y, + y_min: cursor_y, + y_max: cursor_y + self.height(), }; line.sanity_check(); out_lines.push(line); @@ -373,51 +430,4 @@ impl Font { out_lines } - - pub fn layout_multiline(&self, text: &str, max_width_in_points: f32) -> Galley { - let line_spacing = self.line_spacing(); - let mut cursor_y = 0.0; - let mut lines = Vec::new(); - - let mut paragraph_start = 0; - - while paragraph_start < text.len() { - let next_newline = text[paragraph_start..].find('\n'); - let paragraph_end = next_newline - .map(|newline| paragraph_start + newline + 1) - .unwrap_or_else(|| text.len()); - - assert!(paragraph_start < paragraph_end); - let paragraph_text = &text[paragraph_start..paragraph_end]; - let mut paragraph_lines = - self.layout_paragraph_max_width(paragraph_text, max_width_in_points); - assert!(!paragraph_lines.is_empty()); - - let paragraph_height = paragraph_lines.last().unwrap().y_offset + line_spacing; - for line in &mut paragraph_lines { - line.y_offset += cursor_y; - } - lines.append(&mut paragraph_lines); - cursor_y += paragraph_height; // TODO: add extra spacing between paragraphs - - paragraph_start = paragraph_end; - } - - if text.ends_with('\n') { - cursor_y += line_spacing; - } - - let mut widest_line = 0.0; - for line in &lines { - widest_line = line.max_x().max(widest_line); - } - - let galley = Galley { - text: text.to_owned(), - lines, - size: vec2(widest_line, cursor_y), - }; - galley.sanity_check(); - galley - } } diff --git a/emigui/src/mesher.rs b/emigui/src/mesher.rs index c33f1667..06c594a1 100644 --- a/emigui/src/mesher.rs +++ b/emigui/src/mesher.rs @@ -577,7 +577,7 @@ pub fn mesh_command( let c = chars.next().unwrap(); if let Some(glyph) = font.uv_rect(c) { let mut top_left = Vertex { - pos: pos + glyph.offset + vec2(*x_offset, line.y_offset), + pos: pos + glyph.offset + vec2(*x_offset, line.y_min), uv: glyph.min, color, }; diff --git a/emigui/src/widgets.rs b/emigui/src/widgets.rs index 0ceb6501..1c8386ba 100644 --- a/emigui/src/widgets.rs +++ b/emigui/src/widgets.rs @@ -143,7 +143,6 @@ impl Widget for Hyperlink { let text_style = TextStyle::Body; let id = ui.make_child_id(&self.url); let font = &ui.fonts()[text_style]; - let line_spacing = font.line_spacing(); let galley = font.layout_multiline(&self.text, ui.available().width()); let interact = ui.reserve_space(galley.size, Some(id)); if interact.hovered { @@ -157,7 +156,7 @@ impl Widget for Hyperlink { // Underline: for line in &galley.lines { let pos = interact.rect.min; - let y = pos.y + line.y_offset + line_spacing; + let y = pos.y + line.y_max; let y = ui.round_to_pixel(y); let min_x = pos.x + line.min_x(); let max_x = pos.x + line.max_x();