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); paint_icon(ui, &state, &interact);
ui.add_text( ui.add_galley(
text_pos, text_pos,
galley,
label.text_style, label.text_style,
galley.fragments,
Some(ui.style().interact(&interact).stroke_color), Some(ui.style().interact(&interact).stroke_color),
); );

View file

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

View file

@ -9,7 +9,8 @@ use crate::{
}; };
/// A typeset piece of text on a single line. /// 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 start of each character, probably starting at zero.
/// The last element is the end of the last character. /// The last element is the end of the last character.
/// x_offsets.len() == text.chars().count() + 1 /// x_offsets.len() == text.chars().count() + 1
@ -17,16 +18,21 @@ pub struct Fragment {
/// Unit: points. /// Unit: points.
pub x_offsets: Vec<f32>, 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. /// Unit: points.
pub y_offset: f32, 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 { pub fn min_x(&self) -> f32 {
*self.x_offsets.first().unwrap() *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. /// A collection of text locked into place.
#[derive(Default)] #[derive(Clone, Debug, Default)]
pub struct Galley { pub struct Galley {
// TODO: maybe rename/refactor this as `lines`? /// The full text
pub fragments: Vec<Fragment>, 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, 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)] #[derive(Clone, Copy, Debug)]
@ -233,28 +248,24 @@ impl Font {
/// Always returns exactly one frament. /// Always returns exactly one frament.
pub fn layout_single_line(&self, text: &str) -> Galley { pub fn layout_single_line(&self, text: &str) -> Galley {
let x_offsets = self.layout_single_line_fragment(text); let x_offsets = self.layout_single_line_fragment(text);
let fragment = Fragment { let line = Line {
x_offsets, x_offsets,
y_offset: 0.0, y_offset: 0.0,
text: text.to_owned(),
}; };
assert_eq!(fragment.x_offsets.len(), fragment.text.chars().count() + 1); let width = line.max_x();
let width = fragment.max_x();
let size = vec2(width, self.height()); let size = vec2(width, self.height());
Galley { let galley = Galley {
fragments: vec![fragment], text: text.to_owned(),
lines: vec![line],
size, 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`.
/// TODO: return Galley ? 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<Fragment> {
let full_x_offsets = self.layout_single_line_fragment(text); let full_x_offsets = self.layout_single_line_fragment(text);
let mut line_start_x = full_x_offsets[0]; 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. // start index of the last space. A candidate for a new line.
let mut last_space = None; 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() { for (i, (x, chr)) in full_x_offsets.iter().skip(1).zip(text.chars()).enumerate() {
let line_width = x - line_start_x; let line_width = x - line_start_x;
@ -273,35 +284,25 @@ impl Font {
if line_width > max_width_in_points { if line_width > max_width_in_points {
if let Some(last_space_idx) = last_space { if let Some(last_space_idx) = last_space {
let include_trailing_space = true; let include_trailing_space = true;
let fragment = if include_trailing_space { let line = if include_trailing_space {
Fragment { Line {
x_offsets: full_x_offsets[line_start_idx..=last_space_idx + 1] x_offsets: full_x_offsets[line_start_idx..=last_space_idx + 1]
.iter() .iter()
.map(|x| x - line_start_x) .map(|x| x - line_start_x)
.collect(), .collect(),
y_offset: cursor_y, y_offset: cursor_y,
text: text
.chars()
.skip(line_start_idx)
.take(last_space_idx + 1 - line_start_idx)
.collect(),
} }
} else { } else {
Fragment { Line {
x_offsets: full_x_offsets[line_start_idx..=last_space_idx] x_offsets: full_x_offsets[line_start_idx..=last_space_idx]
.iter() .iter()
.map(|x| x - line_start_x) .map(|x| x - line_start_x)
.collect(), .collect(),
y_offset: cursor_y, 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); line.sanity_check();
out_fragments.push(fragment); out_lines.push(line);
line_start_idx = last_space_idx + 1; line_start_idx = last_space_idx + 1;
line_start_x = full_x_offsets[line_start_idx]; line_start_x = full_x_offsets[line_start_idx];
@ -318,49 +319,60 @@ impl Font {
} }
if line_start_idx + 1 < full_x_offsets.len() { if line_start_idx + 1 < full_x_offsets.len() {
let fragment = Fragment { let line = Line {
x_offsets: full_x_offsets[line_start_idx..] x_offsets: full_x_offsets[line_start_idx..]
.iter() .iter()
.map(|x| x - line_start_x) .map(|x| x - line_start_x)
.collect(), .collect(),
y_offset: cursor_y, y_offset: cursor_y,
text: text.chars().skip(line_start_idx).collect(),
}; };
assert_eq!(fragment.x_offsets.len(), fragment.text.chars().count() + 1); line.sanity_check();
out_fragments.push(fragment); out_lines.push(line);
} }
out_fragments out_lines
} }
pub fn layout_multiline(&self, text: &str, max_width_in_points: f32) -> Galley { pub fn layout_multiline(&self, text: &str, max_width_in_points: f32) -> Galley {
let line_spacing = self.line_spacing(); let line_spacing = self.line_spacing();
let mut cursor_y = 0.0; let mut cursor_y = 0.0;
let mut fragments = Vec::new(); let mut lines = Vec::new();
for line in text.split('\n') {
let mut paragraph_fragments = let mut paragraph_start = 0;
self.layout_paragraph_max_width(line, max_width_in_points);
if let Some(last_fragment) = paragraph_fragments.last() { while paragraph_start < text.len() {
let line_height = last_fragment.y_offset + line_spacing; let next_newline = text[paragraph_start..].find('\n');
for fragment in &mut paragraph_fragments { let paragraph_end = next_newline
fragment.y_offset += cursor_y; .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;
} }
fragments.append(&mut paragraph_fragments); lines.append(&mut paragraph_lines);
cursor_y += line_height; // TODO: add extra spacing between paragraphs cursor_y += line_height; // TODO: add extra spacing between paragraphs
} else {
cursor_y += line_spacing; paragraph_start = paragraph_end;
}
cursor_y = self.round_to_pixel(cursor_y);
} }
let mut widest_line = 0.0; let mut widest_line = 0.0;
for fragment in &fragments { for line in &lines {
widest_line = fragment.max_x().max(widest_line); widest_line = line.max_x().max(widest_line);
} }
Galley { let galley = Galley {
fragments, text: text.to_owned(),
lines,
size: vec2(widest_line, cursor_y), size: vec2(widest_line, cursor_y),
} };
galley.sanity_check();
galley
} }
} }

View file

@ -564,17 +564,20 @@ pub fn mesh_command(
} }
} }
PaintCmd::Text { PaintCmd::Text {
color,
pos, pos,
text, galley,
text_style, text_style,
x_offsets, color,
} => { } => {
galley.sanity_check();
let font = &fonts[text_style]; let font = &fonts[text_style];
for (c, x_offset) in text.chars().zip(x_offsets.iter()) { 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) { 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, 0.0), pos: pos + glyph.offset + vec2(*x_offset, line.y_offset),
uv: glyph.min, uv: glyph.min,
color, color,
}; };
@ -589,6 +592,8 @@ pub fn mesh_command(
} }
} }
} }
assert_eq!(chars.next(), None);
}
} }
} }

View file

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

View file

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

View file

@ -97,12 +97,7 @@ impl Widget for Label {
}; };
let galley = self.layout(max_width, ui); let galley = self.layout(max_width, ui);
let interact = ui.reserve_space(galley.size, None); let interact = ui.reserve_space(galley.size, None);
ui.add_text( ui.add_galley(interact.rect.min, galley, self.text_style, self.text_color);
interact.rect.min,
self.text_style,
galley.fragments,
self.text_color,
);
ui.response(interact) ui.response(interact)
} }
} }
@ -160,12 +155,12 @@ impl Widget for Hyperlink {
if interact.hovered { if interact.hovered {
// Underline: // Underline:
for fragment in &galley.fragments { for line in &galley.lines {
let pos = interact.rect.min; 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 y = ui.round_to_pixel(y);
let min_x = pos.x + fragment.min_x(); let min_x = pos.x + line.min_x();
let max_x = pos.x + fragment.max_x(); let max_x = pos.x + line.max_x();
ui.add_paint_cmd(PaintCmd::line_segment( ui.add_paint_cmd(PaintCmd::line_segment(
[pos2(min_x, y), pos2(max_x, y)], [pos2(min_x, y), pos2(max_x, y)],
color, 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) ui.response(interact)
} }
@ -243,7 +238,7 @@ impl Widget for Button {
}); });
let stroke_color = ui.style().interact(&interact).stroke_color; let stroke_color = ui.style().interact(&interact).stroke_color;
let text_color = text_color.unwrap_or(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) ui.response(interact)
} }
} }
@ -313,7 +308,7 @@ impl<'a> Widget for Checkbox<'a> {
} }
let text_color = self.text_color.unwrap_or(stroke_color); 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) ui.response(interact)
} }
} }
@ -384,7 +379,7 @@ impl Widget for RadioButton {
} }
let text_color = self.text_color.unwrap_or(stroke_color); 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) 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_multiline(&full_text, ui.available().width());
let galley = font.layout_single_line(&full_text); let galley = font.layout_single_line(&full_text);
let pos = ui.reserve_space(galley.size, None).rect.min; 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) slider_sans_text.ui(ui)
} else { } else {
ui.columns(2, |columns| { ui.columns(2, |columns| {

View file

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