diff --git a/crates/ecolor/src/color32.rs b/crates/ecolor/src/color32.rs index 56b96589256..32e1487bc7f 100644 --- a/crates/ecolor/src/color32.rs +++ b/crates/ecolor/src/color32.rs @@ -7,6 +7,8 @@ use crate::{gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_f32_from_ /// /// Internally this uses 0-255 gamma space `sRGBA` color with premultiplied alpha. /// Alpha channel is in linear space. +/// +/// The special value of alpha=0 means the color is to be treated as an additive color. #[repr(C)] #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -61,7 +63,16 @@ impl Color32 { pub const DEBUG_COLOR: Color32 = Color32::from_rgba_premultiplied(0, 200, 0, 128); /// An ugly color that is planned to be replaced before making it to the screen. - pub const TEMPORARY_COLOR: Color32 = Color32::from_rgb(64, 254, 0); + /// + /// This is an invalid color, in that it does not correspond to a valid multiplied color, + /// nor to an additive color. + /// + /// This is used as a special color key, + /// i.e. often taken to mean "no color". + pub const PLACEHOLDER: Color32 = Color32::from_rgba_premultiplied(64, 254, 0, 128); + + #[deprecated = "Renmaed to PLACEHOLDER"] + pub const TEMPORARY_COLOR: Color32 = Self::PLACEHOLDER; #[inline] pub const fn from_rgb(r: u8, g: u8, b: u8) -> Self { diff --git a/crates/egui/src/containers/collapsing_header.rs b/crates/egui/src/containers/collapsing_header.rs index 96bf5cdef7e..f4738fc0e0d 100644 --- a/crates/egui/src/containers/collapsing_header.rs +++ b/crates/egui/src/containers/collapsing_header.rs @@ -495,22 +495,22 @@ impl CollapsingHeader { let text_pos = available.min + vec2(ui.spacing().indent, 0.0); let wrap_width = available.right() - text_pos.x; let wrap = Some(false); - let text = text.into_galley(ui, wrap, wrap_width, TextStyle::Button); - let text_max_x = text_pos.x + text.size().x; + let galley = text.into_galley(ui, wrap, 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(); if ui.visuals().collapsing_header_frame { desired_width = desired_width.max(available.width()); // fill full width } - let mut desired_size = vec2(desired_width, text.size().y + 2.0 * button_padding.y); + let mut desired_size = vec2(desired_width, galley.size().y + 2.0 * button_padding.y); desired_size = desired_size.at_least(ui.spacing().interact_size); let (_, rect) = ui.allocate_space(desired_size); let mut header_response = ui.interact(rect, id, Sense::click()); let text_pos = pos2( text_pos.x, - header_response.rect.center().y - text.size().y / 2.0, + header_response.rect.center().y - galley.size().y / 2.0, ); let mut state = CollapsingState::load_with_default_open(ui.ctx(), id, default_open); @@ -525,7 +525,7 @@ impl CollapsingHeader { } header_response - .widget_info(|| WidgetInfo::labeled(WidgetType::CollapsingHeader, text.text())); + .widget_info(|| WidgetInfo::labeled(WidgetType::CollapsingHeader, galley.text())); let openness = state.openness(ui.ctx()); @@ -563,7 +563,7 @@ impl CollapsingHeader { } } - text.paint_with_visuals(ui.painter(), text_pos, &visuals); + ui.painter().galley(text_pos, galley, visuals.text_color()); } Prepared { diff --git a/crates/egui/src/containers/combo_box.rs b/crates/egui/src/containers/combo_box.rs index 4bc3edc4b57..70ed046b396 100644 --- a/crates/egui/src/containers/combo_box.rs +++ b/crates/egui/src/containers/combo_box.rs @@ -327,7 +327,8 @@ fn combo_box_dyn<'c, R>( } let text_rect = Align2::LEFT_CENTER.align_size_within_rect(galley.size(), rect); - galley.paint_with_visuals(ui.painter(), text_rect.min, visuals); + ui.painter() + .galley(text_rect.min, galley, visuals.text_color()); } }); diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index cc0f28e90e5..4182af4e8c3 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -1,7 +1,9 @@ // WARNING: the code in here is horrible. It is a behemoth that needs breaking up into simpler parts. +use std::sync::Arc; + use crate::collapsing_header::CollapsingState; -use crate::{widget_text::WidgetTextGalley, *}; +use crate::*; use epaint::*; use super::*; @@ -885,7 +887,7 @@ struct TitleBar { id: Id, /// Prepared text in the title - title_galley: WidgetTextGalley, + title_galley: Arc, /// Size of the title bar in a collapsed state (if window is collapsible), /// which includes all necessary space for showing the expand button, the @@ -984,11 +986,11 @@ impl TitleBar { let full_top_rect = Rect::from_x_y_ranges(self.rect.x_range(), self.min_rect.y_range()); let text_pos = emath::align::center_size_in_rect(self.title_galley.size(), full_top_rect).left_top(); - let text_pos = text_pos - self.title_galley.galley().rect.min.to_vec2(); + let text_pos = text_pos - self.title_galley.rect.min.to_vec2(); let text_pos = text_pos - 1.5 * Vec2::Y; // HACK: center on x-height of text (looks better) - self.title_galley.paint_with_fallback_color( - ui.painter(), + ui.painter().galley( text_pos, + self.title_galley.clone(), ui.visuals().text_color(), ); diff --git a/crates/egui/src/menu.rs b/crates/egui/src/menu.rs index 34b33a31fb6..bd7305ed80e 100644 --- a/crates/egui/src/menu.rs +++ b/crates/egui/src/menu.rs @@ -503,8 +503,8 @@ impl SubMenuButton { } let text_color = visuals.text_color(); - text_galley.paint_with_fallback_color(ui.painter(), text_pos, text_color); - icon_galley.paint_with_fallback_color(ui.painter(), icon_pos, text_color); + ui.painter().galley(text_pos, text_galley, text_color); + ui.painter().galley(icon_pos, icon_galley, text_color); } response } diff --git a/crates/egui/src/painter.rs b/crates/egui/src/painter.rs index 8376d16ed7b..dfae0a92cfe 100644 --- a/crates/egui/src/painter.rs +++ b/crates/egui/src/painter.rs @@ -88,7 +88,7 @@ impl Painter { /// ## Accessors etc impl Painter { /// Get a reference to the parent [`Context`]. - #[inline(always)] + #[inline] pub fn ctx(&self) -> &Context { &self.ctx } @@ -96,45 +96,45 @@ impl Painter { /// Read-only access to the shared [`Fonts`]. /// /// See [`Context`] documentation for how locks work. - #[inline(always)] + #[inline] pub fn fonts(&self, reader: impl FnOnce(&Fonts) -> R) -> R { self.ctx.fonts(reader) } /// Where we paint - #[inline(always)] + #[inline] pub fn layer_id(&self) -> LayerId { self.layer_id } /// Everything painted in this [`Painter`] will be clipped against this. /// This means nothing outside of this rectangle will be visible on screen. - #[inline(always)] + #[inline] pub fn clip_rect(&self) -> Rect { self.clip_rect } /// Everything painted in this [`Painter`] will be clipped against this. /// This means nothing outside of this rectangle will be visible on screen. - #[inline(always)] + #[inline] pub fn set_clip_rect(&mut self, clip_rect: Rect) { self.clip_rect = clip_rect; } /// Useful for pixel-perfect rendering. - #[inline(always)] + #[inline] pub fn round_to_pixel(&self, point: f32) -> f32 { self.ctx().round_to_pixel(point) } /// Useful for pixel-perfect rendering. - #[inline(always)] + #[inline] pub fn round_vec_to_pixels(&self, vec: Vec2) -> Vec2 { self.ctx().round_vec_to_pixels(vec) } /// Useful for pixel-perfect rendering. - #[inline(always)] + #[inline] pub fn round_pos_to_pixels(&self, pos: Pos2) -> Pos2 { self.ctx().round_pos_to_pixels(pos) } @@ -236,7 +236,7 @@ impl Painter { 0.0, Color32::from_black_alpha(150), )); - self.galley(rect.min, galley); + self.galley(rect.min, galley, color); frame_rect } } @@ -379,14 +379,15 @@ impl Painter { ) -> Rect { let galley = self.layout_no_wrap(text.to_string(), font_id, text_color); let rect = anchor.anchor_rect(Rect::from_min_size(pos, galley.size())); - self.galley(rect.min, galley); + self.galley(rect.min, galley, text_color); rect } /// Will wrap text at the given width and line break at `\n`. /// /// Paint the results with [`Self::galley`]. - #[inline(always)] + #[inline] + #[must_use] pub fn layout( &self, text: String, @@ -400,7 +401,8 @@ impl Painter { /// Will line break at `\n`. /// /// Paint the results with [`Self::galley`]. - #[inline(always)] + #[inline] + #[must_use] pub fn layout_no_wrap( &self, text: String, @@ -414,11 +416,13 @@ impl Painter { /// /// You can create the [`Galley`] with [`Self::layout`]. /// - /// If you want to change the color of the text, use [`Self::galley_with_color`]. - #[inline(always)] - pub fn galley(&self, pos: Pos2, galley: Arc) { + /// Any uncolored parts of the [`Galley`] (using [`Color32::PLACEHOLDER`]) will be replaced with the given color. + /// + /// Any non-placeholder color in the galley takes precedence over this fallback color. + #[inline] + pub fn galley(&self, pos: Pos2, galley: Arc, fallback_color: Color32) { if !galley.is_empty() { - self.add(Shape::galley(pos, galley)); + self.add(Shape::galley(pos, galley, fallback_color)); } } @@ -426,11 +430,28 @@ impl Painter { /// /// You can create the [`Galley`] with [`Self::layout`]. /// - /// The text color in the [`Galley`] will be replaced with the given color. - #[inline(always)] + /// All text color in the [`Galley`] will be replaced with the given color. + #[inline] + pub fn galley_with_override_text_color( + &self, + pos: Pos2, + galley: Arc, + text_color: Color32, + ) { + if !galley.is_empty() { + self.add(Shape::galley_with_override_text_color( + pos, galley, text_color, + )); + } + } + + #[deprecated = "Use `Painter::galley` or `Painter::galley_with_override_text_color` instead"] + #[inline] pub fn galley_with_color(&self, pos: Pos2, galley: Arc, text_color: Color32) { if !galley.is_empty() { - self.add(Shape::galley_with_color(pos, galley, text_color)); + self.add(Shape::galley_with_override_text_color( + pos, galley, text_color, + )); } } } diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 8474bcb0e72..4c4c0501f3a 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -2269,7 +2269,8 @@ fn register_rect(ui: &Ui, rect: Rect) { if !callstack.is_empty() { let font_id = FontId::monospace(12.0); let text = format!("{callstack}\n\n(click to copy)"); - let galley = painter.layout_no_wrap(text, font_id, Color32::WHITE); + let text_color = Color32::WHITE; + let galley = painter.layout_no_wrap(text, font_id, text_color); // Position the text either under or above: let screen_rect = ui.ctx().screen_rect(); @@ -2299,7 +2300,7 @@ fn register_rect(ui: &Ui, rect: Rect) { }; let text_rect = Rect::from_min_size(text_pos, galley.size()); painter.rect(text_rect, 0.0, text_bg_color, (1.0, text_rect_stroke_color)); - painter.galley(text_pos, galley); + painter.galley(text_pos, galley, text_color); if ui.input(|i| i.pointer.any_click()) { ui.ctx().copy_text(callstack); diff --git a/crates/egui/src/widget_text.rs b/crates/egui/src/widget_text.rs index fe0de370c01..9bad481cb38 100644 --- a/crates/egui/src/widget_text.rs +++ b/crates/egui/src/widget_text.rs @@ -1,8 +1,8 @@ use std::{borrow::Cow, sync::Arc}; use crate::{ - style::WidgetVisuals, text::LayoutJob, Align, Color32, FontFamily, FontSelection, Galley, Pos2, - Style, TextStyle, Ui, Visuals, + text::LayoutJob, Align, Color32, FontFamily, FontSelection, Galley, Style, TextStyle, Ui, + Visuals, }; /// Text and optional style choices for it. @@ -247,6 +247,9 @@ impl RichText { } /// Override text color. + /// + /// If not set, [`Color32::PLACEHOLDER`] will be used, + /// which will be replaced with a color chosen by the widget that paints the text. #[inline] pub fn color(mut self, color: impl Into) -> Self { self.text_color = Some(color.into()); @@ -310,17 +313,14 @@ impl RichText { layout_job.append(&text, 0.0, format); } - fn into_text_job( + fn into_layout_job( self, style: &Style, fallback_font: FontSelection, default_valign: Align, - ) -> WidgetTextJob { - let job_has_color = self.get_text_color(&style.visuals).is_some(); + ) -> LayoutJob { let (text, text_format) = self.into_text_and_format(style, fallback_font, default_valign); - - let job = LayoutJob::single_section(text, text_format); - WidgetTextJob { job, job_has_color } + LayoutJob::single_section(text, text_format) } fn into_text_and_format( @@ -350,7 +350,7 @@ impl RichText { } = self; let line_color = text_color.unwrap_or_else(|| style.visuals.text_color()); - let text_color = text_color.unwrap_or(crate::Color32::TEMPORARY_COLOR); + let text_color = text_color.unwrap_or(crate::Color32::PLACEHOLDER); let font_id = { let mut font_id = text_style @@ -429,6 +429,9 @@ impl RichText { /// but it can be a [`RichText`] (text with color, style, etc), /// a [`LayoutJob`] (for when you want full control of how the text looks) /// or text that has already been laid out in a [`Galley`]. +/// +/// 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. #[derive(Clone)] pub enum WidgetText { RichText(RichText), @@ -442,9 +445,15 @@ pub enum WidgetText { /// of the [`Ui`] the widget is placed in. /// If you want all parts of the [`LayoutJob`] respected, then convert it to a /// [`Galley`] and use [`Self::Galley`] instead. + /// + /// 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), /// Use exactly this galley when painting the text. + /// + /// 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. Galley(Arc), } @@ -616,25 +625,16 @@ impl WidgetText { } } - pub fn into_text_job( + pub fn into_layout_job( self, style: &Style, fallback_font: FontSelection, default_valign: Align, - ) -> WidgetTextJob { + ) -> LayoutJob { match self { - Self::RichText(text) => text.into_text_job(style, fallback_font, default_valign), - Self::LayoutJob(job) => WidgetTextJob { - job, - job_has_color: true, - }, - Self::Galley(galley) => { - let job: LayoutJob = (*galley.job).clone(); - WidgetTextJob { - job, - job_has_color: true, - } - } + Self::RichText(text) => text.into_layout_job(style, fallback_font, default_valign), + Self::LayoutJob(job) => job, + Self::Galley(galley) => (*galley.job).clone(), } } @@ -647,31 +647,22 @@ impl WidgetText { wrap: Option, available_width: f32, fallback_font: impl Into, - ) -> WidgetTextGalley { + ) -> Arc { let wrap = wrap.unwrap_or_else(|| ui.wrap_text()); let wrap_width = if wrap { available_width } else { f32::INFINITY }; match self { Self::RichText(text) => { let valign = ui.layout().vertical_align(); - let mut text_job = text.into_text_job(ui.style(), fallback_font.into(), valign); - text_job.job.wrap.max_width = wrap_width; - WidgetTextGalley { - galley: ui.fonts(|f| f.layout_job(text_job.job)), - galley_has_color: text_job.job_has_color, - } + let mut layout_job = text.into_layout_job(ui.style(), fallback_font.into(), valign); + layout_job.wrap.max_width = wrap_width; + ui.fonts(|f| f.layout_job(layout_job)) } Self::LayoutJob(mut job) => { job.wrap.max_width = wrap_width; - WidgetTextGalley { - galley: ui.fonts(|f| f.layout_job(job)), - galley_has_color: true, - } + ui.fonts(|f| f.layout_job(job)) } - Self::Galley(galley) => WidgetTextGalley { - galley, - galley_has_color: true, - }, + Self::Galley(galley) => galley, } } } @@ -724,86 +715,3 @@ impl From> for WidgetText { Self::Galley(galley) } } - -// ---------------------------------------------------------------------------- - -#[derive(Clone, PartialEq)] -pub struct WidgetTextJob { - pub job: LayoutJob, - pub job_has_color: bool, -} - -impl WidgetTextJob { - pub fn into_galley(self, fonts: &crate::text::Fonts) -> WidgetTextGalley { - let Self { job, job_has_color } = self; - let galley = fonts.layout_job(job); - WidgetTextGalley { - galley, - galley_has_color: job_has_color, - } - } -} - -// ---------------------------------------------------------------------------- - -/// Text that has been laid out and ready to be painted. -#[derive(Clone, PartialEq)] -pub struct WidgetTextGalley { - pub galley: Arc, - pub galley_has_color: bool, -} - -impl WidgetTextGalley { - /// Size of the laid out text. - #[inline] - pub fn size(&self) -> crate::Vec2 { - self.galley.size() - } - - /// The full, non-elided text of the input job. - #[inline] - pub fn text(&self) -> &str { - self.galley.text() - } - - #[inline] - pub fn galley(&self) -> &Arc { - &self.galley - } - - /// Use the colors in the original [`WidgetText`] if any, - /// else fall back to the one specified by the [`WidgetVisuals`]. - pub fn paint_with_visuals( - self, - painter: &crate::Painter, - text_pos: Pos2, - visuals: &WidgetVisuals, - ) { - self.paint_with_fallback_color(painter, text_pos, visuals.text_color()); - } - - /// Use the colors in the original [`WidgetText`] if any, - /// else fall back to the given color. - pub fn paint_with_fallback_color( - self, - painter: &crate::Painter, - text_pos: Pos2, - text_color: Color32, - ) { - if self.galley_has_color { - painter.galley(text_pos, self.galley); - } else { - painter.galley_with_color(text_pos, self.galley, text_color); - } - } - - /// Paint with this specific color. - pub fn paint_with_color_override( - self, - painter: &crate::Painter, - text_pos: Pos2, - text_color: Color32, - ) { - painter.galley_with_color(text_pos, self.galley, text_color); - } -} diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 236d68e05c1..6ca7cf86e40 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -210,8 +210,9 @@ impl Widget for Button<'_> { text_wrap_width -= 60.0; // Some space for the shortcut text (which we never wrap). } - let text = text.map(|text| text.into_galley(ui, wrap, text_wrap_width, TextStyle::Button)); - let shortcut_text = (!shortcut_text.is_empty()) + 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)); let mut desired_size = Vec2::ZERO; @@ -219,14 +220,14 @@ impl Widget for Button<'_> { desired_size.x += image_size.x; desired_size.y = desired_size.y.max(image_size.y); } - if image.is_some() && text.is_some() { + if image.is_some() && galley.is_some() { desired_size.x += ui.spacing().icon_spacing; } - if let Some(text) = &text { + if let Some(text) = &galley { desired_size.x += text.size().x; desired_size.y = desired_size.y.max(text.size().y); } - if let Some(shortcut_text) = &shortcut_text { + if let Some(shortcut_text) = &shortcut_galley { desired_size.x += ui.spacing().item_spacing.x + shortcut_text.size().x; desired_size.y = desired_size.y.max(shortcut_text.size().y); } @@ -238,8 +239,8 @@ impl Widget for Button<'_> { let (rect, mut response) = ui.allocate_at_least(desired_size, sense); response.widget_info(|| { - if let Some(text) = &text { - WidgetInfo::labeled(WidgetType::Button, text.text()) + if let Some(galley) = &galley { + WidgetInfo::labeled(WidgetType::Button, galley.text()) } else { WidgetInfo::new(WidgetType::Button) } @@ -297,30 +298,30 @@ impl Widget for Button<'_> { widgets::image::texture_load_result_response(image.source(), &tlr, response); } - if image.is_some() && text.is_some() { + if image.is_some() && galley.is_some() { cursor_x += ui.spacing().icon_spacing; } - if let Some(text) = text { - let text_pos = if image.is_some() || shortcut_text.is_some() { - pos2(cursor_x, rect.center().y - 0.5 * text.size().y) + if let Some(galley) = galley { + let text_pos = if image.is_some() || shortcut_galley.is_some() { + pos2(cursor_x, rect.center().y - 0.5 * galley.size().y) } else { // Make sure button text is centered if within a centered layout ui.layout() - .align_size_within_rect(text.size(), rect.shrink2(button_padding)) + .align_size_within_rect(galley.size(), rect.shrink2(button_padding)) .min }; - text.paint_with_visuals(ui.painter(), text_pos, visuals); + ui.painter().galley(text_pos, galley, visuals.text_color()); } - if let Some(shortcut_text) = shortcut_text { + if let Some(shortcut_galley) = shortcut_galley { let shortcut_text_pos = pos2( - rect.max.x - button_padding.x - shortcut_text.size().x, - rect.center().y - 0.5 * shortcut_text.size().y, + rect.max.x - button_padding.x - shortcut_galley.size().x, + rect.center().y - 0.5 * shortcut_galley.size().y, ); - shortcut_text.paint_with_fallback_color( - ui.painter(), + ui.painter().galley( shortcut_text_pos, + shortcut_galley, ui.visuals().weak_text_color(), ); } @@ -378,18 +379,18 @@ impl<'a> Widget for Checkbox<'a> { let icon_width = spacing.icon_width; let icon_spacing = spacing.icon_spacing; - let (text, mut desired_size) = if text.is_empty() { + let (galley, mut desired_size) = if text.is_empty() { (None, vec2(icon_width, 0.0)) } else { let total_extra = vec2(icon_width + icon_spacing, 0.0); let wrap_width = ui.available_width() - total_extra.x; - let text = text.into_galley(ui, None, wrap_width, TextStyle::Button); + let galley = text.into_galley(ui, None, wrap_width, TextStyle::Button); - let mut desired_size = total_extra + text.size(); + let mut desired_size = total_extra + galley.size(); desired_size = desired_size.at_least(spacing.interact_size); - (Some(text), desired_size) + (Some(galley), desired_size) }; desired_size = desired_size.at_least(Vec2::splat(spacing.interact_size.y)); @@ -404,7 +405,7 @@ impl<'a> Widget for Checkbox<'a> { WidgetInfo::selected( WidgetType::Checkbox, *checked, - text.as_ref().map_or("", |x| x.text()), + galley.as_ref().map_or("", |x| x.text()), ) }); @@ -430,12 +431,12 @@ impl<'a> Widget for Checkbox<'a> { visuals.fg_stroke, )); } - if let Some(text) = text { + if let Some(galley) = galley { let text_pos = pos2( rect.min.x + icon_width + icon_spacing, - rect.center().y - 0.5 * text.size().y, + rect.center().y - 0.5 * galley.size().y, ); - text.paint_with_visuals(ui.painter(), text_pos, visuals); + ui.painter().galley(text_pos, galley, visuals.text_color()); } } @@ -487,7 +488,7 @@ impl Widget for RadioButton { let icon_width = spacing.icon_width; let icon_spacing = spacing.icon_spacing; - let (text, mut desired_size) = if text.is_empty() { + let (galley, mut desired_size) = if text.is_empty() { (None, vec2(icon_width, 0.0)) } else { let total_extra = vec2(icon_width + icon_spacing, 0.0); @@ -509,7 +510,7 @@ impl Widget for RadioButton { WidgetInfo::selected( WidgetType::RadioButton, checked, - text.as_ref().map_or("", |x| x.text()), + galley.as_ref().map_or("", |x| x.text()), ) }); @@ -538,12 +539,12 @@ impl Widget for RadioButton { }); } - if let Some(text) = text { + if let Some(galley) = galley { let text_pos = pos2( rect.min.x + icon_width + icon_spacing, - rect.center().y - 0.5 * text.size().y, + rect.center().y - 0.5 * galley.size().y, ); - text.paint_with_visuals(ui.painter(), text_pos, visuals); + ui.painter().galley(text_pos, galley, visuals.text_color()); } } diff --git a/crates/egui/src/widgets/hyperlink.rs b/crates/egui/src/widgets/hyperlink.rs index 5d592272641..79a833bd77e 100644 --- a/crates/egui/src/widgets/hyperlink.rs +++ b/crates/egui/src/widgets/hyperlink.rs @@ -34,8 +34,8 @@ impl Widget for Link { let Link { text } = self; let label = Label::new(text).sense(Sense::click()); - let (pos, text_galley, response) = label.layout_in_ui(ui); - response.widget_info(|| WidgetInfo::labeled(WidgetType::Link, text_galley.text())); + let (pos, galley, response) = label.layout_in_ui(ui); + response.widget_info(|| WidgetInfo::labeled(WidgetType::Link, galley.text())); if response.hovered() { ui.ctx().set_cursor_icon(CursorIcon::PointingHand); @@ -51,13 +51,8 @@ impl Widget for Link { Stroke::NONE }; - ui.painter().add(epaint::TextShape { - pos, - galley: text_galley.galley, - override_text_color: Some(color), - underline, - angle: 0.0, - }); + ui.painter() + .add(epaint::TextShape::new(pos, galley, color).with_underline(underline)); } response diff --git a/crates/egui/src/widgets/label.rs b/crates/egui/src/widgets/label.rs index eb786e75013..dee9b1c303f 100644 --- a/crates/egui/src/widgets/label.rs +++ b/crates/egui/src/widgets/label.rs @@ -1,4 +1,6 @@ -use crate::{widget_text::WidgetTextGalley, *}; +use std::sync::Arc; + +use crate::*; /// Static text. /// @@ -94,7 +96,7 @@ impl Label { impl Label { /// Do layout and position the galley in the ui, without painting it or adding widget info. - pub fn layout_in_ui(self, ui: &mut Ui) -> (Pos2, WidgetTextGalley, Response) { + pub fn layout_in_ui(self, ui: &mut Ui) -> (Pos2, Arc, Response) { let sense = self.sense.unwrap_or_else(|| { // We only want to focus labels if the screen reader is on. if ui.memory(|mem| mem.options.screen_reader) { @@ -111,17 +113,13 @@ impl Label { Align::Center => rect.center_top(), Align::RIGHT => rect.right_top(), }; - let text_galley = WidgetTextGalley { - galley, - galley_has_color: true, - }; - return (pos, text_galley, response); + return (pos, galley, response); } let valign = ui.layout().vertical_align(); - let mut text_job = self + let mut layout_job = self .text - .into_text_job(ui.style(), FontSelection::Default, valign); + .into_layout_job(ui.style(), FontSelection::Default, valign); let truncate = self.truncate; let wrap = !truncate && self.wrap.unwrap_or_else(|| ui.wrap_text()); @@ -139,70 +137,65 @@ impl Label { let first_row_indentation = available_width - ui.available_size_before_wrap().x; egui_assert!(first_row_indentation.is_finite()); - text_job.job.wrap.max_width = available_width; - text_job.job.first_row_min_height = cursor.height(); - text_job.job.halign = Align::Min; - text_job.job.justify = false; - if let Some(first_section) = text_job.job.sections.first_mut() { + layout_job.wrap.max_width = available_width; + layout_job.first_row_min_height = cursor.height(); + layout_job.halign = Align::Min; + layout_job.justify = false; + if let Some(first_section) = layout_job.sections.first_mut() { first_section.leading_space = first_row_indentation; } - let text_galley = ui.fonts(|f| text_job.into_galley(f)); + let galley = ui.fonts(|fonts| fonts.layout_job(layout_job)); let pos = pos2(ui.max_rect().left(), ui.cursor().top()); - assert!( - !text_galley.galley.rows.is_empty(), - "Galleys are never empty" - ); + assert!(!galley.rows.is_empty(), "Galleys are never empty"); // collect a response from many rows: - let rect = text_galley.galley.rows[0] - .rect - .translate(vec2(pos.x, pos.y)); + let rect = galley.rows[0].rect.translate(vec2(pos.x, pos.y)); let mut response = ui.allocate_rect(rect, sense); - for row in text_galley.galley.rows.iter().skip(1) { + for row in galley.rows.iter().skip(1) { let rect = row.rect.translate(vec2(pos.x, pos.y)); response |= ui.allocate_rect(rect, sense); } - (pos, text_galley, response) + (pos, galley, response) } else { if truncate { - text_job.job.wrap.max_width = available_width; - text_job.job.wrap.max_rows = 1; - text_job.job.wrap.break_anywhere = true; + layout_job.wrap.max_width = available_width; + layout_job.wrap.max_rows = 1; + layout_job.wrap.break_anywhere = true; } else if wrap { - text_job.job.wrap.max_width = available_width; + layout_job.wrap.max_width = available_width; } else { - text_job.job.wrap.max_width = f32::INFINITY; + layout_job.wrap.max_width = f32::INFINITY; }; if ui.is_grid() { // TODO(emilk): remove special Grid hacks like these - text_job.job.halign = Align::LEFT; - text_job.job.justify = false; + layout_job.halign = Align::LEFT; + layout_job.justify = false; } else { - text_job.job.halign = ui.layout().horizontal_placement(); - text_job.job.justify = ui.layout().horizontal_justify(); + layout_job.halign = ui.layout().horizontal_placement(); + layout_job.justify = ui.layout().horizontal_justify(); }; - let text_galley = ui.fonts(|f| text_job.into_galley(f)); - let (rect, response) = ui.allocate_exact_size(text_galley.size(), sense); - let pos = match text_galley.galley.job.halign { + let galley = ui.fonts(|fonts| fonts.layout_job(layout_job)); + let (rect, response) = ui.allocate_exact_size(galley.size(), sense); + let pos = match galley.job.halign { Align::LEFT => rect.left_top(), Align::Center => rect.center_top(), Align::RIGHT => rect.right_top(), }; - (pos, text_galley, response) + (pos, galley, response) } } } impl Widget for Label { fn ui(self, ui: &mut Ui) -> Response { - let (pos, text_galley, mut response) = self.layout_in_ui(ui); - response.widget_info(|| WidgetInfo::labeled(WidgetType::Label, text_galley.text())); + let (pos, galley, mut response) = self.layout_in_ui(ui); + response.widget_info(|| WidgetInfo::labeled(WidgetType::Label, galley.text())); - if text_galley.galley.elided { + if galley.elided { // Show the full (non-elided) text on hover: - response = response.on_hover_text(text_galley.text()); + response = response.on_hover_text(galley.text()); } if ui.is_rect_visible(response.rect) { @@ -214,19 +207,8 @@ impl Widget for Label { Stroke::NONE }; - let override_text_color = if text_galley.galley_has_color { - None - } else { - Some(response_color) - }; - - ui.painter().add(epaint::TextShape { - pos, - galley: text_galley.galley, - override_text_color, - underline, - angle: 0.0, - }); + ui.painter() + .add(epaint::TextShape::new(pos, galley, response_color).with_underline(underline)); } response diff --git a/crates/egui/src/widgets/progress_bar.rs b/crates/egui/src/widgets/progress_bar.rs index ad3c298399a..c8439802544 100644 --- a/crates/egui/src/widgets/progress_bar.rs +++ b/crates/egui/src/widgets/progress_bar.rs @@ -161,11 +161,9 @@ impl Widget for ProgressBar { let text_color = visuals .override_text_color .unwrap_or(visuals.selection.stroke.color); - galley.paint_with_fallback_color( - &ui.painter().with_clip_rect(outer_rect), - text_pos, - text_color, - ); + ui.painter() + .with_clip_rect(outer_rect) + .galley(text_pos, galley, text_color); } } diff --git a/crates/egui/src/widgets/selected_label.rs b/crates/egui/src/widgets/selected_label.rs index 364ba0391e6..105232b40a5 100644 --- a/crates/egui/src/widgets/selected_label.rs +++ b/crates/egui/src/widgets/selected_label.rs @@ -44,19 +44,19 @@ impl Widget for SelectableLabel { let total_extra = button_padding + button_padding; let wrap_width = ui.available_width() - total_extra.x; - let text = text.into_galley(ui, None, wrap_width, TextStyle::Button); + let galley = text.into_galley(ui, None, wrap_width, TextStyle::Button); - let mut desired_size = total_extra + text.size(); + let mut desired_size = total_extra + galley.size(); desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y); let (rect, response) = ui.allocate_at_least(desired_size, Sense::click()); response.widget_info(|| { - WidgetInfo::selected(WidgetType::SelectableLabel, selected, text.text()) + WidgetInfo::selected(WidgetType::SelectableLabel, selected, galley.text()) }); if ui.is_rect_visible(response.rect) { let text_pos = ui .layout() - .align_size_within_rect(text.size(), rect.shrink2(button_padding)) + .align_size_within_rect(galley.size(), rect.shrink2(button_padding)) .min; let visuals = ui.style().interact_selectable(&response, selected); @@ -72,7 +72,7 @@ impl Widget for SelectableLabel { ); } - text.paint_with_visuals(ui.painter(), text_pos, &visuals); + ui.painter().galley(text_pos, galley, visuals.text_color()); } response diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index adf53149146..8b80151975f 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -164,13 +164,14 @@ impl<'t> TextEdit<'t> { /// .desired_width(f32::INFINITY); /// let output = text_edit.show(ui); /// let painter = ui.painter_at(output.response.rect); + /// let text_color = Color32::from_rgba_premultiplied(100, 100, 100, 100); /// let galley = painter.layout( /// String::from("Enter text"), /// FontId::default(), - /// Color32::from_rgba_premultiplied(100, 100, 100, 100), + /// text_color, /// f32::INFINITY /// ); - /// painter.galley(output.text_draw_pos, galley); + /// painter.galley(output.text_draw_pos, galley, text_color); /// # }); /// ``` #[inline] @@ -664,7 +665,7 @@ impl<'t> TextEdit<'t> { }; if ui.is_rect_visible(rect) { - painter.galley(text_draw_pos, galley.clone()); + painter.galley(text_draw_pos, galley.clone(), text_color); if text.as_str().is_empty() && !hint_text.is_empty() { let hint_text_color = ui.visuals().weak_text_color(); @@ -673,7 +674,7 @@ impl<'t> TextEdit<'t> { } else { hint_text.into_galley(ui, Some(false), f32::INFINITY, font_id) }; - galley.paint_with_fallback_color(&painter, response.rect.min, hint_text_color); + painter.galley(response.rect.min, galley, hint_text_color); } if ui.memory(|mem| mem.has_focus(id)) { diff --git a/crates/egui_demo_app/src/apps/http_app.rs b/crates/egui_demo_app/src/apps/http_app.rs index a118a90fcb6..7e8d7e54c48 100644 --- a/crates/egui_demo_app/src/apps/http_app.rs +++ b/crates/egui_demo_app/src/apps/http_app.rs @@ -260,7 +260,11 @@ impl ColoredText { job.wrap.max_width = ui.available_width(); let galley = ui.fonts(|f| f.layout_job(job)); let (response, painter) = ui.allocate_painter(galley.size(), egui::Sense::hover()); - painter.add(egui::Shape::galley(response.rect.min, galley)); + painter.add(egui::Shape::galley( + response.rect.min, + galley, + ui.visuals().text_color(), + )); } } } diff --git a/crates/egui_demo_lib/benches/benchmark.rs b/crates/egui_demo_lib/benches/benchmark.rs index 1baaefd7e01..75c68a0de32 100644 --- a/crates/egui_demo_lib/benches/benchmark.rs +++ b/crates/egui_demo_lib/benches/benchmark.rs @@ -89,7 +89,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { let max_texture_side = 8 * 1024; let wrap_width = 512.0; let font_id = egui::FontId::default(); - let color = egui::Color32::WHITE; + let text_color = egui::Color32::WHITE; let fonts = egui::epaint::text::Fonts::new( pixels_per_point, max_texture_side, @@ -104,7 +104,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { let job = LayoutJob::simple( LOREM_IPSUM_LONG.to_owned(), font_id.clone(), - color, + text_color, wrap_width, ); layout(&mut locked_fonts.fonts, job.into()) @@ -116,13 +116,13 @@ pub fn criterion_benchmark(c: &mut Criterion) { fonts.layout( LOREM_IPSUM_LONG.to_owned(), font_id.clone(), - color, + text_color, wrap_width, ) }); }); - let galley = fonts.layout(LOREM_IPSUM_LONG.to_owned(), font_id, color, wrap_width); + let galley = fonts.layout(LOREM_IPSUM_LONG.to_owned(), font_id, text_color, wrap_width); let font_image_size = fonts.font_image_size(); let prepared_discs = fonts.texture_atlas().lock().prepared_discs(); let mut tessellator = egui::epaint::Tessellator::new( @@ -132,7 +132,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { prepared_discs, ); let mut mesh = egui::epaint::Mesh::default(); - let text_shape = TextShape::new(egui::Pos2::ZERO, galley); + let text_shape = TextShape::new(egui::Pos2::ZERO, galley, text_color); c.bench_function("tessellate_text", |b| { b.iter(|| { tessellator.tessellate_text(&text_shape, &mut mesh); diff --git a/crates/egui_plot/src/axis.rs b/crates/egui_plot/src/axis.rs index 12dcac8828c..fae09e47ed3 100644 --- a/crates/egui_plot/src/axis.rs +++ b/crates/egui_plot/src/axis.rs @@ -1,7 +1,7 @@ use std::{fmt::Debug, ops::RangeInclusive, sync::Arc}; use egui::emath::{remap_clamp, round_to_decimals, Pos2, Rect}; -use egui::epaint::{Shape, Stroke, TextShape}; +use egui::epaint::{Shape, TextShape}; use crate::{Response, Sense, TextStyle, Ui, WidgetText}; @@ -247,14 +247,9 @@ impl AxisWidget { } }, }; - let shape = TextShape { - pos: text_pos, - galley: galley.galley, - underline: Stroke::NONE, - override_text_color: Some(text_color), - angle, - }; - ui.painter().add(shape); + + ui.painter() + .add(TextShape::new(text_pos, galley, text_color).with_angle(angle)); // --- add ticks --- let font_id = TextStyle::Body.resolve(ui.style()); @@ -311,7 +306,8 @@ impl AxisWidget { } }; - ui.painter().add(Shape::galley(text_pos, galley)); + ui.painter() + .add(Shape::galley(text_pos, galley, text_color)); } } } diff --git a/crates/egui_plot/src/items/mod.rs b/crates/egui_plot/src/items/mod.rs index 92d607570d8..c8949f58ce4 100644 --- a/crates/egui_plot/src/items/mod.rs +++ b/crates/egui_plot/src/items/mod.rs @@ -732,11 +732,7 @@ impl PlotItem for Text { .anchor .anchor_rect(Rect::from_min_size(pos, galley.size())); - let mut text_shape = epaint::TextShape::new(rect.min, galley.galley); - if !galley.galley_has_color { - text_shape.override_text_color = Some(color); - } - shapes.push(text_shape.into()); + shapes.push(epaint::TextShape::new(rect.min, galley, color).into()); if self.highlight { shapes.push(Shape::rect_stroke( diff --git a/crates/egui_plot/src/legend.rs b/crates/egui_plot/src/legend.rs index 7d2a3e00954..0a0f459f2f8 100644 --- a/crates/egui_plot/src/legend.rs +++ b/crates/egui_plot/src/legend.rs @@ -144,7 +144,7 @@ impl LegendEntry { }; let text_position = pos2(text_position_x, rect.center().y - 0.5 * galley.size().y); - painter.galley_with_color(text_position, galley, visuals.text_color()); + painter.galley(text_position, galley, visuals.text_color()); *checked ^= response.clicked_by(PointerButton::Primary); *hovered = response.hovered(); diff --git a/crates/epaint/src/shape.rs b/crates/epaint/src/shape.rs index 8de1f0ffc1b..cfae4493d0f 100644 --- a/crates/epaint/src/shape.rs +++ b/crates/epaint/src/shape.rs @@ -211,24 +211,36 @@ impl Shape { ) -> Self { let galley = fonts.layout_no_wrap(text.to_string(), font_id, color); let rect = anchor.anchor_rect(Rect::from_min_size(pos, galley.size())); - Self::galley(rect.min, galley) + Self::galley(rect.min, galley, color) } + /// Any uncolored parts of the [`Galley`] (using [`Color32::PLACEHOLDER`]) will be replaced with the given color. + /// + /// Any non-placeholder color in the galley takes precedence over this fallback color. + #[inline] + pub fn galley(pos: Pos2, galley: Arc, fallback_color: Color32) -> Self { + TextShape::new(pos, galley, fallback_color).into() + } + + /// All text color in the [`Galley`] will be replaced with the given color. #[inline] - pub fn galley(pos: Pos2, galley: Arc) -> Self { - TextShape::new(pos, galley).into() + pub fn galley_with_override_text_color( + pos: Pos2, + galley: Arc, + text_color: Color32, + ) -> Self { + TextShape::new(pos, galley, text_color) + .with_override_text_color(text_color) + .into() } #[inline] - /// The text color in the [`Galley`] will be replaced with the given color. + #[deprecated = "Use `Shape::galley` or `Shape::galley_with_override_text_color` instead"] pub fn galley_with_color(pos: Pos2, galley: Arc, text_color: Color32) -> Self { - TextShape { - override_text_color: Some(text_color), - ..TextShape::new(pos, galley) - } - .into() + Self::galley_with_override_text_color(pos, galley, text_color) } + #[inline] pub fn mesh(mesh: Mesh) -> Self { crate::epaint_assert!(mesh.is_valid()); Self::Mesh(mesh) @@ -669,9 +681,14 @@ pub struct TextShape { /// You can also set an underline when creating the galley. pub underline: Stroke, + /// Any [`Color32::PLACEHOLDER`] in the galley will be replaced by the given color. + /// Affects everything: backgrounds, glyphs, strikethough, underline, etc. + pub fallback_color: Color32, + /// If set, the text color in the galley will be ignored and replaced /// with the given color. - /// This will NOT replace background color nor strikethrough/underline color. + /// + /// This only affects the glyphs and will NOT replace background color nor strikethrough/underline color. pub override_text_color: Option, /// Rotate text by this many radians clockwise. @@ -680,12 +697,16 @@ pub struct TextShape { } impl TextShape { + /// The given fallback color will be used for any uncolored part of the galley (using [`Color32::PLACEHOLDER`]). + /// + /// Any non-placeholder color in the galley takes precedence over this fallback color. #[inline] - pub fn new(pos: Pos2, galley: Arc) -> Self { + pub fn new(pos: Pos2, galley: Arc, fallback_color: Color32) -> Self { Self { pos, galley, underline: Stroke::NONE, + fallback_color, override_text_color: None, angle: 0.0, } @@ -696,6 +717,27 @@ impl TextShape { pub fn visual_bounding_rect(&self) -> Rect { self.galley.mesh_bounds.translate(self.pos.to_vec2()) } + + #[inline] + pub fn with_underline(mut self, underline: Stroke) -> Self { + self.underline = underline; + self + } + + /// Use the given color for the text, regardless of what color is already in the galley. + #[inline] + pub fn with_override_text_color(mut self, override_text_color: Color32) -> Self { + self.override_text_color = Some(override_text_color); + self + } + + /// Rotate text by this many radians clockwise. + /// The pivot is `pos` (the upper left corner of the text). + #[inline] + pub fn with_angle(mut self, angle: f32) -> Self { + self.angle = angle; + self + } } impl From for Shape { diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index fa50f0df435..72899f2844c 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1473,6 +1473,7 @@ impl Tessellator { galley, underline, override_text_color, + fallback_color, angle, } = text_shape; @@ -1539,11 +1540,16 @@ impl Tessellator { let Vertex { pos, uv, mut color } = *vertex; if let Some(override_text_color) = override_text_color { + // Only override the glyph color (not background color, strike-through color, etc) if row.visuals.glyph_vertex_range.contains(&i) { color = *override_text_color; } + } else if color == Color32::PLACEHOLDER { + color = *fallback_color; } + crate::epaint_assert!(color != Color32::PLACEHOLDER, "A placeholder color made it to the tessellator. You forgot to set a fallback color."); + let offset = if *angle == 0.0 { pos.to_vec2() } else { diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 61ed80fe0d4..7165b2655db 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -531,12 +531,7 @@ impl Fonts { font_id: FontId, wrap_width: f32, ) -> Arc { - self.layout_job(LayoutJob::simple( - text, - font_id, - crate::Color32::TEMPORARY_COLOR, - wrap_width, - )) + self.layout(text, font_id, crate::Color32::PLACEHOLDER, wrap_width) } } diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 7b81cd82440..6d9748aa33a 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -509,8 +509,9 @@ pub struct RowVisuals { /// Does NOT include leading or trailing whitespace glyphs!! pub mesh_bounds: Rect, - /// The range of vertices in the mesh the contain glyphs. - /// Before comes backgrounds (if any), and after any underlines and strikethrough. + /// The range of vertices in the mesh that contain glyphs (as opposed to background, underlines, strikethorugh, etc). + /// + /// The glyph vertices comes before backgrounds (if any), and after any underlines and strikethrough. pub glyph_vertex_range: Range, }