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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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