Label text will now intelligently continue and then wrap in wrap-layout

This commit is contained in:
Emil Ernerfeldt 2020-12-09 21:11:13 +01:00
parent a6ffe83349
commit d137ea0443
7 changed files with 134 additions and 48 deletions

View file

@ -5,7 +5,7 @@ TODO-list for the Egui project. If you looking for something to do, look here.
## Layout refactor ## Layout refactor
* Test `allocate_ui` * Test `allocate_ui`
* Text wrapping * Mix wrapping text and other widgets.
## Misc ## Misc

View file

@ -49,6 +49,7 @@ impl Widgets {
ui.add(__egui_github_link_file_line!()); ui.add(__egui_github_link_file_line!());
ui.horizontal_wrapped_for_text(TextStyle::Body, |ui| { ui.horizontal_wrapped_for_text(TextStyle::Body, |ui| {
ui.label("Long text will wrap, just as you would expect.");
ui.add(Label::new("Text can have").text_color(srgba(110, 255, 110, 255))); ui.add(Label::new("Text can have").text_color(srgba(110, 255, 110, 255)));
ui.add(Label::new("color").text_color(srgba(128, 140, 255, 255))); ui.add(Label::new("color").text_color(srgba(128, 140, 255, 255)));
ui.add(Label::new("and tooltips.")).on_hover_text( ui.add(Label::new("and tooltips.")).on_hover_text(
@ -63,7 +64,7 @@ impl Widgets {
let _ = ui.button("A button you can never press"); let _ = ui.button("A button you can never press");
}; };
ui.label("Tooltips can be more than just simple text.").on_hover_ui(tooltip_ui); ui.label("Tooltips can be more than just simple text.").on_hover_ui(tooltip_ui);
ui.label("Ευρηκα! τ = 2×π") ui.label("There is also (limited) non-ASCII support: Ευρηκα! τ = 2×π")
.on_hover_text("The current font supports only a few non-latin characters and Egui does not currently support right-to-left text."); .on_hover_text("The current font supports only a few non-latin characters and Egui does not currently support right-to-left text.");
}); });

View file

@ -209,6 +209,10 @@ impl Layout {
self.main_dir self.main_dir
} }
pub fn main_wrap(self) -> bool {
self.main_wrap
}
pub fn cross_align(self) -> Align { pub fn cross_align(self) -> Align {
self.cross_align self.cross_align
} }
@ -306,8 +310,10 @@ impl Layout {
} }
} }
fn available_size_before_wrap(&self, region: &Region) -> Rect { /// In case of a wrapping layout, how much space is left on this row/column?
pub fn available_size_before_wrap(&self, region: &Region) -> Vec2 {
self.available_from_cursor_max_rect(region.cursor, region.max_rect) self.available_from_cursor_max_rect(region.cursor, region.max_rect)
.size()
} }
// TODO // TODO
@ -351,7 +357,7 @@ impl Layout {
let mut cursor = region.cursor; let mut cursor = region.cursor;
if self.main_wrap { if self.main_wrap {
let available_size = self.available_size_before_wrap(region).size(); let available_size = self.available_size_before_wrap(region);
match self.main_dir { match self.main_dir {
Direction::LeftToRight => { Direction::LeftToRight => {
if available_size.x < child_size.x && region.max_rect.left() < cursor.x { if available_size.x < child_size.x && region.max_rect.left() < cursor.x {

View file

@ -167,7 +167,20 @@ impl Font {
galley galley
} }
/// Always returns at least one row.
pub fn layout_multiline(&self, text: String, max_width_in_points: f32) -> Galley { pub fn layout_multiline(&self, text: String, max_width_in_points: f32) -> Galley {
self.layout_multiline_with_indentation_and_max_width(text, 0.0, max_width_in_points)
}
/// * `first_row_indentation`: extra space before the very first character (in points).
/// * `max_width_in_points`: wrapping width.
/// Always returns at least one row.
pub fn layout_multiline_with_indentation_and_max_width(
&self,
text: String,
first_row_indentation: f32,
max_width_in_points: f32,
) -> Galley {
let row_height = self.row_height(); let row_height = self.row_height();
let mut cursor_y = 0.0; let mut cursor_y = 0.0;
let mut rows = Vec::new(); let mut rows = Vec::new();
@ -182,8 +195,16 @@ impl Font {
assert!(paragraph_start <= paragraph_end); assert!(paragraph_start <= paragraph_end);
let paragraph_text = &text[paragraph_start..paragraph_end]; let paragraph_text = &text[paragraph_start..paragraph_end];
let mut paragraph_rows = let line_indentation = if rows.is_empty() {
self.layout_paragraph_max_width(paragraph_text, max_width_in_points); first_row_indentation
} else {
0.0
};
let mut paragraph_rows = self.layout_paragraph_max_width(
paragraph_text,
line_indentation,
max_width_in_points,
);
assert!(!paragraph_rows.is_empty()); assert!(!paragraph_rows.is_empty());
paragraph_rows.last_mut().unwrap().ends_with_newline = next_newline.is_some(); paragraph_rows.last_mut().unwrap().ends_with_newline = next_newline.is_some();
@ -252,10 +273,16 @@ impl Font {
/// A paragraph is text with no line break character in it. /// A paragraph is text with no line break character in it.
/// The text will be wrapped by the given `max_width_in_points`. /// The text will be wrapped by the given `max_width_in_points`.
fn layout_paragraph_max_width(&self, text: &str, max_width_in_points: f32) -> Vec<Row> { /// Always returns at least one row.
fn layout_paragraph_max_width(
&self,
text: &str,
mut first_row_indentation: f32,
max_width_in_points: f32,
) -> Vec<Row> {
if text == "" { if text == "" {
return vec![Row { return vec![Row {
x_offsets: vec![0.0], x_offsets: vec![first_row_indentation],
y_min: 0.0, y_min: 0.0,
y_max: self.row_height(), y_max: self.row_height(),
ends_with_newline: false, ends_with_newline: false,
@ -264,12 +291,7 @@ impl Font {
let full_x_offsets = self.layout_single_row_fragment(text); let full_x_offsets = self.layout_single_row_fragment(text);
let mut row_start_x = full_x_offsets[0]; let mut row_start_x = 0.0; // NOTE: BEFORE the `first_row_indentation`.
{
#![allow(clippy::float_cmp)]
assert_eq!(row_start_x, 0.0);
}
let mut cursor_y = 0.0; let mut cursor_y = 0.0;
let mut row_start_idx = 0; let mut row_start_idx = 0;
@ -281,40 +303,40 @@ impl Font {
for (i, (x, chr)) in full_x_offsets.iter().skip(1).zip(text.chars()).enumerate() { for (i, (x, chr)) in full_x_offsets.iter().skip(1).zip(text.chars()).enumerate() {
debug_assert!(chr != '\n'); debug_assert!(chr != '\n');
let potential_row_width = 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) = last_space { if let Some(last_space_idx) = last_space {
let include_trailing_space = true; // We include the trailing space in the row:
let row = if include_trailing_space { let row = Row {
Row { x_offsets: full_x_offsets[row_start_idx..=last_space_idx + 1]
x_offsets: full_x_offsets[row_start_idx..=last_space_idx + 1] .iter()
.iter() .map(|x| first_row_indentation + x - row_start_x)
.map(|x| x - row_start_x) .collect(),
.collect(), y_min: cursor_y,
y_min: cursor_y, y_max: cursor_y + self.row_height(),
y_max: cursor_y + self.row_height(), ends_with_newline: false,
ends_with_newline: false,
}
} else {
Row {
x_offsets: full_x_offsets[row_start_idx..=last_space_idx]
.iter()
.map(|x| x - row_start_x)
.collect(),
y_min: cursor_y,
y_max: cursor_y + self.row_height(),
ends_with_newline: false,
}
}; };
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_space_idx + 1;
row_start_x = full_x_offsets[row_start_idx]; row_start_x = first_row_indentation + full_x_offsets[row_start_idx];
last_space = None; last_space = None;
cursor_y += self.row_height(); cursor_y = self.round_to_pixel(cursor_y + self.row_height());
cursor_y = self.round_to_pixel(cursor_y); } else if out_rows.is_empty() && first_row_indentation > 0.0 {
assert_eq!(row_start_idx, 0);
// Allow the first row to be completely empty, because we know there will be more space on the next row:
let row = Row {
x_offsets: vec![first_row_indentation],
y_min: cursor_y,
y_max: cursor_y + self.row_height(),
ends_with_newline: false,
};
row.sanity_check();
out_rows.push(row);
cursor_y = self.round_to_pixel(cursor_y + self.row_height());
first_row_indentation = 0.0; // Continue all other rows as if there is no indentation
} }
} }
@ -328,7 +350,7 @@ impl Font {
let row = Row { let row = Row {
x_offsets: full_x_offsets[row_start_idx..] x_offsets: full_x_offsets[row_start_idx..]
.iter() .iter()
.map(|x| x - row_start_x) .map(|x| first_row_indentation + x - row_start_x)
.collect(), .collect(),
y_min: cursor_y, y_min: cursor_y,
y_max: cursor_y + self.row_height(), y_max: cursor_y + self.row_height(),

View file

@ -196,6 +196,13 @@ impl Row {
self.y_max - self.y_min self.y_max - self.y_min
} }
pub fn rect(&self) -> Rect {
Rect::from_min_max(
pos2(self.min_x(), self.y_min),
pos2(self.max_x(), self.y_max),
)
}
/// Closest char at the desired x coordinate. /// Closest char at the desired x coordinate.
/// Returns something in the range `[0, char_count_excluding_newline()]`. /// Returns something in the range `[0, char_count_excluding_newline()]`.
pub fn char_at(&self, desired_x: f32) -> usize { pub fn char_at(&self, desired_x: f32) -> usize {

View file

@ -323,6 +323,11 @@ impl Ui {
self.available_size().x self.available_size().x
} }
/// In case of a wrapping layout, how much space is left on this row/column?
pub fn available_width_before_wrap(&self) -> f32 {
self.layout.available_size_before_wrap(&self.region).x
}
// TODO: clarify if this is before or after wrap // TODO: clarify if this is before or after wrap
pub fn available(&self) -> Rect { pub fn available(&self) -> Rect {
self.layout.available(&self.region) self.layout.available(&self.region)
@ -464,6 +469,17 @@ impl Ui {
inner_child_rect inner_child_rect
} }
pub(crate) fn advance_cursor_after_rect(&mut self, rect: Rect) {
let item_spacing = self.style().spacing.item_spacing;
self.layout
.advance_after_outer_rect(&mut self.region, rect, rect, item_spacing);
self.region.expand_to_include_rect(rect);
}
pub(crate) fn cursor(&self) -> Pos2 {
self.region.cursor
}
/// Allocated the given space and then adds content to that space. /// Allocated the given space and then adds content to that space.
/// If the contents overflow, more space will be allocated. /// If the contents overflow, more space will be allocated.
/// When finished, the amount of space actually used (`min_rect`) will be allocated. /// When finished, the amount of space actually used (`min_rect`) will be allocated.

View file

@ -80,9 +80,6 @@ impl Label {
pub fn layout(&self, ui: &Ui) -> Galley { pub fn layout(&self, ui: &Ui) -> Galley {
let max_width = ui.available_width(); let max_width = ui.available_width();
// Prevent word-wrapping after a single letter, and other silly shit:
// TODO: general "don't force labels and similar to wrap so early"
// TODO: max_width = max_width.at_least(ui.spacing.first_wrap_width);
self.layout_width(ui, max_width) self.layout_width(ui, max_width)
} }
@ -125,11 +122,48 @@ impl Label {
impl Widget for Label { impl Widget for Label {
fn ui(self, ui: &mut Ui) -> Response { fn ui(self, ui: &mut Ui) -> Response {
let galley = self.layout(ui); if self.multiline
let rect = ui.allocate_space(galley.size); && ui.layout().main_dir() == Direction::LeftToRight
let rect = ui.layout().align_size_within_rect(galley.size, rect); && ui.layout().main_wrap()
self.paint_galley(ui, rect.min, galley); {
ui.interact_hover(rect) // On a wrapping horizontal layout we want text to start after the last widget,
// then continue on the line below! This will take some extra work:
let max_width = ui.available_width();
let first_row_indentation = max_width - ui.available_width_before_wrap();
let text_style = self.text_style_or_default(ui.style());
let font = &ui.fonts()[text_style];
let galley = font.layout_multiline_with_indentation_and_max_width(
self.text.clone(),
first_row_indentation,
max_width,
);
let pos = pos2(ui.min_rect().left(), ui.cursor().y);
let mut total_response = None;
for row in &galley.rows {
let rect = row.rect().translate(vec2(pos.x, pos.y));
ui.advance_cursor_after_rect(rect);
let row_response = ui.interact_hover(rect);
if total_response.is_none() {
total_response = Some(row_response);
} else {
total_response = Some(total_response.unwrap().union(row_response));
}
}
self.paint_galley(ui, pos, galley);
total_response.expect("Galley rows shouldn't be empty")
} else {
let galley = self.layout(ui);
let rect = ui.allocate_space(galley.size);
let rect = ui.layout().align_size_within_rect(galley.size, rect);
self.paint_galley(ui, rect.min, galley);
ui.interact_hover(rect)
}
} }
} }