Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add feature to disable text-to-pixel snapping during layout #5057

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions crates/epaint/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ deadlock_detection = ["dep:backtrace"]
## If you plan on specifying your own fonts you may disable this feature.
default_fonts = ["epaint_default_fonts"]

## Disable text snapping to remove logic in text layout that rounds glyphs and kerning to
## the nearest pixel. This is necessary to provide the sharpest text rendering with `ab_glyph`.
## The side effect of such rounding is that text layout will likely be inconsistent across
## different values of `pixel_per_point`. Disabling such "snapping" will make the text fuzzy,
## but the layout will remain constant across `pixels_per_point`.
disable_text_snapping = []
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be a runtime setting instead

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense. Let me revise it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me, it makes the most sense to add this as a field of LayoutJob which would default to true for snapping. That would allow a user like myself to have, e.g., the UI elements all snap while retaining more granular control over a particular layout job. I'm not sure if adding a Struct field is considered a breaking change, so I'd welcome your thoughts on that. Thanks for a great library @emilk.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think adding it to LayoutJob is the right thing yes


## Turn on the `log` feature, that makes egui log some errors using the [`log`](https://docs.rs/log) crate.
log = ["dep:log"]

Expand Down
40 changes: 37 additions & 3 deletions crates/epaint/src/text/font.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ pub struct FontImpl {
ab_glyph_font: ab_glyph::FontArc,

/// Maximum character height
#[cfg(feature = "disable_text_snapping")]
scale_in_pixels: f32,

#[cfg(not(feature = "disable_text_snapping"))]
scale_in_pixels: u32,

height_in_points: f32,
Expand Down Expand Up @@ -106,21 +110,23 @@ impl FontImpl {
scale_in_points * tweak.baseline_offset_factor
};

let y_offset_points = {
let y_offset_in_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 height = ascent + descent;
let y_offset_points = y_offset_points - (1.0 - tweak.scale) * 0.5 * height;
let y_offset_in_points = y_offset_in_points - (1.0 - tweak.scale) * 0.5 * height;

// Round to an even number of physical pixels to get even kerning.
// See https://github.com/emilk/egui/issues/382
#[cfg(not(feature = "disable_text_snapping"))]
let scale_in_pixels = scale_in_pixels.round() as u32;

// Round to closest pixel:
let y_offset_in_points = (y_offset_points * pixels_per_point).round() / pixels_per_point;
#[cfg(not(feature = "disable_text_snapping"))]
let y_offset_in_points = (y_offset_in_points * pixels_per_point).round() / pixels_per_point;

Self {
name,
Expand Down Expand Up @@ -231,6 +237,21 @@ impl FontImpl {
}
}

#[cfg(feature = "disable_text_snapping")]
#[inline]
pub fn pair_kerning(
&self,
last_glyph_id: ab_glyph::GlyphId,
glyph_id: ab_glyph::GlyphId,
) -> f32 {
use ab_glyph::{Font as _, ScaleFont};
self.ab_glyph_font
.as_scaled(self.scale_in_pixels)
.kern(last_glyph_id, glyph_id)
/ self.pixels_per_point
}

#[cfg(not(feature = "disable_text_snapping"))]
#[inline]
pub fn pair_kerning(
&self,
Expand Down Expand Up @@ -263,6 +284,7 @@ impl FontImpl {
self.ascent
}

#[allow(trivial_numeric_casts, clippy::unnecessary_cast)]
fn allocate_glyph(&self, glyph_id: ab_glyph::GlyphId) -> GlyphInfo {
assert!(glyph_id.0 != 0);
use ab_glyph::{Font as _, ScaleFont};
Expand Down Expand Up @@ -333,6 +355,7 @@ pub struct Font {
characters: Option<BTreeSet<char>>,

replacement_glyph: (FontIndex, GlyphInfo),
#[cfg(not(feature = "disable_text_snapping"))]
pixels_per_point: f32,
row_height: f32,
glyph_info_cache: ahash::HashMap<char, (FontIndex, GlyphInfo)>,
Expand All @@ -345,19 +368,22 @@ impl Font {
fonts,
characters: None,
replacement_glyph: Default::default(),
#[cfg(not(feature = "disable_text_snapping"))]
pixels_per_point: 1.0,
row_height: 0.0,
glyph_info_cache: Default::default(),
};
}

#[cfg(not(feature = "disable_text_snapping"))]
let pixels_per_point = fonts[0].pixels_per_point();
let row_height = fonts[0].row_height();

let mut slf = Self {
fonts,
characters: None,
replacement_glyph: Default::default(),
#[cfg(not(feature = "disable_text_snapping"))]
pixels_per_point,
row_height,
glyph_info_cache: Default::default(),
Expand Down Expand Up @@ -409,11 +435,19 @@ impl Font {
})
}

#[cfg(not(feature = "disable_text_snapping"))]
#[inline(always)]
pub fn round_to_pixel(&self, point: f32) -> f32 {
(point * self.pixels_per_point).round() / self.pixels_per_point
}

#[cfg(feature = "disable_text_snapping")]
#[inline(always)]
#[allow(clippy::unused_self)]
pub fn round_to_pixel(&self, point: f32) -> f32 {
point
}

/// Height of one row of text. In points
#[inline(always)]
pub fn row_height(&self) -> f32 {
Expand Down
7 changes: 7 additions & 0 deletions crates/epaint/src/text/text_layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,18 @@ impl PointScale {
self.pixels_per_point
}

#[cfg(not(feature = "disable_text_snapping"))]
#[inline(always)]
pub fn round_to_pixel(&self, point: f32) -> f32 {
(point * self.pixels_per_point).round() / self.pixels_per_point
}

#[cfg(feature = "disable_text_snapping")]
#[inline(always)]
pub fn round_to_pixel(&self, point: f32) -> f32 {
point
}

#[inline(always)]
pub fn floor_to_pixel(&self, point: f32) -> f32 {
(point * self.pixels_per_point).floor() / self.pixels_per_point
Expand Down
53 changes: 53 additions & 0 deletions crates/epaint/tests/font.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#[cfg(feature = "disable_text_snapping")]
#[test]
fn test_layout_sizes_without_snapping() {
let fonts1_0 = epaint::text::Fonts::new(1.0, 1024, epaint::text::FontDefinitions::default());
let fonts1_1 = epaint::text::Fonts::new(1.1, 1024, epaint::text::FontDefinitions::default());
let wrapping = epaint::text::TextWrapping {
max_width: 72.0 * 8.5 - 72.0,
..Default::default()
};
let mut job = epaint::text::LayoutJob {
wrap: wrapping,
..Default::default()
};
job.append(
"Hello, epaint! Thanks for the awesome GUI library!",
0.0,
epaint::text::TextFormat {
font_id: epaint::FontId::new(10.0, epaint::FontFamily::Proportional),
color: epaint::Color32::WHITE,
..Default::default()
},
);
let galley1_0 = fonts1_0.layout_job(job.clone());
let galley1_1 = fonts1_1.layout_job(job.clone());
assert_eq!(galley1_0.rect.size(), galley1_1.rect.size());
}

#[test]
fn test_layout_sizes_with_snapping() {
let fonts1_0 = epaint::text::Fonts::new(1.0, 1024, epaint::text::FontDefinitions::default());
let fonts1_1 = epaint::text::Fonts::new(1.1, 1024, epaint::text::FontDefinitions::default());
let wrapping = epaint::text::TextWrapping {
max_width: 72.0 * 8.5 - 72.0,
..Default::default()
};
let mut job = epaint::text::LayoutJob {
wrap: wrapping,
..Default::default()
};
job.append(
"Hello, epaint! Thanks for the awesome GUI library!",
0.0,
epaint::text::TextFormat {
font_id: epaint::FontId::new(10.0, epaint::FontFamily::Proportional),
color: epaint::Color32::WHITE,
..Default::default()
},
);
let galley1_0 = fonts1_0.layout_job(job.clone());
let galley1_1 = fonts1_1.layout_job(job.clone());
assert_ne!(galley1_0.rect.height(), galley1_1.rect.height());
assert_ne!(galley1_0.rect.width(), galley1_1.rect.width());
}