refactor text layout with a new struct Galley

This commit is contained in:
Emil Ernerfeldt 2020-05-16 11:27:02 +02:00
parent 152e644fb2
commit cdfd42eb3e
7 changed files with 109 additions and 77 deletions

View File

@ -61,18 +61,18 @@ impl CollapsingHeader {
let available = ui.available_finite();
let text_pos = available.min + vec2(ui.style().indent, 0.0);
let (text, text_size) = label.layout(available.width() - ui.style().indent, ui);
let text_max_x = text_pos.x + text_size.x;
let galley = label.layout(available.width() - ui.style().indent, ui);
let text_max_x = text_pos.x + galley.size.x;
let desired_width = available.width().max(text_max_x - available.left());
let interact = ui.reserve_space(
vec2(
desired_width,
text_size.y + 2.0 * ui.style().button_padding.y,
galley.size.y + 2.0 * ui.style().button_padding.y,
),
Some(id),
);
let text_pos = pos2(text_pos.x, interact.rect.center().y - text_size.y / 2.0);
let text_pos = pos2(text_pos.x, interact.rect.center().y - galley.size.y / 2.0);
let mut state = {
let mut memory = ui.memory();
@ -94,7 +94,7 @@ impl CollapsingHeader {
ui.add_text(
text_pos,
label.text_style,
text,
galley.fragments,
Some(ui.style().interact(&interact).stroke_color),
);

View File

@ -359,8 +359,8 @@ impl Context {
let layer = Layer::debug();
let text_style = TextStyle::Monospace;
let font = &self.fonts[text_style];
let (text, size) = font.layout_multiline(text, f32::INFINITY);
let rect = align_rect(Rect::from_min_size(pos, size), align);
let galley = font.layout_multiline(text, f32::INFINITY);
let rect = align_rect(Rect::from_min_size(pos, galley.size), align);
self.add_paint_cmd(
layer,
PaintCmd::Rect {
@ -370,7 +370,13 @@ impl Context {
rect: rect.expand(2.0),
},
);
self.add_text(layer, rect.min, text_style, text, Some(color::RED));
self.add_text(
layer,
rect.min,
text_style,
galley.fragments,
Some(color::RED),
);
}
pub fn debug_text(&self, pos: Pos2, text: &str) {
@ -414,10 +420,10 @@ impl Context {
text_color: Option<Color>,
) -> Vec2 {
let font = &self.fonts[text_style];
let (text, size) = font.layout_multiline(text, f32::INFINITY);
let rect = align_rect(Rect::from_min_size(pos, size), align);
self.add_text(layer, rect.min, text_style, text, text_color);
size
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
}
/// Already layed out text.
@ -426,7 +432,7 @@ impl Context {
layer: Layer,
pos: Pos2,
text_style: TextStyle,
text: Vec<font::TextFragment>,
text: Vec<font::Fragment>,
color: Option<Color>,
) {
let color = color.unwrap_or_else(|| self.style().text_color());

View File

@ -8,7 +8,8 @@ use crate::{
texture_atlas::TextureAtlas,
};
pub struct TextFragment {
/// A typeset piece of text on a single line. Could be a whole line, or just a word.
pub struct Fragment {
/// The start of each character, starting at zero.
/// Unit: points.
pub x_offsets: Vec<f32>,
@ -16,10 +17,12 @@ pub struct TextFragment {
/// 0 for the first line, n * line_spacing for the rest
/// Unit: points.
pub y_offset: f32,
/// The actual characters
pub text: String,
}
impl TextFragment {
impl Fragment {
pub fn min_x(&self) -> f32 {
*self.x_offsets.first().unwrap()
}
@ -29,7 +32,7 @@ impl TextFragment {
}
}
// pub fn fn_text_width(fragmens: &[TextFragment]) -> f32 {
// pub fn fn_text_width(fragmens: &[Fragment]) -> f32 {
// if fragmens.is_empty() {
// 0.0
// } else {
@ -37,6 +40,12 @@ impl TextFragment {
// }
// }
/// A collection of text locked into place.
pub struct Galley {
pub fragments: Vec<Fragment>,
pub size: Vec2,
}
// ----------------------------------------------------------------------------
#[derive(Clone, Copy, Debug)]
@ -184,17 +193,16 @@ impl Font {
}
/// Returns the a single line of characters separated into words
/// Always returns at least one frament. TODO: Vec1
/// Returns total size.
pub fn layout_single_line(&self, text: &str) -> (Vec<TextFragment>, Vec2) {
/// Always returns at least one frament.
fn layout_words(&self, text: &str) -> Galley {
let scale_in_pixels = Scale::uniform(self.scale_in_pixels);
let mut current_fragment = TextFragment {
let mut current_fragment = Fragment {
x_offsets: vec![0.0],
y_offset: 0.0,
text: String::new(),
};
let mut all_fragments = vec![];
let mut fragments = vec![];
let mut cursor_x_in_points = 0.0f32;
let mut last_glyph_id = None;
@ -214,13 +222,14 @@ impl Font {
if is_space {
// TODO: also break after hyphens etc
if !current_fragment.text.is_empty() {
all_fragments.push(current_fragment);
current_fragment = TextFragment {
fragments.push(current_fragment);
current_fragment = Fragment {
x_offsets: vec![cursor_x_in_points],
y_offset: 0.0,
text: String::new(),
}
}
// TODO: add a fragment for the space aswell
} else {
current_fragment.text.push(c);
current_fragment.x_offsets.push(cursor_x_in_points);
@ -231,29 +240,39 @@ impl Font {
}
if !current_fragment.text.is_empty() {
all_fragments.push(current_fragment)
fragments.push(current_fragment)
}
let width = if all_fragments.is_empty() {
let width = if fragments.is_empty() {
0.0
} else {
all_fragments.last().unwrap().max_x()
fragments.last().unwrap().max_x()
};
let size = vec2(width, self.height());
(all_fragments, size)
Galley { fragments, size }
}
/// Typeset the given text onto one line.
/// Always returns at least one frament.
pub fn layout_single_line(&self, text: &str) -> Galley {
// TODO: return a single Fragment instead of calling layout_words
// saves a lot of allocations
self.layout_words(text)
}
/// 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<TextFragment> {
let (mut words, size) = self.layout_single_line(text);
if words.is_empty() || size.x <= max_width_in_points {
return words; // Early-out
) -> Vec<Fragment> {
let mut galley = self.layout_words(text);
if galley.fragments.is_empty() || galley.size.x <= max_width_in_points {
return galley.fragments; // Early-out
}
let line_spacing = self.line_spacing();
@ -262,7 +281,7 @@ impl Font {
let mut line_start_x = 0.0;
let mut cursor_y = 0.0;
for word in words.iter_mut().skip(1) {
for word in galley.fragments.iter_mut().skip(1) {
if word.max_x() - line_start_x >= max_width_in_points {
// Time for a new line:
cursor_y += line_spacing;
@ -275,18 +294,13 @@ impl Font {
}
}
words
galley.fragments
}
/// Returns each line + total bounding box size.
pub fn layout_multiline(
&self,
text: &str,
max_width_in_points: f32,
) -> (Vec<TextFragment>, Vec2) {
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 text_fragments = Vec::new();
let mut fragments = Vec::new();
for line in text.split('\n') {
let mut line_fragments = self.layout_paragraph_max_width(line, max_width_in_points);
if let Some(last_word) = line_fragments.last() {
@ -294,7 +308,7 @@ impl Font {
for fragment in &mut line_fragments {
fragment.y_offset += cursor_y;
}
text_fragments.append(&mut line_fragments);
fragments.append(&mut line_fragments);
cursor_y += line_height; // TODO: add extra spacing between paragraphs
} else {
cursor_y += line_spacing;
@ -303,11 +317,13 @@ impl Font {
}
let mut widest_line = 0.0;
for fragment in &text_fragments {
for fragment in &fragments {
widest_line = fragment.max_x().max(widest_line);
}
let bounding_size = vec2(widest_line, cursor_y);
(text_fragments, bounding_size)
Galley {
fragments,
size: vec2(widest_line, cursor_y),
}
}
}

View File

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

View File

@ -63,7 +63,7 @@ impl Label {
self
}
pub fn layout(&self, max_width: f32, ui: &Ui) -> (Vec<font::TextFragment>, Vec2) {
pub fn layout(&self, max_width: f32, ui: &Ui) -> font::Galley {
let font = &ui.fonts()[self.text_style];
if self.multiline {
font.layout_multiline(&self.text, max_width)
@ -95,9 +95,14 @@ impl Widget for Label {
} else {
ui.available().width()
};
let (text, text_size) = self.layout(max_width, ui);
let interact = ui.reserve_space(text_size, None);
ui.add_text(interact.rect.min, self.text_style, text, self.text_color);
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.response(interact)
}
}
@ -145,8 +150,8 @@ impl Widget for Hyperlink {
let font = &ui.fonts()[text_style];
let line_spacing = font.line_spacing();
// TODO: underline
let (text, text_size) = font.layout_multiline(&self.text, ui.available().width());
let interact = ui.reserve_space(text_size, Some(id));
let galley = font.layout_multiline(&self.text, ui.available().width());
let interact = ui.reserve_space(galley.size, Some(id));
if interact.hovered {
ui.ctx().output().cursor_icon = CursorIcon::PointingHand;
}
@ -157,7 +162,7 @@ impl Widget for Hyperlink {
if interact.hovered {
// Underline:
// TODO: underline spaces between words too.
for fragment in &text {
for fragment in &galley.fragments {
let pos = interact.rect.min;
let y = pos.y + fragment.y_offset + line_spacing;
let y = ui.round_to_pixel(y);
@ -171,7 +176,7 @@ impl Widget for Hyperlink {
}
}
ui.add_text(interact.rect.min, text_style, text, Some(color));
ui.add_text(interact.rect.min, text_style, galley.fragments, Some(color));
ui.response(interact)
}
@ -224,12 +229,12 @@ impl Widget for Button {
let id = ui.make_position_id();
let font = &ui.fonts()[text_style];
let (text, text_size) = font.layout_multiline(&text, ui.available().width());
let galley = font.layout_multiline(&text, ui.available().width());
let padding = ui.style().button_padding;
let mut size = text_size + 2.0 * padding;
let mut size = galley.size + 2.0 * padding;
size.y = size.y.max(ui.style().clickable_diameter);
let interact = ui.reserve_space(size, Some(id));
let mut text_cursor = interact.rect.left_center() + vec2(padding.x, -0.5 * text_size.y);
let mut text_cursor = interact.rect.left_center() + vec2(padding.x, -0.5 * galley.size.y);
text_cursor.y += 2.0; // TODO: why is this needed?
let fill_color = fill_color.or(ui.style().interact(&interact).fill_color);
ui.add_paint_cmd(PaintCmd::Rect {
@ -240,7 +245,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, text, Some(text_color));
ui.add_text(text_cursor, text_style, galley.fragments, Some(text_color));
ui.response(interact)
}
}
@ -274,11 +279,11 @@ impl<'a> Widget for Checkbox<'a> {
let id = ui.make_position_id();
let text_style = TextStyle::Button;
let font = &ui.fonts()[text_style];
let (text, text_size) = font.layout_single_line(&self.text);
let galley = font.layout_single_line(&self.text);
let interact = ui.reserve_space(
ui.style().button_padding
+ vec2(ui.style().start_icon_width, 0.0)
+ text_size
+ galley.size
+ ui.style().button_padding,
Some(id),
);
@ -310,7 +315,7 @@ impl<'a> Widget for Checkbox<'a> {
}
let text_color = self.text_color.unwrap_or(stroke_color);
ui.add_text(text_cursor, text_style, text, Some(text_color));
ui.add_text(text_cursor, text_style, galley.fragments, Some(text_color));
ui.response(interact)
}
}
@ -348,11 +353,11 @@ impl Widget for RadioButton {
let id = ui.make_position_id();
let text_style = TextStyle::Button;
let font = &ui.fonts()[text_style];
let (text, text_size) = font.layout_multiline(&self.text, ui.available().width());
let galley = font.layout_multiline(&self.text, ui.available().width());
let interact = ui.reserve_space(
ui.style().button_padding
+ vec2(ui.style().start_icon_width, 0.0)
+ text_size
+ galley.size
+ ui.style().button_padding,
Some(id),
);
@ -381,7 +386,7 @@ impl Widget for RadioButton {
}
let text_color = self.text_color.unwrap_or(stroke_color);
ui.add_text(text_cursor, text_style, text, Some(text_color));
ui.add_text(text_cursor, text_style, galley.fragments, Some(text_color));
ui.response(interact)
}
}

View File

@ -116,10 +116,10 @@ impl<'a> Widget for Slider<'a> {
let slider_sans_text = Slider { text: None, ..self };
if text_on_top {
// let (text, text_size) = font.layout_multiline(&full_text, ui.available().width());
let (text, text_size) = font.layout_single_line(&full_text);
let pos = ui.reserve_space(text_size, None).rect.min;
ui.add_text(pos, text_style, text, text_color);
// 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);
slider_sans_text.ui(ui)
} else {
ui.columns(2, |columns| {

View File

@ -40,8 +40,8 @@ impl<'t> Widget for TextEdit<'t> {
let font = &ui.fonts()[self.text_style];
let line_spacing = font.line_spacing();
let (text, text_size) = font.layout_multiline(self.text.as_str(), ui.available().width());
let desired_size = text_size.max(vec2(ui.available().width(), line_spacing));
let galley = font.layout_multiline(self.text.as_str(), ui.available().width());
let desired_size = galley.size.max(vec2(ui.available().width(), line_spacing));
let interact = ui.reserve_space(desired_size, Some(id));
if interact.clicked {
@ -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) = text.last() {
let cursor_pos = if let Some(last) = galley.fragments.last() {
interact.rect.min + vec2(last.max_x(), last.y_offset)
} else {
interact.rect.min
@ -103,7 +103,12 @@ impl<'t> Widget for TextEdit<'t> {
}
}
ui.add_text(interact.rect.min, self.text_style, text, self.text_color);
ui.add_text(
interact.rect.min,
self.text_style,
galley.fragments,
self.text_color,
);
ui.response(interact)
}