Automatic line break when text overflows max_width
This commit is contained in:
parent
984a56aae9
commit
1e8a4d3906
3 changed files with 128 additions and 51 deletions
|
@ -1,5 +1,27 @@
|
|||
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)]
|
||||
pub struct UvRect {
|
||||
/// 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.
|
||||
/// i.e. returns text.chars().count() + 1 numbers.
|
||||
pub fn layout_single_line(&self, text: &str) -> Vec<f32> {
|
||||
/// Returns the a single line of characters separated into words
|
||||
pub fn layout_single_line(&self, text: &str) -> Vec<TextFragment> {
|
||||
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 last_glyph_id = None;
|
||||
|
||||
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(last_glyph_id) = last_glyph_id {
|
||||
cursor_x += self.font.pair_kerning(scale, last_glyph_id, glyph.id)
|
||||
}
|
||||
cursor_x += glyph.advance_width;
|
||||
cursor_x = cursor_x.round();
|
||||
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) {
|
||||
|
|
|
@ -5,7 +5,7 @@ use std::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
font::Font,
|
||||
font::{Font, TextFragment},
|
||||
math::*,
|
||||
types::*,
|
||||
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)]
|
||||
pub enum Direction {
|
||||
Horizontal,
|
||||
|
@ -220,7 +208,7 @@ where
|
|||
dir: Direction::Vertical,
|
||||
cursor: window_pos + window_padding,
|
||||
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);
|
||||
|
@ -283,6 +271,14 @@ impl Region {
|
|||
self.cursor
|
||||
}
|
||||
|
||||
pub fn font(&self) -> &Font {
|
||||
&*self.data.font
|
||||
}
|
||||
|
||||
pub fn width(&self) -> f32 {
|
||||
self.available_space.x
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Sub-regions:
|
||||
|
||||
|
@ -297,7 +293,7 @@ impl Region {
|
|||
);
|
||||
let text: String = text.into();
|
||||
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 (rect, interact) = self.reserve_space(
|
||||
vec2(
|
||||
|
@ -434,7 +430,6 @@ impl Region {
|
|||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
// TODO: Return a Rect
|
||||
pub fn reserve_space(
|
||||
&mut self,
|
||||
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>) {
|
||||
for fragment in text {
|
||||
self.add_graphic(GuiCmd::Text {
|
||||
|
|
|
@ -29,7 +29,7 @@ pub fn label<S: Into<String>>(text: S) -> Label {
|
|||
|
||||
impl Widget for Label {
|
||||
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);
|
||||
let (_, interact) = region.reserve_space(text_size, None);
|
||||
region.response(interact)
|
||||
|
@ -51,7 +51,7 @@ impl Button {
|
|||
impl Widget for Button {
|
||||
fn add_to(self, region: &mut Region) -> GuiResponse {
|
||||
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 (rect, interact) =
|
||||
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> {
|
||||
fn add_to(self, region: &mut Region) -> GuiResponse {
|
||||
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
|
||||
+ 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 {
|
||||
fn add_to(self, region: &mut Region) -> GuiResponse {
|
||||
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
|
||||
+ vec2(region.options().start_icon_width, 0.0);
|
||||
|
@ -196,7 +196,7 @@ impl<'a> Widget for Slider<'a> {
|
|||
naked.text = None;
|
||||
|
||||
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.reserve_space_inner(text_size);
|
||||
naked.add_to(region)
|
||||
|
|
Loading…
Reference in a new issue