Skip to content

Commit

Permalink
Add option to truncate text at wrap width (#3244)
Browse files Browse the repository at this point in the history
* Add option to clip text to wrap width

* Spelling

* Better naming, and report back wether the text was elided

* Improve docstrings

* Simplify

* Fix max_rows with multiple paragraphs

* Add note

* Typos

* fix doclink

* Add `Label::elide`

* Label: show full non-elided text on hover

* Add demo of `Label::elide`

* Call it `Label::truncate`

* Clarify limitations of `break_anywhere`

* Better docstrings
  • Loading branch information
emilk authored Aug 14, 2023
1 parent 1023f93 commit a3ae81c
Show file tree
Hide file tree
Showing 5 changed files with 276 additions and 98 deletions.
2 changes: 1 addition & 1 deletion crates/egui/src/widget_text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -676,7 +676,7 @@ impl WidgetTextGalley {
self.galley.size()
}

/// Size of the laid out text.
/// The full, non-elided text of the input job.
#[inline]
pub fn text(&self) -> &str {
self.galley.text()
Expand Down
42 changes: 38 additions & 4 deletions crates/egui/src/widgets/label.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@ use crate::{widget_text::WidgetTextGalley, *};
/// ui.label(egui::RichText::new("With formatting").underline());
/// # });
/// ```
///
/// For full control of the text you can use [`crate::text::LayoutJob`]
/// as argument to [`Self::new`].
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
pub struct Label {
text: WidgetText,
wrap: Option<bool>,
truncate: bool,
sense: Option<Sense>,
}

Expand All @@ -24,6 +28,7 @@ impl Label {
Self {
text: text.into(),
wrap: None,
truncate: false,
sense: None,
}
}
Expand All @@ -34,6 +39,8 @@ impl Label {

/// If `true`, the text will wrap to stay within the max width of the [`Ui`].
///
/// 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.
Expand All @@ -44,6 +51,23 @@ impl Label {
#[inline]
pub fn wrap(mut self, wrap: bool) -> Self {
self.wrap = Some(wrap);
self.truncate = false;
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`].
#[inline]
pub fn truncate(mut self, truncate: bool) -> Self {
self.wrap = None;
self.truncate = truncate;
self
}

Expand Down Expand Up @@ -98,10 +122,11 @@ impl Label {
.text
.into_text_job(ui.style(), FontSelection::Default, valign);

let should_wrap = self.wrap.unwrap_or_else(|| ui.wrap_text());
let truncate = self.truncate;
let wrap = !truncate && self.wrap.unwrap_or_else(|| ui.wrap_text());
let available_width = ui.available_width();

if should_wrap
if wrap
&& ui.layout().main_dir() == Direction::LeftToRight
&& ui.layout().main_wrap()
&& available_width.is_finite()
Expand Down Expand Up @@ -138,7 +163,11 @@ impl Label {
}
(pos, text_galley, response)
} else {
if should_wrap {
if truncate {
text_job.job.wrap.max_width = available_width;
text_job.job.wrap.max_rows = 1;
text_job.job.wrap.break_anywhere = true;
} else if wrap {
text_job.job.wrap.max_width = available_width;
} else {
text_job.job.wrap.max_width = f32::INFINITY;
Expand Down Expand Up @@ -167,9 +196,14 @@ impl Label {

impl Widget for Label {
fn ui(self, ui: &mut Ui) -> Response {
let (pos, text_galley, response) = self.layout_in_ui(ui);
let (pos, text_galley, mut response) = self.layout_in_ui(ui);
response.widget_info(|| WidgetInfo::labeled(WidgetType::Label, text_galley.text()));

if text_galley.galley.elided {
// Show the full (non-elided) text on hover:
response = response.on_hover_text(text_galley.text());
}

if ui.is_rect_visible(response.rect) {
let response_color = ui.style().interact(&response).text_color();

Expand Down
175 changes: 109 additions & 66 deletions crates/egui_demo_lib/src/demo/misc_demo_window.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use super::*;
use crate::LOREM_IPSUM;
use egui::{epaint::text::TextWrapping, *};

/// Showcase some ui code
Expand All @@ -8,9 +7,7 @@ use egui::{epaint::text::TextWrapping, *};
pub struct MiscDemoWindow {
num_columns: usize,

break_anywhere: bool,
max_rows: usize,
overflow_character: Option<char>,
text_break: TextBreakDemo,

widgets: Widgets,
colors: ColorWidgets,
Expand All @@ -27,9 +24,7 @@ impl Default for MiscDemoWindow {
MiscDemoWindow {
num_columns: 2,

max_rows: 2,
break_anywhere: false,
overflow_character: Some('…'),
text_break: Default::default(),

widgets: Default::default(),
colors: Default::default(),
Expand Down Expand Up @@ -61,21 +56,27 @@ impl View for MiscDemoWindow {
fn ui(&mut self, ui: &mut Ui) {
ui.set_min_width(250.0);

CollapsingHeader::new("Widgets")
CollapsingHeader::new("Label")
.default_open(true)
.show(ui, |ui| {
label_ui(ui);
});

CollapsingHeader::new("Misc widgets")
.default_open(false)
.show(ui, |ui| {
self.widgets.ui(ui);
});

CollapsingHeader::new("Text layout")
.default_open(false)
.show(ui, |ui| {
text_layout_ui(
ui,
&mut self.max_rows,
&mut self.break_anywhere,
&mut self.overflow_character,
);
text_layout_demo(ui);
ui.separator();
self.text_break.ui(ui);
ui.vertical_centered(|ui| {
ui.add(crate::egui_github_link_file_line!());
});
});

CollapsingHeader::new("Colors")
Expand Down Expand Up @@ -177,6 +178,43 @@ impl View for MiscDemoWindow {

// ----------------------------------------------------------------------------

fn label_ui(ui: &mut egui::Ui) {
ui.vertical_centered(|ui| {
ui.add(crate::egui_github_link_file_line!());
});

ui.horizontal_wrapped(|ui| {
// Trick so we don't have to add spaces in the text below:
let width = ui.fonts(|f|f.glyph_width(&TextStyle::Body.resolve(ui.style()), ' '));
ui.spacing_mut().item_spacing.x = width;

ui.label(RichText::new("Text can have").color(Color32::from_rgb(110, 255, 110)));
ui.colored_label(Color32::from_rgb(128, 140, 255), "color"); // Shortcut version
ui.label("and tooltips.").on_hover_text(
"This is a multiline tooltip that demonstrates that you can easily add tooltips to any element.\nThis is the second line.\nThis is the third.",
);

ui.label("You can mix in other widgets into text, like");
let _ = ui.small_button("this button");
ui.label(".");

ui.label("The default font supports all latin and cyrillic characters (ИÅđ…), common math symbols (∫√∞²⅓…), and many emojis (💓🌟🖩…).")
.on_hover_text("There is currently no support for right-to-left languages.");
ui.label("See the 🔤 Font Book for more!");

ui.monospace("There is also a monospace font.");
});

ui.add(
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),
);
}

// ----------------------------------------------------------------------------

#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct Widgets {
Expand All @@ -200,28 +238,6 @@ impl Widgets {
ui.add(crate::egui_github_link_file_line!());
});

ui.horizontal_wrapped(|ui| {
// Trick so we don't have to add spaces in the text below:
let width = ui.fonts(|f|f.glyph_width(&TextStyle::Body.resolve(ui.style()), ' '));
ui.spacing_mut().item_spacing.x = width;

ui.label(RichText::new("Text can have").color(Color32::from_rgb(110, 255, 110)));
ui.colored_label(Color32::from_rgb(128, 140, 255), "color"); // Shortcut version
ui.label("and tooltips.").on_hover_text(
"This is a multiline tooltip that demonstrates that you can easily add tooltips to any element.\nThis is the second line.\nThis is the third.",
);

ui.label("You can mix in other widgets into text, like");
let _ = ui.small_button("this button");
ui.label(".");

ui.label("The default font supports all latin and cyrillic characters (ИÅđ…), common math symbols (∫√∞²⅓…), and many emojis (💓🌟🖩…).")
.on_hover_text("There is currently no support for right-to-left languages.");
ui.label("See the 🔤 Font Book for more!");

ui.monospace("There is also a monospace font.");
});

let tooltip_ui = |ui: &mut Ui| {
ui.heading("The name of the tooltip");
ui.horizontal(|ui| {
Expand Down Expand Up @@ -473,12 +489,7 @@ impl Tree {

// ----------------------------------------------------------------------------

fn text_layout_ui(
ui: &mut egui::Ui,
max_rows: &mut usize,
break_anywhere: &mut bool,
overflow_character: &mut Option<char>,
) {
fn text_layout_demo(ui: &mut Ui) {
use egui::text::LayoutJob;

let mut job = LayoutJob::default();
Expand Down Expand Up @@ -632,32 +643,64 @@ fn text_layout_ui(
);

ui.label(job);
}

ui.separator();
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
struct TextBreakDemo {
break_anywhere: bool,
max_rows: usize,
overflow_character: Option<char>,
}

ui.horizontal(|ui| {
ui.add(DragValue::new(max_rows));
ui.label("Max rows");
});
ui.checkbox(break_anywhere, "Break anywhere");
ui.horizontal(|ui| {
ui.selectable_value(overflow_character, None, "None");
ui.selectable_value(overflow_character, Some('…'), "…");
ui.selectable_value(overflow_character, Some('—'), "—");
ui.selectable_value(overflow_character, Some('-'), " - ");
ui.label("Overflow character");
});
impl Default for TextBreakDemo {
fn default() -> Self {
Self {
max_rows: 1,
break_anywhere: true,
overflow_character: Some('…'),
}
}
}

let mut job = LayoutJob::single_section(LOREM_IPSUM.to_owned(), TextFormat::default());
job.wrap = TextWrapping {
max_rows: *max_rows,
break_anywhere: *break_anywhere,
overflow_character: *overflow_character,
..Default::default()
};
ui.label(job);
impl TextBreakDemo {
pub fn ui(&mut self, ui: &mut Ui) {
let Self {
break_anywhere,
max_rows,
overflow_character,
} = self;

ui.vertical_centered(|ui| {
ui.add(crate::egui_github_link_file_line!());
});
use egui::text::LayoutJob;

ui.horizontal(|ui| {
ui.add(DragValue::new(max_rows));
ui.label("Max rows");
});

ui.horizontal(|ui| {
ui.label("Line-break:");
ui.radio_value(break_anywhere, false, "word boundaries");
ui.radio_value(break_anywhere, true, "anywhere");
});

ui.horizontal(|ui| {
ui.selectable_value(overflow_character, None, "None");
ui.selectable_value(overflow_character, Some('…'), "…");
ui.selectable_value(overflow_character, Some('—'), "—");
ui.selectable_value(overflow_character, Some('-'), " - ");
ui.label("Overflow character");
});

let mut job =
LayoutJob::single_section(crate::LOREM_IPSUM_LONG.to_owned(), TextFormat::default());
job.wrap = TextWrapping {
max_rows: *max_rows,
break_anywhere: *break_anywhere,
overflow_character: *overflow_character,
..Default::default()
};

ui.label(job); // `Label` overrides some of the wrapping settings, e.g. wrap width
}
}
Loading

0 comments on commit a3ae81c

Please sign in to comment.