From cc3b3629b89526a0a05b97c80349c32e4762c06f Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 29 May 2024 18:23:11 +0200 Subject: [PATCH] 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. --- crates/epaint/src/text/text_layout.rs | 27 +++++++++++++++++++-- crates/epaint/src/text/text_layout_types.rs | 7 ++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 276f3f9e..7f1cd865 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -238,6 +238,12 @@ fn rows_from_paragraphs( } fn line_break(paragraph: &Paragraph, job: &LayoutJob, out_rows: &mut Vec, 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, 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, diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index c14348ec..fde36ec6 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -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); } }