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.
|
* Add `Label` methods for code, strong, strikethrough, underline and italics.
|
||||||
* `egui::popup::popup_below_widget`: show a popup area below another widget.
|
* `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.
|
* 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 🔧
|
### Changed 🔧
|
||||||
|
|
||||||
|
|
|
@ -142,13 +142,6 @@ impl FontImpl {
|
||||||
|
|
||||||
type FontIndex = usize;
|
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?
|
// TODO: rename?
|
||||||
/// Wrapper over multiple `FontImpl` (e.g. a primary + fallbacks for emojis)
|
/// Wrapper over multiple `FontImpl` (e.g. a primary + fallbacks for emojis)
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
@ -406,8 +399,8 @@ impl Font {
|
||||||
let mut cursor_y = 0.0;
|
let mut cursor_y = 0.0;
|
||||||
let mut row_start_idx = 0;
|
let mut row_start_idx = 0;
|
||||||
|
|
||||||
// start index of the last space or hieroglyphs. A candidate for a new row.
|
// Keeps track of good places to insert row break if we exceed `max_width_in_points`.
|
||||||
let mut newline_mark = None;
|
let mut row_break_candidates = RowBreakCandidates::default();
|
||||||
|
|
||||||
let mut out_rows = vec![];
|
let mut out_rows = vec![];
|
||||||
|
|
||||||
|
@ -416,10 +409,9 @@ impl Font {
|
||||||
let potential_row_width = first_row_indentation + x - row_start_x;
|
let potential_row_width = first_row_indentation + x - row_start_x;
|
||||||
|
|
||||||
if potential_row_width > max_width_in_points {
|
if potential_row_width > max_width_in_points {
|
||||||
if let Some(last_space_idx) = newline_mark {
|
if let Some(last_kept_index) = row_break_candidates.get() {
|
||||||
// We include the trailing space in the row:
|
|
||||||
let row = Row {
|
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()
|
.iter()
|
||||||
.map(|x| first_row_indentation + x - row_start_x)
|
.map(|x| first_row_indentation + x - row_start_x)
|
||||||
.collect(),
|
.collect(),
|
||||||
|
@ -430,9 +422,9 @@ impl Font {
|
||||||
row.sanity_check();
|
row.sanity_check();
|
||||||
out_rows.push(row);
|
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];
|
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());
|
cursor_y = self.round_to_pixel(cursor_y + self.row_height());
|
||||||
} else if out_rows.is_empty() && first_row_indentation > 0.0 {
|
} else if out_rows.is_empty() && first_row_indentation > 0.0 {
|
||||||
assert_eq!(row_start_idx, 0);
|
assert_eq!(row_start_idx, 0);
|
||||||
|
@ -450,10 +442,7 @@ impl Font {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const NON_BREAKING_SPACE: char = '\u{A0}';
|
row_break_candidates.add(i, chr);
|
||||||
if (chr.is_whitespace() && chr != NON_BREAKING_SPACE) || is_chinese(chr) {
|
|
||||||
newline_mark = Some(i);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if row_start_idx + 1 < full_x_offsets.len() {
|
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(
|
fn allocate_glyph(
|
||||||
atlas: &mut TextureAtlas,
|
atlas: &mut TextureAtlas,
|
||||||
glyph: rusttype::Glyph<'static>,
|
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].ends_with_newline, false);
|
||||||
assert_eq!(galley.rows[1].x_offsets, vec![0.0]);
|
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.len(), 2);
|
||||||
assert_eq!(galley.rows[0].ends_with_newline, true);
|
assert_eq!(galley.rows[0].ends_with_newline, true);
|
||||||
assert_eq!(galley.rows[1].ends_with_newline, false);
|
assert_eq!(galley.rows[1].ends_with_newline, false);
|
||||||
|
|
||||||
// Test wrapping:
|
// 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.len(), 2);
|
||||||
assert_eq!(galley.rows[0].ends_with_newline, false);
|
assert_eq!(galley.rows[0].ends_with_newline, false);
|
||||||
assert_eq!(galley.rows[1].ends_with_newline, false);
|
assert_eq!(galley.rows[1].ends_with_newline, false);
|
||||||
|
|
||||||
{
|
{
|
||||||
// Test wrapping:
|
// 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.len(), 4);
|
||||||
assert_eq!(galley.rows[0].ends_with_newline, false);
|
assert_eq!(galley.rows[0].ends_with_newline, false);
|
||||||
assert_eq!(galley.rows[0].char_count_excluding_newline(), "word ".len());
|
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(),
|
galley.rows[1].char_count_including_newline(),
|
||||||
"wrap.\n".len()
|
"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[2].ends_with_newline, false);
|
||||||
assert_eq!(galley.rows[3].ends_with_newline, false);
|
assert_eq!(galley.rows[3].ends_with_newline, false);
|
||||||
|
|
||||||
|
@ -633,11 +635,11 @@ fn test_text_layout() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
cursor,
|
cursor,
|
||||||
Cursor {
|
Cursor {
|
||||||
ccursor: CCursor::new(25),
|
ccursor: CCursor::new(20),
|
||||||
rcursor: RCursor { row: 3, column: 10 },
|
rcursor: RCursor { row: 3, column: 5 },
|
||||||
pcursor: PCursor {
|
pcursor: PCursor {
|
||||||
paragraph: 1,
|
paragraph: 1,
|
||||||
offset: 14,
|
offset: 9,
|
||||||
prefer_next_row: false,
|
prefer_next_row: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -711,7 +713,7 @@ fn test_text_layout() {
|
||||||
|
|
||||||
{
|
{
|
||||||
// Test cursor movement:
|
// 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.len(), 4);
|
||||||
assert_eq!(galley.rows[0].ends_with_newline, false);
|
assert_eq!(galley.rows[0].ends_with_newline, false);
|
||||||
assert_eq!(galley.rows[1].ends_with_newline, true);
|
assert_eq!(galley.rows[1].ends_with_newline, true);
|
||||||
|
@ -773,7 +775,7 @@ fn test_text_layout() {
|
||||||
galley.cursor_up_one_row(&galley.end()),
|
galley.cursor_up_one_row(&galley.end()),
|
||||||
Cursor {
|
Cursor {
|
||||||
ccursor: CCursor::new(15),
|
ccursor: CCursor::new(15),
|
||||||
rcursor: RCursor { row: 2, column: 10 },
|
rcursor: RCursor { row: 2, column: 5 },
|
||||||
pcursor: PCursor {
|
pcursor: PCursor {
|
||||||
paragraph: 1,
|
paragraph: 1,
|
||||||
offset: 4,
|
offset: 4,
|
||||||
|
|
Loading…
Reference in a new issue