Refactor font layout: introduce y_max for each Line in a Galley
This commit is contained in:
parent
c0e7f947ff
commit
ce0e7f4e09
3 changed files with 125 additions and 116 deletions
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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();
|
||||||
|
|
Loading…
Reference in a new issue