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:
parent
17fdd3bb10
commit
b647592a5a
3 changed files with 55 additions and 26 deletions
|
@ -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 🔧
|
||||
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue