Automatic line break when text overflows max_width

This commit is contained in:
Emil Ernerfeldt 2019-01-10 21:09:37 +01:00
parent 984a56aae9
commit 1e8a4d3906
3 changed files with 128 additions and 51 deletions

View file

@ -1,5 +1,27 @@
use rusttype::{point, Scale}; use rusttype::{point, Scale};
use crate::math::{vec2, Vec2};
pub struct TextFragment {
/// The start of each character, starting at zero.
pub x_offsets: Vec<f32>,
/// 0 for the first line, n * line_spacing for the rest
pub y_offset: f32,
pub text: String,
}
impl TextFragment {
pub fn min_x(&self) -> f32 {
*self.x_offsets.first().unwrap()
}
pub fn max_x(&self) -> f32 {
*self.x_offsets.last().unwrap()
}
}
// ----------------------------------------------------------------------------
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub struct UvRect { pub struct UvRect {
/// X/Y offset for nice rendering /// X/Y offset for nice rendering
@ -190,27 +212,109 @@ impl Font {
} }
} }
/// Returns the start (X) of each character, starting at zero, plus the total width. /// Returns the a single line of characters separated into words
/// i.e. returns text.chars().count() + 1 numbers. pub fn layout_single_line(&self, text: &str) -> Vec<TextFragment> {
pub fn layout_single_line(&self, text: &str) -> Vec<f32> {
let scale = Scale::uniform(self.scale as f32); let scale = Scale::uniform(self.scale as f32);
let mut x_offsets = Vec::new(); let mut current_fragment = TextFragment {
x_offsets: vec![0.0],
y_offset: 0.0,
text: String::new(),
};
let mut all_fragments = vec![];
let mut cursor_x = 0.0f32; let mut cursor_x = 0.0f32;
let mut last_glyph_id = None; let mut last_glyph_id = None;
for c in text.chars() { for c in text.chars() {
cursor_x = cursor_x.round();
x_offsets.push(cursor_x);
if let Some(glyph) = self.glyph_info(c) { if let Some(glyph) = self.glyph_info(c) {
if let Some(last_glyph_id) = last_glyph_id { if let Some(last_glyph_id) = last_glyph_id {
cursor_x += self.font.pair_kerning(scale, last_glyph_id, glyph.id) cursor_x += self.font.pair_kerning(scale, last_glyph_id, glyph.id)
} }
cursor_x += glyph.advance_width; cursor_x += glyph.advance_width;
cursor_x = cursor_x.round();
last_glyph_id = Some(glyph.id); last_glyph_id = Some(glyph.id);
let is_space = glyph.uv.is_none();
if is_space {
// TODO: also break after hyphens etc
if !current_fragment.text.is_empty() {
all_fragments.push(current_fragment);
current_fragment = TextFragment {
x_offsets: vec![cursor_x],
y_offset: 0.0,
text: String::new(),
}
}
} else {
current_fragment.text.push(c);
current_fragment.x_offsets.push(cursor_x);
}
} else {
// Ignore unknown glyph
} }
} }
x_offsets.push(cursor_x);
x_offsets if !current_fragment.text.is_empty() {
all_fragments.push(current_fragment)
}
all_fragments
}
pub fn layout_single_line_max_width(&self, text: &str, max_width: f32) -> Vec<TextFragment> {
let mut words = self.layout_single_line(text);
if words.is_empty() || words.last().unwrap().max_x() <= max_width {
return words; // Early-out
}
let line_spacing = self.line_spacing();
// Break up lines:
let mut line_start_x = 0.0;
let mut cursor_y = 0.0;
for word in words.iter_mut().skip(1) {
if word.max_x() - line_start_x >= max_width {
// Time for a new line:
cursor_y += line_spacing;
line_start_x = word.min_x();
}
word.y_offset += cursor_y;
for x in &mut word.x_offsets {
*x -= line_start_x;
}
}
words
}
/// Returns each line + total bounding box size.
pub fn layout_multiline(&self, text: &str, max_width: f32) -> (Vec<TextFragment>, Vec2) {
let line_spacing = self.line_spacing();
let mut cursor_y = 0.0;
let mut text_fragments = Vec::new();
for line in text.split('\n') {
let mut line_fragments = self.layout_single_line_max_width(&line, max_width);
if let Some(last_word) = line_fragments.last() {
let line_height = last_word.y_offset + line_spacing;
for fragment in &mut line_fragments {
fragment.y_offset += cursor_y;
}
text_fragments.append(&mut line_fragments);
cursor_y += line_height; // TODO: add extra spacing between paragraphs
} else {
cursor_y += line_spacing;
}
cursor_y = cursor_y.round();
}
let mut widest_line = 0.0;
for fragment in &text_fragments {
widest_line = fragment.max_x().max(widest_line);
}
let bounding_size = vec2(widest_line, cursor_y);
(text_fragments, bounding_size)
} }
pub fn debug_print_atlas_ascii_art(&self) { pub fn debug_print_atlas_ascii_art(&self) {

View file

@ -5,7 +5,7 @@ use std::{
}; };
use crate::{ use crate::{
font::Font, font::{Font, TextFragment},
math::*, math::*,
types::*, types::*,
widgets::{label, Widget}, widgets::{label, Widget},
@ -95,18 +95,6 @@ pub struct Memory {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
pub struct TextFragment {
/// The start of each character, starting at zero.
x_offsets: Vec<f32>,
/// 0 for the first line, n * line_spacing for the rest
y_offset: f32,
text: String,
}
pub type TextFragments = Vec<TextFragment>;
// ----------------------------------------------------------------------------
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub enum Direction { pub enum Direction {
Horizontal, Horizontal,
@ -220,7 +208,7 @@ where
dir: Direction::Vertical, dir: Direction::Vertical,
cursor: window_pos + window_padding, cursor: window_pos + window_padding,
bounding_size: vec2(0.0, 0.0), bounding_size: vec2(0.0, 0.0),
available_space: vec2(400.0, std::f32::INFINITY), // TODO available_space: vec2(400.0, std::f32::INFINITY), // TODO: popup/tooltip width
}; };
add_contents(&mut popup_region); add_contents(&mut popup_region);
@ -283,6 +271,14 @@ impl Region {
self.cursor self.cursor
} }
pub fn font(&self) -> &Font {
&*self.data.font
}
pub fn width(&self) -> f32 {
self.available_space.x
}
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// Sub-regions: // Sub-regions:
@ -297,7 +293,7 @@ impl Region {
); );
let text: String = text.into(); let text: String = text.into();
let id = self.make_child_id(&text); let id = self.make_child_id(&text);
let (text, text_size) = self.layout_text(&text); let (text, text_size) = self.font().layout_multiline(&text, self.width());
let text_cursor = self.cursor + self.options().button_padding; let text_cursor = self.cursor + self.options().button_padding;
let (rect, interact) = self.reserve_space( let (rect, interact) = self.reserve_space(
vec2( vec2(
@ -434,7 +430,6 @@ impl Region {
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// TODO: Return a Rect
pub fn reserve_space( pub fn reserve_space(
&mut self, &mut self,
size: Vec2, size: Vec2,
@ -499,28 +494,6 @@ impl Region {
}) })
} }
// TODO: move this function to Font
pub fn layout_text(&self, text: &str) -> (TextFragments, Vec2) {
let line_spacing = self.data.font.line_spacing();
let mut cursor_y = 0.0;
let mut max_width = 0.0;
let mut text_fragments = Vec::new();
for line in text.split('\n') {
let x_offsets = self.data.font.layout_single_line(&line);
let line_width = *x_offsets.last().unwrap();
text_fragments.push(TextFragment {
x_offsets,
y_offset: cursor_y,
text: line.into(),
});
cursor_y += line_spacing;
max_width = line_width.max(max_width);
}
let bounding_size = vec2(max_width, cursor_y);
(text_fragments, bounding_size)
}
pub fn add_text(&mut self, pos: Vec2, text: Vec<TextFragment>) { pub fn add_text(&mut self, pos: Vec2, text: Vec<TextFragment>) {
for fragment in text { for fragment in text {
self.add_graphic(GuiCmd::Text { self.add_graphic(GuiCmd::Text {

View file

@ -29,7 +29,7 @@ pub fn label<S: Into<String>>(text: S) -> Label {
impl Widget for Label { impl Widget for Label {
fn add_to(self, region: &mut Region) -> GuiResponse { fn add_to(self, region: &mut Region) -> GuiResponse {
let (text, text_size) = region.layout_text(&self.text); let (text, text_size) = region.font().layout_multiline(&self.text, region.width());
region.add_text(region.cursor(), text); region.add_text(region.cursor(), text);
let (_, interact) = region.reserve_space(text_size, None); let (_, interact) = region.reserve_space(text_size, None);
region.response(interact) region.response(interact)
@ -51,7 +51,7 @@ impl Button {
impl Widget for Button { impl Widget for Button {
fn add_to(self, region: &mut Region) -> GuiResponse { fn add_to(self, region: &mut Region) -> GuiResponse {
let id = region.make_child_id(&self.text); let id = region.make_child_id(&self.text);
let (text, text_size) = region.layout_text(&self.text); let (text, text_size) = region.font().layout_multiline(&self.text, region.width());
let text_cursor = region.cursor() + region.options().button_padding; let text_cursor = region.cursor() + region.options().button_padding;
let (rect, interact) = let (rect, interact) =
region.reserve_space(text_size + 2.0 * region.options().button_padding, Some(id)); region.reserve_space(text_size + 2.0 * region.options().button_padding, Some(id));
@ -81,7 +81,7 @@ impl<'a> Checkbox<'a> {
impl<'a> Widget for Checkbox<'a> { impl<'a> Widget for Checkbox<'a> {
fn add_to(self, region: &mut Region) -> GuiResponse { fn add_to(self, region: &mut Region) -> GuiResponse {
let id = region.make_child_id(&self.text); let id = region.make_child_id(&self.text);
let (text, text_size) = region.layout_text(&self.text); let (text, text_size) = region.font().layout_multiline(&self.text, region.width());
let text_cursor = region.cursor() let text_cursor = region.cursor()
+ region.options().button_padding + region.options().button_padding
+ vec2(region.options().start_icon_width, 0.0); + vec2(region.options().start_icon_width, 0.0);
@ -129,7 +129,7 @@ pub fn radio<S: Into<String>>(checked: bool, text: S) -> RadioButton {
impl Widget for RadioButton { impl Widget for RadioButton {
fn add_to(self, region: &mut Region) -> GuiResponse { fn add_to(self, region: &mut Region) -> GuiResponse {
let id = region.make_child_id(&self.text); let id = region.make_child_id(&self.text);
let (text, text_size) = region.layout_text(&self.text); let (text, text_size) = region.font().layout_multiline(&self.text, region.width());
let text_cursor = region.cursor() let text_cursor = region.cursor()
+ region.options().button_padding + region.options().button_padding
+ vec2(region.options().start_icon_width, 0.0); + vec2(region.options().start_icon_width, 0.0);
@ -196,7 +196,7 @@ impl<'a> Widget for Slider<'a> {
naked.text = None; naked.text = None;
if text_on_top { if text_on_top {
let (text, text_size) = region.layout_text(&full_text); let (text, text_size) = region.font().layout_multiline(&full_text, region.width());
region.add_text(region.cursor(), text); region.add_text(region.cursor(), text);
region.reserve_space_inner(text_size); region.reserve_space_inner(text_size);
naked.add_to(region) naked.add_to(region)