diff --git a/TODO.md b/TODO.md index 98b1b89d..62dbe301 100644 --- a/TODO.md +++ b/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 diff --git a/egui/src/demos/widgets.rs b/egui/src/demos/widgets.rs index d372f1e6..fb179db3 100644 --- a/egui/src/demos/widgets.rs +++ b/egui/src/demos/widgets.rs @@ -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."); }); diff --git a/egui/src/layout.rs b/egui/src/layout.rs index 0c192505..083248ec 100644 --- a/egui/src/layout.rs +++ b/egui/src/layout.rs @@ -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 { diff --git a/egui/src/paint/font.rs b/egui/src/paint/font.rs index 8edc2ce0..9c7bf5d0 100644 --- a/egui/src/paint/font.rs +++ b/egui/src/paint/font.rs @@ -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 { + /// 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 { 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 { - x_offsets: full_x_offsets[row_start_idx..=last_space_idx + 1] - .iter() - .map(|x| 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, - } + // 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| first_row_indentation + 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(), diff --git a/egui/src/paint/galley.rs b/egui/src/paint/galley.rs index 02202ef9..57f91f58 100644 --- a/egui/src/paint/galley.rs +++ b/egui/src/paint/galley.rs @@ -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 { diff --git a/egui/src/ui.rs b/egui/src/ui.rs index 97a39a2c..1b0ba14d 100644 --- a/egui/src/ui.rs +++ b/egui/src/ui.rs @@ -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. diff --git a/egui/src/widgets/mod.rs b/egui/src/widgets/mod.rs index f61d42d4..c6383f29 100644 --- a/egui/src/widgets/mod.rs +++ b/egui/src/widgets/mod.rs @@ -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,11 +122,48 @@ impl Label { impl Widget for Label { fn ui(self, ui: &mut Ui) -> Response { - 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) + 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); + self.paint_galley(ui, rect.min, galley); + ui.interact_hover(rect) + } } }