From 94f8b02286d141147fa0f7177487091cbdca4c45 Mon Sep 17 00:00:00 2001 From: lictex_ Date: Wed, 29 Mar 2023 20:36:09 +0800 Subject: [PATCH] improve fallback fonts alignment (#2724) * use font metrics in layout * properly center scaled fonts * adjust docs * fix raised text * fix easymark viewer small text alignment caused by variable row heights --- .../src/easy_mark/easy_mark_viewer.rs | 19 +- crates/epaint/src/text/font.rs | 176 ++++++++++-------- crates/epaint/src/text/fonts.rs | 52 +++--- crates/epaint/src/text/text_layout.rs | 25 ++- crates/epaint/src/text/text_layout_types.rs | 11 +- 5 files changed, 173 insertions(+), 110 deletions(-) diff --git a/crates/egui_demo_lib/src/easy_mark/easy_mark_viewer.rs b/crates/egui_demo_lib/src/easy_mark/easy_mark_viewer.rs index 464afd40c8a..44408ff2b73 100644 --- a/crates/egui_demo_lib/src/easy_mark/easy_mark_viewer.rs +++ b/crates/egui_demo_lib/src/easy_mark/easy_mark_viewer.rs @@ -38,11 +38,26 @@ pub fn item_ui(ui: &mut Ui, item: easy_mark::Item<'_>) { } easy_mark::Item::Text(style, text) => { - ui.label(rich_text_from_style(text, &style)); + let label = rich_text_from_style(text, &style); + if style.small && !style.raised { + ui.with_layout(Layout::left_to_right(Align::BOTTOM), |ui| { + ui.set_min_height(row_height); + ui.label(label); + }); + } else { + ui.label(label); + } } easy_mark::Item::Hyperlink(style, text, url) => { let label = rich_text_from_style(text, &style); - ui.add(Hyperlink::from_label_and_url(label, url)); + if style.small && !style.raised { + ui.with_layout(Layout::left_to_right(Align::BOTTOM), |ui| { + ui.set_height(row_height); + ui.add(Hyperlink::from_label_and_url(label, url)); + }); + } else { + ui.add(Hyperlink::from_label_and_url(label, url)); + } } easy_mark::Item::Separator => { diff --git a/crates/epaint/src/text/font.rs b/crates/epaint/src/text/font.rs index 442c8ade2f0..939cdf9ee86 100644 --- a/crates/epaint/src/text/font.rs +++ b/crates/epaint/src/text/font.rs @@ -1,5 +1,6 @@ use crate::{ mutex::{Mutex, RwLock}, + text::FontTweak, TextureAtlas, }; use emath::{vec2, Vec2}; @@ -42,6 +43,17 @@ pub struct GlyphInfo { /// Unit: points. pub advance_width: f32, + /// `ascent` value from the font metrics. + /// this is the distance from the top to the baseline. + /// + /// Unit: points. + pub ascent: f32, + + /// row height computed from the font metrics. + /// + /// Unit: points. + pub row_height: f32, + /// Texture coordinates. pub uv_rect: UvRect, } @@ -52,6 +64,8 @@ impl Default for GlyphInfo { Self { id: ab_glyph::GlyphId(0), advance_width: 0.0, + ascent: 0.0, + row_height: 0.0, uv_rect: Default::default(), } } @@ -69,6 +83,7 @@ pub struct FontImpl { height_in_points: f32, // move each character by this much (hack) y_offset: f32, + ascent: f32, pixels_per_point: f32, glyph_info_cache: RwLock>, // TODO(emilk): standard Mutex atlas: Arc>, @@ -80,20 +95,37 @@ impl FontImpl { pixels_per_point: f32, name: String, ab_glyph_font: ab_glyph::FontArc, - scale_in_pixels: u32, - y_offset_points: f32, + scale_in_pixels: f32, + tweak: FontTweak, ) -> FontImpl { - assert!(scale_in_pixels > 0); + assert!(scale_in_pixels > 0.0); assert!(pixels_per_point > 0.0); - let height_in_points = scale_in_pixels as f32 / pixels_per_point; + use ab_glyph::*; + let scaled = ab_glyph_font.as_scaled(scale_in_pixels); + let ascent = scaled.ascent() / pixels_per_point; + let descent = scaled.descent() / pixels_per_point; + let line_gap = scaled.line_gap() / pixels_per_point; + + // Tweak the scale as the user desired + let scale_in_pixels = scale_in_pixels * tweak.scale; + + let baseline_offset = { + let scale_in_points = scale_in_pixels / pixels_per_point; + scale_in_points * tweak.baseline_offset_factor + }; - // TODO(emilk): use these font metrics? - // use ab_glyph::ScaleFont as _; - // let scaled = ab_glyph_font.as_scaled(scale_in_pixels as f32); - // dbg!(scaled.ascent()); - // dbg!(scaled.descent()); - // dbg!(scaled.line_gap()); + let y_offset_points = { + let scale_in_points = scale_in_pixels / pixels_per_point; + scale_in_points * tweak.y_offset_factor + } + tweak.y_offset; + + // center scaled glyphs properly + let y_offset_points = y_offset_points + (tweak.scale - 1.0) * 0.5 * (ascent + descent); + + // Round to an even number of physical pixels to get even kerning. + // See https://github.com/emilk/egui/issues/382 + let scale_in_pixels = scale_in_pixels.round() as u32; // Round to closest pixel: let y_offset = (y_offset_points * pixels_per_point).round() / pixels_per_point; @@ -102,8 +134,9 @@ impl FontImpl { name, ab_glyph_font, scale_in_pixels, - height_in_points, + height_in_points: ascent - descent + line_gap, y_offset, + ascent: ascent + baseline_offset, pixels_per_point, glyph_info_cache: Default::default(), atlas, @@ -194,15 +227,7 @@ impl FontImpl { if glyph_id.0 == 0 { None // unsupported character } else { - let glyph_info = allocate_glyph( - &mut self.atlas.lock(), - &self.ab_glyph_font, - glyph_id, - self.scale_in_pixels as f32, - self.y_offset, - self.pixels_per_point, - ); - + let glyph_info = self.allocate_glyph(glyph_id); self.glyph_info_cache.write().insert(c, glyph_info); Some(glyph_info) } @@ -231,6 +256,62 @@ impl FontImpl { pub fn pixels_per_point(&self) -> f32 { self.pixels_per_point } + + fn allocate_glyph(&self, glyph_id: ab_glyph::GlyphId) -> GlyphInfo { + assert!(glyph_id.0 != 0); + use ab_glyph::{Font as _, ScaleFont}; + + let glyph = glyph_id.with_scale_and_position( + self.scale_in_pixels as f32, + ab_glyph::Point { x: 0.0, y: 0.0 }, + ); + + let uv_rect = self.ab_glyph_font.outline_glyph(glyph).map(|glyph| { + let bb = glyph.px_bounds(); + let glyph_width = bb.width() as usize; + let glyph_height = bb.height() as usize; + if glyph_width == 0 || glyph_height == 0 { + UvRect::default() + } else { + let atlas = &mut self.atlas.lock(); + let (glyph_pos, image) = atlas.allocate((glyph_width, glyph_height)); + glyph.draw(|x, y, v| { + if v > 0.0 { + let px = glyph_pos.0 + x as usize; + let py = glyph_pos.1 + y as usize; + image[(px, py)] = v; + } + }); + + let offset_in_pixels = vec2(bb.min.x, bb.min.y); + let offset = offset_in_pixels / self.pixels_per_point + self.y_offset * Vec2::Y; + UvRect { + offset, + size: vec2(glyph_width as f32, glyph_height as f32) / self.pixels_per_point, + min: [glyph_pos.0 as u16, glyph_pos.1 as u16], + max: [ + (glyph_pos.0 + glyph_width) as u16, + (glyph_pos.1 + glyph_height) as u16, + ], + } + } + }); + let uv_rect = uv_rect.unwrap_or_default(); + + let advance_width_in_points = self + .ab_glyph_font + .as_scaled(self.scale_in_pixels as f32) + .h_advance(glyph_id) + / self.pixels_per_point; + + GlyphInfo { + id: glyph_id, + advance_width: advance_width_in_points, + ascent: self.ascent, + row_height: self.row_height(), + uv_rect, + } + } } type FontIndex = usize; @@ -429,58 +510,3 @@ fn invisible_char(c: char) -> bool { | '\u{FEFF}' // ZERO WIDTH NO-BREAK SPACE ) } - -fn allocate_glyph( - atlas: &mut TextureAtlas, - font: &ab_glyph::FontArc, - glyph_id: ab_glyph::GlyphId, - scale_in_pixels: f32, - y_offset: f32, - pixels_per_point: f32, -) -> GlyphInfo { - assert!(glyph_id.0 != 0); - use ab_glyph::{Font as _, ScaleFont}; - - let glyph = - glyph_id.with_scale_and_position(scale_in_pixels, ab_glyph::Point { x: 0.0, y: 0.0 }); - - let uv_rect = font.outline_glyph(glyph).map(|glyph| { - let bb = glyph.px_bounds(); - let glyph_width = bb.width() as usize; - let glyph_height = bb.height() as usize; - if glyph_width == 0 || glyph_height == 0 { - UvRect::default() - } else { - let (glyph_pos, image) = atlas.allocate((glyph_width, glyph_height)); - glyph.draw(|x, y, v| { - if v > 0.0 { - let px = glyph_pos.0 + x as usize; - let py = glyph_pos.1 + y as usize; - image[(px, py)] = v; - } - }); - - let offset_in_pixels = vec2(bb.min.x, scale_in_pixels + bb.min.y); - let offset = offset_in_pixels / pixels_per_point + y_offset * Vec2::Y; - UvRect { - offset, - size: vec2(glyph_width as f32, glyph_height as f32) / pixels_per_point, - min: [glyph_pos.0 as u16, glyph_pos.1 as u16], - max: [ - (glyph_pos.0 + glyph_width) as u16, - (glyph_pos.1 + glyph_height) as u16, - ], - } - } - }); - let uv_rect = uv_rect.unwrap_or_default(); - - let advance_width_in_points = - font.as_scaled(scale_in_pixels).h_advance(glyph_id) / pixels_per_point; - - GlyphInfo { - id: glyph_id, - advance_width: advance_width_in_points, - uv_rect, - } -} diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 4857213c21f..68dd5812723 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -153,12 +153,14 @@ impl FontData { #[derive(Copy, Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct FontTweak { - /// Scale the font by this much. + /// Scale the font's glyphs by this much. + /// this is only a visual effect and does not affect the text layout. /// /// Default: `1.0` (no scaling). pub scale: f32, - /// Shift font downwards by this fraction of the font size (in points). + /// Shift font's glyphs downwards by this fraction of the font size (in points). + /// this is only a visual effect and does not affect the text layout. /// /// A positive value shifts the text downwards. /// A negative value shifts it upwards. @@ -166,18 +168,27 @@ pub struct FontTweak { /// Example value: `-0.2`. pub y_offset_factor: f32, - /// Shift font downwards by this amount of logical points. + /// Shift font's glyphs downwards by this amount of logical points. + /// this is only a visual effect and does not affect the text layout. /// /// Example value: `2.0`. pub y_offset: f32, + + /// When using this font's metrics to layout a row, + /// shift the entire row downwards by this fraction of the font size (in points). + /// + /// A positive value shifts the text downwards. + /// A negative value shifts it upwards. + pub baseline_offset_factor: f32, } impl Default for FontTweak { fn default() -> Self { Self { scale: 1.0, - y_offset_factor: -0.2, // makes the default fonts look more centered in buttons and such + 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 } } } @@ -272,9 +283,8 @@ impl Default for FontDefinitions { "NotoEmoji-Regular".to_owned(), FontData::from_static(include_bytes!("../../fonts/NotoEmoji-Regular.ttf")).tweak( FontTweak { - scale: 0.81, // make it smaller - y_offset_factor: -0.2, // move it up - y_offset: 0.0, + scale: 0.81, // make it smaller + ..Default::default() }, ), ); @@ -284,9 +294,12 @@ impl Default for FontDefinitions { "emoji-icon-font".to_owned(), FontData::from_static(include_bytes!("../../fonts/emoji-icon-font.ttf")).tweak( FontTweak { - scale: 0.88, // make it smaller - y_offset_factor: 0.07, // move it down slightly - y_offset: 0.0, + 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 + ..Default::default() }, ), ); @@ -760,20 +773,11 @@ impl FontImplCache { let font_scaling = ab_glyph_font.height_unscaled() / units_per_em; let scale_in_pixels = scale_in_pixels * font_scaling; - // Tweak the scale as the user desired: - let scale_in_pixels = scale_in_pixels * tweak.scale; - - // Round to an even number of physical pixels to get even kerning. - // See https://github.com/emilk/egui/issues/382 - let scale_in_pixels = scale_in_pixels.round() as u32; - - let y_offset_points = { - let scale_in_points = scale_in_pixels as f32 / self.pixels_per_point; - scale_in_points * tweak.y_offset_factor - } + tweak.y_offset; - self.cache - .entry((scale_in_pixels, font_name.to_owned())) + .entry(( + (scale_in_pixels * tweak.scale).round() as u32, + font_name.to_owned(), + )) .or_insert_with(|| { Arc::new(FontImpl::new( self.atlas.clone(), @@ -781,7 +785,7 @@ impl FontImplCache { font_name.to_owned(), ab_glyph_font, scale_in_pixels, - y_offset_points, + tweak, )) }) .clone() diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index bbf677d1795..8b57b2b06f1 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -123,7 +123,8 @@ fn layout_section( paragraph.glyphs.push(Glyph { chr, pos: pos2(paragraph.cursor_x, f32::NAN), - size: vec2(glyph_info.advance_width, font_height), + size: vec2(glyph_info.advance_width, glyph_info.row_height), + ascent: glyph_info.ascent, uv_rect: glyph_info.uv_rect, section_index, }); @@ -434,17 +435,31 @@ fn galley_from_rows(point_scale: PointScale, job: Arc, mut rows: Vec< let mut max_x: f32 = 0.0; for row in &mut rows { let mut row_height = first_row_min_height.max(row.rect.height()); + let mut row_ascent = 0.0f32; first_row_min_height = 0.0; - for glyph in &row.glyphs { - row_height = row_height.max(glyph.size.y); + + // 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()) + { + row_height = glyph.size.y; + row_ascent = glyph.ascent; } row_height = point_scale.round_to_pixel(row_height); // Now positions each glyph: for glyph in &mut row.glyphs { let format = &job.sections[glyph.section_index as usize].format; - glyph.pos.y = cursor_y + format.valign.to_factor() * (row_height - glyph.size.y); - glyph.pos.y = point_scale.round_to_pixel(glyph.pos.y); + + let align_offset = match format.valign { + Align::Center | Align::Max => row_ascent, + + // raised text. + Align::Min => glyph.ascent, + }; + glyph.pos.y = cursor_y + align_offset; } row.rect.min.y = cursor_y; diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 99a1da059dc..bc317e3112b 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -351,7 +351,7 @@ pub struct Galley { pub pixels_per_point: f32, } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct Row { /// One for each `char`. @@ -400,16 +400,19 @@ impl Default for RowVisuals { } } -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct Glyph { /// The character this glyph represents. pub chr: char, - /// Relative to the galley position. + /// Baseline position, relative to the galley. /// 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, + /// Advance width and font row height. pub size: Vec2, @@ -428,7 +431,7 @@ impl Glyph { /// Same y range for all characters with the same [`TextFormat`]. #[inline] pub fn logical_rect(&self) -> Rect { - Rect::from_min_size(self.pos, self.size) + Rect::from_min_size(self.pos - vec2(0.0, self.ascent), self.size) } }