Skip to content

Commit

Permalink
Add support for text truncation to egui::Style (emilk#4556)
Browse files Browse the repository at this point in the history
* Closes emilk#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 <[email protected]>
  • Loading branch information
2 people authored and hacknus committed Oct 30, 2024
1 parent c335bee commit 39f67a0
Show file tree
Hide file tree
Showing 29 changed files with 281 additions and 162 deletions.
8 changes: 6 additions & 2 deletions crates/egui/src/containers/collapsing_header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
90 changes: 49 additions & 41 deletions crates/egui/src/containers/combo_box.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand All @@ -53,7 +53,7 @@ impl ComboBox {
width: None,
height: None,
icon: None,
wrap_enabled: false,
wrap_mode: None,
}
}

Expand All @@ -67,7 +67,7 @@ impl ComboBox {
width: None,
height: None,
icon: None,
wrap_enabled: false,
wrap_mode: None,
}
}

Expand All @@ -80,7 +80,7 @@ impl ComboBox {
width: None,
height: None,
icon: None,
wrap_enabled: false,
wrap_mode: None,
}
}

Expand Down Expand Up @@ -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
}

Expand All @@ -178,7 +197,7 @@ impl ComboBox {
width,
height,
icon,
wrap_enabled,
wrap_mode,
} = self;

let button_id = ui.make_persistent_id(id_source);
Expand All @@ -190,7 +209,7 @@ impl ComboBox {
selected_text,
menu_contents,
icon,
wrap_enabled,
wrap_mode,
(width, height),
);
if let Some(label) = label {
Expand Down Expand Up @@ -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");
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion crates/egui/src/containers/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
6 changes: 2 additions & 4 deletions crates/egui/src/debug_text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down Expand Up @@ -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,
);
Expand Down
2 changes: 1 addition & 1 deletion crates/egui/src/introspection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 3 additions & 1 deletion crates/egui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@
//! ## Code snippets
//!
//! ```
//! # use egui::TextWrapMode;
//! # egui::__run_test_ui(|ui| {
//! # let mut some_bool = true;
//! // Miscellaneous tips and tricks
Expand All @@ -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
Expand Down Expand Up @@ -451,6 +452,7 @@ pub use {
Key,
},
drag_and_drop::DragAndDrop,
epaint::text::TextWrapMode,
grid::Grid,
id::{Id, IdMap},
input_state::{InputState, MultiTouchInfo, PointerState},
Expand Down
15 changes: 12 additions & 3 deletions crates/egui/src/menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
26 changes: 20 additions & 6 deletions crates/egui/src/style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 39f67a0

Please sign in to comment.