diff --git a/crates/egui/src/containers/collapsing_header.rs b/crates/egui/src/containers/collapsing_header.rs index 7c541a1a2f7..87754adfbf8 100644 --- a/crates/egui/src/containers/collapsing_header.rs +++ b/crates/egui/src/containers/collapsing_header.rs @@ -506,8 +506,12 @@ impl CollapsingHeader { let available = ui.available_rect_before_wrap(); let text_pos = available.min + vec2(ui.spacing().indent, 0.0); let wrap_width = available.right() - text_pos.x; - let wrap = Some(false); - let galley = text.into_galley(ui, wrap, wrap_width, TextStyle::Button); + let galley = text.into_galley( + ui, + Some(TextWrapMode::Extend), + wrap_width, + TextStyle::Button, + ); let text_max_x = text_pos.x + galley.size().x; let mut desired_width = text_max_x + button_padding.x - available.left(); diff --git a/crates/egui/src/containers/combo_box.rs b/crates/egui/src/containers/combo_box.rs index 828ee140aa5..db5a9e3e8bd 100644 --- a/crates/egui/src/containers/combo_box.rs +++ b/crates/egui/src/containers/combo_box.rs @@ -5,7 +5,7 @@ use crate::{style::WidgetVisuals, *}; #[allow(unused_imports)] // Documentation use crate::style::Spacing; -/// Indicate whether or not a popup will be shown above or below the box. +/// Indicate whether a popup will be shown above or below the box. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum AboveOrBelow { Above, @@ -40,7 +40,7 @@ pub struct ComboBox { width: Option, height: Option, icon: Option, - wrap_enabled: bool, + wrap_mode: Option, } impl ComboBox { @@ -53,7 +53,7 @@ impl ComboBox { width: None, height: None, icon: None, - wrap_enabled: false, + wrap_mode: None, } } @@ -67,7 +67,7 @@ impl ComboBox { width: None, height: None, icon: None, - wrap_enabled: false, + wrap_mode: None, } } @@ -80,7 +80,7 @@ impl ComboBox { width: None, height: None, icon: None, - wrap_enabled: false, + wrap_mode: None, } } @@ -148,10 +148,29 @@ impl ComboBox { self } - /// Controls whether text wrap is used for the selected text + /// Controls the wrap mode used for the selected text. + /// + /// By default, [`Ui::wrap_mode`] will be used, which can be overridden with [`Style::wrap_mode`]. + /// + /// Note that any `\n` in the text will always produce a new line. #[inline] - pub fn wrap(mut self, wrap: bool) -> Self { - self.wrap_enabled = wrap; + pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self { + self.wrap_mode = Some(wrap_mode); + self + } + + /// Set [`Self::wrap_mode`] to [`TextWrapMode::Wrap`]. + #[inline] + pub fn wrap(mut self) -> Self { + self.wrap_mode = Some(TextWrapMode::Wrap); + + self + } + + /// Set [`Self::wrap_mode`] to [`TextWrapMode::Truncate`]. + #[inline] + pub fn truncate(mut self) -> Self { + self.wrap_mode = Some(TextWrapMode::Truncate); self } @@ -178,7 +197,7 @@ impl ComboBox { width, height, icon, - wrap_enabled, + wrap_mode, } = self; let button_id = ui.make_persistent_id(id_source); @@ -190,7 +209,7 @@ impl ComboBox { selected_text, menu_contents, icon, - wrap_enabled, + wrap_mode, (width, height), ); if let Some(label) = label { @@ -253,13 +272,14 @@ impl ComboBox { } } +#[allow(clippy::too_many_arguments)] fn combo_box_dyn<'c, R>( ui: &mut Ui, button_id: Id, selected_text: WidgetText, menu_contents: Box R + 'c>, icon: Option, - wrap_enabled: bool, + wrap_mode: Option, (width, height): (Option, Option), ) -> InnerResponse> { let popup_id = button_id.with("popup"); @@ -277,45 +297,33 @@ fn combo_box_dyn<'c, R>( AboveOrBelow::Above }; + let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode()); + let margin = ui.spacing().button_padding; let button_response = button_frame(ui, button_id, is_popup_open, Sense::click(), |ui| { let icon_spacing = ui.spacing().icon_spacing; - // We don't want to change width when user selects something new - let full_minimum_width = if wrap_enabled { - // Currently selected value's text will be wrapped if needed, so occupy the available width. - ui.available_width() - } else { - // Occupy at least the minimum width assigned to ComboBox. - let width = width.unwrap_or_else(|| ui.spacing().combo_width); - width - 2.0 * margin.x - }; let icon_size = Vec2::splat(ui.spacing().icon_width); - let wrap_width = if wrap_enabled { - // Use the available width, currently selected value's text will be wrapped if exceeds this value. - ui.available_width() - icon_spacing - icon_size.x - } else { - // Use all the width necessary to display the currently selected value's text. - f32::INFINITY - }; - let galley = - selected_text.into_galley(ui, Some(wrap_enabled), wrap_width, TextStyle::Button); + // The combo box selected text will always have this minimum width. + // Note: the `ComboBox::width()` if set or `Spacing::combo_width` are considered as the + // minimum overall width, regardless of the wrap mode. + let minimum_width = width.unwrap_or_else(|| ui.spacing().combo_width) - 2.0 * margin.x; - // The width necessary to contain the whole widget with the currently selected value's text. - let width = if wrap_enabled { - full_minimum_width + // width against which to lay out the selected text + let wrap_width = if wrap_mode == TextWrapMode::Extend { + // Use all the width necessary to display the currently selected value's text. + f32::INFINITY } else { - // Occupy at least the minimum width needed to contain the widget with the currently selected value's text. - galley.size().x + icon_spacing + icon_size.x + // Use the available width, currently selected value's text will be wrapped if exceeds this value. + ui.available_width() - icon_spacing - icon_size.x }; - // Case : wrap_enabled : occupy all the available width. - // Case : !wrap_enabled : occupy at least the minimum width assigned to Slider and ComboBox, - // increase if the currently selected value needs additional horizontal space to fully display its text (up to wrap_width (f32::INFINITY)). - let width = width.at_least(full_minimum_width); - let height = galley.size().y.max(icon_size.y); + let galley = selected_text.into_galley(ui, Some(wrap_mode), wrap_width, TextStyle::Button); + + let actual_width = (galley.size().x + icon_spacing + icon_size.x).at_least(minimum_width); + let actual_height = galley.size().y.max(icon_size.y); - let (_, rect) = ui.allocate_space(Vec2::new(width, height)); + let (_, rect) = ui.allocate_space(Vec2::new(actual_width, actual_height)); let button_rect = ui.min_rect().expand2(ui.spacing().button_padding); let response = ui.interact(button_rect, button_id, Sense::click()); // response.active |= is_popup_open; @@ -371,7 +379,7 @@ fn combo_box_dyn<'c, R>( // result in labels that wrap very early. // Instead, we turn it off by default so that the labels // expand the width of the menu. - ui.style_mut().wrap = Some(false); + ui.style_mut().wrap_mode = Some(TextWrapMode::Extend); menu_contents(ui) }) .inner diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index 0f336743bd8..9bf3924e0f2 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -1028,7 +1028,12 @@ fn show_title_bar( collapsing.show_default_button_with_size(ui, button_size); } - let title_galley = title.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Heading); + let title_galley = title.into_galley( + ui, + Some(crate::TextWrapMode::Extend), + f32::INFINITY, + TextStyle::Heading, + ); let minimum_width = if collapsible || show_close_button { // If at least one button is shown we make room for both buttons (since title is centered): diff --git a/crates/egui/src/debug_text.rs b/crates/egui/src/debug_text.rs index b2551de9ddf..672bc20c6a6 100644 --- a/crates/egui/src/debug_text.rs +++ b/crates/egui/src/debug_text.rs @@ -12,7 +12,7 @@ use crate::*; /// /// This is a built-in plugin in egui, /// meaning [`Context`] calls this from its `Default` implementation, -/// so this i marked as `pub(crate)`. +/// so this is marked as `pub(crate)`. pub(crate) fn register(ctx: &Context) { ctx.on_end_frame("debug_text", std::sync::Arc::new(State::end_frame)); } @@ -105,13 +105,11 @@ impl State { { // Paint `text` to right of `pos`: - let wrap = true; let available_width = ctx.screen_rect().max.x - pos.x; let galley = text.into_galley_impl( ctx, &ctx.style(), - wrap, - available_width, + text::TextWrapping::wrap_at_width(available_width), font_id.clone().into(), Align::TOP, ); diff --git a/crates/egui/src/introspection.rs b/crates/egui/src/introspection.rs index f71e82194be..d853b1c6196 100644 --- a/crates/egui/src/introspection.rs +++ b/crates/egui/src/introspection.rs @@ -129,7 +129,7 @@ impl Widget for &epaint::stats::PaintStats { } fn label(ui: &mut Ui, alloc_info: &epaint::stats::AllocInfo, what: &str) -> Response { - ui.add(Label::new(alloc_info.format(what)).wrap(false)) + ui.add(Label::new(alloc_info.format(what)).wrap_mode(TextWrapMode::Extend)) } impl Widget for &mut epaint::TessellationOptions { diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index c9d9b37ce48..517ee4c8ca5 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -338,6 +338,7 @@ //! ## Code snippets //! //! ``` +//! # use egui::TextWrapMode; //! # egui::__run_test_ui(|ui| { //! # let mut some_bool = true; //! // Miscellaneous tips and tricks @@ -358,7 +359,7 @@ //! ui.scope(|ui| { //! ui.visuals_mut().override_text_color = Some(egui::Color32::RED); //! ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); -//! ui.style_mut().wrap = Some(false); +//! ui.style_mut().wrap_mode = Some(TextWrapMode::Truncate); //! //! ui.label("This text will be red, monospace, and won't wrap to a new line"); //! }); // the temporary settings are reverted here @@ -451,6 +452,7 @@ pub use { Key, }, drag_and_drop::DragAndDrop, + epaint::text::TextWrapMode, grid::Grid, id::{Id, IdMap}, input_state::{InputState, MultiTouchInfo, PointerState}, diff --git a/crates/egui/src/menu.rs b/crates/egui/src/menu.rs index 84fd1b2e8e3..29441d8a74c 100644 --- a/crates/egui/src/menu.rs +++ b/crates/egui/src/menu.rs @@ -479,11 +479,20 @@ impl SubMenuButton { let button_padding = ui.spacing().button_padding; let total_extra = button_padding + button_padding; let text_available_width = ui.available_width() - total_extra.x; - let text_galley = - text.into_galley(ui, Some(true), text_available_width, text_style.clone()); + let text_galley = text.into_galley( + ui, + Some(TextWrapMode::Wrap), + text_available_width, + text_style.clone(), + ); let icon_available_width = text_available_width - text_galley.size().x; - let icon_galley = icon.into_galley(ui, Some(true), icon_available_width, text_style); + let icon_galley = icon.into_galley( + ui, + Some(TextWrapMode::Wrap), + icon_available_width, + text_style, + ); let text_and_icon_size = Vec2::new( text_galley.size().x + icon_galley.size().x, text_galley.size().y.max(icon_galley.size().y), diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index d2d7ce99188..f2410747dfb 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -182,15 +182,25 @@ pub struct Style { /// The style to use for [`DragValue`] text. pub drag_value_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`. + /// If set, labels, buttons, etc. will use this to determine whether to wrap the text at the + /// right edge of the [`Ui`] they are in. By default, this is `None`. /// - /// * `None`: follow layout - /// * `Some(true)`: default on - /// * `Some(false)`: default off + /// **Note**: this API is deprecated, use `wrap_mode` instead. + /// + /// * `None`: use `wrap_mode` instead + /// * `Some(true)`: wrap mode defaults to [`crate::TextWrapMode::Wrap`] + /// * `Some(false)`: wrap mode defaults to [`crate::TextWrapMode::Extend`] + #[deprecated = "Use wrap_mode instead"] pub wrap: Option, + /// If set, labels, buttons, etc. will use this to determine whether to wrap or truncate the + /// text at the right edge of the [`Ui`] they are in, or to extend it. By default, this is + /// `None`. + /// + /// * `None`: follow layout (with may wrap) + /// * `Some(mode)`: use the specified mode as default + pub wrap_mode: Option, + /// Sizes and distances between widgets pub spacing: Spacing, @@ -1026,12 +1036,14 @@ pub fn default_text_styles() -> BTreeMap { impl Default for Style { fn default() -> Self { + #[allow(deprecated)] Self { override_font_id: None, override_text_style: None, text_styles: default_text_styles(), drag_value_text_style: TextStyle::Button, wrap: None, + wrap_mode: None, spacing: Spacing::default(), interaction: Interaction::default(), visuals: Visuals::default(), @@ -1317,12 +1329,14 @@ use crate::{widgets::*, Ui}; impl Style { pub fn ui(&mut self, ui: &mut crate::Ui) { + #[allow(deprecated)] let Self { override_font_id, override_text_style, text_styles, drag_value_text_style, wrap: _, + wrap_mode: _, spacing, interaction, visuals, diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 7343e9ea723..f3227109e36 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -329,20 +329,45 @@ impl Ui { self.placer.layout() } - /// Should text wrap in this [`Ui`]? + /// Which wrap mode should the text use 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 + /// This is determined first by [`Style::wrap_mode`], and then by the layout of this [`Ui`]. + pub fn wrap_mode(&self) -> TextWrapMode { + #[allow(deprecated)] + if let Some(wrap_mode) = self.style.wrap_mode { + wrap_mode + } + // `wrap` handling for backward compatibility + else if let Some(wrap) = self.style.wrap { + if wrap { + TextWrapMode::Wrap + } else { + TextWrapMode::Extend + } } else if let Some(grid) = self.placer.grid() { - grid.wrap_text() + if grid.wrap_text() { + TextWrapMode::Wrap + } else { + TextWrapMode::Extend + } } else { let layout = self.layout(); - layout.is_vertical() || layout.is_horizontal() && layout.main_wrap() + if layout.is_vertical() || layout.is_horizontal() && layout.main_wrap() { + TextWrapMode::Wrap + } else { + TextWrapMode::Extend + } } } + /// Should text wrap in this [`Ui`]? + /// + /// This is determined first by [`Style::wrap_mode`], and then by the layout of this [`Ui`]. + #[deprecated = "Use `wrap_mode` instead"] + pub fn wrap_text(&self) -> bool { + self.wrap_mode() == TextWrapMode::Wrap + } + /// Create a painter for a sub-region of this Ui. /// /// The clip-rect of the returned [`Painter`] will be the intersection diff --git a/crates/egui/src/widget_text.rs b/crates/egui/src/widget_text.rs index 57643ae4576..29d59ff18d8 100644 --- a/crates/egui/src/widget_text.rs +++ b/crates/egui/src/widget_text.rs @@ -1,8 +1,8 @@ use std::{borrow::Cow, sync::Arc}; use crate::{ - text::LayoutJob, Align, Color32, FontFamily, FontSelection, Galley, Style, TextStyle, Ui, - Visuals, + text::{LayoutJob, TextWrapping}, + Align, Color32, FontFamily, FontSelection, Galley, Style, TextStyle, TextWrapMode, Ui, Visuals, }; /// Text and optional style choices for it. @@ -640,47 +640,39 @@ impl WidgetText { /// Layout with wrap mode based on the containing [`Ui`]. /// - /// wrap: override for [`Ui::wrap_text`]. + /// `wrap_mode`: override for [`Ui::wrap_mode`] pub fn into_galley( self, ui: &Ui, - wrap: Option, + wrap_mode: Option, available_width: f32, fallback_font: impl Into, ) -> Arc { - let wrap = wrap.unwrap_or_else(|| ui.wrap_text()); let valign = ui.layout().vertical_align(); let style = ui.style(); - self.into_galley_impl( - ui.ctx(), - style, - wrap, - available_width, - fallback_font.into(), - valign, - ) + let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode()); + let text_wrapping = TextWrapping::from_wrap_mode_and_width(wrap_mode, available_width); + + self.into_galley_impl(ui.ctx(), style, text_wrapping, fallback_font.into(), valign) } pub fn into_galley_impl( self, ctx: &crate::Context, style: &Style, - wrap: bool, - available_width: f32, + text_wrapping: TextWrapping, fallback_font: FontSelection, default_valign: Align, ) -> Arc { - let wrap_width = if wrap { available_width } else { f32::INFINITY }; - match self { Self::RichText(text) => { let mut layout_job = text.into_layout_job(style, fallback_font, default_valign); - layout_job.wrap.max_width = wrap_width; + layout_job.wrap = text_wrapping; ctx.fonts(|f| f.layout_job(layout_job)) } Self::LayoutJob(mut job) => { - job.wrap.max_width = wrap_width; + job.wrap = text_wrapping; ctx.fonts(|f| f.layout_job(job)) } Self::Galley(galley) => galley, diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 76a62125045..4b5aa6b6bf5 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -23,7 +23,7 @@ pub struct Button<'a> { image: Option>, text: Option, shortcut_text: WidgetText, - wrap: Option, + wrap_mode: Option, /// None means default for interact fill: Option, @@ -58,7 +58,7 @@ impl<'a> Button<'a> { text, image, shortcut_text: Default::default(), - wrap: None, + wrap_mode: None, fill: None, stroke: None, sense: Sense::click(), @@ -70,16 +70,29 @@ impl<'a> Button<'a> { } } - /// If `true`, the text will wrap to stay within the max width of the [`Ui`]. + /// Set the wrap mode for the text. /// - /// By default [`Self::wrap`] will be true in vertical layouts - /// and horizontal layouts with wrapping, - /// and false on non-wrapping horizontal layouts. + /// By default, [`Ui::wrap_mode`] will be used, which can be overridden with [`Style::wrap_mode`]. /// /// Note that any `\n` in the text will always produce a new line. #[inline] - pub fn wrap(mut self, wrap: bool) -> Self { - self.wrap = Some(wrap); + pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self { + self.wrap_mode = Some(wrap_mode); + self + } + + /// Set [`Self::wrap_mode`] to [`TextWrapMode::Wrap`]. + #[inline] + pub fn wrap(mut self) -> Self { + self.wrap_mode = Some(TextWrapMode::Wrap); + + self + } + + /// Set [`Self::wrap_mode`] to [`TextWrapMode::Truncate`]. + #[inline] + pub fn truncate(mut self) -> Self { + self.wrap_mode = Some(TextWrapMode::Truncate); self } @@ -165,7 +178,7 @@ impl Widget for Button<'_> { text, image, shortcut_text, - wrap, + wrap_mode, fill, stroke, sense, @@ -211,9 +224,15 @@ impl Widget for Button<'_> { } let galley = - text.map(|text| text.into_galley(ui, wrap, text_wrap_width, TextStyle::Button)); - let shortcut_galley = (!shortcut_text.is_empty()) - .then(|| shortcut_text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Button)); + text.map(|text| text.into_galley(ui, wrap_mode, text_wrap_width, TextStyle::Button)); + let shortcut_galley = (!shortcut_text.is_empty()).then(|| { + shortcut_text.into_galley( + ui, + Some(TextWrapMode::Extend), + f32::INFINITY, + TextStyle::Button, + ) + }); let mut desired_size = Vec2::ZERO; if image.is_some() { diff --git a/crates/egui/src/widgets/drag_value.rs b/crates/egui/src/widgets/drag_value.rs index 8a7f19028b6..70b957ec151 100644 --- a/crates/egui/src/widgets/drag_value.rs +++ b/crates/egui/src/widgets/drag_value.rs @@ -521,7 +521,7 @@ impl<'a> Widget for DragValue<'a> { RichText::new(format!("{}{}{}", prefix, value_text.clone(), suffix)) .text_style(text_style), ) - .wrap(false) + .wrap_mode(TextWrapMode::Extend) .sense(Sense::click_and_drag()) .min_size(ui.spacing().interact_size); // TODO(emilk): find some more generic solution to `min_size` diff --git a/crates/egui/src/widgets/label.rs b/crates/egui/src/widgets/label.rs index f09a17baeb2..de6cc98dfe2 100644 --- a/crates/egui/src/widgets/label.rs +++ b/crates/egui/src/widgets/label.rs @@ -9,10 +9,11 @@ use self::text_selection::LabelSelectionState; /// Usually it is more convenient to use [`Ui::label`]. /// /// ``` +/// # use egui::TextWrapMode; /// # egui::__run_test_ui(|ui| { /// ui.label("Equivalent"); /// ui.add(egui::Label::new("Equivalent")); -/// ui.add(egui::Label::new("With Options").wrap(false)); +/// ui.add(egui::Label::new("With Options").truncate()); /// ui.label(egui::RichText::new("With formatting").underline()); /// # }); /// ``` @@ -22,8 +23,7 @@ use self::text_selection::LabelSelectionState; #[must_use = "You should put this widget in an ui with `ui.add(widget);`"] pub struct Label { text: WidgetText, - wrap: Option, - truncate: bool, + wrap_mode: Option, sense: Option, selectable: Option, } @@ -32,8 +32,7 @@ impl Label { pub fn new(text: impl Into) -> Self { Self { text: text.into(), - wrap: None, - truncate: false, + wrap_mode: None, sense: None, selectable: None, } @@ -43,37 +42,29 @@ impl Label { self.text.text() } - /// If `true`, the text will wrap to stay within the max width of the [`Ui`]. + /// Set the wrap mode for the text. /// - /// Calling `wrap` will override [`Self::truncate`]. - /// - /// By default [`Self::wrap`] will be `true` in vertical layouts - /// and horizontal layouts with wrapping, - /// and `false` on non-wrapping horizontal layouts. + /// By default, [`Ui::wrap_mode`] will be used, which can be overridden with [`Style::wrap_mode`]. /// /// Note that any `\n` in the text will always produce a new line. - /// - /// You can also use [`crate::Style::wrap`]. #[inline] - pub fn wrap(mut self, wrap: bool) -> Self { - self.wrap = Some(wrap); - self.truncate = false; + pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self { + self.wrap_mode = Some(wrap_mode); self } - /// If `true`, the text will stop at the max width of the [`Ui`], - /// and what doesn't fit will be elided, replaced with `…`. - /// - /// If the text is truncated, the full text will be shown on hover as a tool-tip. - /// - /// Default is `false`, which means the text will expand the parent [`Ui`], - /// or wrap if [`Self::wrap`] is set. - /// - /// Calling `truncate` will override [`Self::wrap`]. + /// Set [`Self::wrap_mode`] to [`TextWrapMode::Wrap`]. + #[inline] + pub fn wrap(mut self) -> Self { + self.wrap_mode = Some(TextWrapMode::Wrap); + + self + } + + /// Set [`Self::wrap_mode`] to [`TextWrapMode::Truncate`]. #[inline] - pub fn truncate(mut self, truncate: bool) -> Self { - self.wrap = None; - self.truncate = truncate; + pub fn truncate(mut self) -> Self { + self.wrap_mode = Some(TextWrapMode::Truncate); self } @@ -156,11 +147,10 @@ impl Label { .text .into_layout_job(ui.style(), FontSelection::Default, valign); - let truncate = self.truncate; - let wrap = !truncate && self.wrap.unwrap_or_else(|| ui.wrap_text()); let available_width = ui.available_width(); - if wrap + let wrap_mode = self.wrap_mode.unwrap_or_else(|| ui.wrap_mode()); + if wrap_mode == TextWrapMode::Wrap && ui.layout().main_dir() == Direction::LeftToRight && ui.layout().main_wrap() && available_width.is_finite() @@ -192,15 +182,8 @@ impl Label { } (pos, galley, response) } else { - if truncate { - layout_job.wrap.max_width = available_width; - layout_job.wrap.max_rows = 1; - layout_job.wrap.break_anywhere = true; - } else if wrap { - layout_job.wrap.max_width = available_width; - } else { - layout_job.wrap.max_width = f32::INFINITY; - }; + layout_job.wrap = + text::TextWrapping::from_wrap_mode_and_width(wrap_mode, available_width); if ui.is_grid() { // TODO(emilk): remove special Grid hacks like these diff --git a/crates/egui/src/widgets/progress_bar.rs b/crates/egui/src/widgets/progress_bar.rs index 99c768bf9af..5f5d2170969 100644 --- a/crates/egui/src/widgets/progress_bar.rs +++ b/crates/egui/src/widgets/progress_bar.rs @@ -183,7 +183,12 @@ impl Widget for ProgressBar { format!("{}%", (progress * 100.0) as usize).into() } }; - let galley = text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Button); + let galley = text.into_galley( + ui, + Some(TextWrapMode::Extend), + f32::INFINITY, + TextStyle::Button, + ); let text_pos = outer_rect.left_center() - Vec2::new(0.0, galley.size().y / 2.0) + vec2(ui.spacing().item_spacing.x, 0.0); let text_color = visuals diff --git a/crates/egui/src/widgets/slider.rs b/crates/egui/src/widgets/slider.rs index 644694cbe81..61aa56b8548 100644 --- a/crates/egui/src/widgets/slider.rs +++ b/crates/egui/src/widgets/slider.rs @@ -904,7 +904,8 @@ impl<'a> Slider<'a> { }; if !self.text.is_empty() { - let label_response = ui.add(Label::new(self.text.clone()).wrap(false)); + let label_response = + ui.add(Label::new(self.text.clone()).wrap_mode(TextWrapMode::Extend)); // The slider already has an accessibility label via widget info, // but sometimes it's useful for a screen reader to know // that a piece of text is a label for another widget, diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 4458fd031f7..164f928bbf0 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -665,9 +665,19 @@ impl<'t> TextEdit<'t> { let hint_text_color = ui.visuals().weak_text_color(); let hint_text_font_id = hint_text_font.unwrap_or(font_id.into()); let galley = if multiline { - hint_text.into_galley(ui, Some(true), desired_inner_size.x, hint_text_font_id) + hint_text.into_galley( + ui, + Some(TextWrapMode::Wrap), + desired_inner_size.x, + hint_text_font_id, + ) } else { - hint_text.into_galley(ui, Some(false), f32::INFINITY, hint_text_font_id) + hint_text.into_galley( + ui, + Some(TextWrapMode::Extend), + f32::INFINITY, + hint_text_font_id, + ) }; painter.galley(rect.min, galley, hint_text_color); } diff --git a/crates/egui_demo_app/src/backend_panel.rs b/crates/egui_demo_app/src/backend_panel.rs index 2176d57a8b6..f82094d71fa 100644 --- a/crates/egui_demo_app/src/backend_panel.rs +++ b/crates/egui_demo_app/src/backend_panel.rs @@ -180,7 +180,7 @@ fn integration_ui(ui: &mut egui::Ui, _frame: &mut eframe::Frame) { #[cfg(target_arch = "wasm32")] ui.collapsing("Web info (location)", |ui| { - ui.style_mut().wrap = Some(false); + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); ui.monospace(format!("{:#?}", _frame.info().web_info.location)); }); diff --git a/crates/egui_demo_lib/src/demo/demo_app_windows.rs b/crates/egui_demo_lib/src/demo/demo_app_windows.rs index 0910df4ef56..1e34c464759 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -335,7 +335,7 @@ fn file_menu_button(ui: &mut Ui) { ui.menu_button("File", |ui| { ui.set_min_width(220.0); - ui.style_mut().wrap = Some(false); + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); // On the web the browser controls the zoom #[cfg(not(target_arch = "wasm32"))] diff --git a/crates/egui_demo_lib/src/demo/frame_demo.rs b/crates/egui_demo_lib/src/demo/frame_demo.rs index 0027a98d9da..bd1661827f9 100644 --- a/crates/egui_demo_lib/src/demo/frame_demo.rs +++ b/crates/egui_demo_lib/src/demo/frame_demo.rs @@ -60,7 +60,7 @@ impl super::View for FrameDemo { .rounding(ui.visuals().widgets.noninteractive.rounding) .show(ui, |ui| { self.frame.show(ui, |ui| { - ui.style_mut().wrap = Some(false); + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); ui.label(egui::RichText::new("Content").color(egui::Color32::WHITE)); }); }); diff --git a/crates/egui_demo_lib/src/demo/layout_test.rs b/crates/egui_demo_lib/src/demo/layout_test.rs index a5726c7ab2f..2bc1fd0d4e9 100644 --- a/crates/egui_demo_lib/src/demo/layout_test.rs +++ b/crates/egui_demo_lib/src/demo/layout_test.rs @@ -176,7 +176,7 @@ impl LayoutTest { } fn demo_ui(ui: &mut Ui) { - ui.add(egui::Label::new("Wrapping text followed by example widgets:").wrap(true)); + ui.add(egui::Label::new("Wrapping text followed by example widgets:").wrap()); let mut dummy = false; ui.checkbox(&mut dummy, "checkbox"); ui.radio_value(&mut dummy, false, "radio"); diff --git a/crates/egui_demo_lib/src/demo/misc_demo_window.rs b/crates/egui_demo_lib/src/demo/misc_demo_window.rs index f3c7a8dcfec..6b73fe4048b 100644 --- a/crates/egui_demo_lib/src/demo/misc_demo_window.rs +++ b/crates/egui_demo_lib/src/demo/misc_demo_window.rs @@ -223,7 +223,7 @@ fn label_ui(ui: &mut egui::Ui) { egui::Label::new( "Labels containing long text can be set to elide the text that doesn't fit on a single line using `Label::elide`. When hovered, the label will show the full text.", ) - .truncate(true), + .truncate(), ); } diff --git a/crates/egui_demo_lib/src/demo/pan_zoom.rs b/crates/egui_demo_lib/src/demo/pan_zoom.rs index 40323346887..6ee75e3039a 100644 --- a/crates/egui_demo_lib/src/demo/pan_zoom.rs +++ b/crates/egui_demo_lib/src/demo/pan_zoom.rs @@ -1,4 +1,5 @@ use egui::emath::TSTransform; +use egui::TextWrapMode; #[derive(Clone, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -122,7 +123,7 @@ impl super::View for PanZoom { .stroke(ui.ctx().style().visuals.window_stroke) .fill(ui.style().visuals.panel_fill) .show(ui, |ui| { - ui.style_mut().wrap = Some(false); + ui.style_mut().wrap_mode = Some(TextWrapMode::Extend); callback(ui, self) }); }) diff --git a/crates/egui_demo_lib/src/demo/plot_demo.rs b/crates/egui_demo_lib/src/demo/plot_demo.rs index 12e45aa0f09..17eceba1855 100644 --- a/crates/egui_demo_lib/src/demo/plot_demo.rs +++ b/crates/egui_demo_lib/src/demo/plot_demo.rs @@ -200,7 +200,7 @@ impl LineDemo { }); ui.vertical(|ui| { - ui.style_mut().wrap = Some(false); + ui.style_mut().wrap_mode = Some(TextWrapMode::Extend); ui.checkbox(animate, "Animate"); ui.checkbox(square, "Square view") .on_hover_text("Always keep the viewport square."); diff --git a/crates/egui_demo_lib/src/demo/scrolling.rs b/crates/egui_demo_lib/src/demo/scrolling.rs index 71d47f9d84c..ec6ca5e1a98 100644 --- a/crates/egui_demo_lib/src/demo/scrolling.rs +++ b/crates/egui_demo_lib/src/demo/scrolling.rs @@ -82,7 +82,7 @@ impl super::View for Scrolling { } ScrollDemo::Bidirectional => { egui::ScrollArea::both().show(ui, |ui| { - ui.style_mut().wrap = Some(false); + ui.style_mut().wrap_mode = Some(TextWrapMode::Extend); for _ in 0..100 { ui.label(crate::LOREM_IPSUM); } diff --git a/crates/egui_demo_lib/src/demo/table_demo.rs b/crates/egui_demo_lib/src/demo/table_demo.rs index c7029d44e49..ec6fa58c349 100644 --- a/crates/egui_demo_lib/src/demo/table_demo.rs +++ b/crates/egui_demo_lib/src/demo/table_demo.rs @@ -1,4 +1,4 @@ -use egui::TextStyle; +use egui::{TextStyle, TextWrapMode}; #[derive(PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -196,7 +196,7 @@ impl TableDemo { ui.label(long_text(row_index)); }); row.col(|ui| { - ui.style_mut().wrap = Some(false); + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); if is_thick { ui.heading("Extra thick row"); } else { @@ -227,7 +227,8 @@ impl TableDemo { }); row.col(|ui| { ui.add( - egui::Label::new("Thousands of rows of even height").wrap(false), + egui::Label::new("Thousands of rows of even height") + .wrap_mode(TextWrapMode::Extend), ); }); @@ -253,7 +254,7 @@ impl TableDemo { ui.label(long_text(row_index)); }); row.col(|ui| { - ui.style_mut().wrap = Some(false); + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); if thick_row(row_index) { ui.heading("Extra thick row"); } else { diff --git a/crates/egui_demo_lib/src/demo/widget_gallery.rs b/crates/egui_demo_lib/src/demo/widget_gallery.rs index d68af17db80..6f5d26f5e57 100644 --- a/crates/egui_demo_lib/src/demo/widget_gallery.rs +++ b/crates/egui_demo_lib/src/demo/widget_gallery.rs @@ -172,7 +172,7 @@ impl WidgetGallery { egui::ComboBox::from_label("Take your pick") .selected_text(format!("{radio:?}")) .show_ui(ui, |ui| { - ui.style_mut().wrap = Some(false); + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); ui.set_min_width(60.0); ui.selectable_value(radio, Enum::First, "First"); ui.selectable_value(radio, Enum::Second, "Second"); diff --git a/crates/egui_plot/src/axis.rs b/crates/egui_plot/src/axis.rs index 059349d6673..3827307a2ae 100644 --- a/crates/egui_plot/src/axis.rs +++ b/crates/egui_plot/src/axis.rs @@ -3,7 +3,7 @@ use std::{fmt::Debug, ops::RangeInclusive, sync::Arc}; use egui::{ emath::{remap_clamp, round_to_decimals, Rot2}, epaint::TextShape, - Pos2, Rangef, Rect, Response, Sense, TextStyle, Ui, Vec2, WidgetText, + Pos2, Rangef, Rect, Response, Sense, TextStyle, TextWrapMode, Ui, Vec2, WidgetText, }; use super::{transform::PlotTransform, GridMark}; @@ -264,7 +264,12 @@ impl<'a> AxisWidget<'a> { { let text = self.hints.label; - let galley = text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Body); + let galley = text.into_galley( + ui, + Some(TextWrapMode::Extend), + f32::INFINITY, + TextStyle::Body, + ); let text_color = visuals .override_text_color .unwrap_or_else(|| ui.visuals().text_color()); diff --git a/crates/egui_plot/src/items/mod.rs b/crates/egui_plot/src/items/mod.rs index 0470d955965..8e31c1fb610 100644 --- a/crates/egui_plot/src/items/mod.rs +++ b/crates/egui_plot/src/items/mod.rs @@ -850,10 +850,12 @@ impl PlotItem for Text { self.color }; - let galley = - self.text - .clone() - .into_galley(ui, Some(false), f32::INFINITY, TextStyle::Small); + let galley = self.text.clone().into_galley( + ui, + Some(egui::TextWrapMode::Extend), + f32::INFINITY, + TextStyle::Small, + ); let pos = transform.position_from_point(&self.position); let rect = self.anchor.anchor_size(pos, galley.size()); diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index dec2cb29057..c14348ecf29 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -319,6 +319,24 @@ impl TextFormat { // ---------------------------------------------------------------------------- +/// How to wrap and elide text. +/// +/// This enum is used in high-level APIs where providing a [`TextWrapping`] is too verbose. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum TextWrapMode { + /// The text should expand the `Ui` size when reaching its boundary. + Extend, + + /// The text should wrap to the next line when reaching the `Ui` boundary. + Wrap, + + /// The text should be elided using "…" when reaching the `Ui` boundary. + /// + /// Note that using [`TextWrapping`] and [`LayoutJob`] offers more control over the elision. + Truncate, +} + /// Controls the text wrapping and elision of a [`LayoutJob`]. #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -335,7 +353,7 @@ pub struct TextWrapping { /// Maximum amount of rows the text galley should have. /// - /// If this limit is reached, text will be truncated and + /// If this limit is reached, text will be truncated /// and [`Self::overflow_character`] appended to the final row. /// You can detect this by checking [`Galley::elided`]. /// @@ -394,6 +412,15 @@ impl Default for TextWrapping { } impl TextWrapping { + /// Create a [`TextWrapping`] from a [`TextWrapMode`] and an available width. + pub fn from_wrap_mode_and_width(mode: TextWrapMode, max_width: f32) -> Self { + match mode { + TextWrapMode::Extend => Self::no_max_width(), + TextWrapMode::Wrap => Self::wrap_at_width(max_width), + TextWrapMode::Truncate => Self::truncate_at_width(max_width), + } + } + /// A row can be as long as it need to be. pub fn no_max_width() -> Self { Self { @@ -402,6 +429,14 @@ impl TextWrapping { } } + /// A row can be at most `max_width` wide but can wrap in any number of lines. + pub fn wrap_at_width(max_width: f32) -> Self { + Self { + max_width, + ..Default::default() + } + } + /// Elide text that doesn't fit within the given width, replaced with `…`. pub fn truncate_at_width(max_width: f32) -> Self { Self {