Round text galley sizes to nearest ui point size (#4578)

Previously, many labels had non-integer widths. This lead to rounding
errors.

This was most notable for the new `Area` sizing code:

We would run the initial sizing pass, to measure the size of e.g. a
tooltip.
Say the tooltip contains text that was 100.123 ui points wide. With a
16pt border, that becomes 116.123, which is stored in the `Area` state
as the width. The next frame, we use that stored size as the wrapping
width. With perfect precision, we would then tell the label to wrap to
100.123 pts, which the text would _just_ fit in. However, due to
rounding errors we might end up asking it to wrap to 100.12**2** pts,
meaning the last word would now wrap and end up on the next line.

By rounding label sizes to perfect integers, we avoid such rounding
errors, and most ui elements will now end up on perfect integer point
coordinates (and `f32` can precisely express and do arithmetic on all
integers < 2^24).

Visually this has very little impact. Some labels move by a pixel here
and there, mostly for the better.
This commit is contained in:
Emil Ernerfeldt 2024-05-29 18:23:11 +02:00 committed by GitHub
parent 66f40de7a1
commit cc3b3629b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 32 additions and 2 deletions

View File

@ -238,6 +238,12 @@ fn rows_from_paragraphs(
}
fn line_break(paragraph: &Paragraph, job: &LayoutJob, out_rows: &mut Vec<Row>, elided: &mut bool) {
let wrap_width_margin = if job.round_output_size_to_nearest_ui_point {
0.5
} else {
0.0
};
// Keeps track of good places to insert row break if we exceed `wrap_width`.
let mut row_break_candidates = RowBreakCandidates::default();
@ -253,7 +259,7 @@ fn line_break(paragraph: &Paragraph, job: &LayoutJob, out_rows: &mut Vec<Row>, e
let potential_row_width = paragraph.glyphs[i].max_x() - row_start_x;
if job.wrap.max_width < potential_row_width {
if job.wrap.max_width + wrap_width_margin < potential_row_width {
// Row break:
if first_row_indentation > 0.0
@ -630,7 +636,24 @@ fn galley_from_rows(
num_indices += row.visuals.mesh.indices.len();
}
let rect = Rect::from_min_max(pos2(min_x, 0.0), pos2(max_x, cursor_y));
let mut rect = Rect::from_min_max(pos2(min_x, 0.0), pos2(max_x, cursor_y));
if job.round_output_size_to_nearest_ui_point {
let did_exceed_wrap_width_by_a_lot = rect.width() > job.wrap.max_width + 1.0;
// We round the size to whole ui points here (not pixels!) so that the egui layout code
// can have the advantage of working in integer units, avoiding rounding errors.
rect.min = rect.min.round();
rect.max = rect.max.round();
if did_exceed_wrap_width_by_a_lot {
// If the user picked a too aggressive wrap width (e.g. more narrow than any individual glyph),
// we should let the user know.
} else {
// Make sure we don't over the max wrap width the user picked:
rect.max.x = rect.max.x.at_most(rect.min.x + job.wrap.max_width);
}
}
Galley {
job,

View File

@ -74,6 +74,10 @@ pub struct LayoutJob {
/// Justify text so that word-wrapped rows fill the whole [`TextWrapping::max_width`].
pub justify: bool,
/// Rounding to the closest ui point (not pixel!) allows the rest of the
/// layout code to run on perfect integers, avoiding rounding errors.
pub round_output_size_to_nearest_ui_point: bool,
}
impl Default for LayoutJob {
@ -87,6 +91,7 @@ impl Default for LayoutJob {
break_on_newline: true,
halign: Align::LEFT,
justify: false,
round_output_size_to_nearest_ui_point: true,
}
}
}
@ -180,6 +185,7 @@ impl std::hash::Hash for LayoutJob {
break_on_newline,
halign,
justify,
round_output_size_to_nearest_ui_point,
} = self;
text.hash(state);
@ -189,6 +195,7 @@ impl std::hash::Hash for LayoutJob {
break_on_newline.hash(state);
halign.hash(state);
justify.hash(state);
round_output_size_to_nearest_ui_point.hash(state);
}
}