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:
parent
4b59c6d414
commit
bcd91f27a1
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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"))]
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
});
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue