Skip to content

Commit 7ea3f76

Browse files
authored
Make text underline and strikethrough pixel perfect crisp (emilk#5857)
Small visual fix: pixel-align any text underline or strikethrough. Before they could be often be blurry.
1 parent 884be34 commit 7ea3f76

File tree

4 files changed

+67
-56
lines changed

4 files changed

+67
-56
lines changed
Loading

crates/epaint/src/stroke.rs

+60
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
use std::{fmt::Debug, sync::Arc};
44

5+
use emath::GuiRounding as _;
6+
57
use super::{emath, Color32, ColorMode, Pos2, Rect};
68

79
/// Describes the width and color of a line.
@@ -34,6 +36,46 @@ impl Stroke {
3436
pub fn is_empty(&self) -> bool {
3537
self.width <= 0.0 || self.color == Color32::TRANSPARENT
3638
}
39+
40+
/// For vertical or horizontal lines:
41+
/// round the stroke center to produce a sharp, pixel-aligned line.
42+
pub fn round_center_to_pixel(&self, pixels_per_point: f32, coord: &mut f32) {
43+
// If the stroke is an odd number of pixels wide,
44+
// we want to round the center of it to the center of a pixel.
45+
//
46+
// If however it is an even number of pixels wide,
47+
// we want to round the center to be between two pixels.
48+
//
49+
// We also want to treat strokes that are _almost_ odd as it it was odd,
50+
// to make it symmetric. Same for strokes that are _almost_ even.
51+
//
52+
// For strokes less than a pixel wide we also round to the center,
53+
// because it will rendered as a single row of pixels by the tessellator.
54+
55+
let pixel_size = 1.0 / pixels_per_point;
56+
57+
if self.width <= pixel_size || is_nearest_integer_odd(pixels_per_point * self.width) {
58+
*coord = coord.round_to_pixel_center(pixels_per_point);
59+
} else {
60+
*coord = coord.round_to_pixels(pixels_per_point);
61+
}
62+
}
63+
64+
pub(crate) fn round_rect_to_pixel(&self, pixels_per_point: f32, rect: &mut Rect) {
65+
// We put odd-width strokes in the center of pixels.
66+
// To understand why, see `fn round_center_to_pixel`.
67+
68+
let pixel_size = 1.0 / pixels_per_point;
69+
70+
let width = self.width;
71+
if width <= 0.0 {
72+
*rect = rect.round_to_pixels(pixels_per_point);
73+
} else if width <= pixel_size || is_nearest_integer_odd(pixels_per_point * width) {
74+
*rect = rect.round_to_pixel_center(pixels_per_point);
75+
} else {
76+
*rect = rect.round_to_pixels(pixels_per_point);
77+
}
78+
}
3779
}
3880

3981
impl<Color> From<(f32, Color)> for Stroke
@@ -182,3 +224,21 @@ impl From<Stroke> for PathStroke {
182224
}
183225
}
184226
}
227+
228+
/// Returns true if the nearest integer is odd.
229+
fn is_nearest_integer_odd(x: f32) -> bool {
230+
(x * 0.5 + 0.25).fract() > 0.5
231+
}
232+
233+
#[test]
234+
fn test_is_nearest_integer_odd() {
235+
assert!(is_nearest_integer_odd(0.6));
236+
assert!(is_nearest_integer_odd(1.0));
237+
assert!(is_nearest_integer_odd(1.4));
238+
assert!(!is_nearest_integer_odd(1.6));
239+
assert!(!is_nearest_integer_odd(2.0));
240+
assert!(!is_nearest_integer_odd(2.4));
241+
assert!(is_nearest_integer_odd(2.6));
242+
assert!(is_nearest_integer_odd(3.0));
243+
assert!(is_nearest_integer_odd(3.4));
244+
}

crates/epaint/src/tessellator.rs

