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
|
||||
|
||||
* Test `allocate_ui`
|
||||
* Text wrapping
|
||||
* Mix wrapping text and other widgets.
|
||||
|
||||
## Misc
|
||||
|
||||
|
|
|
@ -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.");
|
||||
});
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue