Replace a special `Color32::PLACEHOLDER` with widget fallback color (#3727)

This introduces a special `Color32::PLACEHOLDER` which, during text
painting, will be replaced with `TextShape::fallback_color`.

The fallback color is mandatory to set in all text painting. Usually
this comes from the current visual style.

This lets users color only parts of a `WidgetText` (using e.g. a
`LayoutJob` or a `Galley`), where the uncolored parts (using
`Color32::PLACEHOLDER`) will be replaced by a default widget color (e.g.
blue for a hyperlink).

For instance, you can color the `⚠️`-emoji red in a piece of text red
and leave the rest of the text uncolored. The color of the rest of the
text will then depend on wether or not you put that text in a label, a
button, or a hyperlink.

Overall this simplifies a lot of complexity in the code but comes with a
few breaking changes:

* `TextShape::new`, `Shape::galley`, and `Painter::galley` now take a
fallback color by argument
* `Shape::galley_with_color` has been deprecated (use `Shape::galley`
instead)
* `Painter::galley_with_color` has been deprecated (use
`Painter::galley` instead)
* `WidgetTextGalley` is gone (use `Arc<Galley>` instead)
* `WidgetTextJob` is gone (use `LayoutJob` instead)
* `RichText::into_text_job` has been replaced with
`RichText::into_layout_job`
* `WidgetText::into_text_job` has been replaced with
`WidgetText::into_layout_job`
This commit is contained in:
Emil Ernerfeldt 2023-12-22 15:09:10 +01:00 committed by GitHub
parent e36b981118
commit 0561fcaba9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 270 additions and 309 deletions

View File

@ -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 {

View File

@ -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 {

View File

@ -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());
}
});

View File

@ -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<Galley>,
/// 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(),
);

View File

@ -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
}

View File

@ -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<R>(&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<Galley>) {
/// 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<Galley>, 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<Galley>,
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<Galley>, 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,
));
}
}
}

View File

@ -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);

View File

@ -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<Color32>) -> 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<Galley>),
}
@ -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<bool>,
available_width: f32,
fallback_font: impl Into<FontSelection>,
) -> WidgetTextGalley {
) -> Arc<Galley> {
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<Arc<Galley>> 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<Galley>,
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<Galley> {
&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);
}
}

View File

@ -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());
}
}

View File

@ -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

View File

@ -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<Galley>, 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

View File

@ -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);
}
}

View File

@ -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

View File

@ -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)) {

View File

@ -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(),
));
}
}
}

View File

@ -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);

View File

@ -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));
}
}
}

View File

@ -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(

View File

@ -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();

View File

@ -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<Galley>, 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_with_override_text_color(
pos: Pos2,
galley: Arc<Galley>,
text_color: Color32,
) -> Self {
TextShape::new(pos, galley, text_color)
.with_override_text_color(text_color)
.into()
}
#[inline]
pub fn galley(pos: Pos2, galley: Arc<Galley>) -> Self {
TextShape::new(pos, galley).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<Galley>, 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<Color32>,
/// 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<Galley>) -> Self {
pub fn new(pos: Pos2, galley: Arc<Galley>, 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<TextShape> for Shape {

View File

@ -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 {

View File

@ -531,12 +531,7 @@ impl Fonts {
font_id: FontId,
wrap_width: f32,
) -> Arc<Galley> {
self.layout_job(LayoutJob::simple(
text,
font_id,
crate::Color32::TEMPORARY_COLOR,
wrap_width,
))
self.layout(text, font_id, crate::Color32::PLACEHOLDER, wrap_width)
}
}

View File

@ -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<usize>,
}