+3-53
Original file line numberDiff line numberDiff line change
@@ -1656,7 +1656,7 @@ impl Tessellator {
16561656
if a.x == b.x {
16571657
// Vertical line
16581658
let mut x = a.x;
1659-
round_line_segment(&mut x, &stroke, self.pixels_per_point);
1659+
stroke.round_center_to_pixel(self.pixels_per_point, &mut x);
16601660
a.x = x;
16611661
b.x = x;
16621662

@@ -1677,7 +1677,7 @@ impl Tessellator {
16771677
if a.y == b.y {
16781678
// Horizontal line
16791679
let mut y = a.y;
1680-
round_line_segment(&mut y, &stroke, self.pixels_per_point);
1680+
stroke.round_center_to_pixel(self.pixels_per_point, &mut y);
16811681
a.y = y;
16821682
b.y = y;
16831683

@@ -1778,7 +1778,6 @@ impl Tessellator {
17781778

17791779
let mut corner_radius = CornerRadiusF32::from(corner_radius);
17801780
let round_to_pixels = round_to_pixels.unwrap_or(self.options.round_rects_to_pixels);
1781-
let pixel_size = 1.0 / self.pixels_per_point;
17821781

17831782
if stroke.width == 0.0 {
17841783
stroke.color = Color32::TRANSPARENT;
@@ -1849,17 +1848,7 @@ impl Tessellator {
18491848
}
18501849
StrokeKind::Middle => {
18511850
// On this path we optimize for crisp and symmetric strokes.
1852-
// We put odd-width strokes in the center of pixels.
1853-
// To understand why, see `fn round_line_segment`.
1854-
if stroke.width <= 0.0 {
1855-
rect = rect.round_to_pixels(self.pixels_per_point);
1856-
} else if stroke.width <= pixel_size
1857-
|| is_nearest_integer_odd(self.pixels_per_point * stroke.width)
1858-
{
1859-
rect = rect.round_to_pixel_center(self.pixels_per_point);
1860-
} else {
1861-
rect = rect.round_to_pixels(self.pixels_per_point);
1862-
}
1851+
stroke.round_rect_to_pixel(self.pixels_per_point, &mut rect);
18631852
}
18641853
StrokeKind::Outside => {
18651854
// Put the inside of the stroke on a pixel boundary.
@@ -2203,45 +2192,6 @@ impl Tessellator {
22032192
}
22042193
}
22052194

2206-
fn round_line_segment(coord: &mut f32, stroke: &Stroke, pixels_per_point: f32) {
2207-
// If the stroke is an odd number of pixels wide,
2208-
// we want to round the center of it to the center of a pixel.
2209-
//
2210-
// If however it is an even number of pixels wide,
2211-
// we want to round the center to be between two pixels.
2212-
//
2213-
// We also want to treat strokes that are _almost_ odd as it it was odd,
2214-
// to make it symmetric. Same for strokes that are _almost_ even.
2215-
//
2216-
// For strokes less than a pixel wide we also round to the center,
2217-
// because it will rendered as a single row of pixels by the tessellator.
2218-
2219-
let pixel_size = 1.0 / pixels_per_point;
2220-
2221-
if stroke.width <= pixel_size || is_nearest_integer_odd(pixels_per_point * stroke.width) {
2222-
*coord = coord.round_to_pixel_center(pixels_per_point);
2223-
} else {
2224-
*coord = coord.round_to_pixels(pixels_per_point);
2225-
}
2226-
}
2227-
2228-
fn is_nearest_integer_odd(width: f32) -> bool {
2229-
(width * 0.5 + 0.25).fract() > 0.5
2230-
}
2231-
2232-
#[test]
2233-
fn test_is_nearest_integer_odd() {
2234-
assert!(is_nearest_integer_odd(0.6));
2235-
assert!(is_nearest_integer_odd(1.0));
2236-
assert!(is_nearest_integer_odd(1.4));
2237-
assert!(!is_nearest_integer_odd(1.6));
2238-
assert!(!is_nearest_integer_odd(2.0));
2239-
assert!(!is_nearest_integer_odd(2.4));
2240-
assert!(is_nearest_integer_odd(2.6));
2241-
assert!(is_nearest_integer_odd(3.0));
2242-
assert!(is_nearest_integer_odd(3.4));
2243-
}
2244-
22452195
#[deprecated = "Use `Tessellator::new(…).tessellate_shapes(…)` instead"]
22462196
pub fn tessellate_shapes(
22472197
pixels_per_point: f32,

crates/epaint/src/text/text_layout.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -866,7 +866,8 @@ fn add_row_hline(
866866
let mut last_right_x = f32::NAN;
867867

868868
for glyph in &row.glyphs {
869-
let (stroke, y) = stroke_and_y(glyph);
869+
let (stroke, mut y) = stroke_and_y(glyph);
870+
stroke.round_center_to_pixel(point_scale.pixels_per_point, &mut y);
870871

871872
if stroke == Stroke::NONE {
872873
end_line(line_start.take(), last_right_x);

0 commit comments

Comments
 (0)