diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index be402150..c460312b 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -4,6 +4,7 @@ use std::{collections::BTreeMap, ops::RangeInclusive, sync::Arc}; +use emath::Align; use epaint::{text::FontTweak, Rounding, Shadow, Stroke}; use crate::{ @@ -196,6 +197,11 @@ pub struct Style { /// which will take precedence over this. pub override_font_id: Option, + /// How to vertically align text. + /// + /// Set to `None` to use align that depends on the current layout. + pub override_text_valign: Option, + /// The [`FontFamily`] and size you want to use for a specific [`TextStyle`]. /// /// The most convenient way to look something up in this is to use [`TextStyle::resolve`]. @@ -1201,6 +1207,7 @@ impl Default for Style { Self { override_font_id: None, override_text_style: None, + override_text_valign: Some(Align::Center), text_styles: default_text_styles(), drag_value_text_style: TextStyle::Button, number_formatter: NumberFormatter(Arc::new(emath::format_with_decimals_in_range)), @@ -1501,6 +1508,7 @@ impl Style { let Self { override_font_id, override_text_style, + override_text_valign, text_styles, drag_value_text_style, number_formatter: _, // can't change callbacks in the UI @@ -1534,7 +1542,7 @@ impl Style { ui.end_row(); ui.label("Override text style"); - crate::ComboBox::from_id_salt("Override text style") + crate::ComboBox::from_id_salt("override_text_style") .selected_text(match override_text_style { None => "None".to_owned(), Some(override_text_style) => override_text_style.to_string(), @@ -1550,6 +1558,28 @@ impl Style { }); ui.end_row(); + fn valign_name(valign: Align) -> &'static str { + match valign { + Align::TOP => "Top", + Align::Center => "Center", + Align::BOTTOM => "Bottom", + } + } + + ui.label("Override text valign"); + crate::ComboBox::from_id_salt("override_text_valign") + .selected_text(match override_text_valign { + None => "None", + Some(override_text_valign) => valign_name(*override_text_valign), + }) + .show_ui(ui, |ui| { + ui.selectable_value(override_text_valign, None, "None"); + for align in [Align::TOP, Align::Center, Align::BOTTOM] { + ui.selectable_value(override_text_valign, Some(align), valign_name(align)); + } + }); + ui.end_row(); + ui.label("Text style of DragValue"); crate::ComboBox::from_id_salt("drag_value_text_style") .selected_text(drag_value_text_style.to_string()) diff --git a/crates/egui/src/text_selection/accesskit_text.rs b/crates/egui/src/text_selection/accesskit_text.rs index 99599d01..f56ab9ed 100644 --- a/crates/egui/src/text_selection/accesskit_text.rs +++ b/crates/egui/src/text_selection/accesskit_text.rs @@ -77,7 +77,7 @@ pub fn update_accesskit_for_text_widget( value.push(glyph.chr); character_lengths.push((value.len() - old_len) as _); character_positions.push(glyph.pos.x - row.rect.min.x); - character_widths.push(glyph.size.x); + character_widths.push(glyph.advance_width); } if row.ends_with_newline { diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 3a75afb2..2a2c790c 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -617,6 +617,14 @@ impl Ui { self.wrap_mode() == TextWrapMode::Wrap } + /// How to vertically align text + #[inline] + pub fn text_valign(&self) -> Align { + self.style() + .override_text_valign + .unwrap_or_else(|| self.layout().vertical_align()) + } + /// Create a painter for a sub-region of this Ui. /// /// The clip-rect of the returned [`Painter`] will be the intersection diff --git a/crates/egui/src/widget_text.rs b/crates/egui/src/widget_text.rs index 29d59ff1..03b8cbb9 100644 --- a/crates/egui/src/widget_text.rs +++ b/crates/egui/src/widget_text.rs @@ -648,7 +648,7 @@ impl WidgetText { available_width: f32, fallback_font: impl Into, ) -> Arc { - let valign = ui.layout().vertical_align(); + let valign = ui.text_valign(); let style = ui.style(); let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode()); diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 8f12e4e0..f5425980 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -251,9 +251,9 @@ impl Widget for Button<'_> { if image.is_some() && galley.is_some() { desired_size.x += ui.spacing().icon_spacing; } - if let Some(text) = &galley { - desired_size.x += text.size().x; - desired_size.y = desired_size.y.max(text.size().y); + if let Some(galley) = &galley { + desired_size.x += galley.size().x; + desired_size.y = desired_size.y.max(galley.size().y); } if let Some(shortcut_galley) = &shortcut_galley { desired_size.x += gap_before_shortcut_text + shortcut_galley.size().x; diff --git a/crates/egui/src/widgets/label.rs b/crates/egui/src/widgets/label.rs index 23ece9bb..1119bc14 100644 --- a/crates/egui/src/widgets/label.rs +++ b/crates/egui/src/widgets/label.rs @@ -162,7 +162,7 @@ impl Label { return (pos, galley, response); } - let valign = ui.layout().vertical_align(); + let valign = ui.text_valign(); let mut layout_job = self .text .into_layout_job(ui.style(), FontSelection::Default, valign); diff --git a/crates/egui_demo_lib/src/demo/misc_demo_window.rs b/crates/egui_demo_lib/src/demo/misc_demo_window.rs index df85a31b..0fd6b7fb 100644 --- a/crates/egui_demo_lib/src/demo/misc_demo_window.rs +++ b/crates/egui_demo_lib/src/demo/misc_demo_window.rs @@ -580,8 +580,17 @@ fn text_layout_demo(ui: &mut Ui) { }; job.append( - "This is a demonstration of ", + "This", first_row_indentation, + TextFormat { + color: default_color, + font_id: FontId::proportional(20.0), + ..Default::default() + }, + ); + job.append( + " is a demonstration of ", + 0.0, TextFormat { color: default_color, ..Default::default() @@ -632,7 +641,7 @@ fn text_layout_demo(ui: &mut Ui) { "mixing ", 0.0, TextFormat { - font_id: FontId::proportional(17.0), + font_id: FontId::proportional(20.0), color: default_color, ..Default::default() }, diff --git a/crates/epaint/src/text/font.rs b/crates/epaint/src/text/font.rs index 8fc6e9ef..d717c3e7 100644 --- a/crates/epaint/src/text/font.rs +++ b/crates/epaint/src/text/font.rs @@ -468,6 +468,14 @@ impl Font { (Some(font_impl), glyph_info) } + pub(crate) fn ascent(&self) -> f32 { + if let Some(first) = self.fonts.first() { + first.ascent() + } else { + self.row_height + } + } + fn glyph_info_no_cache_or_fallback(&mut self, c: char) -> Option<(FontIndex, GlyphInfo)> { for (font_index, font_impl) in self.fonts.iter().enumerate() { if let Some(glyph_info) = font_impl.glyph_info(c) { diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index dcfbbd7f..df583c96 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -189,7 +189,7 @@ impl Default for FontTweak { scale: 1.0, y_offset_factor: 0.0, y_offset: 0.0, - baseline_offset_factor: -0.0333, // makes the default fonts look more centered in buttons and such + baseline_offset_factor: 0.0, } } } @@ -271,29 +271,26 @@ impl Default for FontDefinitions { let mut families = BTreeMap::new(); font_data.insert("Hack".to_owned(), FontData::from_static(HACK_REGULAR)); - font_data.insert( - "Ubuntu-Light".to_owned(), - FontData::from_static(UBUNTU_LIGHT), - ); // Some good looking emojis. Use as first priority: font_data.insert( "NotoEmoji-Regular".to_owned(), FontData::from_static(NOTO_EMOJI_REGULAR).tweak(FontTweak { - scale: 0.81, // make it smaller + scale: 0.81, // Make smaller ..Default::default() }), ); + font_data.insert( + "Ubuntu-Light".to_owned(), + FontData::from_static(UBUNTU_LIGHT), + ); + // Bigger emojis, and more. : font_data.insert( "emoji-icon-font".to_owned(), FontData::from_static(EMOJI_ICON).tweak(FontTweak { - scale: 0.88, // make it smaller - - // probably not correct, but this does make texts look better (#2724 for details) - y_offset_factor: 0.11, // move glyphs down to better align with common fonts - baseline_offset_factor: -0.11, // ...now the entire row is a bit down so shift it back + scale: 0.90, // Make smaller ..Default::default() }), ); diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 96dd0675..caf14612 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -171,8 +171,12 @@ fn layout_section( paragraph.glyphs.push(Glyph { chr, pos: pos2(paragraph.cursor_x, f32::NAN), - size: vec2(glyph_info.advance_width, line_height), - ascent: font_impl.map_or(0.0, |font| font.ascent()), // Failure to find the font here would be weird + advance_width: glyph_info.advance_width, + line_height, + font_impl_height: font_impl.map_or(0.0, |f| f.row_height()), + font_impl_ascent: font_impl.map_or(0.0, |f| f.ascent()), + font_height: font.row_height(), + font_ascent: font.ascent(), uv_rect: glyph_info.uv_rect, section_index, }); @@ -376,7 +380,7 @@ fn replace_last_glyph_with_overflow_character( let (_, last_glyph_info) = font.font_impl_and_glyph_info(last_glyph.chr); - let mut x = last_glyph.pos.x + last_glyph.size.x; + let mut x = last_glyph.pos.x + last_glyph.advance_width; let (font_impl, replacement_glyph_info) = font.font_impl_and_glyph_info(overflow_character); @@ -391,8 +395,12 @@ fn replace_last_glyph_with_overflow_character( row.glyphs.push(Glyph { chr: overflow_character, pos: pos2(x, f32::NAN), - size: vec2(replacement_glyph_info.advance_width, line_height), - ascent: font_impl.map_or(0.0, |font| font.ascent()), // Failure to find the font here would be weird + advance_width: replacement_glyph_info.advance_width, + line_height, + font_impl_height: font_impl.map_or(0.0, |f| f.row_height()), + font_impl_ascent: font_impl.map_or(0.0, |f| f.ascent()), + font_height: font.row_height(), + font_ascent: font.ascent(), uv_rect: replacement_glyph_info.uv_rect, section_index, }); @@ -409,8 +417,12 @@ fn replace_last_glyph_with_overflow_character( row.glyphs.push(Glyph { chr: overflow_character, pos: pos2(x, f32::NAN), - size: vec2(replacement_glyph_info.advance_width, line_height), - ascent: font_impl.map_or(0.0, |font| font.ascent()), // Failure to find the font here would be weird + advance_width: replacement_glyph_info.advance_width, + line_height, + font_impl_height: font_impl.map_or(0.0, |f| f.row_height()), + font_impl_ascent: font_impl.map_or(0.0, |f| f.ascent()), + font_height: font.row_height(), + font_ascent: font.ascent(), uv_rect: replacement_glyph_info.uv_rect, section_index, }); @@ -438,7 +450,6 @@ fn replace_last_glyph_with_overflow_character( let section = &job.sections[last_glyph.section_index as usize]; let extra_letter_spacing = section.format.extra_letter_spacing; let font = fonts.font(§ion.format.font_id); - let line_height = row_height(section, font); if let Some(prev_glyph) = prev_glyph { let prev_glyph_id = font.font_impl_and_glyph_info(prev_glyph.chr).1.id; @@ -453,7 +464,9 @@ fn replace_last_glyph_with_overflow_character( // Replace the glyph: last_glyph.chr = overflow_character; let (font_impl, glyph_info) = font.font_impl_and_glyph_info(last_glyph.chr); - last_glyph.size = vec2(glyph_info.advance_width, line_height); + last_glyph.advance_width = glyph_info.advance_width; + last_glyph.font_impl_ascent = font_impl.map_or(0.0, |f| f.ascent()); + last_glyph.font_impl_height = font_impl.map_or(0.0, |f| f.row_height()); last_glyph.uv_rect = glyph_info.uv_rect; // Reapply kerning: @@ -472,8 +485,10 @@ fn replace_last_glyph_with_overflow_character( } else { // Just replace and be done with it. last_glyph.chr = overflow_character; - let (_, glyph_info) = font.font_impl_and_glyph_info(last_glyph.chr); - last_glyph.size = vec2(glyph_info.advance_width, line_height); + let (font_impl, glyph_info) = font.font_impl_and_glyph_info(last_glyph.chr); + last_glyph.advance_width = glyph_info.advance_width; + last_glyph.font_impl_ascent = font_impl.map_or(0.0, |f| f.ascent()); + last_glyph.font_impl_height = font_impl.map_or(0.0, |f| f.row_height()); last_glyph.uv_rect = glyph_info.uv_rect; return; } @@ -585,40 +600,36 @@ fn galley_from_rows( let mut min_x: f32 = 0.0; let mut max_x: f32 = 0.0; for row in &mut rows { - let mut line_height = first_row_min_height.max(row.rect.height()); - let mut row_ascent = 0.0f32; + let mut max_row_height = first_row_min_height.max(row.rect.height()); first_row_min_height = 0.0; - - // take metrics from the highest font in this row - if let Some(glyph) = row - .glyphs - .iter() - .max_by(|a, b| a.size.y.partial_cmp(&b.size.y).unwrap()) - { - line_height = glyph.size.y; - row_ascent = glyph.ascent; + for glyph in &row.glyphs { + max_row_height = max_row_height.max(glyph.line_height); } - line_height = point_scale.round_to_pixel(line_height); + max_row_height = point_scale.round_to_pixel(max_row_height); - // Now positions each glyph: + // Now position each glyph vertically: for glyph in &mut row.glyphs { let format = &job.sections[glyph.section_index as usize].format; - let align_offset = match format.valign { - Align::Center | Align::Max => row_ascent, + glyph.pos.y = cursor_y + + glyph.font_impl_ascent - // raised text. - Align::Min => glyph.ascent, - }; - glyph.pos.y = cursor_y + align_offset; + // Apply valign to the different in height of the entire row, and the height of this `Font`: + + format.valign.to_factor() * (max_row_height - glyph.line_height) + + // When mixing different `FontImpl` (e.g. latin and emojis), + // we always center the difference: + + 0.5 * (glyph.font_height - glyph.font_impl_height); + + glyph.pos.y = point_scale.round_to_pixel(glyph.pos.y); } row.rect.min.y = cursor_y; - row.rect.max.y = cursor_y + line_height; + row.rect.max.y = cursor_y + max_row_height; min_x = min_x.min(row.rect.min.x); max_x = max_x.max(row.rect.max.x); - cursor_y += line_height; + cursor_y += max_row_height; cursor_y = point_scale.round_to_pixel(cursor_y); } diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 2099e3cf..17826e6a 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -279,8 +279,14 @@ pub struct TextFormat { /// If you use a small font and [`Align::TOP`] you /// can get the effect of raised text. + /// + /// If you use a small font and [`Align::BOTTOM`] + /// you get the effect of a subscript. + /// + /// If you use [`Align::Center`], you get text that is centered + /// around a common center-line, which is nice when mixining emojis + /// and normal text in e.g. a button. pub valign: Align, - // TODO(emilk): lowered } impl Default for TextFormat { @@ -602,13 +608,26 @@ pub struct Glyph { /// Logical position: pos.y is the same for all chars of the same [`TextFormat`]. pub pos: Pos2, - /// `ascent` value from the font - pub ascent: f32, + /// Logical width of the glyph. + pub advance_width: f32, - /// Advance width and line height. + /// Height of this row of text. /// - /// Does not control the visual size of the glyph (see [`Self::uv_rect`] for that). - pub size: Vec2, + /// Usually same as [`Self::font_height`], + /// unless explicitly overridden by [`TextFormat::line_height`]. + pub line_height: f32, + + /// The ascent of this font. + pub font_ascent: f32, + + /// The row/line height of this font. + pub font_height: f32, + + /// The ascent of the sub-font within the font ("FontImpl"). + pub font_impl_ascent: f32, + + /// The row/line height of the sub-font within the font ("FontImpl"). + pub font_impl_height: f32, /// Position and size of the glyph in the font texture, in texels. pub uv_rect: UvRect, @@ -618,14 +637,20 @@ pub struct Glyph { } impl Glyph { + #[inline] + pub fn size(&self) -> Vec2 { + Vec2::new(self.advance_width, self.line_height) + } + + #[inline] pub fn max_x(&self) -> f32 { - self.pos.x + self.size.x + self.pos.x + self.advance_width } /// Same y range for all characters with the same [`TextFormat`]. #[inline] pub fn logical_rect(&self) -> Rect { - Rect::from_min_size(self.pos - vec2(0.0, self.ascent), self.size) + Rect::from_min_size(self.pos - vec2(0.0, self.font_ascent), self.size()) } }