From 641446bcc9073cc99be28975d7ee4adf23ef4128 Mon Sep 17 00:00:00 2001 From: w4nderlust Date: Mon, 2 Mar 2026 00:13:47 -0800 Subject: [PATCH 1/2] Add Style::strong_font and Style::emphasis_font for real bold/italic rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently, `RichText::strong()` fakes bold by changing the text color, and `RichText::italics()` fakes italic by applying a vertex skew. This works as a zero-configuration default, but produces noticeably different results from real bold/italic typefaces. This commit adds two optional fields to `Style`: - `strong_font: Option` — when set, strong text uses this font family instead of applying a color change. - `emphasis_font: Option` — when set, italic text uses this font family instead of applying a vertex skew. Both default to `None`, preserving full backwards compatibility. Usage: 1. Register bold/italic font data via `Context::set_fonts()` 2. Set `style.strong_font` / `style.emphasis_font` to the family names When a bold font is configured, `get_text_color()` skips the strong color so the typeface weight alone provides emphasis. When an italic font is configured, the vertex skew is disabled in `TextFormat`. When text is both strong and italic and both overrides are set, `emphasis_font` takes precedence since a single `FontId` can only reference one font family. Users needing combined bold-italic can register a dedicated BoldItalic family via `RichText::family()`. --- crates/egui/src/style.rs | 76 ++++++++++++++++++++++++++++++++++ crates/egui/src/widget_text.rs | 66 +++++++++++++++++++++++++---- 2 files changed, 135 insertions(+), 7 deletions(-) diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 1114334703b..a04b4928dda 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -339,6 +339,78 @@ pub struct Style { /// Use a more compact style for menus. pub compact_menu_style: bool, + + /// Optional [`FontFamily`] to use for bold/strong text. + /// + /// By default ([`None`]), [`RichText::strong()`] applies a color change + /// (using [`Visuals::strong_text_color`]) to convey emphasis. + /// When this is set to [`Some`], strong text will instead be rendered with the + /// specified font family, and the color change is skipped. + /// + /// To use this, first register a bold font via [`crate::Context::set_fonts`], + /// then point this field at it: + /// + /// ``` + /// # let ctx = egui::Context::default(); + /// // 1. Register the bold font data and family + /// let mut fonts = egui::FontDefinitions::default(); + /// let bold_font_data: &[u8] = &[]; // bytes of a .ttf file + /// fonts.font_data.insert( + /// "my_bold_font".to_owned(), + /// egui::FontData::from_static(bold_font_data).into(), + /// ); + /// fonts.families.insert( + /// egui::FontFamily::Name("Bold".into()), + /// vec!["my_bold_font".to_owned()], + /// ); + /// ctx.set_fonts(fonts); + /// + /// // 2. Tell egui to use it for strong text + /// ctx.global_style_mut(|style| { + /// style.strong_font = Some(egui::FontFamily::Name("Bold".into())); + /// }); + /// ``` + /// + /// **Note:** When text is both strong and italic, and both `strong_font` and + /// [`Self::emphasis_font`] are configured, `emphasis_font` takes precedence + /// because a single [`FontId`] can only reference one font family. + pub strong_font: Option, + + /// Optional [`FontFamily`] to use for italic/emphasis text. + /// + /// By default ([`None`]), [`RichText::italics()`] fakes italics by applying a + /// horizontal vertex skew during tessellation. + /// When this is set to [`Some`], italic text will instead be rendered with the + /// specified font family, and the vertex skew is skipped. + /// + /// To use this, first register an italic font via [`crate::Context::set_fonts`], + /// then point this field at it: + /// + /// ``` + /// # let ctx = egui::Context::default(); + /// // 1. Register the italic font data and family + /// let mut fonts = egui::FontDefinitions::default(); + /// let italic_font_data: &[u8] = &[]; // bytes of a .ttf file + /// fonts.font_data.insert( + /// "my_italic_font".to_owned(), + /// egui::FontData::from_static(italic_font_data).into(), + /// ); + /// fonts.families.insert( + /// egui::FontFamily::Name("Italic".into()), + /// vec!["my_italic_font".to_owned()], + /// ); + /// ctx.set_fonts(fonts); + /// + /// // 2. Tell egui to use it for italic text + /// ctx.global_style_mut(|style| { + /// style.emphasis_font = Some(egui::FontFamily::Name("Italic".into())); + /// }); + /// ``` + /// + /// **Note:** When text is both strong and italic, and both [`Self::strong_font`] and + /// `emphasis_font` are configured, `emphasis_font` takes precedence + /// because a single [`FontId`] can only reference one font family. + pub emphasis_font: Option, } #[test] @@ -1409,6 +1481,8 @@ impl Default for Style { always_scroll_the_only_direction: false, scroll_animation: ScrollAnimation::default(), compact_menu_style: true, + strong_font: None, + emphasis_font: None, } } } @@ -1721,6 +1795,8 @@ impl Style { always_scroll_the_only_direction, scroll_animation, compact_menu_style, + strong_font: _, + emphasis_font: _, } = self; crate::Grid::new("_options").show(ui, |ui| { diff --git a/crates/egui/src/widget_text.rs b/crates/egui/src/widget_text.rs index 8670398fae4..2019f167292 100644 --- a/crates/egui/src/widget_text.rs +++ b/crates/egui/src/widget_text.rs @@ -3,7 +3,7 @@ use std::fmt::Formatter; use std::{borrow::Cow, sync::Arc}; use crate::{ - Align, Color32, FontFamily, FontSelection, Galley, Style, TextStyle, TextWrapMode, Ui, Visuals, + Align, Color32, FontFamily, FontSelection, Galley, Style, TextStyle, TextWrapMode, Ui, text::{LayoutJob, TextWrapping}, }; @@ -397,7 +397,28 @@ impl RichText { fallback_font: FontSelection, default_valign: Align, ) -> (String, crate::text::TextFormat) { - let text_color = self.get_text_color(&style.visuals); + let text_color = self.get_text_color(style); + + // Resolve font family overrides for bold/italic before destructuring `self`. + // + // When `Style::strong_font` or `Style::emphasis_font` is configured, we + // swap the font family to a real bold/italic typeface instead of relying on + // the default color-change (bold) or vertex-skew (italic) approximations. + // + // If both are configured and the text is both strong and italic, + // `emphasis_font` takes precedence (a single FontId can only reference one + // family). Users who need a combined bold-italic face can register a + // dedicated BoldItalic font family and apply it via `RichText::family()`. + let bold_family = if self.strong { + style.strong_font.clone() + } else { + None + }; + let italic_family = if self.italics { + style.emphasis_font.clone() + } else { + None + }; let Self { text, @@ -434,15 +455,28 @@ impl RichText { if let Some(family) = family { font_id.family = family; } + + // Apply bold/italic font family overrides. + // Italic is applied second so it wins when both are set. + if let Some(bold_family) = bold_family { + font_id.family = bold_family; + } + if let Some(italic_family) = italic_family { + font_id.family = italic_family; + } + font_id }; + // When a real italic font is configured, skip the vertex skew — + // the typeface itself provides proper italic letterforms. + let italics = italics && style.emphasis_font.is_none(); + let background_color = if code { style.visuals.code_bg_color } else { background_color }; - let underline = if underline { crate::Stroke::new(1.0, line_color) } else { @@ -478,15 +512,33 @@ impl RichText { ) } - fn get_text_color(&self, visuals: &Visuals) -> Option { + /// Resolve the text color, accounting for bold font overrides. + /// + /// Priority (highest first): + /// 1. Explicit color set via [`RichText::color()`] + /// 2. Strong color — but only when no [`Style::strong_font`] is configured, + /// because a real bold typeface provides visual distinction on its own + /// 3. Weak color + /// 4. Global [`Visuals::override_text_color`] + fn get_text_color(&self, style: &Style) -> Option { if let Some(text_color) = self.text_color { + // Explicit color always wins. Some(text_color) } else if self.strong { - Some(visuals.strong_text_color()) + if style.strong_font.is_some() { + // A real bold font provides visual weight through thicker strokes, + // so we don't need the fallback strong color. Use the default + // text color instead. + style.visuals.override_text_color + } else { + // No bold font configured — fall back to the strong color + // (brighter than normal text) to convey emphasis. + Some(style.visuals.strong_text_color()) + } } else if self.weak { - Some(visuals.weak_text_color()) + Some(style.visuals.weak_text_color()) } else { - visuals.override_text_color + style.visuals.override_text_color } } } From febc028ca3fc21490d3de1a873000bba80e2fce9 Mon Sep 17 00:00:00 2001 From: w4nderlust Date: Mon, 2 Mar 2026 05:56:39 -0800 Subject: [PATCH 2/2] Fix broken intra-doc link for Visuals::override_text_color Use fully qualified `crate::Visuals::override_text_color` since Visuals is not imported in widget_text.rs. Fixes cargo doc with -D warnings. --- crates/egui/src/widget_text.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui/src/widget_text.rs b/crates/egui/src/widget_text.rs index 2019f167292..43fe6a6a622 100644 --- a/crates/egui/src/widget_text.rs +++ b/crates/egui/src/widget_text.rs @@ -519,7 +519,7 @@ impl RichText { /// 2. Strong color — but only when no [`Style::strong_font`] is configured, /// because a real bold typeface provides visual distinction on its own /// 3. Weak color - /// 4. Global [`Visuals::override_text_color`] + /// 4. Global [`crate::Visuals::override_text_color`] fn get_text_color(&self, style: &Style) -> Option { if let Some(text_color) = self.text_color { // Explicit color always wins.