Simplify text layout further with even less allocations
This commit is contained in:
parent
d3a3e4fa73
commit
89aa285255
9 changed files with 152 additions and 158 deletions
|
@ -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),
|
||||
);
|
||||
|
||||
|
|
|
@ -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,37 +412,34 @@ 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,
|
||||
pos,
|
||||
galley,
|
||||
text_style,
|
||||
x_offsets: fragment.x_offsets,
|
||||
color,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_paint_cmd(&self, layer: Layer, paint_cmd: PaintCmd) {
|
||||
self.graphics()
|
||||
|
|
|
@ -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;
|
||||
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;
|
||||
}
|
||||
fragments.append(&mut paragraph_fragments);
|
||||
lines.append(&mut paragraph_lines);
|
||||
cursor_y += line_height; // TODO: add extra spacing between paragraphs
|
||||
} else {
|
||||
cursor_y += line_spacing;
|
||||
}
|
||||
cursor_y = self.round_to_pixel(cursor_y);
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -564,17 +564,20 @@ 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()) {
|
||||
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, 0.0),
|
||||
pos: pos + glyph.offset + vec2(*x_offset, line.y_offset),
|
||||
uv: glyph.min,
|
||||
color,
|
||||
};
|
||||
|
@ -589,6 +592,8 @@ pub fn mesh_command(
|
|||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(chars.next(), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,33 +457,30 @@ 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,
|
||||
pos,
|
||||
galley,
|
||||
text_style,
|
||||
x_offsets: fragment.x_offsets,
|
||||
color,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Addding Widgets
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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| {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue