refactor text layout with a new struct Galley
This commit is contained in:
parent
152e644fb2
commit
cdfd42eb3e
|
|
@ -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),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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| {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue