diff --git a/CHANGELOG.md b/CHANGELOG.md index 64eda620..5b2d5595 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,11 +19,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). * Add `Slider::clamp_to_range(bool)`: if set, clamp the incoming and outgoing values to the slider range. * Add: `ui.spacing()`, `ui.spacing_mut()`, `ui.visuals()`, `ui.visuals_mut()`. * Add: `ctx.set_visuals()`. +* You can now control text wrapping with `Style::wrap`. ### Changed 🔧 * Text will now wrap at newlines, spaces, dashes, punctuation or in the middle of a words if necessary, in that order of priority. -* `mouse` has be renamed `pointer` everywhere (to make it clear it includes touches too). +* Widgets will now always line break at `\n` characters. +* `mouse` has been renamed `pointer` everywhere (to make it clear it includes touches too). * Most parts of `Response` are now methods, so `if ui.button("…").clicked {` is now `if ui.button("…").clicked() {`. * `Response::active` is now gone. You can use `response.dragged()` or `response.clicked()` instead. * Backend: pointer (mouse/touch) position and buttons are now passed to egui in the event stream. diff --git a/egui/src/containers/collapsing_header.rs b/egui/src/containers/collapsing_header.rs index 160ea2bc..59b3b6d7 100644 --- a/egui/src/containers/collapsing_header.rs +++ b/egui/src/containers/collapsing_header.rs @@ -134,9 +134,7 @@ pub struct CollapsingHeader { impl CollapsingHeader { /// The `CollapsingHeader` starts out collapsed unless you call `default_open`. pub fn new(label: impl Into) -> Self { - let label = Label::new(label) - .text_style(TextStyle::Button) - .multiline(false); + let label = Label::new(label).text_style(TextStyle::Button).wrap(false); let id_source = Id::new(label.text()); Self { label, diff --git a/egui/src/containers/combo_box.rs b/egui/src/containers/combo_box.rs index 15fe9981..ef19b220 100644 --- a/egui/src/containers/combo_box.rs +++ b/egui/src/containers/combo_box.rs @@ -64,7 +64,7 @@ pub fn combo_box( let text_style = TextStyle::Button; let font = &ui.fonts()[text_style]; - let galley = font.layout_single_line(selected.into()); + let galley = font.layout_no_wrap(selected.into()); let width = galley.size.x + ui.spacing().item_spacing.x + icon_size.x; let width = width.at_least(full_minimum_width); diff --git a/egui/src/containers/window.rs b/egui/src/containers/window.rs index 398287d9..87e4700e 100644 --- a/egui/src/containers/window.rs +++ b/egui/src/containers/window.rs @@ -38,9 +38,7 @@ impl<'open> Window<'open> { pub fn new(title: impl Into) -> Self { let title = title.into(); let area = Area::new(&title); - let title_label = Label::new(title) - .text_style(TextStyle::Heading) - .multiline(false); + let title_label = Label::new(title).text_style(TextStyle::Heading).wrap(false); Self { title_label, open: None, diff --git a/egui/src/introspection.rs b/egui/src/introspection.rs index 54817c31..36c6ab7f 100644 --- a/egui/src/introspection.rs +++ b/egui/src/introspection.rs @@ -116,7 +116,7 @@ impl Widget for &epaint::stats::PaintStats { } pub fn label(ui: &mut Ui, alloc_info: &epaint::stats::AllocInfo, what: &str) -> Response { - ui.add(Label::new(alloc_info.format(what)).multiline(false)) + ui.add(Label::new(alloc_info.format(what)).wrap(false)) } impl Widget for &mut epaint::TessellationOptions { diff --git a/egui/src/style.rs b/egui/src/style.rs index e02c0d53..c04b2619 100644 --- a/egui/src/style.rs +++ b/egui/src/style.rs @@ -17,6 +17,11 @@ pub struct Style { /// Default `TextStyle` for normal text (i.e. for `Label` and `TextEdit`). pub body_text_style: TextStyle, + /// If set, labels buttons wtc will use this to determine whether or not + /// to wrap the text at the right edge of the `Ui` they are in. + /// By default this is `None`. + pub wrap: Option, + pub spacing: Spacing, pub interaction: Interaction, pub visuals: Visuals, @@ -263,6 +268,7 @@ impl Default for Style { fn default() -> Self { Self { body_text_style: TextStyle::Body, + wrap: None, spacing: Spacing::default(), interaction: Interaction::default(), visuals: Visuals::default(), @@ -460,6 +466,7 @@ impl Style { pub fn ui(&mut self, ui: &mut crate::Ui) { let Self { body_text_style, + wrap: _, spacing, interaction, visuals, diff --git a/egui/src/ui.rs b/egui/src/ui.rs index 737455d7..4d49ccd1 100644 --- a/egui/src/ui.rs +++ b/egui/src/ui.rs @@ -170,6 +170,17 @@ impl Ui { self.placer.layout() } + /// Should text wrap in this `Ui`? + /// This is determined first by [`Style::wrap`], and then by the layout of this `Ui`. + pub fn wrap_text(&self) -> bool { + if let Some(wrap) = self.style.wrap { + wrap + } else { + // In vertical layouts we wrap text, but in horizontal we keep going. + self.layout().is_vertical() + } + } + /// Create a painter for a sub-region of this Ui. /// /// The clip-rect of the returned `Painter` will be the intersection diff --git a/egui/src/widgets/button.rs b/egui/src/widgets/button.rs index 597023ff..5713fbce 100644 --- a/egui/src/widgets/button.rs +++ b/egui/src/widgets/button.rs @@ -87,19 +87,19 @@ impl Widget for Button { small, frame, } = self; - let font = &ui.fonts()[text_style]; - - let single_line = ui.layout().is_horizontal(); - let galley = if single_line { - font.layout_single_line(text) - } else { - font.layout_multiline(text, ui.available_width()) - }; let mut button_padding = ui.spacing().button_padding; if small { button_padding.y = 0.0; } + let total_extra = button_padding + button_padding; + + let font = &ui.fonts()[text_style]; + let galley = if ui.wrap_text() { + font.layout_multiline(text, ui.available_width() - total_extra.x) + } else { + font.layout_no_wrap(text) + }; let mut desired_size = galley.size + 2.0 * button_padding; if !small { @@ -180,11 +180,10 @@ impl<'a> Widget for Checkbox<'a> { let button_padding = spacing.button_padding; let total_extra = button_padding + vec2(icon_width + icon_spacing, 0.0) + button_padding; - let single_line = ui.layout().is_horizontal(); - let galley = if single_line { - font.layout_single_line(text) - } else { + let galley = if ui.wrap_text() { font.layout_multiline(text, ui.available_width() - total_extra.x) + } else { + font.layout_no_wrap(text) }; let mut desired_size = total_extra + galley.size; @@ -272,11 +271,10 @@ impl Widget for RadioButton { let button_padding = ui.spacing().button_padding; let total_extra = button_padding + vec2(icon_width + icon_spacing, 0.0) + button_padding; - let single_line = ui.layout().is_horizontal(); - let galley = if single_line { - font.layout_single_line(text) - } else { + let galley = if ui.wrap_text() { font.layout_multiline(text, ui.available_width() - total_extra.x) + } else { + font.layout_no_wrap(text) }; let mut desired_size = total_extra + galley.size; diff --git a/egui/src/widgets/label.rs b/egui/src/widgets/label.rs index 8a5067b5..f4d419bd 100644 --- a/egui/src/widgets/label.rs +++ b/egui/src/widgets/label.rs @@ -5,7 +5,7 @@ use crate::{paint::Galley, *}; pub struct Label { // TODO: not pub pub(crate) text: String, - pub(crate) multiline: Option, + pub(crate) wrap: Option, pub(crate) text_style: Option, pub(crate) background_color: Color32, pub(crate) text_color: Option, @@ -21,7 +21,7 @@ impl Label { pub fn new(text: impl Into) -> Self { Self { text: text.into(), - multiline: None, + wrap: None, text_style: None, background_color: Color32::TRANSPARENT, text_color: None, @@ -39,16 +39,21 @@ impl Label { } /// If `true`, the text will wrap at the `max_width`. - /// By default `multiline` will be true in vertical layouts + /// By default [`wrap`] will be true in vertical layouts /// and horizontal layouts with wrapping, /// and false on non-wrapping horizontal layouts. /// - /// If the text has any newlines (`\n`) in it, multiline will automatically turn on. - pub fn multiline(mut self, multiline: bool) -> Self { - self.multiline = Some(multiline); + /// Note that any `\n` in the text label will always produce a new line. + pub fn wrap(mut self, wrap: bool) -> Self { + self.wrap = Some(wrap); self } + #[deprecated = "Use Label::wrap instead"] + pub fn multiline(self, multiline: bool) -> Self { + self.wrap(multiline) + } + /// The default is [`Style::body_text_style`] (generally [`TextStyle::Body`]). pub fn text_style(mut self, text_style: TextStyle) -> Self { self.text_style = Some(text_style); @@ -122,11 +127,12 @@ impl Label { pub fn layout_width(&self, ui: &Ui, max_width: f32) -> Galley { let text_style = self.text_style_or_default(ui.style()); let font = &ui.fonts()[text_style]; - if self.is_multiline(ui) { - font.layout_multiline(self.text.clone(), max_width) // TODO: avoid clone + let wrap_width = if self.should_wrap(ui) { + max_width } else { - font.layout_single_line(self.text.clone()) // TODO: avoid clone - } + f32::INFINITY + }; + font.layout_multiline(self.text.clone(), wrap_width) // TODO: avoid clone } pub fn font_height(&self, fonts: &paint::text::Fonts, style: &Style) -> f32 { @@ -207,19 +213,17 @@ impl Label { self.text_style.unwrap_or(style.body_text_style) } - fn is_multiline(&self, ui: &Ui) -> bool { - self.multiline.unwrap_or_else(|| { + fn should_wrap(&self, ui: &Ui) -> bool { + self.wrap.or(ui.style().wrap).unwrap_or_else(|| { let layout = ui.layout(); - layout.is_vertical() - || layout.is_horizontal() && layout.main_wrap() - || self.text.contains('\n') + layout.is_vertical() || layout.is_horizontal() && layout.main_wrap() }) } } impl Widget for Label { fn ui(self, ui: &mut Ui) -> Response { - if self.is_multiline(ui) + if self.should_wrap(ui) && ui.layout().main_dir() == Direction::LeftToRight && ui.layout().main_wrap() { diff --git a/egui/src/widgets/selected_label.rs b/egui/src/widgets/selected_label.rs index b520a7fc..40ce0d8b 100644 --- a/egui/src/widgets/selected_label.rs +++ b/egui/src/widgets/selected_label.rs @@ -29,11 +29,10 @@ impl Widget for SelectableLabel { let button_padding = ui.spacing().button_padding; let total_extra = button_padding + button_padding; - let single_line = ui.layout().is_horizontal(); - let galley = if single_line { - font.layout_single_line(text) - } else { + let galley = if ui.wrap_text() { font.layout_multiline(text, ui.available_width() - total_extra.x) + } else { + font.layout_no_wrap(text) }; let mut desired_size = total_extra + galley.size; diff --git a/egui/src/widgets/slider.rs b/egui/src/widgets/slider.rs index 1610ca62..15f38e5b 100644 --- a/egui/src/widgets/slider.rs +++ b/egui/src/widgets/slider.rs @@ -324,12 +324,7 @@ impl<'a> Slider<'a> { fn label_ui(&mut self, ui: &mut Ui) { if let Some(label_text) = self.text.as_deref() { let text_color = self.text_color.unwrap_or_else(|| ui.visuals().text_color()); - - ui.add( - Label::new(label_text) - .multiline(false) - .text_color(text_color), - ); + ui.add(Label::new(label_text).wrap(false).text_color(text_color)); } } diff --git a/epaint/src/text/font.rs b/epaint/src/text/font.rs index 89a2dfe6..e55029ea 100644 --- a/epaint/src/text/font.rs +++ b/epaint/src/text/font.rs @@ -275,6 +275,9 @@ impl Font { /// Typeset the given text onto one row. /// Any `\n` will show up as the replacement character. /// Always returns exactly one `Row` in the `Galley`. + /// + /// Most often you probably want `\n` to produce a new row, + /// and so [`Self::layout_no_wrap`] may be a better choice. pub fn layout_single_line(&self, text: String) -> Galley { let x_offsets = self.layout_single_row_fragment(&text); let row = Row { @@ -295,6 +298,13 @@ impl Font { } /// Always returns at least one row. + /// Will line break at `\n`. + pub fn layout_no_wrap(&self, text: String) -> Galley { + self.layout_multiline(text, f32::INFINITY) + } + + /// Always returns at least one row. + /// Will wrap text at the given width. 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) }