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
* Test `allocate_ui`
* Text wrapping
* Mix wrapping text and other widgets.
## Misc

View file

@ -49,6 +49,7 @@ impl Widgets {
ui.add(__egui_github_link_file_line!());
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("color").text_color(srgba(128, 140, 255, 255)));
ui.add(Label::new("and tooltips.")).on_hover_text(
@ -63,7 +64,7 @@ impl Widgets {
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("Ευρηκα! τ = 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.");
});

View file

@ -209,6 +209,10 @@ impl Layout {
self.main_dir
}
pub fn main_wrap(self) -> bool {
self.main_wrap
}
pub fn cross_align(self) -> 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)
.size()
}
// TODO
@ -351,7 +357,7 @@ impl Layout {
let mut cursor = region.cursor;
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 {
Direction::LeftToRight => {
if available_size.x < child_size.x && region.max_rect.left() < cursor.x {

View file

@ -167,7 +167,20 @@ impl Font {
galley
}
/// Always returns at least one row.
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 mut cursor_y = 0.0;
let mut rows = Vec::new();
@ -182,8 +195,16 @@ impl Font {
assert!(paragraph_start <= paragraph_end);
let paragraph_text = &text[paragraph_start..paragraph_end];
let mut paragraph_rows =
self.layout_paragraph_max_width(paragraph_text, max_width_in_points);
let line_indentation = if rows.is_empty() {
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());
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.
/// 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 == "" {
return vec![Row {
x_offsets: vec![0.0],
x_offsets: vec![first_row_indentation],
y_min: 0.0,
y_max: self.row_height(),
ends_with_newline: false,
@ -264,12 +291,7 @@ impl Font {
let full_x_offsets = self.layout_single_row_fragment(text);
let mut row_start_x = full_x_offsets[0];
{
#![allow(clippy::float_cmp)]
assert_eq!(row_start_x, 0.0);
}
let mut row_start_x = 0.0; // NOTE: BEFORE the `first_row_indentation`.
let mut cursor_y = 0.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() {
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 let Some(last_space_idx) = last_space {
let include_trailing_space = true;
let row = if include_trailing_space {
Row {
// We include the trailing space in the row:
let row = Row {
x_offsets: full_x_offsets[row_start_idx..=last_space_idx + 1]
.iter()
.map(|x| x - row_start_x)
.map(|x| first_row_indentation + x - row_start_x)
.collect(),
y_min: cursor_y,
y_max: cursor_y + self.row_height(),
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();
out_rows.push(row);
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;
cursor_y += self.row_height();
cursor_y = self.round_to_pixel(cursor_y);
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);
// 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 {
x_offsets: full_x_offsets[row_start_idx..]
.iter()
.map(|x| x - row_start_x)
.map(|x| first_row_indentation + x - row_start_x)
.collect(),
y_min: cursor_y,
y_max: cursor_y + self.row_height(),

View file

@ -196,6 +196,13 @@ impl Row {
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.
/// Returns something in the range `[0, char_count_excluding_newline()]`.
pub fn char_at(&self, desired_x: f32) -> usize {

View file

@ -323,6 +323,11 @@ impl Ui {
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
pub fn available(&self) -> Rect {
self.layout.available(&self.region)
@ -464,6 +469,17 @@ impl Ui {
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.
/// If the contents overflow, more space 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 {
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)
}
@ -125,6 +122,42 @@ impl Label {
impl Widget for Label {
fn ui(self, ui: &mut Ui) -> Response {
if self.multiline
&& ui.layout().main_dir() == Direction::LeftToRight
&& ui.layout().main_wrap()
{
// 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);
@ -132,6 +165,7 @@ impl Widget for Label {
ui.interact_hover(rect)
}
}
}
impl Into<Label> for &str {
fn into(self) -> Label {