From b647592a5a34832283e11822a6efe1a1488a4ed3 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 31 Jan 2021 15:53:20 +0100 Subject: [PATCH] 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 --- CHANGELOG.md | 1 + epaint/src/text/font.rs | 62 +++++++++++++++++++++++++++------------ epaint/src/text/galley.rs | 18 +++++++----- 3 files changed, 55 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50228c0a..b7f705b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 🔧 diff --git a/epaint/src/text/font.rs b/epaint/src/text/font.rs index 4947af24..c17f7962 100644 --- a/epaint/src/text/font.rs +++ b/epaint/src/text/font.rs @@ -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, + /// Breaking at a dash is super- + /// good idea. + dash: Option, + /// This is nicer for things like URLs, e.g. www. + /// example.com. + punctuation: Option, + /// Breaking after just random character is some + /// times necessary. + any: Option, +} + +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 { + self.space.or(self.dash).or(self.punctuation).or(self.any) + } +} + fn allocate_glyph( atlas: &mut TextureAtlas, glyph: rusttype::Glyph<'static>, diff --git a/epaint/src/text/galley.rs b/epaint/src/text/galley.rs index 4e28fdd9..6982bf0f 100644 --- a/epaint/src/text/galley.rs +++ b/epaint/src/text/galley.rs @@ -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,