Simplify text layout further with even less allocations

This commit is contained in:
Emil Ernerfeldt 2020-05-16 18:17:35 +02:00
parent d3a3e4fa73
commit 89aa285255
9 changed files with 152 additions and 158 deletions

View file

@ -91,10 +91,10 @@ impl CollapsingHeader {
paint_icon(ui, &state, &interact);
ui.add_text(
ui.add_galley(
text_pos,
galley,
label.text_style,
galley.fragments,
Some(ui.style().interact(&interact).stroke_color),
);

View file

@ -370,13 +370,7 @@ impl Context {
rect: rect.expand(2.0),
},
);
self.add_text(
layer,
rect.min,
text_style,
galley.fragments,
Some(color::RED),
);
self.add_galley(layer, rect.min, galley, text_style, Some(color::RED));
}
pub fn debug_text(&self, pos: Pos2, text: &str) {
@ -418,36 +412,33 @@ impl Context {
text_style: TextStyle,
align: (Align, Align),
text_color: Option<Color>,
) -> Vec2 {
) -> Rect {
let font = &self.fonts[text_style];
let galley = font.layout_multiline(text, f32::INFINITY);
let rect = align_rect(Rect::from_min_size(pos, galley.size), align);
self.add_text(layer, rect.min, text_style, galley.fragments, text_color);
galley.size
self.add_galley(layer, rect.min, galley, text_style, text_color);
rect
}
/// Already layed out text.
pub fn add_text(
pub fn add_galley(
&self,
layer: Layer,
pos: Pos2,
galley: font::Galley,
text_style: TextStyle,
text: Vec<font::Fragment>,
color: Option<Color>,
) {
let color = color.unwrap_or_else(|| self.style().text_color());
for fragment in text {
self.add_paint_cmd(
layer,
PaintCmd::Text {
color,
pos: pos + vec2(0.0, fragment.y_offset),
text: fragment.text,
text_style,
x_offsets: fragment.x_offsets,
},
);
}
self.add_paint_cmd(
layer,
PaintCmd::Text {
pos,
galley,
text_style,
color,
},
);
}
pub fn add_paint_cmd(&self, layer: Layer, paint_cmd: PaintCmd) {

View file

@ -9,7 +9,8 @@ use crate::{
};
/// A typeset piece of text on a single line.
pub struct Fragment {
#[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
@ -17,16 +18,21 @@ pub struct Fragment {
/// Unit: points.
pub x_offsets: Vec<f32>,
/// 0 for the first line, n * line_spacing for the rest
/// Top y offset of this line. 0.0 for the first line, n * line_spacing for the rest.
/// Unit: points.
pub y_offset: f32,
// TODO: make this a str reference into a String owned by Galley
/// The actual characters.
pub text: String,
}
impl Fragment {
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()
}
@ -36,22 +42,31 @@ impl Fragment {
}
}
// pub fn fn_text_width(fragmens: &[Fragment]) -> f32 {
// if fragmens.is_empty() {
// 0.0
// } else {
// fragmens.last().unwrap().max_x() - fragmens.first().unwrap().min_x()
// }
// }
/// A collection of text locked into place.
#[derive(Default)]
#[derive(Clone, Debug, Default)]
pub struct Galley {
// TODO: maybe rename/refactor this as `lines`?
pub fragments: Vec<Fragment>,
/// The full text
pub text: String,
/// Lines of text, from top to bottom.
/// The number of chars in all lines sum up to text.chars().count()
pub lines: Vec<Line>,
// TODO: remove? Can just calculate on the fly
pub size: Vec2,
}
impl Galley {
pub fn sanity_check(&self) {
let mut char_count = 0;
for line in &self.lines {
line.sanity_check();
char_count += line.char_count();
}
assert_eq!(char_count, self.text.chars().count());
}
}
// ----------------------------------------------------------------------------
#[derive(Clone, Copy, Debug)]
@ -233,28 +248,24 @@ impl Font {
/// Always returns exactly one frament.
pub fn layout_single_line(&self, text: &str) -> Galley {
let x_offsets = self.layout_single_line_fragment(text);
let fragment = Fragment {
let line = Line {
x_offsets,
y_offset: 0.0,
text: text.to_owned(),
};
assert_eq!(fragment.x_offsets.len(), fragment.text.chars().count() + 1);
let width = fragment.max_x();
let width = line.max_x();
let size = vec2(width, self.height());
Galley {
fragments: vec![fragment],
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`.
/// TODO: return Galley ?
pub fn layout_paragraph_max_width(
&self,
text: &str,
max_width_in_points: f32,
) -> Vec<Fragment> {
pub fn layout_paragraph_max_width(&self, text: &str, max_width_in_points: f32) -> Vec<Line> {
let full_x_offsets = self.layout_single_line_fragment(text);
let mut line_start_x = full_x_offsets[0];
@ -265,7 +276,7 @@ impl Font {
// start index of the last space. A candidate for a new line.
let mut last_space = None;
let mut out_fragments = vec![];
let mut out_lines = vec![];
for (i, (x, chr)) in full_x_offsets.iter().skip(1).zip(text.chars()).enumerate() {
let line_width = x - line_start_x;
@ -273,35 +284,25 @@ impl Font {
if line_width > max_width_in_points {
if let Some(last_space_idx) = last_space {
let include_trailing_space = true;
let fragment = if include_trailing_space {
Fragment {
let line = if include_trailing_space {
Line {
x_offsets: full_x_offsets[line_start_idx..=last_space_idx + 1]
.iter()
.map(|x| x - line_start_x)
.collect(),
y_offset: cursor_y,
text: text
.chars()
.skip(line_start_idx)
.take(last_space_idx + 1 - line_start_idx)
.collect(),
}
} else {
Fragment {
Line {
x_offsets: full_x_offsets[line_start_idx..=last_space_idx]
.iter()
.map(|x| x - line_start_x)
.collect(),
y_offset: cursor_y,
text: text
.chars()
.skip(line_start_idx)
.take(last_space_idx - line_start_idx)
.collect(),
}
};
assert_eq!(fragment.x_offsets.len(), fragment.text.chars().count() + 1);
out_fragments.push(fragment);
line.sanity_check();
out_lines.push(line);
line_start_idx = last_space_idx + 1;
line_start_x = full_x_offsets[line_start_idx];
@ -318,49 +319,60 @@ impl Font {
}
if line_start_idx + 1 < full_x_offsets.len() {
let fragment = Fragment {
let line = Line {
x_offsets: full_x_offsets[line_start_idx..]
.iter()
.map(|x| x - line_start_x)
.collect(),
y_offset: cursor_y,
text: text.chars().skip(line_start_idx).collect(),
};
assert_eq!(fragment.x_offsets.len(), fragment.text.chars().count() + 1);
out_fragments.push(fragment);
line.sanity_check();
out_lines.push(line);
}
out_fragments
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 fragments = Vec::new();
for line in text.split('\n') {
let mut paragraph_fragments =
self.layout_paragraph_max_width(line, max_width_in_points);
if let Some(last_fragment) = paragraph_fragments.last() {
let line_height = last_fragment.y_offset + line_spacing;
for fragment in &mut paragraph_fragments {
fragment.y_offset += cursor_y;
}
fragments.append(&mut paragraph_fragments);
cursor_y += line_height; // TODO: add extra spacing between paragraphs
} else {
cursor_y += line_spacing;
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 line_height = paragraph_lines.last().unwrap().y_offset + line_spacing;
for line in &mut paragraph_lines {
line.y_offset += cursor_y;
}
cursor_y = self.round_to_pixel(cursor_y);
lines.append(&mut paragraph_lines);
cursor_y += line_height; // TODO: add extra spacing between paragraphs
paragraph_start = paragraph_end;
}
let mut widest_line = 0.0;
for fragment in &fragments {
widest_line = fragment.max_x().max(widest_line);
for line in &lines {
widest_line = line.max_x().max(widest_line);
}
Galley {
fragments,
let galley = Galley {
text: text.to_owned(),
lines,
size: vec2(widest_line, cursor_y),
}
};
galley.sanity_check();
galley
}
}

View file

@ -564,30 +564,35 @@ pub fn mesh_command(
}
}
PaintCmd::Text {
color,
pos,
text,
galley,
text_style,
x_offsets,
color,
} => {
galley.sanity_check();
let font = &fonts[text_style];
for (c, x_offset) in text.chars().zip(x_offsets.iter()) {
if let Some(glyph) = font.uv_rect(c) {
let mut top_left = Vertex {
pos: pos + glyph.offset + vec2(*x_offset, 0.0),
uv: glyph.min,
color,
};
top_left.pos.x = font.round_to_pixel(top_left.pos.x); // Pixel-perfection.
top_left.pos.y = font.round_to_pixel(top_left.pos.y); // Pixel-perfection.
let bottom_right = Vertex {
pos: top_left.pos + glyph.size,
uv: glyph.max,
color,
};
out_mesh.add_rect(top_left, bottom_right);
let mut chars = galley.text.chars();
for line in &galley.lines {
for x_offset in line.x_offsets.iter().take(line.x_offsets.len() - 1) {
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),
uv: glyph.min,
color,
};
top_left.pos.x = font.round_to_pixel(top_left.pos.x); // Pixel-perfection.
top_left.pos.y = font.round_to_pixel(top_left.pos.y); // Pixel-perfection.
let bottom_right = Vertex {
pos: top_left.pos + glyph.size,
uv: glyph.max,
color,
};
out_mesh.add_rect(top_left, bottom_right);
}
}
}
assert_eq!(chars.next(), None);
}
}
}

View file

@ -4,6 +4,7 @@ use serde_derive::{Deserialize, Serialize};
use crate::{
color::Color,
font::Galley,
fonts::TextStyle,
math::{Pos2, Rect},
mesher::{Mesh, Path},
@ -153,14 +154,12 @@ pub enum PaintCmd {
},
/// Paint a single line of text
Text {
color: Color,
/// Top left corner of the first character.
pos: Pos2,
text: String,
text_style: TextStyle, // TODO: Font
/// Start each character in the text, as offset from pos.
x_offsets: Vec<f32>,
// TODO: font info
/// The layed out text
galley: Galley,
text_style: TextStyle, // TODO: Font?
color: Color,
},
/// Low-level triangle mesh
Mesh(Mesh),

View file

@ -1,6 +1,6 @@
use std::{hash::Hash, sync::Arc};
use crate::{color::*, containers::*, font::Fragment, layout::*, widgets::*, *};
use crate::{color::*, containers::*, layout::*, widgets::*, *};
/// Represents a region of the screen
/// with a type of layout (horizontal or vertical).
@ -457,32 +457,29 @@ impl Ui {
text_style: TextStyle,
align: (Align, Align),
text_color: Option<Color>,
) -> Vec2 {
) -> Rect {
let font = &self.fonts()[text_style];
let galley = font.layout_multiline(text, f32::INFINITY);
let rect = align_rect(Rect::from_min_size(pos, galley.size), align);
self.add_text(rect.min, text_style, galley.fragments, text_color);
galley.size
self.add_galley(rect.min, galley, text_style, text_color);
rect
}
/// Already layed out text.
pub fn add_text(
pub fn add_galley(
&mut self,
pos: Pos2,
galley: font::Galley,
text_style: TextStyle,
fragments: Vec<Fragment>,
color: Option<Color>,
) {
let color = color.unwrap_or_else(|| self.style().text_color());
for fragment in fragments {
self.add_paint_cmd(PaintCmd::Text {
color,
pos: pos + vec2(0.0, fragment.y_offset),
text: fragment.text,
text_style,
x_offsets: fragment.x_offsets,
});
}
self.add_paint_cmd(PaintCmd::Text {
pos,
galley,
text_style,
color,
});
}
// ------------------------------------------------------------------------

View file

@ -97,12 +97,7 @@ impl Widget for Label {
};
let galley = self.layout(max_width, ui);
let interact = ui.reserve_space(galley.size, None);
ui.add_text(
interact.rect.min,
self.text_style,
galley.fragments,
self.text_color,
);
ui.add_galley(interact.rect.min, galley, self.text_style, self.text_color);
ui.response(interact)
}
}
@ -160,12 +155,12 @@ impl Widget for Hyperlink {
if interact.hovered {
// Underline:
for fragment in &galley.fragments {
for line in &galley.lines {
let pos = interact.rect.min;
let y = pos.y + fragment.y_offset + line_spacing;
let y = pos.y + line.y_offset + line_spacing;
let y = ui.round_to_pixel(y);
let min_x = pos.x + fragment.min_x();
let max_x = pos.x + fragment.max_x();
let min_x = pos.x + line.min_x();
let max_x = pos.x + line.max_x();
ui.add_paint_cmd(PaintCmd::line_segment(
[pos2(min_x, y), pos2(max_x, y)],
color,
@ -174,7 +169,7 @@ impl Widget for Hyperlink {
}
}
ui.add_text(interact.rect.min, text_style, galley.fragments, Some(color));
ui.add_galley(interact.rect.min, galley, text_style, Some(color));
ui.response(interact)
}
@ -243,7 +238,7 @@ impl Widget for Button {
});
let stroke_color = ui.style().interact(&interact).stroke_color;
let text_color = text_color.unwrap_or(stroke_color);
ui.add_text(text_cursor, text_style, galley.fragments, Some(text_color));
ui.add_galley(text_cursor, galley, text_style, Some(text_color));
ui.response(interact)
}
}
@ -313,7 +308,7 @@ impl<'a> Widget for Checkbox<'a> {
}
let text_color = self.text_color.unwrap_or(stroke_color);
ui.add_text(text_cursor, text_style, galley.fragments, Some(text_color));
ui.add_galley(text_cursor, galley, text_style, Some(text_color));
ui.response(interact)
}
}
@ -384,7 +379,7 @@ impl Widget for RadioButton {
}
let text_color = self.text_color.unwrap_or(stroke_color);
ui.add_text(text_cursor, text_style, galley.fragments, Some(text_color));
ui.add_galley(text_cursor, galley, text_style, Some(text_color));
ui.response(interact)
}
}

View file

@ -119,7 +119,7 @@ impl<'a> Widget for Slider<'a> {
// let galley = font.layout_multiline(&full_text, ui.available().width());
let galley = font.layout_single_line(&full_text);
let pos = ui.reserve_space(galley.size, None).rect.min;
ui.add_text(pos, text_style, galley.fragments, text_color);
ui.add_galley(pos, galley, text_style, text_color);
slider_sans_text.ui(ui)
} else {
ui.columns(2, |columns| {

View file

@ -90,7 +90,7 @@ impl<'t> Widget for TextEdit<'t> {
let show_cursor =
(ui.input().time * cursor_blink_hz as f64 * 3.0).floor() as i64 % 3 != 0;
if show_cursor {
let cursor_pos = if let Some(last) = galley.fragments.last() {
let cursor_pos = if let Some(last) = galley.lines.last() {
interact.rect.min + vec2(last.max_x(), last.y_offset)
} else {
interact.rect.min
@ -103,12 +103,7 @@ impl<'t> Widget for TextEdit<'t> {
}
}
ui.add_text(
interact.rect.min,
self.text_style,
galley.fragments,
self.text_color,
);
ui.add_galley(interact.rect.min, galley, self.text_style, self.text_color);
ui.response(interact)
}