Skip to content

Commit

Permalink
Wrap text at dashes, punctuations or anywhere if necessary
Browse files Browse the repository at this point in the history
Closes #55

Supersedes #104
  • Loading branch information
emilk committed Jan 31, 2021
1 parent 17fdd3b commit b647592
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 26 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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 🔧

Expand Down
62 changes: 44 additions & 18 deletions epaint/src/text/font.rs
Expand Up @@ -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)]
Expand Down Expand Up @@ -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![];

Expand All @@ -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(),
Expand All @@ -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);
Expand All @@ -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() {
Expand All @@ -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>,
Expand Down
18 changes: 10 additions & 8 deletions epaint/src/text/galley.rs
Expand Up @@ -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());
Expand All @@ -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);

Expand All @@ -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,
}
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit b647592

Please sign in to comment.