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 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) {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue