Wrap text at dashes, punctuations or anywhere if necessary

Closes https://github.com/emilk/egui/issues/55

Supersedes https://github.com/emilk/egui/pull/104
This commit is contained in:
Emil Ernerfeldt 2021-01-31 15:53:20 +01:00
parent 17fdd3bb10
commit b647592a5a
3 changed files with 55 additions and 26 deletions

View file

@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
* Add `Label` methods for code, strong, strikethrough, underline and italics.
* `egui::popup::popup_below_widget`: show a popup area below another widget.
* Add `Slider::clamp_to_range(bool)`: if set, clamp the incoming and outgoing values to the slider range.
* Text will now wrap at newlines, spaces, dashes, punctuation or in the middle of a words if necessary, in that order of priority.
### Changed 🔧

View file

@ -142,13 +142,6 @@ impl FontImpl {
type FontIndex = usize;
#[inline]
fn is_chinese(c: char) -> bool {
(c >= '\u{4E00}' && c <= '\u{9FFF}')
|| (c >= '\u{3400}' && c <= '\u{4DBF}')
|| (c >= '\u{2B740}' && c <= '\u{2B81F}')
}
// TODO: rename?
/// Wrapper over multiple `FontImpl` (e.g. a primary + fallbacks for emojis)
#[derive(Default)]
@ -406,8 +399,8 @@ impl Font {
let mut cursor_y = 0.0;
let mut row_start_idx = 0;
// start index of the last space or hieroglyphs. A candidate for a new row.
let mut newline_mark = None;
// Keeps track of good places to insert row break if we exceed `max_width_in_points`.
let mut row_break_candidates = RowBreakCandidates::default();
let mut out_rows = vec![];
@ -416,10 +409,9 @@ impl Font {
let potential_row_width = first_row_indentation + x - row_start_x;
if potential_row_width > max_width_in_points {
if let Some(last_space_idx) = newline_mark {
// We include the trailing space in the row:
if let Some(last_kept_index) = row_break_candidates.get() {
let row = Row {
x_offsets: full_x_offsets[row_start_idx..=last_space_idx + 1]
x_offsets: full_x_offsets[row_start_idx..=last_kept_index + 1]
.iter()
.map(|x| first_row_indentation + x - row_start_x)
.collect(),
@ -430,9 +422,9 @@ impl Font {
row.sanity_check();
out_rows.push(row);
row_start_idx = last_space_idx + 1;
row_start_idx = last_kept_index + 1;
row_start_x = first_row_indentation + full_x_offsets[row_start_idx];
newline_mark = None;
row_break_candidates = Default::default();
cursor_y = self.round_to_pixel(cursor_y + self.row_height());
} else if out_rows.is_empty() && first_row_indentation > 0.0 {
assert_eq!(row_start_idx, 0);
@ -450,10 +442,7 @@ impl Font {
}
}
const NON_BREAKING_SPACE: char = '\u{A0}';
if (chr.is_whitespace() && chr != NON_BREAKING_SPACE) || is_chinese(chr) {
newline_mark = Some(i);
}
row_break_candidates.add(i, chr);
}
if row_start_idx + 1 < full_x_offsets.len() {
@ -474,6 +463,43 @@ impl Font {
}
}
/// Keeps track of good places to break a long row of text.
/// Will focus primarily on spaces, secondarily on things like `-`
#[derive(Clone, Copy, Default)]
struct RowBreakCandidates {
/// Breaking at ` ` or other whitespace
/// is always the primary candidate.
space: Option<usize>,
/// Breaking at a dash is super-
/// good idea.
dash: Option<usize>,
/// This is nicer for things like URLs, e.g. www.
/// example.com.
punctuation: Option<usize>,
/// Breaking after just random character is some
/// times necessary.
any: Option<usize>,
}
impl RowBreakCandidates {
fn add(&mut self, index: usize, chr: char) {
const NON_BREAKING_SPACE: char = '\u{A0}';
if chr.is_whitespace() && chr != NON_BREAKING_SPACE {
self.space = Some(index);
}
if chr == '-' {
self.dash = Some(index);
} else if chr.is_ascii_punctuation() {
self.punctuation = Some(index);
}
self.any = Some(index);
}
fn get(&self) -> Option<usize> {
self.space.or(self.dash).or(self.punctuation).or(self.any)
}
}
fn allocate_glyph(
atlas: &mut TextureAtlas,
glyph: rusttype::Glyph<'static>,

View file

@ -594,20 +594,20 @@ fn test_text_layout() {
assert_eq!(galley.rows[1].ends_with_newline, false);
assert_eq!(galley.rows[1].x_offsets, vec![0.0]);
let galley = font.layout_multiline("line\nbreak".to_owned(), 10.0);
let galley = font.layout_multiline("line\nbreak".to_owned(), 40.0);
assert_eq!(galley.rows.len(), 2);
assert_eq!(galley.rows[0].ends_with_newline, true);
assert_eq!(galley.rows[1].ends_with_newline, false);
// Test wrapping:
let galley = font.layout_multiline("word wrap".to_owned(), 10.0);
let galley = font.layout_multiline("word wrap".to_owned(), 40.0);
assert_eq!(galley.rows.len(), 2);
assert_eq!(galley.rows[0].ends_with_newline, false);
assert_eq!(galley.rows[1].ends_with_newline, false);
{
// Test wrapping:
let galley = font.layout_multiline("word wrap.\nNew paragraph.".to_owned(), 10.0);
let galley = font.layout_multiline("word wrap.\nNew para.".to_owned(), 40.0);
assert_eq!(galley.rows.len(), 4);
assert_eq!(galley.rows[0].ends_with_newline, false);
assert_eq!(galley.rows[0].char_count_excluding_newline(), "word ".len());
@ -618,6 +618,8 @@ fn test_text_layout() {
galley.rows[1].char_count_including_newline(),
"wrap.\n".len()
);
assert_eq!(galley.rows[2].char_count_excluding_newline(), "New ".len());
assert_eq!(galley.rows[3].char_count_excluding_newline(), "para.".len());
assert_eq!(galley.rows[2].ends_with_newline, false);
assert_eq!(galley.rows[3].ends_with_newline, false);
@ -633,11 +635,11 @@ fn test_text_layout() {
assert_eq!(
cursor,
Cursor {
ccursor: CCursor::new(25),
rcursor: RCursor { row: 3, column: 10 },
ccursor: CCursor::new(20),
rcursor: RCursor { row: 3, column: 5 },
pcursor: PCursor {
paragraph: 1,
offset: 14,
offset: 9,
prefer_next_row: false,
}
}
@ -711,7 +713,7 @@ fn test_text_layout() {
{
// Test cursor movement:
let galley = font.layout_multiline("word wrap.\nNew paragraph.".to_owned(), 10.0);
let galley = font.layout_multiline("word wrap.\nNew para.".to_owned(), 40.0);
assert_eq!(galley.rows.len(), 4);
assert_eq!(galley.rows[0].ends_with_newline, false);
assert_eq!(galley.rows[1].ends_with_newline, true);
@ -773,7 +775,7 @@ fn test_text_layout() {
galley.cursor_up_one_row(&galley.end()),
Cursor {
ccursor: CCursor::new(15),
rcursor: RCursor { row: 2, column: 10 },
rcursor: RCursor { row: 2, column: 5 },
pcursor: PCursor {
paragraph: 1,
offset: 4,