Add support for text truncation to `egui::Style` (#4556)

* Closes #4473

This PR introduce `Style::wrap_mode`, which adds support for text
truncation in addition to text wrapping. This PR also update some width
calculation of the ComboBox.

#### Core

- Add `egui::TextWrapMode` (pure enum with `Extend`, `Wrap`, `Truncate`)
- Add `Style::wrap_mode: Option<tTextWrapMode>`
- **DEPRECATED**: `Style::wrap`, use `Style::wrap_mode` instead.
- Add `Ui::wrap_mode()` to return the wrap mode to use in the current
ui. If specified in `Style`, return it. Otherwise, return
`TextWrapMode::Wrap` for vertical layout and wrapping horizontal layout,
and `TextWrapMode::Extend` otherwise.
- **DEPRECATED**: `Ui::wrap_text()`, use `Ui::wrap_mode` instead.

#### Widget

- Update the width calculation of the `ComboBox` button (_not_ its popup
menu).
- Now, `ComboBox::width()` (defaulting to `Spacing::combo_width`) is
always considered a minimum width and will extend the `Ui`, regardless
of the selected text width and wrap mode.
- Introduce `ComboBox::wrap_mode`, which overrides `Ui::wrap_mode` for
the selected text layout.
- Note: since `ComboBox` uses `ui.horizontal` internally, the default
wrap mode is always `TextWrapMode::Extend`, regardless of the caller's
`Ui`'s layout.
- The `ComboBox` button no longer extend to `ui.available_width()` with
wrapping is enabled.
- **BREAKING**: `ComboBox::wrap()` no longer has a `bool` argument and
is now a short-hand for `ComboBox::wrap_mode(TextWrapMode::Wrap)`.
- Added `ComboBox::truncate()` as short-hand for
`ComboBox::wrap_mode(TextWrapMode::Truncate)`.
- Update `Label`
  - Add `Label::wrap_mode()` to specify the text wrap mode.
- **BREAKING**: `Label::wrap()` no longer has a `bool` argument and is
now a short-hand for `Label::wrap_mode(TextWrapMode::Wrap)`.
- **BREAKING**: `Label::truncate()` no longer has a `bool` argument and
is now a short-hand for `Label::wrap_mode(TextWrapMode::Truncate)`.
- Update `Button`
  - Add `Button::wrap_mode()` to specify the text wrap mode.
- **BREAKING**: `Button::wrap()` no longer has a `bool` argument and is
now a short-hand for `Button::wrap_mode(TextWrapMode::Wrap)`.
- Added `Button::truncate()` as short-hand for
`Button::wrap_mode(TextWrapMode::Truncate)`.

#### Low-level

- **BREAKING**: `WidgetText::into_galley()` now takes an
`Option<TextWrapMode>` instead of a `Option<bool>` argument.
- **BREAKING**: `WidgetText::into_galley_impl(()` now takes a
`TextWrapping` argument instead of `wrap: bool` and `availalbe_width:
f32` arguments.

---------

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
Antoine Beyeler 2024-05-28 13:10:41 +02:00 committed by GitHub
parent 4b59c6d414
commit bcd91f27a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 281 additions and 162 deletions

View File

@ -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();

View File

@ -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<f32>,
height: Option<f32>,
icon: Option<IconPainter>,
wrap_enabled: bool,
wrap_mode: Option<TextWrapMode>,
}
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<dyn FnOnce(&mut Ui) -> R + 'c>,
icon: Option<IconPainter>,
wrap_enabled: bool,
wrap_mode: Option<TextWrapMode>,
(width, height): (Option<f32>, Option<f32>),
) -> InnerResponse<Option<R>> {
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 {
// 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;
// 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
};
let galley =
selected_text.into_galley(ui, Some(wrap_enabled), wrap_width, TextStyle::Button);
// The width necessary to contain the whole widget with the currently selected value's text.
let width = if wrap_enabled {
full_minimum_width
} 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 (_, rect) = ui.allocate_space(Vec2::new(width, height));
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(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

View File

@ -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):

View File

@ -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,
);

View File

@ -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 {

View File

@ -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},

View File

@ -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),

View File

@ -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<bool>,
/// 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<crate::TextWrapMode>,
/// Sizes and distances between widgets
pub spacing: Spacing,
@ -1026,12 +1036,14 @@ pub fn default_text_styles() -> BTreeMap<TextStyle, FontId> {
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,

View File

@ -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

View File

@ -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<bool>,
wrap_mode: Option<TextWrapMode>,
available_width: f32,
fallback_font: impl Into<FontSelection>,
) -> Arc<Galley> {
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<Galley> {
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,

View File

@ -23,7 +23,7 @@ pub struct Button<'a> {
image: Option<Image<'a>>,
text: Option<WidgetText>,
shortcut_text: WidgetText,
wrap: Option<bool>,
wrap_mode: Option<TextWrapMode>,
/// None means default for interact
fill: Option<Color32>,
@ -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() {

View File

@ -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`

View File

@ -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<bool>,
truncate: bool,
wrap_mode: Option<TextWrapMode>,
sense: Option<Sense>,
selectable: Option<bool>,
}
@ -32,8 +32,7 @@ impl Label {
pub fn new(text: impl Into<WidgetText>) -> 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 truncate(mut self, truncate: bool) -> Self {
self.wrap = None;
self.truncate = truncate;
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
}
@ -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

View File

@ -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

View File

@ -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,

View File

@ -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);
}

View File

@ -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));
});

View File

@ -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"))]

View File

@ -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));
});
});

View File

@ -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");

View File

@ -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(),
);
}

View File

@ -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)
});
})

View File

@ -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.");

View File

@ -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);
}

View File

@ -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 {

View File

@ -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");

View File

@ -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());

View File

@ -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());

View File

@ -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 {