Refactor font layout: introduce y_max for each Line in a Galley

This commit is contained in:
Emil Ernerfeldt 2020-05-16 20:05:52 +02:00
parent c0e7f947ff
commit ce0e7f4e09
3 changed files with 125 additions and 116 deletions

View file

@ -8,40 +8,6 @@ use crate::{
texture_atlas::TextureAtlas, 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<f32>,
/// 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. /// A collection of text locked into place.
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct Galley { pub struct Galley {
@ -52,11 +18,29 @@ pub struct Galley {
/// The number of chars in all lines sum up to text.chars().count() /// The number of chars in all lines sum up to text.chars().count()
pub lines: Vec<Line>, pub lines: Vec<Line>,
// We need size here to keep track of extra newline at the end. Hacky. Should fix. // Optimization: calculate once and reuse.
// Newlines should probably be part of the start of the line?
pub size: Vec2, 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<f32>,
/// 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 { impl Galley {
pub fn sanity_check(&self) { pub fn sanity_check(&self) {
let mut char_count = 0; let mut char_count = 0;
@ -75,18 +59,13 @@ impl Galley {
let line_char_count = line.char_count(); let line_char_count = line.char_count();
if char_count <= char_idx && char_idx < 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; 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; char_count += line_char_count;
} }
if let Some(last) = self.lines.last() { if let Some(last) = self.lines.last() {
if self.text.ends_with('\n') { vec2(last.max_x(), last.y_min)
// 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)
}
} else { } else {
// Empty galley // Empty galley
vec2(0.0, 0.0) 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. // 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. /// Typeset the given text onto one line.
/// Assumes there are no \n in the text. /// Assumes there are no \n in the text.
/// Return x_offsets, one longer than the number of characters in the text. /// Return x_offsets, one longer than the number of characters in the text.
@ -284,26 +358,6 @@ impl Font {
x_offsets 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. /// A paragraph is text with no line break character in it.
/// The text will be linebreaked by the given `max_width_in_points`. /// 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<Line> { pub fn layout_paragraph_max_width(&self, text: &str, max_width_in_points: f32) -> Vec<Line> {
@ -331,7 +385,8 @@ impl Font {
.iter() .iter()
.map(|x| x - line_start_x) .map(|x| x - line_start_x)
.collect(), .collect(),
y_offset: cursor_y, y_min: cursor_y,
y_max: cursor_y + self.height(),
} }
} else { } else {
Line { Line {
@ -339,7 +394,8 @@ impl Font {
.iter() .iter()
.map(|x| x - line_start_x) .map(|x| x - line_start_x)
.collect(), .collect(),
y_offset: cursor_y, y_min: cursor_y,
y_max: cursor_y + self.height(),
} }
}; };
line.sanity_check(); line.sanity_check();
@ -365,7 +421,8 @@ impl Font {
.iter() .iter()
.map(|x| x - line_start_x) .map(|x| x - line_start_x)
.collect(), .collect(),
y_offset: cursor_y, y_min: cursor_y,
y_max: cursor_y + self.height(),
}; };
line.sanity_check(); line.sanity_check();
out_lines.push(line); out_lines.push(line);
@ -373,51 +430,4 @@ impl Font {
out_lines 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
}
} }

View file

@ -577,7 +577,7 @@ pub fn mesh_command(
let c = chars.next().unwrap(); let c = chars.next().unwrap();
if let Some(glyph) = font.uv_rect(c) { if let Some(glyph) = font.uv_rect(c) {
let mut top_left = Vertex { 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, uv: glyph.min,
color, color,
}; };

View file

@ -143,7 +143,6 @@ impl Widget for Hyperlink {
let text_style = TextStyle::Body; let text_style = TextStyle::Body;
let id = ui.make_child_id(&self.url); let id = ui.make_child_id(&self.url);
let font = &ui.fonts()[text_style]; let font = &ui.fonts()[text_style];
let line_spacing = font.line_spacing();
let galley = font.layout_multiline(&self.text, ui.available().width()); let galley = font.layout_multiline(&self.text, ui.available().width());
let interact = ui.reserve_space(galley.size, Some(id)); let interact = ui.reserve_space(galley.size, Some(id));
if interact.hovered { if interact.hovered {
@ -157,7 +156,7 @@ impl Widget for Hyperlink {
// Underline: // Underline:
for line in &galley.lines { for line in &galley.lines {
let pos = interact.rect.min; 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 y = ui.round_to_pixel(y);
let min_x = pos.x + line.min_x(); let min_x = pos.x + line.min_x();
let max_x = pos.x + line.max_x(); let max_x = pos.x + line.max_x();