Label text will now intelligently continue and then wrap in wrap-layout
This commit is contained in:
parent
a6ffe83349
commit
d137ea0443
7 changed files with 134 additions and 48 deletions
2
TODO.md
2
TODO.md
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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.");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue