From 71e0b0859cd3dd9cf362f39345ed6a66c3889032 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 6 May 2025 17:35:56 +0200 Subject: [PATCH] Make `WidgetText` smaller and faster (#6903) * In preparation of #5830, this should reduce the performance impact of that PR --------- Co-authored-by: Emil Ernerfeldt --- crates/egui/src/widget_text.rs | 202 ++++++++++++-------- crates/egui/src/widgets/label.rs | 14 +- crates/egui_demo_app/src/wrap_app.rs | 2 +- crates/epaint/src/text/text_layout_types.rs | 15 ++ 4 files changed, 150 insertions(+), 83 deletions(-) diff --git a/crates/egui/src/widget_text.rs b/crates/egui/src/widget_text.rs index e66cb1bc..d9f98859 100644 --- a/crates/egui/src/widget_text.rs +++ b/crates/egui/src/widget_text.rs @@ -1,6 +1,7 @@ use std::{borrow::Cow, sync::Arc}; use emath::GuiRounding as _; +use epaint::text::TextFormat; use crate::{ text::{LayoutJob, TextWrapping}, @@ -488,7 +489,16 @@ impl RichText { /// which will be replaced with a color chosen by the widget that paints the text. #[derive(Clone)] pub enum WidgetText { - RichText(RichText), + /// Plain unstyled text. + /// + /// We have this as a special case, as it is the common-case, + /// and it uses less memory than [`Self::RichText`]. + Text(String), + + /// Text and optional style choices for it. + /// + /// Prefer [`Self::Text`] if there is no styling, as it will be faster. + RichText(Arc), /// Use this [`LayoutJob`] when laying out the text. /// @@ -502,7 +512,7 @@ pub enum WidgetText { /// /// You can color the text however you want, or use [`Color32::PLACEHOLDER`] /// which will be replaced with a color chosen by the widget that paints the text. - LayoutJob(LayoutJob), + LayoutJob(Arc), /// Use exactly this galley when painting the text. /// @@ -513,7 +523,7 @@ pub enum WidgetText { impl Default for WidgetText { fn default() -> Self { - Self::RichText(RichText::default()) + Self::Text(String::new()) } } @@ -521,6 +531,7 @@ impl WidgetText { #[inline] pub fn is_empty(&self) -> bool { match self { + Self::Text(text) => text.is_empty(), Self::RichText(text) => text.is_empty(), Self::LayoutJob(job) => job.is_empty(), Self::Galley(galley) => galley.is_empty(), @@ -530,21 +541,36 @@ impl WidgetText { #[inline] pub fn text(&self) -> &str { match self { + Self::Text(text) => text, Self::RichText(text) => text.text(), Self::LayoutJob(job) => &job.text, Self::Galley(galley) => galley.text(), } } + /// Map the contents based on the provided closure. + /// + /// - [`Self::Text`] => convert to [`RichText`] and call f + /// - [`Self::RichText`] => call f + /// - else do nothing + #[must_use] + fn map_rich_text(self, f: F) -> Self + where + F: FnOnce(RichText) -> RichText, + { + match self { + Self::Text(text) => Self::RichText(Arc::new(f(RichText::new(text)))), + Self::RichText(text) => Self::RichText(Arc::new(f(Arc::unwrap_or_clone(text)))), + other => other, + } + } + /// Override the [`TextStyle`] if, and only if, this is a [`RichText`]. /// /// Prefer using [`RichText`] directly! #[inline] pub fn text_style(self, text_style: TextStyle) -> Self { - match self { - Self::RichText(text) => Self::RichText(text.text_style(text_style)), - Self::LayoutJob(_) | Self::Galley(_) => self, - } + self.map_rich_text(|text| text.text_style(text_style)) } /// Set the [`TextStyle`] unless it has already been set @@ -552,10 +578,7 @@ impl WidgetText { /// Prefer using [`RichText`] directly! #[inline] pub fn fallback_text_style(self, text_style: TextStyle) -> Self { - match self { - Self::RichText(text) => Self::RichText(text.fallback_text_style(text_style)), - Self::LayoutJob(_) | Self::Galley(_) => self, - } + self.map_rich_text(|text| text.fallback_text_style(text_style)) } /// Override text color if, and only if, this is a [`RichText`]. @@ -563,111 +586,85 @@ impl WidgetText { /// Prefer using [`RichText`] directly! #[inline] pub fn color(self, color: impl Into) -> Self { - match self { - Self::RichText(text) => Self::RichText(text.color(color)), - Self::LayoutJob(_) | Self::Galley(_) => self, - } + self.map_rich_text(|text| text.color(color)) } /// Prefer using [`RichText`] directly! + #[inline] pub fn heading(self) -> Self { - match self { - Self::RichText(text) => Self::RichText(text.heading()), - Self::LayoutJob(_) | Self::Galley(_) => self, - } + self.map_rich_text(|text| text.heading()) } /// Prefer using [`RichText`] directly! + #[inline] pub fn monospace(self) -> Self { - match self { - Self::RichText(text) => Self::RichText(text.monospace()), - Self::LayoutJob(_) | Self::Galley(_) => self, - } + self.map_rich_text(|text| text.monospace()) } /// Prefer using [`RichText`] directly! + #[inline] pub fn code(self) -> Self { - match self { - Self::RichText(text) => Self::RichText(text.code()), - Self::LayoutJob(_) | Self::Galley(_) => self, - } + self.map_rich_text(|text| text.code()) } /// Prefer using [`RichText`] directly! + #[inline] pub fn strong(self) -> Self { - match self { - Self::RichText(text) => Self::RichText(text.strong()), - Self::LayoutJob(_) | Self::Galley(_) => self, - } + self.map_rich_text(|text| text.strong()) } /// Prefer using [`RichText`] directly! + #[inline] pub fn weak(self) -> Self { - match self { - Self::RichText(text) => Self::RichText(text.weak()), - Self::LayoutJob(_) | Self::Galley(_) => self, - } + self.map_rich_text(|text| text.weak()) } /// Prefer using [`RichText`] directly! + #[inline] pub fn underline(self) -> Self { - match self { - Self::RichText(text) => Self::RichText(text.underline()), - Self::LayoutJob(_) | Self::Galley(_) => self, - } + self.map_rich_text(|text| text.underline()) } /// Prefer using [`RichText`] directly! + #[inline] pub fn strikethrough(self) -> Self { - match self { - Self::RichText(text) => Self::RichText(text.strikethrough()), - Self::LayoutJob(_) | Self::Galley(_) => self, - } + self.map_rich_text(|text| text.strikethrough()) } /// Prefer using [`RichText`] directly! + #[inline] pub fn italics(self) -> Self { - match self { - Self::RichText(text) => Self::RichText(text.italics()), - Self::LayoutJob(_) | Self::Galley(_) => self, - } + self.map_rich_text(|text| text.italics()) } /// Prefer using [`RichText`] directly! + #[inline] pub fn small(self) -> Self { - match self { - Self::RichText(text) => Self::RichText(text.small()), - Self::LayoutJob(_) | Self::Galley(_) => self, - } + self.map_rich_text(|text| text.small()) } /// Prefer using [`RichText`] directly! + #[inline] pub fn small_raised(self) -> Self { - match self { - Self::RichText(text) => Self::RichText(text.small_raised()), - Self::LayoutJob(_) | Self::Galley(_) => self, - } + self.map_rich_text(|text| text.small_raised()) } /// Prefer using [`RichText`] directly! + #[inline] pub fn raised(self) -> Self { - match self { - Self::RichText(text) => Self::RichText(text.raised()), - Self::LayoutJob(_) | Self::Galley(_) => self, - } + self.map_rich_text(|text| text.raised()) } /// Prefer using [`RichText`] directly! + #[inline] pub fn background_color(self, background_color: impl Into) -> Self { - match self { - Self::RichText(text) => Self::RichText(text.background_color(background_color)), - Self::LayoutJob(_) | Self::Galley(_) => self, - } + self.map_rich_text(|text| text.background_color(background_color)) } /// Returns a value rounded to [`emath::GUI_ROUNDING`]. pub(crate) fn font_height(&self, fonts: &epaint::Fonts, style: &Style) -> f32 { match self { + Self::Text(_) => fonts.row_height(&FontSelection::Default.resolve(style)), Self::RichText(text) => text.font_height(fonts, style), Self::LayoutJob(job) => job.font_height(fonts), Self::Galley(galley) => { @@ -685,11 +682,24 @@ impl WidgetText { style: &Style, fallback_font: FontSelection, default_valign: Align, - ) -> LayoutJob { + ) -> Arc { match self { - Self::RichText(text) => text.into_layout_job(style, fallback_font, default_valign), + Self::Text(text) => Arc::new(LayoutJob::simple_format( + text, + TextFormat { + font_id: FontSelection::Default.resolve(style), + color: crate::Color32::PLACEHOLDER, + valign: default_valign, + ..Default::default() + }, + )), + Self::RichText(text) => Arc::new(Arc::unwrap_or_clone(text).into_layout_job( + style, + fallback_font, + default_valign, + )), Self::LayoutJob(job) => job, - Self::Galley(galley) => (*galley.job).clone(), + Self::Galley(galley) => galley.job.clone(), } } @@ -721,12 +731,30 @@ impl WidgetText { default_valign: Align, ) -> Arc { match self { - Self::RichText(text) => { - let mut layout_job = text.into_layout_job(style, fallback_font, default_valign); + Self::Text(text) => { + let mut layout_job = LayoutJob::simple_format( + text, + TextFormat { + font_id: FontSelection::Default.resolve(style), + color: crate::Color32::PLACEHOLDER, + valign: default_valign, + ..Default::default() + }, + ); layout_job.wrap = text_wrapping; ctx.fonts(|f| f.layout_job(layout_job)) } - Self::LayoutJob(mut job) => { + Self::RichText(text) => { + let mut layout_job = Arc::unwrap_or_clone(text).into_layout_job( + style, + fallback_font, + default_valign, + ); + layout_job.wrap = text_wrapping; + ctx.fonts(|f| f.layout_job(layout_job)) + } + Self::LayoutJob(job) => { + let mut job = Arc::unwrap_or_clone(job); job.wrap = text_wrapping; ctx.fonts(|f| f.layout_job(job)) } @@ -738,48 +766,55 @@ impl WidgetText { impl From<&str> for WidgetText { #[inline] fn from(text: &str) -> Self { - Self::RichText(RichText::new(text)) + Self::Text(text.to_owned()) } } impl From<&String> for WidgetText { #[inline] fn from(text: &String) -> Self { - Self::RichText(RichText::new(text)) + Self::Text(text.clone()) } } impl From for WidgetText { #[inline] fn from(text: String) -> Self { - Self::RichText(RichText::new(text)) + Self::Text(text) } } impl From<&Box> for WidgetText { #[inline] fn from(text: &Box) -> Self { - Self::RichText(RichText::new(text.clone())) + Self::Text(text.to_string()) } } impl From> for WidgetText { #[inline] fn from(text: Box) -> Self { - Self::RichText(RichText::new(text)) + Self::Text(text.into()) } } impl From> for WidgetText { #[inline] fn from(text: Cow<'_, str>) -> Self { - Self::RichText(RichText::new(text)) + Self::Text(text.into_owned()) } } impl From for WidgetText { #[inline] fn from(rich_text: RichText) -> Self { + Self::RichText(Arc::new(rich_text)) + } +} + +impl From> for WidgetText { + #[inline] + fn from(rich_text: Arc) -> Self { Self::RichText(rich_text) } } @@ -787,6 +822,13 @@ impl From for WidgetText { impl From for WidgetText { #[inline] fn from(layout_job: LayoutJob) -> Self { + Self::LayoutJob(Arc::new(layout_job)) + } +} + +impl From> for WidgetText { + #[inline] + fn from(layout_job: Arc) -> Self { Self::LayoutJob(layout_job) } } @@ -797,3 +839,13 @@ impl From> for WidgetText { Self::Galley(galley) } } + +#[cfg(test)] +mod tests { + use crate::WidgetText; + + #[test] + fn ensure_small_widget_text() { + assert_eq!(size_of::(), size_of::()); + } +} diff --git a/crates/egui/src/widgets/label.rs b/crates/egui/src/widgets/label.rs index 3656af92..d90bdf96 100644 --- a/crates/egui/src/widgets/label.rs +++ b/crates/egui/src/widgets/label.rs @@ -1,12 +1,10 @@ use std::sync::Arc; use crate::{ - epaint, pos2, text_selection, Align, Direction, FontSelection, Galley, Pos2, Response, Sense, - Stroke, TextWrapMode, Ui, Widget, WidgetInfo, WidgetText, WidgetType, + epaint, pos2, text_selection::LabelSelectionState, Align, Direction, FontSelection, Galley, + Pos2, Response, Sense, Stroke, TextWrapMode, Ui, Widget, WidgetInfo, WidgetText, WidgetType, }; -use self::text_selection::LabelSelectionState; - /// Static text. /// /// Usually it is more convenient to use [`Ui::label`]. @@ -182,9 +180,11 @@ impl Label { } let valign = ui.text_valign(); - let mut layout_job = self - .text - .into_layout_job(ui.style(), FontSelection::Default, valign); + let mut layout_job = Arc::unwrap_or_clone(self.text.into_layout_job( + ui.style(), + FontSelection::Default, + valign, + )); let available_width = ui.available_width(); diff --git a/crates/egui_demo_app/src/wrap_app.rs b/crates/egui_demo_app/src/wrap_app.rs index ea5fbfab..3775d93d 100644 --- a/crates/egui_demo_app/src/wrap_app.rs +++ b/crates/egui_demo_app/src/wrap_app.rs @@ -134,7 +134,7 @@ impl std::fmt::Display for Anchor { impl From for egui::WidgetText { fn from(value: Anchor) -> Self { - Self::RichText(egui::RichText::new(value.to_string())) + Self::from(value.to_string()) } } diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 49ec2908..795b9c9f 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -118,6 +118,21 @@ impl LayoutJob { } } + /// Break on `\n` + #[inline] + pub fn simple_format(text: String, format: TextFormat) -> Self { + Self { + sections: vec![LayoutSection { + leading_space: 0.0, + byte_range: 0..text.len(), + format, + }], + text, + break_on_newline: true, + ..Default::default() + } + } + /// Does not break on `\n`, but shows the replacement character instead. #[inline] pub fn simple_singleline(text: String, font_id: FontId, color: Color32) -> Self {