From 622b848c008f292ebea2411e870072106c7b72c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20G=C5=82uchowski?= Date: Thu, 28 Nov 2024 16:47:05 +0100 Subject: [PATCH 01/68] Fix typo --- crates/epaint/src/text/fonts.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 4be2fdfe8d0..6e19a7f34fb 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -733,7 +733,7 @@ impl GalleyCache { // Say the user asks to wrap at width 200.0. // The text layout wraps, and reports that the final width was 196.0 points. - // This than trickles up the `Ui` chain and gets stored as the width for a tooltip (say). + // This then trickles up the `Ui` chain and gets stored as the width for a tooltip (say). // On the next frame, this is then set as the max width for the tooltip, // and we end up calling the text layout code again, this time with a wrap width of 196.0. // Except, somewhere in the `Ui` chain with added margins etc, a rounding error was introduced, From 150c0f662b36cab7bd5d5a294911e6233150b45d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20G=C5=82uchowski?= Date: Thu, 28 Nov 2024 16:47:40 +0100 Subject: [PATCH 02/68] Cache individual lines of text in GalleyCache --- crates/epaint/src/text/fonts.rs | 134 ++++++++++++++++++++++++++++++-- 1 file changed, 127 insertions(+), 7 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 6e19a7f34fb..c981279dd93 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -762,13 +762,133 @@ impl GalleyCache { cached.galley.clone() } std::collections::hash_map::Entry::Vacant(entry) => { - let galley = super::layout(fonts, job.into()); - let galley = Arc::new(galley); - entry.insert(CachedGalley { - last_used: self.generation, - galley: galley.clone(), - }); - galley + if job.break_on_newline { + let mut current_section = 0; + let mut current = 0; + let mut left_max_rows = job.wrap.max_rows; + let mut galleys = Vec::new(); + let mut text_left = job.text.as_str(); + loop { + let end = text_left + .find('\n') + .map(|i| i + current) + .unwrap_or(job.text.len()); + + let mut line_job = LayoutJob::default(); + line_job.text = job.text[current..end].to_string(); + line_job.wrap = crate::text::TextWrapping { + max_rows: left_max_rows, + ..job.wrap + }; + line_job.halign = job.halign; + line_job.justify = job.justify; + + let line_start = current; + while current < end { + let mut s = &job.sections[current_section]; + while s.byte_range.end <= current { + current_section += 1; + s = &job.sections[current_section]; + } + + assert!(s.byte_range.contains(¤t)); + let section_end = s.byte_range.end.min(end); + line_job.sections.push(crate::text::LayoutSection { + leading_space: s.leading_space, + byte_range: current - line_start..section_end - line_start, + format: s.format.clone(), + }); + current = section_end; + } + + // Prevent an infinite recursion + line_job.break_on_newline = false; + + let galley = self.layout(fonts, line_job); + // This will prevent us from invalidating cache entries unnecessarily + if left_max_rows != usize::MAX { + left_max_rows -= galley.rows.len(); + } + galleys.push(galley); + + current = end + 1; + if current >= job.text.len() { + break; + } else { + text_left = &job.text[current..]; + } + } + + let mut merged_galley = Galley { + job: Arc::new(job), + rows: Vec::new(), + elided: false, + rect: emath::Rect::ZERO, + mesh_bounds: emath::Rect::ZERO, + num_vertices: 0, + num_indices: 0, + pixels_per_point: fonts.pixels_per_point, + }; + + for galley in galleys { + let current_offset = emath::vec2(0.0, merged_galley.rect.height()); + merged_galley.rows.extend(galley.rows.iter().map(|row| { + super::Row { + // FIXME: what is this??? + section_index_at_start: row.section_index_at_start, + glyphs: row + .glyphs + .iter() + .cloned() + .map(|mut p| { + p.pos.y += current_offset.y; + p + }) + .collect(), + rect: row.rect.translate(current_offset), + visuals: { + let mut visuals = row.visuals.clone(); + for vertex in visuals.mesh.vertices.iter_mut() { + vertex.pos.y += current_offset.y; + } + visuals.mesh_bounds = + visuals.mesh_bounds.translate(current_offset); + merged_galley.mesh_bounds = + merged_galley.mesh_bounds.union(visuals.mesh_bounds); + visuals + }, + ends_with_newline: row.ends_with_newline, + } + })); + merged_galley.rect = merged_galley + .rect + .union(galley.rect.translate(current_offset)); + merged_galley.num_vertices += galley.num_vertices; + merged_galley.num_indices += galley.num_indices; + if galley.elided { + merged_galley.elided = true; + break; + } + } + + let galley = Arc::new(merged_galley); + self.cache.insert( + hash, + CachedGalley { + last_used: self.generation, + galley: galley.clone(), + }, + ); + galley + } else { + let galley = super::layout(fonts, job.into()); + let galley = Arc::new(galley); + entry.insert(CachedGalley { + last_used: self.generation, + galley: galley.clone(), + }); + galley + } } } } From db32a1ed4474d4bf36d06878cca771ab08b385e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20G=C5=82uchowski?= Date: Thu, 28 Nov 2024 17:40:59 +0100 Subject: [PATCH 03/68] Make Galleys share Rows and store their offsets --- .../egui/src/text_selection/accesskit_text.rs | 4 +- .../egui/src/text_selection/cursor_range.rs | 2 +- .../text_selection/label_text_selection.rs | 7 +- crates/egui/src/text_selection/visuals.rs | 4 +- crates/egui/src/widget_text.rs | 2 +- crates/egui/src/widgets/label.rs | 13 +- crates/epaint/src/shape.rs | 4 +- crates/epaint/src/shape_transform.rs | 3 +- crates/epaint/src/stats.rs | 10 +- crates/epaint/src/tessellator.rs | 8 +- crates/epaint/src/text/fonts.rs | 36 +---- crates/epaint/src/text/text_layout.rs | 142 +++++++++++------- crates/epaint/src/text/text_layout_types.rs | 66 ++++---- 13 files changed, 174 insertions(+), 127 deletions(-) diff --git a/crates/egui/src/text_selection/accesskit_text.rs b/crates/egui/src/text_selection/accesskit_text.rs index d0c3869038d..ad11c16a4d9 100644 --- a/crates/egui/src/text_selection/accesskit_text.rs +++ b/crates/egui/src/text_selection/accesskit_text.rs @@ -39,11 +39,11 @@ pub fn update_accesskit_for_text_widget( }; ctx.with_accessibility_parent(parent_id, || { - for (row_index, row) in galley.rows.iter().enumerate() { + for (row_index, (row, offset)) in galley.rows.iter().enumerate() { let row_id = parent_id.with(row_index); ctx.accesskit_node_builder(row_id, |builder| { builder.set_role(accesskit::Role::TextRun); - let rect = row.rect.translate(galley_pos.to_vec2()); + let rect = row.rect.translate(offset.to_vec2() + galley_pos.to_vec2()); builder.set_bounds(accesskit::Rect { x0: rect.min.x.into(), y0: rect.min.y.into(), diff --git a/crates/egui/src/text_selection/cursor_range.rs b/crates/egui/src/text_selection/cursor_range.rs index bd3f496fd8e..dbdba5ba8eb 100644 --- a/crates/egui/src/text_selection/cursor_range.rs +++ b/crates/egui/src/text_selection/cursor_range.rs @@ -284,7 +284,7 @@ fn ccursor_from_accesskit_text_position( position: &accesskit::TextPosition, ) -> Option { let mut total_length = 0usize; - for (i, row) in galley.rows.iter().enumerate() { + for (i, (row, _)) in galley.rows.iter().enumerate() { let row_id = id.with(i); if row_id.accesskit_id() == position.node { return Some(CCursor { diff --git a/crates/egui/src/text_selection/label_text_selection.rs b/crates/egui/src/text_selection/label_text_selection.rs index fe5eac00e78..321d8d94609 100644 --- a/crates/egui/src/text_selection/label_text_selection.rs +++ b/crates/egui/src/text_selection/label_text_selection.rs @@ -179,7 +179,10 @@ impl LabelSelectionState { if let epaint::Shape::Text(text_shape) = &mut shape.shape { let galley = Arc::make_mut(&mut text_shape.galley); for row_selection in row_selections { - if let Some(row) = galley.rows.get_mut(row_selection.row) { + if let Some((row, _)) = + galley.rows.get_mut(row_selection.row) + { + let row = Arc::make_mut(row); for vertex_index in row_selection.vertex_indices { if let Some(vertex) = row .visuals @@ -659,7 +662,7 @@ fn selected_text(galley: &Galley, cursor_range: &CursorRange) -> String { } fn estimate_row_height(galley: &Galley) -> f32 { - if let Some(row) = galley.rows.first() { + if let Some((row, _)) = galley.rows.first() { row.rect.height() } else { galley.size().y diff --git a/crates/egui/src/text_selection/visuals.rs b/crates/egui/src/text_selection/visuals.rs index d86f9dc56c2..0000632e77c 100644 --- a/crates/egui/src/text_selection/visuals.rs +++ b/crates/egui/src/text_selection/visuals.rs @@ -31,7 +31,9 @@ pub fn paint_text_selection( let max = max.rcursor; for ri in min.row..=max.row { - let row = &mut galley.rows[ri]; + let (row, _) = &mut galley.rows[ri]; + let row = Arc::make_mut(row); + let left = if ri == min.row { row.x_offset(min.column) } else { diff --git a/crates/egui/src/widget_text.rs b/crates/egui/src/widget_text.rs index 011a4adcbb0..bde229828be 100644 --- a/crates/egui/src/widget_text.rs +++ b/crates/egui/src/widget_text.rs @@ -640,7 +640,7 @@ impl WidgetText { Self::RichText(text) => text.font_height(fonts, style), Self::LayoutJob(job) => job.font_height(fonts), Self::Galley(galley) => { - if let Some(row) = galley.rows.first() { + if let Some((row, _)) = galley.rows.first() { row.height() } else { galley.size().y diff --git a/crates/egui/src/widgets/label.rs b/crates/egui/src/widgets/label.rs index b6ade45ae30..276c9576b61 100644 --- a/crates/egui/src/widgets/label.rs +++ b/crates/egui/src/widgets/label.rs @@ -1,8 +1,8 @@ use std::sync::Arc; use crate::{ - epaint, pos2, text_selection, vec2, Align, Direction, FontSelection, Galley, Pos2, Response, - Sense, Stroke, TextWrapMode, Ui, Widget, WidgetInfo, WidgetText, WidgetType, + epaint, pos2, text_selection, Align, Direction, FontSelection, Galley, Pos2, Response, Sense, + Stroke, TextWrapMode, Ui, Widget, WidgetInfo, WidgetText, WidgetType, }; use self::text_selection::LabelSelectionState; @@ -194,10 +194,13 @@ impl Label { let pos = pos2(ui.max_rect().left(), ui.cursor().top()); assert!(!galley.rows.is_empty(), "Galleys are never empty"); // collect a response from many rows: - let rect = galley.rows[0].rect.translate(vec2(pos.x, pos.y)); + let rect = galley.rows[0] + .0 + .rect + .translate(galley.rows[0].1.to_vec2() + pos.to_vec2()); let mut response = ui.allocate_rect(rect, sense); - for row in galley.rows.iter().skip(1) { - let rect = row.rect.translate(vec2(pos.x, pos.y)); + for (row, offset) in galley.rows.iter().skip(1) { + let rect = row.rect.translate(offset.to_vec2() + pos.to_vec2()); response |= ui.allocate_rect(rect, sense); } (pos, galley, response) diff --git a/crates/epaint/src/shape.rs b/crates/epaint/src/shape.rs index 6f67a2bc6a6..0d2d77d47ad 100644 --- a/crates/epaint/src/shape.rs +++ b/crates/epaint/src/shape.rs @@ -433,7 +433,9 @@ impl Shape { // Scale text: let galley = Arc::make_mut(&mut text_shape.galley); - for row in &mut galley.rows { + for (row, offset) in &mut galley.rows { + let row = Arc::make_mut(row); + *offset = *offset * transform.scaling; row.visuals.mesh_bounds = transform.scaling * row.visuals.mesh_bounds; for v in &mut row.visuals.mesh.vertices { v.pos = Pos2::new(transform.scaling * v.pos.x, transform.scaling * v.pos.y); diff --git a/crates/epaint/src/shape_transform.rs b/crates/epaint/src/shape_transform.rs index f072393f557..4d74c938513 100644 --- a/crates/epaint/src/shape_transform.rs +++ b/crates/epaint/src/shape_transform.rs @@ -88,7 +88,8 @@ pub fn adjust_colors( if !galley.is_empty() { let galley = std::sync::Arc::make_mut(galley); - for row in &mut galley.rows { + for (row, _) in &mut galley.rows { + let row = Arc::make_mut(row); for vertex in &mut row.visuals.mesh.vertices { adjust_color(&mut vertex.color); } diff --git a/crates/epaint/src/stats.rs b/crates/epaint/src/stats.rs index 68bba622ed2..ad3705bea4b 100644 --- a/crates/epaint/src/stats.rs +++ b/crates/epaint/src/stats.rs @@ -88,7 +88,11 @@ impl AllocInfo { pub fn from_galley(galley: &Galley) -> Self { Self::from_slice(galley.text().as_bytes()) + Self::from_slice(&galley.rows) - + galley.rows.iter().map(Self::from_galley_row).sum() + + galley + .rows + .iter() + .map(|(row, _)| Self::from_galley_row(row)) + .sum() } fn from_galley_row(row: &crate::text::Row) -> Self { @@ -213,8 +217,8 @@ impl PaintStats { self.shape_text += AllocInfo::from_galley(&text_shape.galley); for row in &text_shape.galley.rows { - self.text_shape_indices += AllocInfo::from_slice(&row.visuals.mesh.indices); - self.text_shape_vertices += AllocInfo::from_slice(&row.visuals.mesh.vertices); + self.text_shape_indices += AllocInfo::from_slice(&row.0.visuals.mesh.indices); + self.text_shape_vertices += AllocInfo::from_slice(&row.0.visuals.mesh.vertices); } } Shape::Mesh(mesh) => { diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index fdbe270914e..393edfbbf67 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1778,16 +1778,18 @@ impl Tessellator { let rotator = Rot2::from_angle(*angle); - for row in &galley.rows { + for (row, row_pos) in &galley.rows { if row.visuals.mesh.is_empty() { continue; } + let final_pos = galley_pos + row_pos.to_vec2(); + let mut row_rect = row.visuals.mesh_bounds; if *angle != 0.0 { row_rect = row_rect.rotate_bb(rotator); } - row_rect = row_rect.translate(galley_pos.to_vec2()); + row_rect = row_rect.translate(final_pos.to_vec2()); if self.options.coarse_tessellation_culling && !self.clip_rect.intersects(row_rect) { // culling individual lines of text is important, since a single `Shape::Text` @@ -1836,7 +1838,7 @@ impl Tessellator { }; Vertex { - pos: galley_pos + offset, + pos: final_pos + offset, uv: (uv.to_vec2() * uv_normalizer).to_pos2(), color, } diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index c981279dd93..eccd2fd06b8 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -832,34 +832,14 @@ impl GalleyCache { for galley in galleys { let current_offset = emath::vec2(0.0, merged_galley.rect.height()); - merged_galley.rows.extend(galley.rows.iter().map(|row| { - super::Row { - // FIXME: what is this??? - section_index_at_start: row.section_index_at_start, - glyphs: row - .glyphs - .iter() - .cloned() - .map(|mut p| { - p.pos.y += current_offset.y; - p - }) - .collect(), - rect: row.rect.translate(current_offset), - visuals: { - let mut visuals = row.visuals.clone(); - for vertex in visuals.mesh.vertices.iter_mut() { - vertex.pos.y += current_offset.y; - } - visuals.mesh_bounds = - visuals.mesh_bounds.translate(current_offset); - merged_galley.mesh_bounds = - merged_galley.mesh_bounds.union(visuals.mesh_bounds); - visuals - }, - ends_with_newline: row.ends_with_newline, - } - })); + merged_galley + .rows + .extend(galley.rows.iter().map(|(row, prev_offset)| { + merged_galley.mesh_bounds = + merged_galley.mesh_bounds.union(row.visuals.mesh_bounds); + + (row.clone(), *prev_offset + current_offset) + })); merged_galley.rect = merged_galley .rect .union(galley.rect.translate(current_offset)); diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 9db77f888af..ed7347418e0 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -96,7 +96,8 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc) -> Galley { let mut elided = false; let mut rows = rows_from_paragraphs(paragraphs, &job, &mut elided); if elided { - if let Some(last_row) = rows.last_mut() { + if let Some((last_row, _)) = rows.last_mut() { + let last_row = Arc::get_mut(last_row).unwrap(); replace_last_glyph_with_overflow_character(fonts, &job, last_row); if let Some(last) = last_row.glyphs.last() { last_row.rect.max.x = last.max_x(); @@ -108,12 +109,12 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc) -> Galley { if justify || job.halign != Align::LEFT { let num_rows = rows.len(); - for (i, row) in rows.iter_mut().enumerate() { + for (i, (row, _)) in rows.iter_mut().enumerate() { let is_last_row = i + 1 == num_rows; let justify_row = justify && !row.ends_with_newline && !is_last_row; halign_and_justify_row( point_scale, - row, + Arc::get_mut(row).unwrap(), job.halign, job.wrap.max_width, justify_row, @@ -198,7 +199,7 @@ fn rows_from_paragraphs( paragraphs: Vec, job: &LayoutJob, elided: &mut bool, -) -> Vec { +) -> Vec<(Arc, Pos2)> { let num_paragraphs = paragraphs.len(); let mut rows = vec![]; @@ -212,31 +213,38 @@ fn rows_from_paragraphs( let is_last_paragraph = (i + 1) == num_paragraphs; if paragraph.glyphs.is_empty() { - rows.push(Row { - section_index_at_start: paragraph.section_index_at_start, - glyphs: vec![], - visuals: Default::default(), - rect: Rect::from_min_size( - pos2(paragraph.cursor_x, 0.0), - vec2(0.0, paragraph.empty_paragraph_height), - ), - ends_with_newline: !is_last_paragraph, - }); + rows.push(( + Arc::new(Row { + section_index_at_start: paragraph.section_index_at_start, + glyphs: vec![], + visuals: Default::default(), + rect: Rect::from_min_size( + pos2(paragraph.cursor_x, 0.0), + vec2(0.0, paragraph.empty_paragraph_height), + ), + ends_with_newline: !is_last_paragraph, + }), + Pos2::ZERO, + )); } else { let paragraph_max_x = paragraph.glyphs.last().unwrap().max_x(); if paragraph_max_x <= job.effective_wrap_width() { // Early-out optimization: the whole paragraph fits on one row. let paragraph_min_x = paragraph.glyphs[0].pos.x; - rows.push(Row { - section_index_at_start: paragraph.section_index_at_start, - glyphs: paragraph.glyphs, - visuals: Default::default(), - rect: rect_from_x_range(paragraph_min_x..=paragraph_max_x), - ends_with_newline: !is_last_paragraph, - }); + rows.push(( + Arc::new(Row { + section_index_at_start: paragraph.section_index_at_start, + glyphs: paragraph.glyphs, + visuals: Default::default(), + rect: rect_from_x_range(paragraph_min_x..=paragraph_max_x), + ends_with_newline: !is_last_paragraph, + }), + Pos2::ZERO, + )); } else { line_break(¶graph, job, &mut rows, elided); - rows.last_mut().unwrap().ends_with_newline = !is_last_paragraph; + let last_row = Arc::get_mut(&mut rows.last_mut().unwrap().0).unwrap(); + last_row.ends_with_newline = !is_last_paragraph; } } } @@ -244,7 +252,12 @@ fn rows_from_paragraphs( rows } -fn line_break(paragraph: &Paragraph, job: &LayoutJob, out_rows: &mut Vec, elided: &mut bool) { +fn line_break( + paragraph: &Paragraph, + job: &LayoutJob, + out_rows: &mut Vec<(Arc, Pos2)>, + elided: &mut bool, +) { let wrap_width = job.effective_wrap_width(); // Keeps track of good places to insert row break if we exceed `wrap_width`. @@ -270,13 +283,16 @@ fn line_break(paragraph: &Paragraph, job: &LayoutJob, out_rows: &mut Vec, e { // Allow the first row to be completely empty, because we know there will be more space on the next row: // TODO(emilk): this records the height of this first row as zero, though that is probably fine since first_row_indentation usually comes with a first_row_min_height. - out_rows.push(Row { - section_index_at_start: paragraph.section_index_at_start, - glyphs: vec![], - visuals: Default::default(), - rect: rect_from_x_range(first_row_indentation..=first_row_indentation), - ends_with_newline: false, - }); + out_rows.push(( + Arc::new(Row { + section_index_at_start: paragraph.section_index_at_start, + glyphs: vec![], + visuals: Default::default(), + rect: rect_from_x_range(first_row_indentation..=first_row_indentation), + ends_with_newline: false, + }), + Pos2::ZERO, + )); row_start_x += first_row_indentation; first_row_indentation = 0.0; } else if let Some(last_kept_index) = row_break_candidates.get(job.wrap.break_anywhere) @@ -294,13 +310,16 @@ fn line_break(paragraph: &Paragraph, job: &LayoutJob, out_rows: &mut Vec, e let paragraph_min_x = glyphs[0].pos.x; let paragraph_max_x = glyphs.last().unwrap().max_x(); - out_rows.push(Row { - section_index_at_start, - glyphs, - visuals: Default::default(), - rect: rect_from_x_range(paragraph_min_x..=paragraph_max_x), - ends_with_newline: false, - }); + out_rows.push(( + Arc::new(Row { + section_index_at_start, + glyphs, + visuals: Default::default(), + rect: rect_from_x_range(paragraph_min_x..=paragraph_max_x), + ends_with_newline: false, + }), + Pos2::ZERO, + )); // Start a new row: row_start_idx = last_kept_index + 1; @@ -333,13 +352,16 @@ fn line_break(paragraph: &Paragraph, job: &LayoutJob, out_rows: &mut Vec, e let paragraph_min_x = glyphs[0].pos.x; let paragraph_max_x = glyphs.last().unwrap().max_x(); - out_rows.push(Row { - section_index_at_start, - glyphs, - visuals: Default::default(), - rect: rect_from_x_range(paragraph_min_x..=paragraph_max_x), - ends_with_newline: false, - }); + out_rows.push(( + Arc::new(Row { + section_index_at_start, + glyphs, + visuals: Default::default(), + rect: rect_from_x_range(paragraph_min_x..=paragraph_max_x), + ends_with_newline: false, + }), + Pos2::ZERO, + )); } } } @@ -592,14 +614,15 @@ fn halign_and_justify_row( fn galley_from_rows( point_scale: PointScale, job: Arc, - mut rows: Vec, + mut rows: Vec<(Arc, Pos2)>, elided: bool, ) -> Galley { let mut first_row_min_height = job.first_row_min_height; let mut cursor_y = 0.0; let mut min_x: f32 = 0.0; let mut max_x: f32 = 0.0; - for row in &mut rows { + for (row, _) in &mut rows { + let row = Arc::get_mut(row).unwrap(); let mut max_row_height = first_row_min_height.max(row.rect.height()); first_row_min_height = 0.0; for glyph in &row.glyphs { @@ -639,7 +662,8 @@ fn galley_from_rows( let mut num_vertices = 0; let mut num_indices = 0; - for row in &mut rows { + for (row, _) in &mut rows { + let row = Arc::get_mut(row).unwrap(); row.visuals = tessellate_row(point_scale, &job, &format_summary, row); mesh_bounds = mesh_bounds.union(row.visuals.mesh_bounds); num_vertices += row.visuals.mesh.vertices.len(); @@ -1072,7 +1096,7 @@ mod tests { assert!(galley.elided); assert_eq!(galley.rows.len(), 1); - let row_text = galley.rows[0].text(); + let row_text = galley.rows[0].0.text(); assert!( row_text.ends_with('…'), "Expected row to end with `…`, got {row_text:?} when line-breaking the text {text:?} with max_width {max_width} and break_anywhere {break_anywhere}.", @@ -1091,7 +1115,7 @@ mod tests { assert!(galley.elided); assert_eq!(galley.rows.len(), 1); - let row_text = galley.rows[0].text(); + let row_text = galley.rows[0].0.text(); assert_eq!(row_text, "Hello…"); } } @@ -1106,7 +1130,11 @@ mod tests { layout_job.wrap.max_width = 90.0; let galley = layout(&mut fonts, layout_job.into()); assert_eq!( - galley.rows.iter().map(|row| row.text()).collect::>(), + galley + .rows + .iter() + .map(|row| row.0.text()) + .collect::>(), vec!["日本語と", "Englishの混在", "した文章"] ); } @@ -1121,7 +1149,11 @@ mod tests { layout_job.wrap.max_width = 110.0; let galley = layout(&mut fonts, layout_job.into()); assert_eq!( - galley.rows.iter().map(|row| row.text()).collect::>(), + galley + .rows + .iter() + .map(|row| row.0.text()) + .collect::>(), vec!["日本語とEnglish", "の混在した文章"] ); } @@ -1136,10 +1168,14 @@ mod tests { let galley = layout(&mut fonts, layout_job.into()); assert!(galley.elided); assert_eq!( - galley.rows.iter().map(|row| row.text()).collect::>(), + galley + .rows + .iter() + .map(|row| row.0.text()) + .collect::>(), vec!["# DNA…"] ); let row = &galley.rows[0]; - assert_eq!(row.rect.max.x, row.glyphs.last().unwrap().max_x()); + assert_eq!(row.0.rect.max.x, row.0.glyphs.last().unwrap().max_x()); } } diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 17826e6afb1..31a44c5dd3a 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -499,14 +499,14 @@ pub struct Galley { /// Contains the original string and style sections. pub job: Arc, - /// Rows of text, from top to bottom. + /// Rows of text, from top to bottom, and their offsets. /// /// The number of characters in all rows sum up to `job.text.chars().count()` /// unless [`Self::elided`] is `true`. /// /// Note that a paragraph (a piece of text separated with `\n`) /// can be split up into multiple rows. - pub rows: Vec, + pub rows: Vec<(Arc, Pos2)>, /// Set to true the text was truncated due to [`TextWrapping::max_rows`]. pub elided: bool, @@ -755,7 +755,7 @@ impl std::ops::Deref for Galley { impl Galley { /// Zero-width rect past the last character. fn end_pos(&self) -> Rect { - if let Some(row) = self.rows.last() { + if let Some((row, _)) = self.rows.last() { let x = row.rect.right(); Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y())) } else { @@ -773,7 +773,7 @@ impl Galley { pub fn pos_from_pcursor(&self, pcursor: PCursor) -> Rect { let mut it = PCursor::default(); - for row in &self.rows { + for (row, offset) in &self.rows { if it.paragraph == pcursor.paragraph { // Right paragraph, but is it the right row in the paragraph? @@ -787,8 +787,11 @@ impl Galley { && !row.ends_with_newline && column >= row.char_count_excluding_newline(); if !select_next_row_instead { - let x = row.x_offset(column); - return Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y())); + let x = row.x_offset(column) + offset.x; + return Rect::from_min_max( + pos2(x, row.min_y() + offset.y), + pos2(x, row.max_y() + offset.y), + ); } } } @@ -822,13 +825,13 @@ impl Galley { /// same as a cursor at the end. /// This allows implementing text-selection by dragging above/below the galley. pub fn cursor_from_pos(&self, pos: Vec2) -> Cursor { - if let Some(first_row) = self.rows.first() { - if pos.y < first_row.min_y() { + if let Some((first_row, offset)) = self.rows.first() { + if pos.y < first_row.min_y() + offset.y { return self.begin(); } } - if let Some(last_row) = self.rows.last() { - if last_row.max_y() < pos.y { + if let Some((last_row, offset)) = self.rows.last() { + if last_row.max_y() + offset.y < pos.y { return self.end(); } } @@ -839,9 +842,12 @@ impl Galley { let mut ccursor_index = 0; let mut pcursor_it = PCursor::default(); - for (row_nr, row) in self.rows.iter().enumerate() { - let is_pos_within_row = row.min_y() <= pos.y && pos.y <= row.max_y(); - let y_dist = (row.min_y() - pos.y).abs().min((row.max_y() - pos.y).abs()); + for (row_nr, (row, offset)) in self.rows.iter().enumerate() { + let min_y = row.min_y() + offset.y; + let max_y = row.max_y() + offset.y; + + let is_pos_within_row = min_y <= pos.y && pos.y <= max_y; + let y_dist = (min_y - pos.y).abs().min((max_y - pos.y).abs()); if is_pos_within_row || y_dist < best_y_dist { best_y_dist = y_dist; let column = row.char_at(pos.x); @@ -904,7 +910,7 @@ impl Galley { offset: 0, prefer_next_row: true, }; - for row in &self.rows { + for (row, _) in &self.rows { let row_char_count = row.char_count_including_newline(); ccursor.index += row_char_count; if row.ends_with_newline { @@ -922,7 +928,7 @@ impl Galley { } pub fn end_rcursor(&self) -> RCursor { - if let Some(last_row) = self.rows.last() { + if let Some((last_row, _)) = self.rows.last() { RCursor { row: self.rows.len() - 1, column: last_row.char_count_including_newline(), @@ -948,7 +954,7 @@ impl Galley { prefer_next_row, }; - for (row_nr, row) in self.rows.iter().enumerate() { + for (row_nr, (row, _)) in self.rows.iter().enumerate() { let row_char_count = row.char_count_excluding_newline(); if ccursor_it.index <= ccursor.index @@ -993,7 +999,7 @@ impl Galley { } let prefer_next_row = - rcursor.column < self.rows[rcursor.row].char_count_excluding_newline(); + rcursor.column < self.rows[rcursor.row].0.char_count_excluding_newline(); let mut ccursor_it = CCursor { index: 0, prefer_next_row, @@ -1004,7 +1010,7 @@ impl Galley { prefer_next_row, }; - for (row_nr, row) in self.rows.iter().enumerate() { + for (row_nr, (row, _)) in self.rows.iter().enumerate() { if row_nr == rcursor.row { ccursor_it.index += rcursor.column.at_most(row.char_count_excluding_newline()); @@ -1048,7 +1054,7 @@ impl Galley { prefer_next_row, }; - for (row_nr, row) in self.rows.iter().enumerate() { + for (row_nr, (row, _)) in self.rows.iter().enumerate() { if pcursor_it.paragraph == pcursor.paragraph { // Right paragraph, but is it the right row in the paragraph? @@ -1122,7 +1128,9 @@ impl Galley { let new_row = cursor.rcursor.row - 1; let cursor_is_beyond_end_of_current_row = cursor.rcursor.column - >= self.rows[cursor.rcursor.row].char_count_excluding_newline(); + >= self.rows[cursor.rcursor.row] + .0 + .char_count_excluding_newline(); let new_rcursor = if cursor_is_beyond_end_of_current_row { // keep same column @@ -1133,11 +1141,12 @@ impl Galley { } else { // keep same X coord let x = self.pos_from_cursor(cursor).center().x; - let column = if x > self.rows[new_row].rect.right() { + let (row, offset) = &self.rows[new_row]; + let column = if x > row.rect.right() + offset.x { // beyond the end of this row - keep same column cursor.rcursor.column } else { - self.rows[new_row].char_at(x) + row.char_at(x) }; RCursor { row: new_row, @@ -1153,7 +1162,9 @@ impl Galley { let new_row = cursor.rcursor.row + 1; let cursor_is_beyond_end_of_current_row = cursor.rcursor.column - >= self.rows[cursor.rcursor.row].char_count_excluding_newline(); + >= self.rows[cursor.rcursor.row] + .0 + .char_count_excluding_newline(); let new_rcursor = if cursor_is_beyond_end_of_current_row { // keep same column @@ -1164,11 +1175,12 @@ impl Galley { } else { // keep same X coord let x = self.pos_from_cursor(cursor).center().x; - let column = if x > self.rows[new_row].rect.right() { + let (row, offset) = &self.rows[new_row]; + let column = if x > row.rect.right() + offset.x { // beyond the end of the next row - keep same column cursor.rcursor.column } else { - self.rows[new_row].char_at(x) + row.char_at(x) }; RCursor { row: new_row, @@ -1192,7 +1204,9 @@ impl Galley { pub fn cursor_end_of_row(&self, cursor: &Cursor) -> Cursor { self.from_rcursor(RCursor { row: cursor.rcursor.row, - column: self.rows[cursor.rcursor.row].char_count_excluding_newline(), + column: self.rows[cursor.rcursor.row] + .0 + .char_count_excluding_newline(), }) } } From 4e3f1628016a4ca9a84454643206e1137c810fe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20G=C5=82uchowski?= Date: Fri, 29 Nov 2024 11:50:24 +0100 Subject: [PATCH 04/68] Fix lints --- crates/epaint/src/text/fonts.rs | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index eccd2fd06b8..2d434ed707a 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -769,21 +769,20 @@ impl GalleyCache { let mut galleys = Vec::new(); let mut text_left = job.text.as_str(); loop { - let end = text_left - .find('\n') - .map(|i| i + current) - .unwrap_or(job.text.len()); - - let mut line_job = LayoutJob::default(); - line_job.text = job.text[current..end].to_string(); - line_job.wrap = crate::text::TextWrapping { - max_rows: left_max_rows, - ..job.wrap + let end = text_left.find('\n').map_or(job.text.len(), |i| i + current); + let start = current; + + let mut line_job = LayoutJob { + text: job.text[current..end].to_string(), + wrap: crate::text::TextWrapping { + max_rows: left_max_rows, + ..job.wrap + }, + halign: job.halign, + justify: job.justify, + ..Default::default() }; - line_job.halign = job.halign; - line_job.justify = job.justify; - let line_start = current; while current < end { let mut s = &job.sections[current_section]; while s.byte_range.end <= current { @@ -795,7 +794,7 @@ impl GalleyCache { let section_end = s.byte_range.end.min(end); line_job.sections.push(crate::text::LayoutSection { leading_space: s.leading_space, - byte_range: current - line_start..section_end - line_start, + byte_range: current - start..section_end - start, format: s.format.clone(), }); current = section_end; From 3de1723659b9433fd6937ca8a4308b13015e74ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20G=C5=82uchowski?= Date: Fri, 29 Nov 2024 11:55:15 +0100 Subject: [PATCH 05/68] Don't add leading space to more than one split layout section --- crates/epaint/src/text/fonts.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 2d434ed707a..b4566d14da8 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -793,7 +793,14 @@ impl GalleyCache { assert!(s.byte_range.contains(¤t)); let section_end = s.byte_range.end.min(end); line_job.sections.push(crate::text::LayoutSection { - leading_space: s.leading_space, + // Leading space should only be added to the first section + // if the there are multiple sections that will be created + // from splitting the current section. + leading_space: if current == s.byte_range.start { + s.leading_space + } else { + 0.0 + }, byte_range: current - start..section_end - start, format: s.format.clone(), }); From f028154da81d1080744b2c6c6b45aff990878538 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20G=C5=82uchowski?= Date: Fri, 29 Nov 2024 13:01:15 +0100 Subject: [PATCH 06/68] Move cached-multiline-layout code into a helper function --- crates/epaint/src/text/fonts.rs | 195 ++++++++++++++++---------------- 1 file changed, 100 insertions(+), 95 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index b4566d14da8..4933d3d4a4e 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -727,6 +727,104 @@ struct GalleyCache { } impl GalleyCache { + fn layout_multiline(&mut self, fonts: &mut FontsImpl, job: LayoutJob) -> Galley { + let mut current_section = 0; + let mut current = 0; + let mut left_max_rows = job.wrap.max_rows; + let mut galleys = Vec::new(); + let mut text_left = job.text.as_str(); + loop { + let end = text_left.find('\n').map_or(job.text.len(), |i| i + current); + let start = current; + + let mut line_job = LayoutJob { + text: job.text[current..end].to_string(), + wrap: crate::text::TextWrapping { + max_rows: left_max_rows, + ..job.wrap + }, + halign: job.halign, + justify: job.justify, + ..Default::default() + }; + + while current < end { + let mut s = &job.sections[current_section]; + while s.byte_range.end <= current { + current_section += 1; + s = &job.sections[current_section]; + } + + assert!(s.byte_range.contains(¤t)); + let section_end = s.byte_range.end.min(end); + line_job.sections.push(crate::text::LayoutSection { + // Leading space should only be added to the first section + // if the there are multiple sections that will be created + // from splitting the current section. + leading_space: if current == s.byte_range.start { + s.leading_space + } else { + 0.0 + }, + byte_range: current - start..section_end - start, + format: s.format.clone(), + }); + current = section_end; + } + + // Prevent an infinite recursion + line_job.break_on_newline = false; + + let galley = self.layout(fonts, line_job); + // This will prevent us from invalidating cache entries unnecessarily + if left_max_rows != usize::MAX { + left_max_rows -= galley.rows.len(); + } + galleys.push(galley); + + current = end + 1; + if current >= job.text.len() { + break; + } else { + text_left = &job.text[current..]; + } + } + + let mut merged_galley = Galley { + job: Arc::new(job), + rows: Vec::new(), + elided: false, + rect: emath::Rect::ZERO, + mesh_bounds: emath::Rect::ZERO, + num_vertices: 0, + num_indices: 0, + pixels_per_point: fonts.pixels_per_point, + }; + + for galley in galleys { + let current_offset = emath::vec2(0.0, merged_galley.rect.height()); + merged_galley + .rows + .extend(galley.rows.iter().map(|(row, prev_offset)| { + merged_galley.mesh_bounds = + merged_galley.mesh_bounds.union(row.visuals.mesh_bounds); + + (row.clone(), *prev_offset + current_offset) + })); + merged_galley.rect = merged_galley + .rect + .union(galley.rect.translate(current_offset)); + merged_galley.num_vertices += galley.num_vertices; + merged_galley.num_indices += galley.num_indices; + if galley.elided { + merged_galley.elided = true; + break; + } + } + + merged_galley + } + fn layout(&mut self, fonts: &mut FontsImpl, mut job: LayoutJob) -> Arc { if job.wrap.max_width.is_finite() { // Protect against rounding errors in egui layout code. @@ -763,101 +861,8 @@ impl GalleyCache { } std::collections::hash_map::Entry::Vacant(entry) => { if job.break_on_newline { - let mut current_section = 0; - let mut current = 0; - let mut left_max_rows = job.wrap.max_rows; - let mut galleys = Vec::new(); - let mut text_left = job.text.as_str(); - loop { - let end = text_left.find('\n').map_or(job.text.len(), |i| i + current); - let start = current; - - let mut line_job = LayoutJob { - text: job.text[current..end].to_string(), - wrap: crate::text::TextWrapping { - max_rows: left_max_rows, - ..job.wrap - }, - halign: job.halign, - justify: job.justify, - ..Default::default() - }; - - while current < end { - let mut s = &job.sections[current_section]; - while s.byte_range.end <= current { - current_section += 1; - s = &job.sections[current_section]; - } - - assert!(s.byte_range.contains(¤t)); - let section_end = s.byte_range.end.min(end); - line_job.sections.push(crate::text::LayoutSection { - // Leading space should only be added to the first section - // if the there are multiple sections that will be created - // from splitting the current section. - leading_space: if current == s.byte_range.start { - s.leading_space - } else { - 0.0 - }, - byte_range: current - start..section_end - start, - format: s.format.clone(), - }); - current = section_end; - } - - // Prevent an infinite recursion - line_job.break_on_newline = false; - - let galley = self.layout(fonts, line_job); - // This will prevent us from invalidating cache entries unnecessarily - if left_max_rows != usize::MAX { - left_max_rows -= galley.rows.len(); - } - galleys.push(galley); - - current = end + 1; - if current >= job.text.len() { - break; - } else { - text_left = &job.text[current..]; - } - } - - let mut merged_galley = Galley { - job: Arc::new(job), - rows: Vec::new(), - elided: false, - rect: emath::Rect::ZERO, - mesh_bounds: emath::Rect::ZERO, - num_vertices: 0, - num_indices: 0, - pixels_per_point: fonts.pixels_per_point, - }; - - for galley in galleys { - let current_offset = emath::vec2(0.0, merged_galley.rect.height()); - merged_galley - .rows - .extend(galley.rows.iter().map(|(row, prev_offset)| { - merged_galley.mesh_bounds = - merged_galley.mesh_bounds.union(row.visuals.mesh_bounds); - - (row.clone(), *prev_offset + current_offset) - })); - merged_galley.rect = merged_galley - .rect - .union(galley.rect.translate(current_offset)); - merged_galley.num_vertices += galley.num_vertices; - merged_galley.num_indices += galley.num_indices; - if galley.elided { - merged_galley.elided = true; - break; - } - } - - let galley = Arc::new(merged_galley); + let galley = self.layout_multiline(fonts, job); + let galley = Arc::new(galley); self.cache.insert( hash, CachedGalley { From bc86bec1cb69a99e0b2d49272608f298021824d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20G=C5=82uchowski?= Date: Fri, 29 Nov 2024 15:53:50 +0100 Subject: [PATCH 07/68] Properly handle row repositioning --- .../egui/src/text_selection/accesskit_text.rs | 8 +- .../egui/src/text_selection/cursor_range.rs | 2 +- .../text_selection/label_text_selection.rs | 8 +- crates/egui/src/text_selection/visuals.rs | 19 ++- crates/egui/src/widget_text.rs | 4 +- crates/egui/src/widgets/label.rs | 9 +- crates/epaint/src/shape.rs | 5 +- crates/epaint/src/shape_transform.rs | 4 +- crates/epaint/src/stats.rs | 12 +- crates/epaint/src/tessellator.rs | 4 +- crates/epaint/src/text/fonts.rs | 23 ++- crates/epaint/src/text/text_layout.rs | 147 ++++++++--------- crates/epaint/src/text/text_layout_types.rs | 155 ++++++++++-------- 13 files changed, 209 insertions(+), 191 deletions(-) diff --git a/crates/egui/src/text_selection/accesskit_text.rs b/crates/egui/src/text_selection/accesskit_text.rs index ad11c16a4d9..2197bb6b178 100644 --- a/crates/egui/src/text_selection/accesskit_text.rs +++ b/crates/egui/src/text_selection/accesskit_text.rs @@ -39,11 +39,11 @@ pub fn update_accesskit_for_text_widget( }; ctx.with_accessibility_parent(parent_id, || { - for (row_index, (row, offset)) in galley.rows.iter().enumerate() { + for (row_index, row) in galley.rows.iter().enumerate() { let row_id = parent_id.with(row_index); ctx.accesskit_node_builder(row_id, |builder| { builder.set_role(accesskit::Role::TextRun); - let rect = row.rect.translate(offset.to_vec2() + galley_pos.to_vec2()); + let rect = row.rect().translate(galley_pos.to_vec2()); builder.set_bounds(accesskit::Rect { x0: rect.min.x.into(), y0: rect.min.y.into(), @@ -74,14 +74,14 @@ pub fn update_accesskit_for_text_widget( let old_len = value.len(); value.push(glyph.chr); character_lengths.push((value.len() - old_len) as _); - character_positions.push(glyph.pos.x - row.rect.min.x); + character_positions.push(glyph.pos.x - row.pos.x); character_widths.push(glyph.advance_width); } if row.ends_with_newline { value.push('\n'); character_lengths.push(1); - character_positions.push(row.rect.max.x - row.rect.min.x); + character_positions.push(row.size.x); character_widths.push(0.0); } word_lengths.push((character_lengths.len() - last_word_start) as _); diff --git a/crates/egui/src/text_selection/cursor_range.rs b/crates/egui/src/text_selection/cursor_range.rs index dbdba5ba8eb..bd3f496fd8e 100644 --- a/crates/egui/src/text_selection/cursor_range.rs +++ b/crates/egui/src/text_selection/cursor_range.rs @@ -284,7 +284,7 @@ fn ccursor_from_accesskit_text_position( position: &accesskit::TextPosition, ) -> Option { let mut total_length = 0usize; - for (i, (row, _)) in galley.rows.iter().enumerate() { + for (i, row) in galley.rows.iter().enumerate() { let row_id = id.with(i); if row_id.accesskit_id() == position.node { return Some(CCursor { diff --git a/crates/egui/src/text_selection/label_text_selection.rs b/crates/egui/src/text_selection/label_text_selection.rs index 321d8d94609..41210b3718c 100644 --- a/crates/egui/src/text_selection/label_text_selection.rs +++ b/crates/egui/src/text_selection/label_text_selection.rs @@ -179,10 +179,10 @@ impl LabelSelectionState { if let epaint::Shape::Text(text_shape) = &mut shape.shape { let galley = Arc::make_mut(&mut text_shape.galley); for row_selection in row_selections { - if let Some((row, _)) = + if let Some(placed_row) = galley.rows.get_mut(row_selection.row) { - let row = Arc::make_mut(row); + let row = Arc::make_mut(&mut placed_row.row); for vertex_index in row_selection.vertex_indices { if let Some(vertex) = row .visuals @@ -662,8 +662,8 @@ fn selected_text(galley: &Galley, cursor_range: &CursorRange) -> String { } fn estimate_row_height(galley: &Galley) -> f32 { - if let Some((row, _)) = galley.rows.first() { - row.rect.height() + if let Some(placed_row) = galley.rows.first() { + placed_row.height() } else { galley.size().y } diff --git a/crates/egui/src/text_selection/visuals.rs b/crates/egui/src/text_selection/visuals.rs index 0000632e77c..3eecfaf4bf3 100644 --- a/crates/egui/src/text_selection/visuals.rs +++ b/crates/egui/src/text_selection/visuals.rs @@ -31,26 +31,27 @@ pub fn paint_text_selection( let max = max.rcursor; for ri in min.row..=max.row { - let (row, _) = &mut galley.rows[ri]; - let row = Arc::make_mut(row); + let placed_row = &mut galley.rows[ri]; let left = if ri == min.row { - row.x_offset(min.column) + placed_row.x_offset(min.column) } else { - row.rect.left() + 0.0 }; let right = if ri == max.row { - row.x_offset(max.column) + placed_row.x_offset(max.column) } else { - let newline_size = if row.ends_with_newline { - row.height() / 2.0 // visualize that we select the newline + let newline_size = if placed_row.ends_with_newline { + placed_row.height() / 2.0 // visualize that we select the newline } else { 0.0 }; - row.rect.right() + newline_size + placed_row.size.x + newline_size }; - let rect = Rect::from_min_max(pos2(left, row.min_y()), pos2(right, row.max_y())); + let rect = Rect::from_min_max(pos2(left, 0.0), pos2(right, placed_row.size.y)); + + let row = Arc::make_mut(&mut placed_row.row); let mesh = &mut row.visuals.mesh; // Time to insert the selection rectangle into the row mesh. diff --git a/crates/egui/src/widget_text.rs b/crates/egui/src/widget_text.rs index bde229828be..2aae0320d96 100644 --- a/crates/egui/src/widget_text.rs +++ b/crates/egui/src/widget_text.rs @@ -640,8 +640,8 @@ impl WidgetText { Self::RichText(text) => text.font_height(fonts, style), Self::LayoutJob(job) => job.font_height(fonts), Self::Galley(galley) => { - if let Some((row, _)) = galley.rows.first() { - row.height() + if let Some(placed_row) = galley.rows.first() { + placed_row.height() } else { galley.size().y } diff --git a/crates/egui/src/widgets/label.rs b/crates/egui/src/widgets/label.rs index 276c9576b61..d542940c631 100644 --- a/crates/egui/src/widgets/label.rs +++ b/crates/egui/src/widgets/label.rs @@ -194,13 +194,10 @@ impl Label { let pos = pos2(ui.max_rect().left(), ui.cursor().top()); assert!(!galley.rows.is_empty(), "Galleys are never empty"); // collect a response from many rows: - let rect = galley.rows[0] - .0 - .rect - .translate(galley.rows[0].1.to_vec2() + pos.to_vec2()); + let rect = galley.rows[0].rect().translate(pos.to_vec2()); let mut response = ui.allocate_rect(rect, sense); - for (row, offset) in galley.rows.iter().skip(1) { - let rect = row.rect.translate(offset.to_vec2() + pos.to_vec2()); + for placed_row in galley.rows.iter().skip(1) { + let rect = placed_row.rect().translate(pos.to_vec2()); response |= ui.allocate_rect(rect, sense); } (pos, galley, response) diff --git a/crates/epaint/src/shape.rs b/crates/epaint/src/shape.rs index 0d2d77d47ad..4d2c48b2c0a 100644 --- a/crates/epaint/src/shape.rs +++ b/crates/epaint/src/shape.rs @@ -433,9 +433,8 @@ impl Shape { // Scale text: let galley = Arc::make_mut(&mut text_shape.galley); - for (row, offset) in &mut galley.rows { - let row = Arc::make_mut(row); - *offset = *offset * transform.scaling; + for placed_row in &mut galley.rows { + let row = Arc::make_mut(&mut placed_row.row); row.visuals.mesh_bounds = transform.scaling * row.visuals.mesh_bounds; for v in &mut row.visuals.mesh.vertices { v.pos = Pos2::new(transform.scaling * v.pos.x, transform.scaling * v.pos.y); diff --git a/crates/epaint/src/shape_transform.rs b/crates/epaint/src/shape_transform.rs index 4d74c938513..58281d09b8d 100644 --- a/crates/epaint/src/shape_transform.rs +++ b/crates/epaint/src/shape_transform.rs @@ -88,8 +88,8 @@ pub fn adjust_colors( if !galley.is_empty() { let galley = std::sync::Arc::make_mut(galley); - for (row, _) in &mut galley.rows { - let row = Arc::make_mut(row); + for placed_row in &mut galley.rows { + let row = Arc::make_mut(&mut placed_row.row); for vertex in &mut row.visuals.mesh.vertices { adjust_color(&mut vertex.color); } diff --git a/crates/epaint/src/stats.rs b/crates/epaint/src/stats.rs index ad3705bea4b..456dea85fcf 100644 --- a/crates/epaint/src/stats.rs +++ b/crates/epaint/src/stats.rs @@ -88,14 +88,10 @@ impl AllocInfo { pub fn from_galley(galley: &Galley) -> Self { Self::from_slice(galley.text().as_bytes()) + Self::from_slice(&galley.rows) - + galley - .rows - .iter() - .map(|(row, _)| Self::from_galley_row(row)) - .sum() + + galley.rows.iter().map(Self::from_galley_row).sum() } - fn from_galley_row(row: &crate::text::Row) -> Self { + fn from_galley_row(row: &crate::text::PlacedRow) -> Self { Self::from_mesh(&row.visuals.mesh) + Self::from_slice(&row.glyphs) } @@ -217,8 +213,8 @@ impl PaintStats { self.shape_text += AllocInfo::from_galley(&text_shape.galley); for row in &text_shape.galley.rows { - self.text_shape_indices += AllocInfo::from_slice(&row.0.visuals.mesh.indices); - self.text_shape_vertices += AllocInfo::from_slice(&row.0.visuals.mesh.vertices); + self.text_shape_indices += AllocInfo::from_slice(&row.visuals.mesh.indices); + self.text_shape_vertices += AllocInfo::from_slice(&row.visuals.mesh.vertices); } } Shape::Mesh(mesh) => { diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 393edfbbf67..a6290e9c636 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1778,12 +1778,12 @@ impl Tessellator { let rotator = Rot2::from_angle(*angle); - for (row, row_pos) in &galley.rows { + for row in &galley.rows { if row.visuals.mesh.is_empty() { continue; } - let final_pos = galley_pos + row_pos.to_vec2(); + let final_pos = galley_pos + row.pos.to_vec2(); let mut row_rect = row.visuals.mesh_bounds; if *angle != 0.0 { diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 4933d3d4a4e..234752e81f3 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -805,12 +805,21 @@ impl GalleyCache { let current_offset = emath::vec2(0.0, merged_galley.rect.height()); merged_galley .rows - .extend(galley.rows.iter().map(|(row, prev_offset)| { - merged_galley.mesh_bounds = - merged_galley.mesh_bounds.union(row.visuals.mesh_bounds); - - (row.clone(), *prev_offset + current_offset) + .extend(galley.rows.iter().map(|placed_row| { + let new_pos = placed_row.pos + current_offset; + merged_galley.mesh_bounds = merged_galley + .mesh_bounds + .union(placed_row.visuals.mesh_bounds.translate(new_pos.to_vec2())); + + super::PlacedRow { + row: placed_row.row.clone(), + pos: new_pos, + ends_with_newline: placed_row.ends_with_newline, + } })); + if let Some(last) = merged_galley.rows.last_mut() { + last.ends_with_newline = true; + } merged_galley.rect = merged_galley .rect .union(galley.rect.translate(current_offset)); @@ -822,6 +831,10 @@ impl GalleyCache { } } + if let Some(last) = merged_galley.rows.last_mut() { + last.ends_with_newline = false; + } + merged_galley } diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index ed7347418e0..9203dfd0559 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -5,7 +5,7 @@ use emath::{pos2, vec2, Align, NumExt, Pos2, Rect, Vec2}; use crate::{stroke::PathStroke, text::font::Font, Color32, Mesh, Stroke, Vertex}; -use super::{FontsImpl, Galley, Glyph, LayoutJob, LayoutSection, Row, RowVisuals}; +use super::{FontsImpl, Galley, Glyph, LayoutJob, LayoutSection, PlacedRow, Row, RowVisuals}; // ---------------------------------------------------------------------------- @@ -96,11 +96,11 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc) -> Galley { let mut elided = false; let mut rows = rows_from_paragraphs(paragraphs, &job, &mut elided); if elided { - if let Some((last_row, _)) = rows.last_mut() { - let last_row = Arc::get_mut(last_row).unwrap(); + if let Some(last_placed) = rows.last_mut() { + let last_row = Arc::get_mut(&mut last_placed.row).unwrap(); replace_last_glyph_with_overflow_character(fonts, &job, last_row); if let Some(last) = last_row.glyphs.last() { - last_row.rect.max.x = last.max_x(); + last_row.size.x = last.max_x(); } } } @@ -109,12 +109,13 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc) -> Galley { if justify || job.halign != Align::LEFT { let num_rows = rows.len(); - for (i, (row, _)) in rows.iter_mut().enumerate() { + for (i, placed_row) in rows.iter_mut().enumerate() { let is_last_row = i + 1 == num_rows; - let justify_row = justify && !row.ends_with_newline && !is_last_row; + let justify_row = justify && !placed_row.ends_with_newline && !is_last_row; halign_and_justify_row( point_scale, - Arc::get_mut(row).unwrap(), + Arc::get_mut(&mut placed_row.row).unwrap(), + &mut placed_row.pos, job.halign, job.wrap.max_width, justify_row, @@ -199,7 +200,7 @@ fn rows_from_paragraphs( paragraphs: Vec, job: &LayoutJob, elided: &mut bool, -) -> Vec<(Arc, Pos2)> { +) -> Vec { let num_paragraphs = paragraphs.len(); let mut rows = vec![]; @@ -213,38 +214,35 @@ fn rows_from_paragraphs( let is_last_paragraph = (i + 1) == num_paragraphs; if paragraph.glyphs.is_empty() { - rows.push(( - Arc::new(Row { + rows.push(PlacedRow { + row: Arc::new(Row { section_index_at_start: paragraph.section_index_at_start, glyphs: vec![], visuals: Default::default(), - rect: Rect::from_min_size( - pos2(paragraph.cursor_x, 0.0), - vec2(0.0, paragraph.empty_paragraph_height), - ), - ends_with_newline: !is_last_paragraph, + size: vec2(0.0, paragraph.empty_paragraph_height), }), - Pos2::ZERO, - )); + pos: pos2(paragraph.cursor_x, 0.0), + ends_with_newline: !is_last_paragraph, + }); } else { let paragraph_max_x = paragraph.glyphs.last().unwrap().max_x(); if paragraph_max_x <= job.effective_wrap_width() { // Early-out optimization: the whole paragraph fits on one row. let paragraph_min_x = paragraph.glyphs[0].pos.x; - rows.push(( - Arc::new(Row { + let rect = rect_from_x_range(paragraph_min_x..=paragraph_max_x); + rows.push(PlacedRow { + row: Arc::new(Row { section_index_at_start: paragraph.section_index_at_start, glyphs: paragraph.glyphs, visuals: Default::default(), - rect: rect_from_x_range(paragraph_min_x..=paragraph_max_x), - ends_with_newline: !is_last_paragraph, + size: rect.size(), }), - Pos2::ZERO, - )); + pos: rect.min, + ends_with_newline: !is_last_paragraph, + }); } else { line_break(¶graph, job, &mut rows, elided); - let last_row = Arc::get_mut(&mut rows.last_mut().unwrap().0).unwrap(); - last_row.ends_with_newline = !is_last_paragraph; + rows.last_mut().unwrap().ends_with_newline = !is_last_paragraph; } } } @@ -255,7 +253,7 @@ fn rows_from_paragraphs( fn line_break( paragraph: &Paragraph, job: &LayoutJob, - out_rows: &mut Vec<(Arc, Pos2)>, + out_rows: &mut Vec, elided: &mut bool, ) { let wrap_width = job.effective_wrap_width(); @@ -283,16 +281,17 @@ fn line_break( { // Allow the first row to be completely empty, because we know there will be more space on the next row: // TODO(emilk): this records the height of this first row as zero, though that is probably fine since first_row_indentation usually comes with a first_row_min_height. - out_rows.push(( - Arc::new(Row { + let rect = rect_from_x_range(first_row_indentation..=first_row_indentation); + out_rows.push(PlacedRow { + row: Arc::new(Row { section_index_at_start: paragraph.section_index_at_start, glyphs: vec![], visuals: Default::default(), - rect: rect_from_x_range(first_row_indentation..=first_row_indentation), - ends_with_newline: false, + size: rect.size(), }), - Pos2::ZERO, - )); + pos: rect.min, + ends_with_newline: false, + }); row_start_x += first_row_indentation; first_row_indentation = 0.0; } else if let Some(last_kept_index) = row_break_candidates.get(job.wrap.break_anywhere) @@ -310,16 +309,17 @@ fn line_break( let paragraph_min_x = glyphs[0].pos.x; let paragraph_max_x = glyphs.last().unwrap().max_x(); - out_rows.push(( - Arc::new(Row { + let rect = rect_from_x_range(paragraph_min_x..=paragraph_max_x); + out_rows.push(PlacedRow { + row: Arc::new(Row { section_index_at_start, glyphs, visuals: Default::default(), - rect: rect_from_x_range(paragraph_min_x..=paragraph_max_x), - ends_with_newline: false, + size: rect.size(), }), - Pos2::ZERO, - )); + pos: rect.min, + ends_with_newline: false, + }); // Start a new row: row_start_idx = last_kept_index + 1; @@ -352,16 +352,17 @@ fn line_break( let paragraph_min_x = glyphs[0].pos.x; let paragraph_max_x = glyphs.last().unwrap().max_x(); - out_rows.push(( - Arc::new(Row { + let rect = rect_from_x_range(paragraph_min_x..=paragraph_max_x); + out_rows.push(PlacedRow { + row: Arc::new(Row { section_index_at_start, glyphs, visuals: Default::default(), - rect: rect_from_x_range(paragraph_min_x..=paragraph_max_x), - ends_with_newline: false, + size: rect.size(), }), - Pos2::ZERO, - )); + pos: rect.min, + ends_with_newline: false, + }); } } } @@ -523,6 +524,7 @@ fn replace_last_glyph_with_overflow_character( fn halign_and_justify_row( point_scale: PointScale, row: &mut Row, + pos: &mut Pos2, halign: Align, wrap_width: f32, justify: bool, @@ -606,24 +608,25 @@ fn halign_and_justify_row( } // Note we ignore the leading/trailing whitespace here! - row.rect.min.x = target_min_x; - row.rect.max.x = target_max_x; + pos.x = target_min_x; + row.size.x = target_max_x - target_min_x; } /// Calculate the Y positions and tessellate the text. fn galley_from_rows( point_scale: PointScale, job: Arc, - mut rows: Vec<(Arc, Pos2)>, + mut rows: Vec, elided: bool, ) -> Galley { let mut first_row_min_height = job.first_row_min_height; let mut cursor_y = 0.0; let mut min_x: f32 = 0.0; let mut max_x: f32 = 0.0; - for (row, _) in &mut rows { - let row = Arc::get_mut(row).unwrap(); - let mut max_row_height = first_row_min_height.max(row.rect.height()); + for placed_row in &mut rows { + let mut max_row_height = first_row_min_height.max(placed_row.rect().height()); + let row = Arc::get_mut(&mut placed_row.row).unwrap(); + first_row_min_height = 0.0; for glyph in &row.glyphs { max_row_height = max_row_height.max(glyph.line_height); @@ -634,8 +637,7 @@ fn galley_from_rows( for glyph in &mut row.glyphs { let format = &job.sections[glyph.section_index as usize].format; - glyph.pos.y = cursor_y - + glyph.font_impl_ascent + glyph.pos.y = glyph.font_impl_ascent // Apply valign to the different in height of the entire row, and the height of this `Font`: + format.valign.to_factor() * (max_row_height - glyph.line_height) @@ -644,14 +646,16 @@ fn galley_from_rows( // we always center the difference: + 0.5 * (glyph.font_height - glyph.font_impl_height); - glyph.pos.y = point_scale.round_to_pixel(glyph.pos.y); + // FIXME(afishhh): HACK! change the proper code above instead!! + // this should probably not be merged like this! + glyph.pos.x -= placed_row.pos.x; } - row.rect.min.y = cursor_y; - row.rect.max.y = cursor_y + max_row_height; + placed_row.pos.y = cursor_y; + row.size.y = max_row_height; - min_x = min_x.min(row.rect.min.x); - max_x = max_x.max(row.rect.max.x); + min_x = min_x.min(placed_row.rect().min.x); + max_x = max_x.max(placed_row.rect().max.x); cursor_y += max_row_height; cursor_y = point_scale.round_to_pixel(cursor_y); } @@ -662,8 +666,8 @@ fn galley_from_rows( let mut num_vertices = 0; let mut num_indices = 0; - for (row, _) in &mut rows { - let row = Arc::get_mut(row).unwrap(); + for placed_row in &mut rows { + let row = Arc::get_mut(&mut placed_row.row).unwrap(); row.visuals = tessellate_row(point_scale, &job, &format_summary, row); mesh_bounds = mesh_bounds.union(row.visuals.mesh_bounds); num_vertices += row.visuals.mesh.vertices.len(); @@ -1096,7 +1100,7 @@ mod tests { assert!(galley.elided); assert_eq!(galley.rows.len(), 1); - let row_text = galley.rows[0].0.text(); + let row_text = galley.rows[0].text(); assert!( row_text.ends_with('…'), "Expected row to end with `…`, got {row_text:?} when line-breaking the text {text:?} with max_width {max_width} and break_anywhere {break_anywhere}.", @@ -1115,7 +1119,7 @@ mod tests { assert!(galley.elided); assert_eq!(galley.rows.len(), 1); - let row_text = galley.rows[0].0.text(); + let row_text = galley.rows[0].text(); assert_eq!(row_text, "Hello…"); } } @@ -1130,11 +1134,7 @@ mod tests { layout_job.wrap.max_width = 90.0; let galley = layout(&mut fonts, layout_job.into()); assert_eq!( - galley - .rows - .iter() - .map(|row| row.0.text()) - .collect::>(), + galley.rows.iter().map(|row| row.text()).collect::>(), vec!["日本語と", "Englishの混在", "した文章"] ); } @@ -1149,11 +1149,7 @@ mod tests { layout_job.wrap.max_width = 110.0; let galley = layout(&mut fonts, layout_job.into()); assert_eq!( - galley - .rows - .iter() - .map(|row| row.0.text()) - .collect::>(), + galley.rows.iter().map(|row| row.text()).collect::>(), vec!["日本語とEnglish", "の混在した文章"] ); } @@ -1168,14 +1164,11 @@ mod tests { let galley = layout(&mut fonts, layout_job.into()); assert!(galley.elided); assert_eq!( - galley - .rows - .iter() - .map(|row| row.0.text()) - .collect::>(), + galley.rows.iter().map(|row| row.text()).collect::>(), vec!["# DNA…"] ); let row = &galley.rows[0]; - assert_eq!(row.0.rect.max.x, row.0.glyphs.last().unwrap().max_x()); + assert_eq!(row.pos, Pos2::ZERO); + assert_eq!(row.rect().max.x, row.glyphs.last().unwrap().max_x()); } } diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 31a44c5dd3a..0229665cbec 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -506,7 +506,7 @@ pub struct Galley { /// /// Note that a paragraph (a piece of text separated with `\n`) /// can be split up into multiple rows. - pub rows: Vec<(Arc, Pos2)>, + pub rows: Vec, /// Set to true the text was truncated due to [`TextWrapping::max_rows`]. pub elided: bool, @@ -538,6 +538,39 @@ pub struct Galley { pub pixels_per_point: f32, } +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct PlacedRow { + /// The underlying row unpositioned [`Row`]. + pub row: Arc, + + /// The position of this [`Row`] relative to the galley. + pub pos: Pos2, + + /// If true, this [`PlacedRow`] came from a paragraph ending with a `\n`. + /// The `\n` itself is omitted from [`Row::glyphs`]. + /// A `\n` in the input text always creates a new [`PlacedRow`] below it, + /// so that text that ends with `\n` has an empty [`PlacedRow`] last. + /// This also implies that the last [`PlacedRow`] in a [`Galley`] always has `ends_with_newline == false`. + pub ends_with_newline: bool, +} + +impl PlacedRow { + /// Logical bounding rectangle on font heights etc. + /// Use this when drawing a selection or similar! + pub fn rect(&self) -> Rect { + Rect::from_min_size(self.pos, self.row.size) + } +} + +impl std::ops::Deref for PlacedRow { + type Target = Row; + + fn deref(&self) -> &Self::Target { + &self.row + } +} + #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct Row { @@ -547,20 +580,12 @@ pub struct Row { /// One for each `char`. pub glyphs: Vec, - /// Logical bounding rectangle based on font heights etc. - /// Use this when drawing a selection or similar! + /// Logical size based on font heights etc. /// Includes leading and trailing whitespace. - pub rect: Rect, + pub size: Vec2, /// The mesh, ready to be rendered. pub visuals: RowVisuals, - - /// If true, this [`Row`] came from a paragraph ending with a `\n`. - /// The `\n` itself is omitted from [`Self::glyphs`]. - /// A `\n` in the input text always creates a new [`Row`] below it, - /// so that text that ends with `\n` has an empty [`Row`] last. - /// This also implies that the last [`Row`] in a [`Galley`] always has `ends_with_newline == false`. - pub ends_with_newline: bool, } /// The tessellated output of a row. @@ -668,6 +693,27 @@ impl Row { self.glyphs.len() } + /// Closest char at the desired x coordinate in row-relative coordinates. + /// Returns something in the range `[0, char_count_excluding_newline()]`. + pub fn char_at(&self, desired_x: f32) -> usize { + for (i, glyph) in self.glyphs.iter().enumerate() { + if desired_x < glyph.logical_rect().center().x { + return i; + } + } + self.char_count_excluding_newline() + } + + pub fn x_offset(&self, column: usize) -> f32 { + if let Some(glyph) = self.glyphs.get(column) { + glyph.pos.x + } else { + self.size.x + } + } +} + +impl PlacedRow { /// Includes the implicit `\n` after the [`Row`], if any. #[inline] pub fn char_count_including_newline(&self) -> usize { @@ -676,36 +722,17 @@ impl Row { #[inline] pub fn min_y(&self) -> f32 { - self.rect.top() + self.rect().top() } #[inline] pub fn max_y(&self) -> f32 { - self.rect.bottom() + self.rect().bottom() } #[inline] pub fn height(&self) -> f32 { - self.rect.height() - } - - /// Closest char at the desired x coordinate. - /// Returns something in the range `[0, char_count_excluding_newline()]`. - pub fn char_at(&self, desired_x: f32) -> usize { - for (i, glyph) in self.glyphs.iter().enumerate() { - if desired_x < glyph.logical_rect().center().x { - return i; - } - } - self.char_count_excluding_newline() - } - - pub fn x_offset(&self, column: usize) -> f32 { - if let Some(glyph) = self.glyphs.get(column) { - glyph.pos.x - } else { - self.rect.right() - } + self.row.size.y } } @@ -755,8 +782,8 @@ impl std::ops::Deref for Galley { impl Galley { /// Zero-width rect past the last character. fn end_pos(&self) -> Rect { - if let Some((row, _)) = self.rows.last() { - let x = row.rect.right(); + if let Some(row) = self.rows.last() { + let x = row.rect().right(); Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y())) } else { // Empty galley @@ -773,7 +800,7 @@ impl Galley { pub fn pos_from_pcursor(&self, pcursor: PCursor) -> Rect { let mut it = PCursor::default(); - for (row, offset) in &self.rows { + for row in &self.rows { if it.paragraph == pcursor.paragraph { // Right paragraph, but is it the right row in the paragraph? @@ -787,11 +814,8 @@ impl Galley { && !row.ends_with_newline && column >= row.char_count_excluding_newline(); if !select_next_row_instead { - let x = row.x_offset(column) + offset.x; - return Rect::from_min_max( - pos2(x, row.min_y() + offset.y), - pos2(x, row.max_y() + offset.y), - ); + let x = row.x_offset(column); + return Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y())); } } } @@ -825,13 +849,13 @@ impl Galley { /// same as a cursor at the end. /// This allows implementing text-selection by dragging above/below the galley. pub fn cursor_from_pos(&self, pos: Vec2) -> Cursor { - if let Some((first_row, offset)) = self.rows.first() { - if pos.y < first_row.min_y() + offset.y { + if let Some(first_row) = self.rows.first() { + if pos.y < first_row.min_y() { return self.begin(); } } - if let Some((last_row, offset)) = self.rows.last() { - if last_row.max_y() + offset.y < pos.y { + if let Some(last_row) = self.rows.last() { + if last_row.max_y() < pos.y { return self.end(); } } @@ -842,15 +866,16 @@ impl Galley { let mut ccursor_index = 0; let mut pcursor_it = PCursor::default(); - for (row_nr, (row, offset)) in self.rows.iter().enumerate() { - let min_y = row.min_y() + offset.y; - let max_y = row.max_y() + offset.y; + for (row_nr, row) in self.rows.iter().enumerate() { + let min_y = row.min_y(); + let max_y = row.max_y(); let is_pos_within_row = min_y <= pos.y && pos.y <= max_y; let y_dist = (min_y - pos.y).abs().min((max_y - pos.y).abs()); if is_pos_within_row || y_dist < best_y_dist { best_y_dist = y_dist; - let column = row.char_at(pos.x); + // char_at is `Row` not `PlacedRow` relative which means we have to subtract the pos. + let column = row.char_at(pos.x - row.pos.x); let prefer_next_row = column < row.char_count_excluding_newline(); cursor = Cursor { ccursor: CCursor { @@ -910,7 +935,7 @@ impl Galley { offset: 0, prefer_next_row: true, }; - for (row, _) in &self.rows { + for row in &self.rows { let row_char_count = row.char_count_including_newline(); ccursor.index += row_char_count; if row.ends_with_newline { @@ -928,7 +953,7 @@ impl Galley { } pub fn end_rcursor(&self) -> RCursor { - if let Some((last_row, _)) = self.rows.last() { + if let Some(last_row) = self.rows.last() { RCursor { row: self.rows.len() - 1, column: last_row.char_count_including_newline(), @@ -954,7 +979,7 @@ impl Galley { prefer_next_row, }; - for (row_nr, (row, _)) in self.rows.iter().enumerate() { + for (row_nr, row) in self.rows.iter().enumerate() { let row_char_count = row.char_count_excluding_newline(); if ccursor_it.index <= ccursor.index @@ -999,7 +1024,7 @@ impl Galley { } let prefer_next_row = - rcursor.column < self.rows[rcursor.row].0.char_count_excluding_newline(); + rcursor.column < self.rows[rcursor.row].char_count_excluding_newline(); let mut ccursor_it = CCursor { index: 0, prefer_next_row, @@ -1010,7 +1035,7 @@ impl Galley { prefer_next_row, }; - for (row_nr, (row, _)) in self.rows.iter().enumerate() { + for (row_nr, row) in self.rows.iter().enumerate() { if row_nr == rcursor.row { ccursor_it.index += rcursor.column.at_most(row.char_count_excluding_newline()); @@ -1054,7 +1079,7 @@ impl Galley { prefer_next_row, }; - for (row_nr, (row, _)) in self.rows.iter().enumerate() { + for (row_nr, row) in self.rows.iter().enumerate() { if pcursor_it.paragraph == pcursor.paragraph { // Right paragraph, but is it the right row in the paragraph? @@ -1128,9 +1153,7 @@ impl Galley { let new_row = cursor.rcursor.row - 1; let cursor_is_beyond_end_of_current_row = cursor.rcursor.column - >= self.rows[cursor.rcursor.row] - .0 - .char_count_excluding_newline(); + >= self.rows[cursor.rcursor.row].char_count_excluding_newline(); let new_rcursor = if cursor_is_beyond_end_of_current_row { // keep same column @@ -1141,8 +1164,8 @@ impl Galley { } else { // keep same X coord let x = self.pos_from_cursor(cursor).center().x; - let (row, offset) = &self.rows[new_row]; - let column = if x > row.rect.right() + offset.x { + let row = &self.rows[new_row]; + let column = if x > row.rect().right() { // beyond the end of this row - keep same column cursor.rcursor.column } else { @@ -1162,9 +1185,7 @@ impl Galley { let new_row = cursor.rcursor.row + 1; let cursor_is_beyond_end_of_current_row = cursor.rcursor.column - >= self.rows[cursor.rcursor.row] - .0 - .char_count_excluding_newline(); + >= self.rows[cursor.rcursor.row].char_count_excluding_newline(); let new_rcursor = if cursor_is_beyond_end_of_current_row { // keep same column @@ -1175,8 +1196,8 @@ impl Galley { } else { // keep same X coord let x = self.pos_from_cursor(cursor).center().x; - let (row, offset) = &self.rows[new_row]; - let column = if x > row.rect.right() + offset.x { + let row = &self.rows[new_row]; + let column = if x > row.rect().right() { // beyond the end of the next row - keep same column cursor.rcursor.column } else { @@ -1204,9 +1225,7 @@ impl Galley { pub fn cursor_end_of_row(&self, cursor: &Cursor) -> Cursor { self.from_rcursor(RCursor { row: cursor.rcursor.row, - column: self.rows[cursor.rcursor.row] - .0 - .char_count_excluding_newline(), + column: self.rows[cursor.rcursor.row].char_count_excluding_newline(), }) } } From abbc561ae15760460c69101f40047f66b6dd4e8c Mon Sep 17 00:00:00 2001 From: Fishhh Date: Sat, 30 Nov 2024 15:59:50 +0100 Subject: [PATCH 08/68] Correctly handle empty lines --- crates/epaint/src/text/fonts.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 234752e81f3..c323fb0d6db 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -772,6 +772,20 @@ impl GalleyCache { current = section_end; } + // If the current line is empty, add an extra offset to make sure it's not omitted + // because the resulting galley will have a height of zero. + let extra_y_offset = if start == end && end != job.text.len() { + while job.sections[current_section].byte_range.end == end { + current_section += 1; + } + let format = &job.sections[current_section].format; + format + .line_height + .unwrap_or(fonts.row_height(&format.font_id)) + } else { + 0.0 + }; + // Prevent an infinite recursion line_job.break_on_newline = false; @@ -780,7 +794,7 @@ impl GalleyCache { if left_max_rows != usize::MAX { left_max_rows -= galley.rows.len(); } - galleys.push(galley); + galleys.push((galley, extra_y_offset)); current = end + 1; if current >= job.text.len() { @@ -801,7 +815,7 @@ impl GalleyCache { pixels_per_point: fonts.pixels_per_point, }; - for galley in galleys { + for (galley, extra_y_offset) in galleys { let current_offset = emath::vec2(0.0, merged_galley.rect.height()); merged_galley .rows @@ -823,6 +837,7 @@ impl GalleyCache { merged_galley.rect = merged_galley .rect .union(galley.rect.translate(current_offset)); + merged_galley.rect.max.y += extra_y_offset; merged_galley.num_vertices += galley.num_vertices; merged_galley.num_indices += galley.num_indices; if galley.elided { From 6d6bc3befbeedb26b342900f9801d551d8ea2f0d Mon Sep 17 00:00:00 2001 From: Fishhh Date: Sat, 30 Nov 2024 16:46:49 +0100 Subject: [PATCH 09/68] Respect first_row_min_height during multiline Galley layout --- crates/epaint/src/text/fonts.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index c323fb0d6db..b2fa099e0f6 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -733,6 +733,7 @@ impl GalleyCache { let mut left_max_rows = job.wrap.max_rows; let mut galleys = Vec::new(); let mut text_left = job.text.as_str(); + let mut first_row_min_height = job.first_row_min_height; loop { let end = text_left.find('\n').map_or(job.text.len(), |i| i + current); let start = current; @@ -743,10 +744,15 @@ impl GalleyCache { max_rows: left_max_rows, ..job.wrap }, + sections: Vec::new(), + // Prevent an infinite recursion + break_on_newline: false, halign: job.halign, justify: job.justify, - ..Default::default() + first_row_min_height, + round_output_size_to_nearest_ui_point: job.round_output_size_to_nearest_ui_point, }; + first_row_min_height = 0.0; while current < end { let mut s = &job.sections[current_section]; @@ -786,9 +792,6 @@ impl GalleyCache { 0.0 }; - // Prevent an infinite recursion - line_job.break_on_newline = false; - let galley = self.layout(fonts, line_job); // This will prevent us from invalidating cache entries unnecessarily if left_max_rows != usize::MAX { From 6147ff3668ab680a4ec57f73552cab95c050420a Mon Sep 17 00:00:00 2001 From: Fishhh Date: Sat, 30 Nov 2024 16:59:38 +0100 Subject: [PATCH 10/68] Round `PlacedRow` positions to pixels during multiline layout --- crates/epaint/src/text/fonts.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index b2fa099e0f6..a2644299120 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -728,6 +728,10 @@ struct GalleyCache { impl GalleyCache { fn layout_multiline(&mut self, fonts: &mut FontsImpl, job: LayoutJob) -> Galley { + let pixels_per_point = fonts.pixels_per_point; + let round_to_pixel = + move |point: emath::Pos2| (point * pixels_per_point).round() / pixels_per_point; + let mut current_section = 0; let mut current = 0; let mut left_max_rows = job.wrap.max_rows; @@ -830,7 +834,7 @@ impl GalleyCache { super::PlacedRow { row: placed_row.row.clone(), - pos: new_pos, + pos: round_to_pixel(new_pos), ends_with_newline: placed_row.ends_with_newline, } })); From 66c83c31ce59e85969b74c9f65c842a25268aea3 Mon Sep 17 00:00:00 2001 From: Fishhh Date: Sat, 30 Nov 2024 15:38:58 +0100 Subject: [PATCH 11/68] Move `ends_with_newline` back into `Row` --- crates/epaint/src/text/fonts.rs | 95 +++++++++++---------- crates/epaint/src/text/text_layout.rs | 14 +-- crates/epaint/src/text/text_layout_types.rs | 14 +-- 3 files changed, 65 insertions(+), 58 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index a2644299120..dbd2fa3c045 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -739,7 +739,9 @@ impl GalleyCache { let mut text_left = job.text.as_str(); let mut first_row_min_height = job.first_row_min_height; loop { - let end = text_left.find('\n').map_or(job.text.len(), |i| i + current); + let end = text_left + .find('\n') + .map_or(job.text.len(), |i| i + current + 1); let start = current; let mut line_job = LayoutJob { @@ -749,8 +751,7 @@ impl GalleyCache { ..job.wrap }, sections: Vec::new(), - // Prevent an infinite recursion - break_on_newline: false, + break_on_newline: true, halign: job.halign, justify: job.justify, first_row_min_height, @@ -782,28 +783,14 @@ impl GalleyCache { current = section_end; } - // If the current line is empty, add an extra offset to make sure it's not omitted - // because the resulting galley will have a height of zero. - let extra_y_offset = if start == end && end != job.text.len() { - while job.sections[current_section].byte_range.end == end { - current_section += 1; - } - let format = &job.sections[current_section].format; - format - .line_height - .unwrap_or(fonts.row_height(&format.font_id)) - } else { - 0.0 - }; - - let galley = self.layout(fonts, line_job); + let galley = self.layout_component_line(fonts, line_job); // This will prevent us from invalidating cache entries unnecessarily if left_max_rows != usize::MAX { left_max_rows -= galley.rows.len(); } - galleys.push((galley, extra_y_offset)); + galleys.push(galley); - current = end + 1; + current = end; if current >= job.text.len() { break; } else { @@ -822,29 +809,30 @@ impl GalleyCache { pixels_per_point: fonts.pixels_per_point, }; - for (galley, extra_y_offset) in galleys { + for (i, galley) in galleys.iter().enumerate() { let current_offset = emath::vec2(0.0, merged_galley.rect.height()); - merged_galley - .rows - .extend(galley.rows.iter().map(|placed_row| { - let new_pos = placed_row.pos + current_offset; - merged_galley.mesh_bounds = merged_galley - .mesh_bounds - .union(placed_row.visuals.mesh_bounds.translate(new_pos.to_vec2())); - - super::PlacedRow { - row: placed_row.row.clone(), - pos: round_to_pixel(new_pos), - ends_with_newline: placed_row.ends_with_newline, - } - })); - if let Some(last) = merged_galley.rows.last_mut() { - last.ends_with_newline = true; + + let mut rows = galley.rows.iter(); + if i != galleys.len() - 1 && !galley.elided { + let popped = rows.next_back(); + debug_assert_eq!(popped.unwrap().row.glyphs.len(), 0); } - merged_galley.rect = merged_galley - .rect - .union(galley.rect.translate(current_offset)); - merged_galley.rect.max.y += extra_y_offset; + + merged_galley.rows.extend(rows.map(|placed_row| { + let new_pos = round_to_pixel(placed_row.pos + current_offset); + merged_galley.mesh_bounds = merged_galley + .mesh_bounds + .union(placed_row.visuals.mesh_bounds.translate(new_pos.to_vec2())); + merged_galley.rect = merged_galley + .rect + .union(emath::Rect::from_min_size(new_pos, placed_row.size)); + + super::PlacedRow { + row: placed_row.row.clone(), + pos: new_pos, + } + })); + merged_galley.num_vertices += galley.num_vertices; merged_galley.num_indices += galley.num_indices; if galley.elided { @@ -853,13 +841,30 @@ impl GalleyCache { } } - if let Some(last) = merged_galley.rows.last_mut() { - last.ends_with_newline = false; - } - merged_galley } + fn layout_component_line(&mut self, fonts: &mut FontsImpl, job: LayoutJob) -> Arc { + let hash = crate::util::hash(&job); + + match self.cache.entry(hash) { + std::collections::hash_map::Entry::Occupied(entry) => { + let cached = entry.into_mut(); + cached.last_used = self.generation; + cached.galley.clone() + } + std::collections::hash_map::Entry::Vacant(entry) => { + let galley = super::layout(fonts, job.into()); + let galley = Arc::new(galley); + entry.insert(CachedGalley { + last_used: self.generation, + galley: galley.clone(), + }); + galley + } + } + } + fn layout(&mut self, fonts: &mut FontsImpl, mut job: LayoutJob) -> Arc { if job.wrap.max_width.is_finite() { // Protect against rounding errors in egui layout code. diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 9203dfd0559..798fa1d3bac 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -220,9 +220,9 @@ fn rows_from_paragraphs( glyphs: vec![], visuals: Default::default(), size: vec2(0.0, paragraph.empty_paragraph_height), + ends_with_newline: !is_last_paragraph, }), pos: pos2(paragraph.cursor_x, 0.0), - ends_with_newline: !is_last_paragraph, }); } else { let paragraph_max_x = paragraph.glyphs.last().unwrap().max_x(); @@ -236,13 +236,15 @@ fn rows_from_paragraphs( glyphs: paragraph.glyphs, visuals: Default::default(), size: rect.size(), + ends_with_newline: !is_last_paragraph, }), pos: rect.min, - ends_with_newline: !is_last_paragraph, }); } else { line_break(¶graph, job, &mut rows, elided); - rows.last_mut().unwrap().ends_with_newline = !is_last_paragraph; + let placed_row = rows.last_mut().unwrap(); + let row = Arc::get_mut(&mut placed_row.row).unwrap(); + row.ends_with_newline = !is_last_paragraph; } } } @@ -288,9 +290,9 @@ fn line_break( glyphs: vec![], visuals: Default::default(), size: rect.size(), + ends_with_newline: false, }), pos: rect.min, - ends_with_newline: false, }); row_start_x += first_row_indentation; first_row_indentation = 0.0; @@ -316,9 +318,9 @@ fn line_break( glyphs, visuals: Default::default(), size: rect.size(), + ends_with_newline: false, }), pos: rect.min, - ends_with_newline: false, }); // Start a new row: @@ -359,9 +361,9 @@ fn line_break( glyphs, visuals: Default::default(), size: rect.size(), + ends_with_newline: false, }), pos: rect.min, - ends_with_newline: false, }); } } diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 0229665cbec..b3d26764e18 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -546,13 +546,6 @@ pub struct PlacedRow { /// The position of this [`Row`] relative to the galley. pub pos: Pos2, - - /// If true, this [`PlacedRow`] came from a paragraph ending with a `\n`. - /// The `\n` itself is omitted from [`Row::glyphs`]. - /// A `\n` in the input text always creates a new [`PlacedRow`] below it, - /// so that text that ends with `\n` has an empty [`PlacedRow`] last. - /// This also implies that the last [`PlacedRow`] in a [`Galley`] always has `ends_with_newline == false`. - pub ends_with_newline: bool, } impl PlacedRow { @@ -586,6 +579,13 @@ pub struct Row { /// The mesh, ready to be rendered. pub visuals: RowVisuals, + + /// If true, this [`Row`] came from a paragraph ending with a `\n`. + /// The `\n` itself is omitted from [`glyphs`]. + /// A `\n` in the input text always creates a new [`Row`] below it, + /// so that text that ends with `\n` has an empty [`Row`] last. + /// This also implies that the last [`Row`] in a [`Galley`] always has `ends_with_newline == false`. + pub ends_with_newline: bool, } /// The tessellated output of a row. From 1be24ba470429960e751edd62dcf3e59ec48255f Mon Sep 17 00:00:00 2001 From: Fishhh Date: Sat, 30 Nov 2024 17:49:59 +0100 Subject: [PATCH 12/68] Respect `LayoutJob::round_output_size_to_nearest_ui_point` --- crates/epaint/src/text/fonts.rs | 7 ++++++ crates/epaint/src/text/mod.rs | 2 +- crates/epaint/src/text/text_layout.rs | 32 +++++++++++++++------------ 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index dbd2fa3c045..df6547428e5 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -841,6 +841,13 @@ impl GalleyCache { } } + if merged_galley.job.round_output_size_to_nearest_ui_point { + super::round_output_size_to_nearest_ui_point( + &mut merged_galley.rect, + &merged_galley.job, + ); + } + merged_galley } diff --git a/crates/epaint/src/text/mod.rs b/crates/epaint/src/text/mod.rs index 3cb0e98cbc5..cf5c8ebfc99 100644 --- a/crates/epaint/src/text/mod.rs +++ b/crates/epaint/src/text/mod.rs @@ -14,7 +14,7 @@ pub use { FontData, FontDefinitions, FontFamily, FontId, FontInsert, FontPriority, FontTweak, Fonts, FontsImpl, InsertFontFamily, }, - text_layout::layout, + text_layout::*, text_layout_types::*, }; diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 798fa1d3bac..5f52235c566 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -679,20 +679,7 @@ fn galley_from_rows( 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 by reporting that our width is wider than the wrap width. - } else { - // Make sure we don't report being wider than the wrap width the user picked: - rect.max.x = rect.max.x.at_most(rect.min.x + job.wrap.max_width).floor(); - } + round_output_size_to_nearest_ui_point(&mut rect, &job); } Galley { @@ -707,6 +694,23 @@ fn galley_from_rows( } } +pub(crate) fn round_output_size_to_nearest_ui_point(rect: &mut Rect, job: &LayoutJob) { + 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 by reporting that our width is wider than the wrap width. + } else { + // Make sure we don't report being wider than the wrap width the user picked: + rect.max.x = rect.max.x.at_most(rect.min.x + job.wrap.max_width).floor(); + } +} + #[derive(Default)] struct FormatSummary { any_background: bool, From fd8413c62a2e038d9b1bdc4235f402168dcca863 Mon Sep 17 00:00:00 2001 From: Fishhh Date: Sat, 30 Nov 2024 17:54:04 +0100 Subject: [PATCH 13/68] Simplify `layout_multiline` `loop` loop into a `while` loop --- crates/epaint/src/text/fonts.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index df6547428e5..871cecabdb1 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -736,10 +736,9 @@ impl GalleyCache { let mut current = 0; let mut left_max_rows = job.wrap.max_rows; let mut galleys = Vec::new(); - let mut text_left = job.text.as_str(); let mut first_row_min_height = job.first_row_min_height; - loop { - let end = text_left + while current != job.text.len() { + let end = job.text[current..] .find('\n') .map_or(job.text.len(), |i| i + current + 1); let start = current; @@ -791,11 +790,6 @@ impl GalleyCache { galleys.push(galley); current = end; - if current >= job.text.len() { - break; - } else { - text_left = &job.text[current..]; - } } let mut merged_galley = Galley { From bbe566256ef5271b33a80801720910636eccd2f8 Mon Sep 17 00:00:00 2001 From: Fishhh Date: Sat, 30 Nov 2024 18:17:38 +0100 Subject: [PATCH 14/68] Fix `Row::ends_with_newline` docs, explain skipping of last galley row --- crates/epaint/src/text/fonts.rs | 3 +++ crates/epaint/src/text/text_layout_types.rs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 871cecabdb1..2e0343d6d4d 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -807,6 +807,9 @@ impl GalleyCache { let current_offset = emath::vec2(0.0, merged_galley.rect.height()); let mut rows = galley.rows.iter(); + // As documented in `Row::ends_with_newline`, a '\n' will always create a + // new `Row` immediately below the current one. Here it doesn't make sense + // for us to append this new row so we just ignore it. if i != galleys.len() - 1 && !galley.elided { let popped = rows.next_back(); debug_assert_eq!(popped.unwrap().row.glyphs.len(), 0); diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index b3d26764e18..3ebf07b8f5e 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -581,7 +581,7 @@ pub struct Row { pub visuals: RowVisuals, /// If true, this [`Row`] came from a paragraph ending with a `\n`. - /// The `\n` itself is omitted from [`glyphs`]. + /// The `\n` itself is omitted from [`Self::glyphs`]. /// A `\n` in the input text always creates a new [`Row`] below it, /// so that text that ends with `\n` has an empty [`Row`] last. /// This also implies that the last [`Row`] in a [`Galley`] always has `ends_with_newline == false`. From 110a9c39e3c0a5b9bf7f7838c6288a0c05a9c0ae Mon Sep 17 00:00:00 2001 From: Fishhh Date: Sat, 30 Nov 2024 18:25:02 +0100 Subject: [PATCH 15/68] Move some `PlacedRow` methods back to `Row` --- crates/epaint/src/text/text_layout_types.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 3ebf07b8f5e..f90210ca136 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -693,6 +693,12 @@ impl Row { self.glyphs.len() } + /// Includes the implicit `\n` after the [`Row`], if any. + #[inline] + pub fn char_count_including_newline(&self) -> usize { + self.glyphs.len() + (self.ends_with_newline as usize) + } + /// Closest char at the desired x coordinate in row-relative coordinates. /// Returns something in the range `[0, char_count_excluding_newline()]`. pub fn char_at(&self, desired_x: f32) -> usize { @@ -711,15 +717,14 @@ impl Row { self.size.x } } -} -impl PlacedRow { - /// Includes the implicit `\n` after the [`Row`], if any. #[inline] - pub fn char_count_including_newline(&self) -> usize { - self.glyphs.len() + (self.ends_with_newline as usize) + pub fn height(&self) -> f32 { + self.size.y } +} +impl PlacedRow { #[inline] pub fn min_y(&self) -> f32 { self.rect().top() @@ -729,11 +734,6 @@ impl PlacedRow { pub fn max_y(&self) -> f32 { self.rect().bottom() } - - #[inline] - pub fn height(&self) -> f32 { - self.row.size.y - } } impl Galley { From 139f28640d8349cea751644caeaf9455492b316d Mon Sep 17 00:00:00 2001 From: Fishhh Date: Sat, 30 Nov 2024 19:23:32 +0100 Subject: [PATCH 16/68] Replace a hack with a proper implementation --- crates/epaint/src/text/fonts.rs | 5 ++-- crates/epaint/src/text/text_layout.rs | 39 +++++++-------------------- 2 files changed, 13 insertions(+), 31 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 2e0343d6d4d..36f2aa5d195 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -730,7 +730,7 @@ impl GalleyCache { fn layout_multiline(&mut self, fonts: &mut FontsImpl, job: LayoutJob) -> Galley { let pixels_per_point = fonts.pixels_per_point; let round_to_pixel = - move |point: emath::Pos2| (point * pixels_per_point).round() / pixels_per_point; + move |point: f32| (point * pixels_per_point).round() / pixels_per_point; let mut current_section = 0; let mut current = 0; @@ -816,7 +816,8 @@ impl GalleyCache { } merged_galley.rows.extend(rows.map(|placed_row| { - let new_pos = round_to_pixel(placed_row.pos + current_offset); + let mut new_pos = placed_row.pos + current_offset; + new_pos.y = round_to_pixel(new_pos.y); merged_galley.mesh_bounds = merged_galley .mesh_bounds .union(placed_row.visuals.mesh_bounds.translate(new_pos.to_vec2())); diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 5f52235c566..c5105966647 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -1,4 +1,3 @@ -use std::ops::RangeInclusive; use std::sync::Arc; use emath::{pos2, vec2, Align, NumExt, Pos2, Rect, Vec2}; @@ -115,7 +114,6 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc) -> Galley { halign_and_justify_row( point_scale, Arc::get_mut(&mut placed_row.row).unwrap(), - &mut placed_row.pos, job.halign, job.wrap.max_width, justify_row, @@ -190,11 +188,6 @@ fn layout_section( } } -/// We ignore y at this stage -fn rect_from_x_range(x_range: RangeInclusive) -> Rect { - Rect::from_x_y_ranges(x_range, 0.0..=0.0) -} - // Ignores the Y coordinate. fn rows_from_paragraphs( paragraphs: Vec, @@ -222,23 +215,21 @@ fn rows_from_paragraphs( size: vec2(0.0, paragraph.empty_paragraph_height), ends_with_newline: !is_last_paragraph, }), - pos: pos2(paragraph.cursor_x, 0.0), + pos: pos2(0.0, 0.0), }); } else { let paragraph_max_x = paragraph.glyphs.last().unwrap().max_x(); if paragraph_max_x <= job.effective_wrap_width() { // Early-out optimization: the whole paragraph fits on one row. - let paragraph_min_x = paragraph.glyphs[0].pos.x; - let rect = rect_from_x_range(paragraph_min_x..=paragraph_max_x); rows.push(PlacedRow { row: Arc::new(Row { section_index_at_start: paragraph.section_index_at_start, glyphs: paragraph.glyphs, visuals: Default::default(), - size: rect.size(), + size: vec2(paragraph_max_x, 0.0), ends_with_newline: !is_last_paragraph, }), - pos: rect.min, + pos: pos2(0.0, f32::NAN), }); } else { line_break(¶graph, job, &mut rows, elided); @@ -283,16 +274,15 @@ fn line_break( { // Allow the first row to be completely empty, because we know there will be more space on the next row: // TODO(emilk): this records the height of this first row as zero, though that is probably fine since first_row_indentation usually comes with a first_row_min_height. - let rect = rect_from_x_range(first_row_indentation..=first_row_indentation); out_rows.push(PlacedRow { row: Arc::new(Row { section_index_at_start: paragraph.section_index_at_start, glyphs: vec![], visuals: Default::default(), - size: rect.size(), + size: vec2(0.0, 0.0), ends_with_newline: false, }), - pos: rect.min, + pos: pos2(0.0, f32::NAN), }); row_start_x += first_row_indentation; first_row_indentation = 0.0; @@ -308,19 +298,17 @@ fn line_break( .collect(); let section_index_at_start = glyphs[0].section_index; - let paragraph_min_x = glyphs[0].pos.x; let paragraph_max_x = glyphs.last().unwrap().max_x(); - let rect = rect_from_x_range(paragraph_min_x..=paragraph_max_x); out_rows.push(PlacedRow { row: Arc::new(Row { section_index_at_start, glyphs, visuals: Default::default(), - size: rect.size(), + size: vec2(paragraph_max_x, 0.0), ends_with_newline: false, }), - pos: rect.min, + pos: pos2(0.0, f32::NAN), }); // Start a new row: @@ -354,16 +342,15 @@ fn line_break( let paragraph_min_x = glyphs[0].pos.x; let paragraph_max_x = glyphs.last().unwrap().max_x(); - let rect = rect_from_x_range(paragraph_min_x..=paragraph_max_x); out_rows.push(PlacedRow { row: Arc::new(Row { section_index_at_start, glyphs, visuals: Default::default(), - size: rect.size(), + size: vec2(paragraph_max_x - paragraph_min_x, 0.0), ends_with_newline: false, }), - pos: rect.min, + pos: pos2(paragraph_min_x, 0.0), }); } } @@ -526,7 +513,6 @@ fn replace_last_glyph_with_overflow_character( fn halign_and_justify_row( point_scale: PointScale, row: &mut Row, - pos: &mut Pos2, halign: Align, wrap_width: f32, justify: bool, @@ -609,8 +595,7 @@ fn halign_and_justify_row( } } - // Note we ignore the leading/trailing whitespace here! - pos.x = target_min_x; + // Note we **don't** ignore the leading/trailing whitespace here! row.size.x = target_max_x - target_min_x; } @@ -647,10 +632,6 @@ fn galley_from_rows( // When mixing different `FontImpl` (e.g. latin and emojis), // we always center the difference: + 0.5 * (glyph.font_height - glyph.font_impl_height); - - // FIXME(afishhh): HACK! change the proper code above instead!! - // this should probably not be merged like this! - glyph.pos.x -= placed_row.pos.x; } placed_row.pos.y = cursor_y; From e15b34b9849239f76b1028c1488b9c5ef4e0036d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20G=C5=82uchowski?= Date: Mon, 2 Dec 2024 00:52:12 +0100 Subject: [PATCH 17/68] Slightly simplify `paint_text_selection` code --- crates/egui/src/text_selection/visuals.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/crates/egui/src/text_selection/visuals.rs b/crates/egui/src/text_selection/visuals.rs index 3eecfaf4bf3..b2c2e8d9ea7 100644 --- a/crates/egui/src/text_selection/visuals.rs +++ b/crates/egui/src/text_selection/visuals.rs @@ -31,27 +31,25 @@ pub fn paint_text_selection( let max = max.rcursor; for ri in min.row..=max.row { - let placed_row = &mut galley.rows[ri]; + let row = Arc::make_mut(&mut galley.rows[ri].row); let left = if ri == min.row { - placed_row.x_offset(min.column) + row.x_offset(min.column) } else { 0.0 }; let right = if ri == max.row { - placed_row.x_offset(max.column) + row.x_offset(max.column) } else { - let newline_size = if placed_row.ends_with_newline { - placed_row.height() / 2.0 // visualize that we select the newline + let newline_size = if row.ends_with_newline { + row.height() / 2.0 // visualize that we select the newline } else { 0.0 }; - placed_row.size.x + newline_size + row.size.x + newline_size }; - let rect = Rect::from_min_max(pos2(left, 0.0), pos2(right, placed_row.size.y)); - - let row = Arc::make_mut(&mut placed_row.row); + let rect = Rect::from_min_max(pos2(left, 0.0), pos2(right, row.size.y)); let mesh = &mut row.visuals.mesh; // Time to insert the selection rectangle into the row mesh. From c6592ec8980a6872ebef31c03e338d17b31918e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20G=C5=82uchowski?= Date: Wed, 4 Dec 2024 19:30:13 +0100 Subject: [PATCH 18/68] Fix nits --- crates/epaint/src/tessellator.rs | 6 +- crates/epaint/src/text/fonts.rs | 118 +++++++++++++------------- crates/epaint/src/text/text_layout.rs | 2 +- 3 files changed, 63 insertions(+), 63 deletions(-) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index a6290e9c636..05da7cc04cc 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1783,13 +1783,13 @@ impl Tessellator { continue; } - let final_pos = galley_pos + row.pos.to_vec2(); + let final_row_pos = galley_pos + row.pos.to_vec2(); let mut row_rect = row.visuals.mesh_bounds; if *angle != 0.0 { row_rect = row_rect.rotate_bb(rotator); } - row_rect = row_rect.translate(final_pos.to_vec2()); + row_rect = row_rect.translate(final_row_pos.to_vec2()); if self.options.coarse_tessellation_culling && !self.clip_rect.intersects(row_rect) { // culling individual lines of text is important, since a single `Shape::Text` @@ -1838,7 +1838,7 @@ impl Tessellator { }; Vertex { - pos: final_pos + offset, + pos: final_row_pos + offset, uv: (uv.to_vec2() * uv_normalizer).to_pos2(), color, } diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 36f2aa5d195..75dcf1958cc 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -727,6 +727,65 @@ struct GalleyCache { } impl GalleyCache { + fn layout(&mut self, fonts: &mut FontsImpl, mut job: LayoutJob) -> Arc { + if job.wrap.max_width.is_finite() { + // Protect against rounding errors in egui layout code. + + // Say the user asks to wrap at width 200.0. + // The text layout wraps, and reports that the final width was 196.0 points. + // This then trickles up the `Ui` chain and gets stored as the width for a tooltip (say). + // On the next frame, this is then set as the max width for the tooltip, + // and we end up calling the text layout code again, this time with a wrap width of 196.0. + // Except, somewhere in the `Ui` chain with added margins etc, a rounding error was introduced, + // so that we actually set a wrap-width of 195.9997 instead. + // Now the text that fit perfrectly at 196.0 needs to wrap one word earlier, + // and so the text re-wraps and reports a new width of 185.0 points. + // And then the cycle continues. + + // So we limit max_width to integers. + + // Related issues: + // * https://github.com/emilk/egui/issues/4927 + // * https://github.com/emilk/egui/issues/4928 + // * https://github.com/emilk/egui/issues/5084 + // * https://github.com/emilk/egui/issues/5163 + + job.wrap.max_width = job.wrap.max_width.round(); + } + + let hash = crate::util::hash(&job); // TODO(emilk): even faster hasher? + + match self.cache.entry(hash) { + std::collections::hash_map::Entry::Occupied(entry) => { + let cached = entry.into_mut(); + cached.last_used = self.generation; + cached.galley.clone() + } + std::collections::hash_map::Entry::Vacant(entry) => { + if job.break_on_newline { + let galley = self.layout_multiline(fonts, job); + let galley = Arc::new(galley); + self.cache.insert( + hash, + CachedGalley { + last_used: self.generation, + galley: galley.clone(), + }, + ); + galley + } else { + let galley = super::layout(fonts, job.into()); + let galley = Arc::new(galley); + entry.insert(CachedGalley { + last_used: self.generation, + galley: galley.clone(), + }); + galley + } + } + } + } + fn layout_multiline(&mut self, fonts: &mut FontsImpl, job: LayoutJob) -> Galley { let pixels_per_point = fonts.pixels_per_point; let round_to_pixel = @@ -870,65 +929,6 @@ impl GalleyCache { } } - fn layout(&mut self, fonts: &mut FontsImpl, mut job: LayoutJob) -> Arc { - if job.wrap.max_width.is_finite() { - // Protect against rounding errors in egui layout code. - - // Say the user asks to wrap at width 200.0. - // The text layout wraps, and reports that the final width was 196.0 points. - // This then trickles up the `Ui` chain and gets stored as the width for a tooltip (say). - // On the next frame, this is then set as the max width for the tooltip, - // and we end up calling the text layout code again, this time with a wrap width of 196.0. - // Except, somewhere in the `Ui` chain with added margins etc, a rounding error was introduced, - // so that we actually set a wrap-width of 195.9997 instead. - // Now the text that fit perfrectly at 196.0 needs to wrap one word earlier, - // and so the text re-wraps and reports a new width of 185.0 points. - // And then the cycle continues. - - // So we limit max_width to integers. - - // Related issues: - // * https://github.com/emilk/egui/issues/4927 - // * https://github.com/emilk/egui/issues/4928 - // * https://github.com/emilk/egui/issues/5084 - // * https://github.com/emilk/egui/issues/5163 - - job.wrap.max_width = job.wrap.max_width.round(); - } - - let hash = crate::util::hash(&job); // TODO(emilk): even faster hasher? - - match self.cache.entry(hash) { - std::collections::hash_map::Entry::Occupied(entry) => { - let cached = entry.into_mut(); - cached.last_used = self.generation; - cached.galley.clone() - } - std::collections::hash_map::Entry::Vacant(entry) => { - if job.break_on_newline { - let galley = self.layout_multiline(fonts, job); - let galley = Arc::new(galley); - self.cache.insert( - hash, - CachedGalley { - last_used: self.generation, - galley: galley.clone(), - }, - ); - galley - } else { - let galley = super::layout(fonts, job.into()); - let galley = Arc::new(galley); - entry.insert(CachedGalley { - last_used: self.generation, - galley: galley.clone(), - }); - galley - } - } - } - } - pub fn num_galleys_in_cache(&self) -> usize { self.cache.len() } diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index c5105966647..4217bffb064 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -595,7 +595,7 @@ fn halign_and_justify_row( } } - // Note we **don't** ignore the leading/trailing whitespace here! + // Note we ignore the leading/trailing whitespace here! row.size.x = target_max_x - target_min_x; } From 25da82279f02f4a344c806879b0cea765025c3d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20G=C5=82uchowski?= Date: Thu, 5 Dec 2024 18:27:38 +0100 Subject: [PATCH 19/68] Fix text horizontal alignment --- crates/epaint/src/text/text_layout.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 4217bffb064..2d4c30a92b2 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -113,7 +113,7 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc) -> Galley { let justify_row = justify && !placed_row.ends_with_newline && !is_last_row; halign_and_justify_row( point_scale, - Arc::get_mut(&mut placed_row.row).unwrap(), + placed_row, job.halign, job.wrap.max_width, justify_row, @@ -512,11 +512,13 @@ fn replace_last_glyph_with_overflow_character( /// Ignores the Y coordinate. fn halign_and_justify_row( point_scale: PointScale, - row: &mut Row, + placed_row: &mut PlacedRow, halign: Align, wrap_width: f32, justify: bool, ) { + let row = Arc::get_mut(&mut placed_row.row).unwrap(); + if row.glyphs.is_empty() { return; } @@ -584,7 +586,8 @@ fn halign_and_justify_row( / (num_spaces_in_range as f32); } - let mut translate_x = target_min_x - original_min_x - extra_x_per_glyph * glyph_range.0 as f32; + placed_row.pos.x = point_scale.round_to_pixel(target_min_x); + let mut translate_x = -original_min_x - extra_x_per_glyph * glyph_range.0 as f32; for glyph in &mut row.glyphs { glyph.pos.x += translate_x; From 40f237d43c8fc913a2e30f09fb932edde6155adc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20G=C5=82uchowski?= Date: Thu, 5 Dec 2024 18:43:22 +0100 Subject: [PATCH 20/68] Add comment and check for newline before multiline layout --- crates/epaint/src/text/fonts.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 75dcf1958cc..1db2050caeb 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -762,7 +762,11 @@ impl GalleyCache { cached.galley.clone() } std::collections::hash_map::Entry::Vacant(entry) => { - if job.break_on_newline { + // If the text contains newlines that will always break into a new row then + // we can easily lay out all the lines individually and then merge the `Galley`s. + // This allows individual lines to be cached separately which means small + // modifications to the source text will only cause impacted lines to be laid out again. + if job.break_on_newline && job.text.contains('\n') { let galley = self.layout_multiline(fonts, job); let galley = Arc::new(galley); self.cache.insert( From 17a5f1f75e320fabb71e02581b12d2fd77c38c56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20G=C5=82uchowski?= Date: Thu, 5 Dec 2024 19:15:23 +0100 Subject: [PATCH 21/68] Fix incorrect behavior with `LayoutJob::max_rows` --- crates/epaint/src/text/fonts.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 1db2050caeb..a681c3f0575 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -849,8 +849,17 @@ impl GalleyCache { // This will prevent us from invalidating cache entries unnecessarily if left_max_rows != usize::MAX { left_max_rows -= galley.rows.len(); + // Ignore extra trailing row, see merging counterpart below for more details. + if end < job.text.len() && !galley.elided { + left_max_rows += 1; + } } + + let elided = galley.elided; galleys.push(galley); + if elided { + break; + } current = end; } @@ -896,10 +905,9 @@ impl GalleyCache { merged_galley.num_vertices += galley.num_vertices; merged_galley.num_indices += galley.num_indices; - if galley.elided { - merged_galley.elided = true; - break; - } + // Note that if `galley.elided` is true this will be the last `Galley` in + // the vector and the loop will end. + merged_galley.elided |= galley.elided; } if merged_galley.job.round_output_size_to_nearest_ui_point { From 3e1ed1829991cca5f6d32a7b6b175fe0a0940c24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20G=C5=82uchowski?= Date: Wed, 11 Dec 2024 18:42:40 +0100 Subject: [PATCH 22/68] Add benchmark --- Cargo.lock | 1 + crates/egui_demo_lib/Cargo.toml | 1 + crates/egui_demo_lib/benches/benchmark.rs | 31 +++++++++++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index a2e858d4b41..aa57f075462 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1313,6 +1313,7 @@ dependencies = [ "egui_demo_lib", "egui_extras", "egui_kittest", + "rand", "serde", "unicode_names2", "wgpu", diff --git a/crates/egui_demo_lib/Cargo.toml b/crates/egui_demo_lib/Cargo.toml index f8f1f47f686..fc59280f232 100644 --- a/crates/egui_demo_lib/Cargo.toml +++ b/crates/egui_demo_lib/Cargo.toml @@ -58,6 +58,7 @@ serde = { workspace = true, optional = true } # when running tests we always want to use the `chrono` feature egui_demo_lib = { workspace = true, features = ["chrono"] } +rand = "0.8" criterion.workspace = true egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] } wgpu = { workspace = true, features = ["metal"] } diff --git a/crates/egui_demo_lib/benches/benchmark.rs b/crates/egui_demo_lib/benches/benchmark.rs index d3820603d5c..8be00f72d2b 100644 --- a/crates/egui_demo_lib/benches/benchmark.rs +++ b/crates/egui_demo_lib/benches/benchmark.rs @@ -1,7 +1,10 @@ +use std::fmt::Write as _; + use criterion::{criterion_group, criterion_main, Criterion}; use egui::epaint::TextShape; use egui_demo_lib::LOREM_IPSUM_LONG; +use rand::Rng as _; pub fn criterion_benchmark(c: &mut Criterion) { use egui::RawInput; @@ -122,6 +125,34 @@ pub fn criterion_benchmark(c: &mut Criterion) { }); }); + c.bench_function("text_layout_cached_with_modify", |b| { + const MAX_REMOVED_BYTES: usize = 5000; + + let mut string = String::new(); + // 2000 lines * 200 bytes * ~3 characters = 1.2MB + string.reserve(2000 * 200 * 3 + 2000); + for _ in 0..2000 { + for i in 0..200u8 { + write!(string, "{i:02X} ").unwrap(); + } + string.push('\n'); + } + + let mut rng = rand::thread_rng(); + b.iter(|| { + fonts.begin_pass(pixels_per_point, max_texture_side); + let mut temp_string = String::with_capacity(string.len()); + let modified_start = rng.gen_range(0..string.len()); + let max_end = (modified_start + MAX_REMOVED_BYTES).min(string.len()); + let modified_end = rng.gen_range(modified_start..max_end); + + temp_string.push_str(&string[..modified_start]); + temp_string.push_str(&string[modified_end..]); + + fonts.layout(temp_string, font_id.clone(), text_color, wrap_width); + }); + }); + let galley = fonts.layout(LOREM_IPSUM_LONG.to_owned(), font_id, text_color, wrap_width); let font_image_size = fonts.font_image_size(); let prepared_discs = fonts.texture_atlas().lock().prepared_discs(); From 2b5327164a80a373044e3548e697dc07aa67f53f Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 2 Jan 2025 02:09:05 +0100 Subject: [PATCH 23/68] Better benchmark --- crates/egui_demo_lib/benches/benchmark.rs | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/crates/egui_demo_lib/benches/benchmark.rs b/crates/egui_demo_lib/benches/benchmark.rs index 8be00f72d2b..6c25646d257 100644 --- a/crates/egui_demo_lib/benches/benchmark.rs +++ b/crates/egui_demo_lib/benches/benchmark.rs @@ -125,14 +125,12 @@ pub fn criterion_benchmark(c: &mut Criterion) { }); }); - c.bench_function("text_layout_cached_with_modify", |b| { - const MAX_REMOVED_BYTES: usize = 5000; + c.bench_function("text_layout_cached_many_lines_modified", |b| { + const NUM_LINES: usize = 2_000; let mut string = String::new(); - // 2000 lines * 200 bytes * ~3 characters = 1.2MB - string.reserve(2000 * 200 * 3 + 2000); - for _ in 0..2000 { - for i in 0..200u8 { + for _ in 0..NUM_LINES { + for i in 0..30_u8 { write!(string, "{i:02X} ").unwrap(); } string.push('\n'); @@ -141,15 +139,13 @@ pub fn criterion_benchmark(c: &mut Criterion) { let mut rng = rand::thread_rng(); b.iter(|| { fonts.begin_pass(pixels_per_point, max_texture_side); - let mut temp_string = String::with_capacity(string.len()); - let modified_start = rng.gen_range(0..string.len()); - let max_end = (modified_start + MAX_REMOVED_BYTES).min(string.len()); - let modified_end = rng.gen_range(modified_start..max_end); - temp_string.push_str(&string[..modified_start]); - temp_string.push_str(&string[modified_end..]); + // Delete a random character, simulating a user making an edit in a long file: + let mut new_string = string.clone(); + let idx = rng.gen_range(0..string.len()); + new_string.remove(idx); - fonts.layout(temp_string, font_id.clone(), text_color, wrap_width); + fonts.layout(new_string, font_id.clone(), text_color, wrap_width); }); }); From 7fb85d19abede910fc6c7c5bd1a5d2d296d976f7 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 2 Jan 2025 02:16:35 +0100 Subject: [PATCH 24/68] Fix typos --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4642da637f5..b6733405cfe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1230,7 +1230,7 @@ egui_extras::install_image_loaders(egui_ctx); * [Tweaked the default visuals style](https://github.com/emilk/egui/pull/450). * Plot: Renamed `Curve` to `Line`. * `TopPanel::top` is now `TopBottomPanel::top`. -* `SidePanel::left` no longet takes the default width by argument, but by a builder call. +* `SidePanel::left` no longer takes the default width by argument, but by a builder call. * `SidePanel::left` is resizable by default. ### 🐛 Fixed From 9fa294f21ff75df5d77f956b3133820d9bd53625 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 2 Jan 2025 02:20:29 +0100 Subject: [PATCH 25/68] Split out long function --- crates/epaint/src/text/fonts.rs | 108 +++++++++++++++++--------------- 1 file changed, 57 insertions(+), 51 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 49e2818cf9c..e69c402ffc4 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -8,7 +8,7 @@ use crate::{ }, TextureAtlas, }; -use emath::{NumExt as _, OrderedFloat}; +use emath::{GuiRounding, NumExt as _, OrderedFloat}; #[cfg(feature = "default_fonts")] use epaint_default_fonts::{EMOJI_ICON, HACK_REGULAR, NOTO_EMOJI_REGULAR, UBUNTU_LIGHT}; @@ -868,57 +868,9 @@ impl GalleyCache { current = end; } - let mut merged_galley = Galley { - job: Arc::new(job), - rows: Vec::new(), - elided: false, - rect: emath::Rect::ZERO, - mesh_bounds: emath::Rect::ZERO, - num_vertices: 0, - num_indices: 0, - pixels_per_point: fonts.pixels_per_point, - }; - - for (i, galley) in galleys.iter().enumerate() { - let current_offset = emath::vec2(0.0, merged_galley.rect.height()); - - let mut rows = galley.rows.iter(); - // As documented in `Row::ends_with_newline`, a '\n' will always create a - // new `Row` immediately below the current one. Here it doesn't make sense - // for us to append this new row so we just ignore it. - if i != galleys.len() - 1 && !galley.elided { - let popped = rows.next_back(); - debug_assert_eq!(popped.unwrap().row.glyphs.len(), 0); - } - - merged_galley.rows.extend(rows.map(|placed_row| { - let mut new_pos = placed_row.pos + current_offset; - new_pos.y = round_to_pixel(new_pos.y); - merged_galley.mesh_bounds = merged_galley - .mesh_bounds - .union(placed_row.visuals.mesh_bounds.translate(new_pos.to_vec2())); - merged_galley.rect = merged_galley - .rect - .union(emath::Rect::from_min_size(new_pos, placed_row.size)); - - super::PlacedRow { - row: placed_row.row.clone(), - pos: new_pos, - } - })); - - merged_galley.num_vertices += galley.num_vertices; - merged_galley.num_indices += galley.num_indices; - // Note that if `galley.elided` is true this will be the last `Galley` in - // the vector and the loop will end. - merged_galley.elided |= galley.elided; - } - - if merged_galley.job.round_output_to_gui { - super::round_output_to_gui(&mut merged_galley.rect, &merged_galley.job); - } + let pixels_per_point = fonts.pixels_per_point; - merged_galley + concat_galleys(job, &galleys, pixels_per_point) } fn layout_component_line(&mut self, fonts: &mut FontsImpl, job: LayoutJob) -> Arc { @@ -956,6 +908,60 @@ impl GalleyCache { } } +fn concat_galleys(job: LayoutJob, galleys: &[Arc], pixels_per_point: f32) -> Galley { + let mut merged_galley = Galley { + job: Arc::new(job), + rows: Vec::new(), + elided: false, + rect: emath::Rect::ZERO, + mesh_bounds: emath::Rect::ZERO, + num_vertices: 0, + num_indices: 0, + pixels_per_point, + }; + + for (i, galley) in galleys.iter().enumerate() { + let current_offset = emath::vec2(0.0, merged_galley.rect.height()); + + let mut rows = galley.rows.iter(); + // As documented in `Row::ends_with_newline`, a '\n' will always create a + // new `Row` immediately below the current one. Here it doesn't make sense + // for us to append this new row so we just ignore it. + let is_last_row = i + 1 == galleys.len(); + if !is_last_row && !galley.elided { + let popped = rows.next_back(); + debug_assert_eq!(popped.unwrap().row.glyphs.len(), 0); + } + + merged_galley.rows.extend(rows.map(|placed_row| { + let mut new_pos = placed_row.pos + current_offset; + new_pos.y = new_pos.y.round_to_pixels(pixels_per_point); + merged_galley.mesh_bounds = merged_galley + .mesh_bounds + .union(placed_row.visuals.mesh_bounds.translate(new_pos.to_vec2())); + merged_galley.rect = merged_galley + .rect + .union(emath::Rect::from_min_size(new_pos, placed_row.size)); + + super::PlacedRow { + row: placed_row.row.clone(), + pos: new_pos, + } + })); + + merged_galley.num_vertices += galley.num_vertices; + merged_galley.num_indices += galley.num_indices; + // Note that if `galley.elided` is true this will be the last `Galley` in + // the vector and the loop will end. + merged_galley.elided |= galley.elided; + } + + if merged_galley.job.round_output_to_gui { + super::round_output_to_gui(&mut merged_galley.rect, &merged_galley.job); + } + merged_galley +} + // ---------------------------------------------------------------------------- struct FontImplCache { From 77c9fd85bd3b917bdf0ce8076c1b992e8ace2200 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 2 Jan 2025 02:29:19 +0100 Subject: [PATCH 26/68] cleanup --- crates/epaint/src/text/fonts.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index e69c402ffc4..2c9d840d328 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -795,10 +795,6 @@ impl GalleyCache { } fn layout_multiline(&mut self, fonts: &mut FontsImpl, job: LayoutJob) -> Galley { - let pixels_per_point = fonts.pixels_per_point; - let round_to_pixel = - move |point: f32| (point * pixels_per_point).round() / pixels_per_point; - let mut current_section = 0; let mut current = 0; let mut left_max_rows = job.wrap.max_rows; @@ -868,9 +864,7 @@ impl GalleyCache { current = end; } - let pixels_per_point = fonts.pixels_per_point; - - concat_galleys(job, &galleys, pixels_per_point) + concat_galleys(job, &galleys, fonts.pixels_per_point) } fn layout_component_line(&mut self, fonts: &mut FontsImpl, job: LayoutJob) -> Arc { From b36311a98ae9475f83aa6d8f5a3a91fe4df5d2b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20G=C5=82uchowski?= Date: Sat, 4 Jan 2025 19:45:29 +0100 Subject: [PATCH 27/68] Fix Glyph::pos doc comment --- crates/epaint/src/text/text_layout_types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 528e8846071..330796b412a 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -630,7 +630,7 @@ pub struct Glyph { /// The character this glyph represents. pub chr: char, - /// Baseline position, relative to the galley. + /// Baseline position, relative to the row. /// Logical position: pos.y is the same for all chars of the same [`TextFormat`]. pub pos: Pos2, From 8a45db62f173793a924b21b5c648b617a7c9a80e Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 28 Mar 2025 11:33:56 +0100 Subject: [PATCH 28/68] Create `should_cache_each_paragraph_individually` and heed `max_rows` --- crates/epaint/src/text/fonts.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 2c6f4e4a316..f2b670afb96 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -772,11 +772,7 @@ impl GalleyCache { cached.galley.clone() } std::collections::hash_map::Entry::Vacant(entry) => { - // If the text contains newlines that will always break into a new row then - // we can easily lay out all the lines individually and then merge the `Galley`s. - // This allows individual lines to be cached separately which means small - // modifications to the source text will only cause impacted lines to be laid out again. - if job.break_on_newline && job.text.contains('\n') { + if should_cache_each_paragraph_individually(&job) { let galley = self.layout_multiline(fonts, job); let galley = Arc::new(galley); self.cache.insert( @@ -908,6 +904,17 @@ impl GalleyCache { } } +/// If true, lay out and cache each paragraph (sections separated by newlines) individually. +/// +/// This makes it much faster to re-lauout the full tet when only a portion of it has changed since last frame, i.e. when editing somewhere in a file with thousands of lines/paragraphs. +fn should_cache_each_paragraph_individually(job: &LayoutJob) -> bool { + // We currently don't support this elided text, i.e. when `max_rows` is set. + // Most often, elided text is elided to one row, + // and so will always be fast to lay out. + job.break_on_newline && job.wrap.max_rows == usize::MAX && job.text.contains('\n') +} + +/// Append each galley under the previous one. fn concat_galleys(job: LayoutJob, galleys: &[Arc], pixels_per_point: f32) -> Galley { let mut merged_galley = Galley { job: Arc::new(job), From a5bc318e20dab7133c2d56802102b73f919b448f Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 28 Mar 2025 11:36:47 +0100 Subject: [PATCH 29/68] Update to rand 0.9 --- crates/egui_demo_lib/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui_demo_lib/Cargo.toml b/crates/egui_demo_lib/Cargo.toml index 5a6a0455e43..77b8fdcb3e1 100644 --- a/crates/egui_demo_lib/Cargo.toml +++ b/crates/egui_demo_lib/Cargo.toml @@ -58,7 +58,7 @@ serde = { workspace = true, optional = true } criterion.workspace = true egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] } egui = { workspace = true, features = ["default_fonts"] } -rand = "0.8" +rand = "0.9" [[bench]] name = "benchmark" From 9bd23ef79ab60536aa8108e68df32ce785388167 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 28 Mar 2025 12:29:00 +0100 Subject: [PATCH 30/68] Stop using deprecated `rand` functions --- crates/egui_demo_lib/benches/benchmark.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/egui_demo_lib/benches/benchmark.rs b/crates/egui_demo_lib/benches/benchmark.rs index 9b13283334d..dab6bdd7b12 100644 --- a/crates/egui_demo_lib/benches/benchmark.rs +++ b/crates/egui_demo_lib/benches/benchmark.rs @@ -142,13 +142,13 @@ pub fn criterion_benchmark(c: &mut Criterion) { string.push('\n'); } - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); b.iter(|| { fonts.begin_pass(pixels_per_point, max_texture_side); // Delete a random character, simulating a user making an edit in a long file: let mut new_string = string.clone(); - let idx = rng.gen_range(0..string.len()); + let idx = rng.random_range(0..string.len()); new_string.remove(idx); fonts.layout(new_string, font_id.clone(), text_color, wrap_width); From 3e89613dc1f3151ff2c720dcd351a971586aefbf Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 28 Mar 2025 12:29:12 +0100 Subject: [PATCH 31/68] Add messages to asserts --- crates/epaint/src/text/fonts.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index f2b670afb96..70e206338d7 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -802,7 +802,7 @@ impl GalleyCache { let mut left_max_rows = job.wrap.max_rows; let mut galleys = Vec::new(); let mut first_row_min_height = job.first_row_min_height; - while current != job.text.len() { + while current < job.text.len() { let end = job.text[current..] .find('\n') .map_or(job.text.len(), |i| i + current + 1); @@ -830,7 +830,7 @@ impl GalleyCache { s = &job.sections[current_section]; } - assert!(s.byte_range.contains(¤t)); + debug_assert!(s.byte_range.contains(¤t), "Bug in LayoutJob splitter"); let section_end = s.byte_range.end.min(end); line_job.sections.push(crate::text::LayoutSection { // Leading space should only be added to the first section @@ -937,7 +937,7 @@ fn concat_galleys(job: LayoutJob, galleys: &[Arc], pixels_per_point: f32 let is_last_row = i + 1 == galleys.len(); if !is_last_row && !galley.elided { let popped = rows.next_back(); - debug_assert_eq!(popped.unwrap().row.glyphs.len(), 0); + debug_assert_eq!(popped.unwrap().row.glyphs.len(), 0, "Bug in concat_galleys"); } merged_galley.rows.extend(rows.map(|placed_row| { From 6f4732b06832eb0cd017b32c21e8164372b555f0 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 28 Mar 2025 12:38:55 +0100 Subject: [PATCH 32/68] Simplify the code slightly --- crates/epaint/src/text/fonts.rs | 39 ++++++++++----------------------- 1 file changed, 12 insertions(+), 27 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 70e206338d7..91f243ac146 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -617,7 +617,7 @@ pub struct FontsAndCache { impl FontsAndCache { fn layout_job(&mut self, job: LayoutJob) -> Arc { - self.galley_cache.layout(&mut self.fonts, job) + self.galley_cache.layout(&mut self.fonts, job, true) } } @@ -737,7 +737,12 @@ struct GalleyCache { } impl GalleyCache { - fn layout(&mut self, fonts: &mut FontsImpl, mut job: LayoutJob) -> Arc { + fn layout( + &mut self, + fonts: &mut FontsImpl, + mut job: LayoutJob, + allow_split_paragraphs: bool, + ) -> Arc { if job.wrap.max_width.is_finite() { // Protect against rounding errors in egui layout code. @@ -767,12 +772,13 @@ impl GalleyCache { match self.cache.entry(hash) { std::collections::hash_map::Entry::Occupied(entry) => { + // The job was found in cache - no need to re-layout. let cached = entry.into_mut(); cached.last_used = self.generation; cached.galley.clone() } std::collections::hash_map::Entry::Vacant(entry) => { - if should_cache_each_paragraph_individually(&job) { + if allow_split_paragraphs && should_cache_each_paragraph_individually(&job) { let galley = self.layout_multiline(fonts, job); let galley = Arc::new(galley); self.cache.insert( @@ -808,7 +814,7 @@ impl GalleyCache { .map_or(job.text.len(), |i| i + current + 1); let start = current; - let mut line_job = LayoutJob { + let mut paragraph_job = LayoutJob { text: job.text[current..end].to_string(), wrap: crate::text::TextWrapping { max_rows: left_max_rows, @@ -832,7 +838,7 @@ impl GalleyCache { debug_assert!(s.byte_range.contains(¤t), "Bug in LayoutJob splitter"); let section_end = s.byte_range.end.min(end); - line_job.sections.push(crate::text::LayoutSection { + paragraph_job.sections.push(crate::text::LayoutSection { // Leading space should only be added to the first section // if the there are multiple sections that will be created // from splitting the current section. @@ -847,7 +853,7 @@ impl GalleyCache { current = section_end; } - let galley = self.layout_component_line(fonts, line_job); + let galley = self.layout(fonts, paragraph_job, false); // This will prevent us from invalidating cache entries unnecessarily if left_max_rows != usize::MAX { left_max_rows -= galley.rows.len(); @@ -869,27 +875,6 @@ impl GalleyCache { concat_galleys(job, &galleys, fonts.pixels_per_point) } - fn layout_component_line(&mut self, fonts: &mut FontsImpl, job: LayoutJob) -> Arc { - let hash = crate::util::hash(&job); - - match self.cache.entry(hash) { - std::collections::hash_map::Entry::Occupied(entry) => { - let cached = entry.into_mut(); - cached.last_used = self.generation; - cached.galley.clone() - } - std::collections::hash_map::Entry::Vacant(entry) => { - let galley = super::layout(fonts, job.into()); - let galley = Arc::new(galley); - entry.insert(CachedGalley { - last_used: self.generation, - galley: galley.clone(), - }); - galley - } - } - } - pub fn num_galleys_in_cache(&self) -> usize { self.cache.len() } From d8a45d0836141be5f229710bdbc38d99d5bee4a7 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 28 Mar 2025 12:39:02 +0100 Subject: [PATCH 33/68] Silence a warning --- crates/egui_extras/src/syntax_highlighting.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/egui_extras/src/syntax_highlighting.rs b/crates/egui_extras/src/syntax_highlighting.rs index 77ad0cc2d44..ac51c673cba 100644 --- a/crates/egui_extras/src/syntax_highlighting.rs +++ b/crates/egui_extras/src/syntax_highlighting.rs @@ -33,6 +33,7 @@ pub fn highlight( // performing it at a separate thread (ctx, ctx.style()) can be used and when ui is available // (ui.ctx(), ui.style()) can be used + #[allow(non_local_definitions)] impl egui::cache::ComputerMut<(&egui::FontId, &CodeTheme, &str, &str), LayoutJob> for Highlighter { fn compute( &mut self, From 29d47c1641c0e2e40b379530ee4f19c1c64a84f2 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 28 Mar 2025 12:52:07 +0100 Subject: [PATCH 34/68] Clean up code slightly --- crates/epaint/src/text/fonts.rs | 47 ++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 91f243ac146..61da72a838c 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -778,8 +778,9 @@ impl GalleyCache { cached.galley.clone() } std::collections::hash_map::Entry::Vacant(entry) => { + let job = Arc::new(job); if allow_split_paragraphs && should_cache_each_paragraph_individually(&job) { - let galley = self.layout_multiline(fonts, job); + let galley = self.layout_each_paragraph_individuallly(fonts, job); let galley = Arc::new(galley); self.cache.insert( hash, @@ -790,7 +791,7 @@ impl GalleyCache { ); galley } else { - let galley = super::layout(fonts, job.into()); + let galley = super::layout(fonts, job); let galley = Arc::new(galley); entry.insert(CachedGalley { last_used: self.generation, @@ -802,32 +803,41 @@ impl GalleyCache { } } - fn layout_multiline(&mut self, fonts: &mut FontsImpl, job: LayoutJob) -> Galley { + /// Split on `\n` and lay out (and cache) each paragraph individually. + fn layout_each_paragraph_individuallly( + &mut self, + fonts: &mut FontsImpl, + job: Arc, + ) -> Galley { let mut current_section = 0; let mut current = 0; - let mut left_max_rows = job.wrap.max_rows; + let mut max_rows_remaining = job.wrap.max_rows; let mut galleys = Vec::new(); - let mut first_row_min_height = job.first_row_min_height; + while current < job.text.len() { + let is_first_paragraph = current == 0; let end = job.text[current..] .find('\n') - .map_or(job.text.len(), |i| i + current + 1); + .map_or(job.text.len(), |i| current + i + 1); let start = current; let mut paragraph_job = LayoutJob { - text: job.text[current..end].to_string(), + text: job.text[current..end].to_owned(), wrap: crate::text::TextWrapping { - max_rows: left_max_rows, + max_rows: max_rows_remaining, ..job.wrap }, sections: Vec::new(), - break_on_newline: true, + break_on_newline: job.break_on_newline, halign: job.halign, justify: job.justify, - first_row_min_height, + first_row_min_height: if is_first_paragraph { + job.first_row_min_height + } else { + 0.0 + }, round_output_to_gui: job.round_output_to_gui, }; - first_row_min_height = 0.0; while current < end { let mut s = &job.sections[current_section]; @@ -854,12 +864,13 @@ impl GalleyCache { } let galley = self.layout(fonts, paragraph_job, false); - // This will prevent us from invalidating cache entries unnecessarily - if left_max_rows != usize::MAX { - left_max_rows -= galley.rows.len(); - // Ignore extra trailing row, see merging counterpart below for more details. + + // This will prevent us from invalidating cache entries unnecessarily: + if max_rows_remaining != usize::MAX { + max_rows_remaining -= galley.rows.len(); + // Ignore extra trailing row, see merging concat_galleys for more details. if end < job.text.len() && !galley.elided { - left_max_rows += 1; + max_rows_remaining += 1; } } @@ -900,9 +911,9 @@ fn should_cache_each_paragraph_individually(job: &LayoutJob) -> bool { } /// Append each galley under the previous one. -fn concat_galleys(job: LayoutJob, galleys: &[Arc], pixels_per_point: f32) -> Galley { +fn concat_galleys(job: Arc, galleys: &[Arc], pixels_per_point: f32) -> Galley { let mut merged_galley = Galley { - job: Arc::new(job), + job, rows: Vec::new(), elided: false, rect: emath::Rect::ZERO, From 2ac4c93cc3befe79606b27b57e29e51723ef26a5 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 28 Mar 2025 12:54:53 +0100 Subject: [PATCH 35/68] Move `Galley::concat` --- crates/epaint/src/text/fonts.rs | 61 +-------------------- crates/epaint/src/text/text_layout_types.rs | 57 ++++++++++++++++++- 2 files changed, 59 insertions(+), 59 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 61da72a838c..9966efdc344 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -8,7 +8,7 @@ use crate::{ }, TextureAtlas, }; -use emath::{GuiRounding, NumExt as _, OrderedFloat}; +use emath::{NumExt as _, OrderedFloat}; #[cfg(feature = "default_fonts")] use epaint_default_fonts::{EMOJI_ICON, HACK_REGULAR, NOTO_EMOJI_REGULAR, UBUNTU_LIGHT}; @@ -868,7 +868,7 @@ impl GalleyCache { // This will prevent us from invalidating cache entries unnecessarily: if max_rows_remaining != usize::MAX { max_rows_remaining -= galley.rows.len(); - // Ignore extra trailing row, see merging concat_galleys for more details. + // Ignore extra trailing row, see merging `Galley::concat` for more details. if end < job.text.len() && !galley.elided { max_rows_remaining += 1; } @@ -883,7 +883,7 @@ impl GalleyCache { current = end; } - concat_galleys(job, &galleys, fonts.pixels_per_point) + Galley::concat(job, &galleys, fonts.pixels_per_point) } pub fn num_galleys_in_cache(&self) -> usize { @@ -910,61 +910,6 @@ fn should_cache_each_paragraph_individually(job: &LayoutJob) -> bool { job.break_on_newline && job.wrap.max_rows == usize::MAX && job.text.contains('\n') } -/// Append each galley under the previous one. -fn concat_galleys(job: Arc, galleys: &[Arc], pixels_per_point: f32) -> Galley { - let mut merged_galley = Galley { - job, - rows: Vec::new(), - elided: false, - rect: emath::Rect::ZERO, - mesh_bounds: emath::Rect::ZERO, - num_vertices: 0, - num_indices: 0, - pixels_per_point, - }; - - for (i, galley) in galleys.iter().enumerate() { - let current_offset = emath::vec2(0.0, merged_galley.rect.height()); - - let mut rows = galley.rows.iter(); - // As documented in `Row::ends_with_newline`, a '\n' will always create a - // new `Row` immediately below the current one. Here it doesn't make sense - // for us to append this new row so we just ignore it. - let is_last_row = i + 1 == galleys.len(); - if !is_last_row && !galley.elided { - let popped = rows.next_back(); - debug_assert_eq!(popped.unwrap().row.glyphs.len(), 0, "Bug in concat_galleys"); - } - - merged_galley.rows.extend(rows.map(|placed_row| { - let mut new_pos = placed_row.pos + current_offset; - new_pos.y = new_pos.y.round_to_pixels(pixels_per_point); - merged_galley.mesh_bounds = merged_galley - .mesh_bounds - .union(placed_row.visuals.mesh_bounds.translate(new_pos.to_vec2())); - merged_galley.rect = merged_galley - .rect - .union(emath::Rect::from_min_size(new_pos, placed_row.size)); - - super::PlacedRow { - row: placed_row.row.clone(), - pos: new_pos, - } - })); - - merged_galley.num_vertices += galley.num_vertices; - merged_galley.num_indices += galley.num_indices; - // Note that if `galley.elided` is true this will be the last `Galley` in - // the vector and the loop will end. - merged_galley.elided |= galley.elided; - } - - if merged_galley.job.round_output_to_gui { - super::round_output_to_gui(&mut merged_galley.rect, &merged_galley.job); - } - merged_galley -} - // ---------------------------------------------------------------------------- struct FontImplCache { diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 101a10d2f9b..22e7cd203b4 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -9,7 +9,7 @@ use super::{ font::UvRect, }; use crate::{Color32, FontId, Mesh, Stroke}; -use emath::{pos2, vec2, Align, NumExt, OrderedFloat, Pos2, Rect, Vec2}; +use emath::{pos2, vec2, Align, GuiRounding as _, NumExt, OrderedFloat, Pos2, Rect, Vec2}; /// Describes the task of laying out text. /// @@ -864,6 +864,61 @@ impl Galley { cursor } + + /// Append each galley under the previous one. + pub fn concat(job: Arc, galleys: &[Arc], pixels_per_point: f32) -> Self { + let mut merged_galley = Self { + job, + rows: Vec::new(), + elided: false, + rect: emath::Rect::ZERO, + mesh_bounds: emath::Rect::ZERO, + num_vertices: 0, + num_indices: 0, + pixels_per_point, + }; + + for (i, galley) in galleys.iter().enumerate() { + let current_offset = emath::vec2(0.0, merged_galley.rect.height()); + + let mut rows = galley.rows.iter(); + // As documented in `Row::ends_with_newline`, a '\n' will always create a + // new `Row` immediately below the current one. Here it doesn't make sense + // for us to append this new row so we just ignore it. + let is_last_row = i + 1 == galleys.len(); + if !is_last_row && !galley.elided { + let popped = rows.next_back(); + debug_assert_eq!(popped.unwrap().row.glyphs.len(), 0, "Bug in Galley::concat"); + } + + merged_galley.rows.extend(rows.map(|placed_row| { + let mut new_pos = placed_row.pos + current_offset; + new_pos.y = new_pos.y.round_to_pixels(pixels_per_point); + merged_galley.mesh_bounds = merged_galley + .mesh_bounds + .union(placed_row.visuals.mesh_bounds.translate(new_pos.to_vec2())); + merged_galley.rect = merged_galley + .rect + .union(emath::Rect::from_min_size(new_pos, placed_row.size)); + + super::PlacedRow { + row: placed_row.row.clone(), + pos: new_pos, + } + })); + + merged_galley.num_vertices += galley.num_vertices; + merged_galley.num_indices += galley.num_indices; + // Note that if `galley.elided` is true this will be the last `Galley` in + // the vector and the loop will end. + merged_galley.elided |= galley.elided; + } + + if merged_galley.job.round_output_to_gui { + super::round_output_to_gui(&mut merged_galley.rect, &merged_galley.job); + } + merged_galley + } } /// ## Cursor positions From 79d114dfb0dfb31e171bc0b3f7852a95045d690d Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 28 Mar 2025 15:11:30 +0100 Subject: [PATCH 36/68] Use similar-asserts for better test output --- Cargo.lock | 52 +++++++++++++++++++- Cargo.toml | 1 + crates/epaint/Cargo.toml | 1 + crates/epaint/src/text/fonts.rs | 85 +++++++++++++++++++++++++++++++++ 4 files changed, 138 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 038973ccfc8..4276896bac8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -642,6 +642,17 @@ dependencies = [ "piper", ] +[[package]] +name = "bstr" +version = "1.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -896,6 +907,18 @@ dependencies = [ "env_logger", ] +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "windows-sys 0.59.0", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1421,6 +1444,12 @@ dependencies = [ "serde", ] +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "endi" version = "1.1.0" @@ -1521,6 +1550,7 @@ dependencies = [ "profiling", "rayon", "serde", + "similar-asserts", ] [[package]] @@ -2390,7 +2420,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -3670,6 +3700,26 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +dependencies = [ + "bstr", + "unicode-segmentation", +] + +[[package]] +name = "similar-asserts" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b441962c817e33508847a22bd82f03a30cff43642dc2fae8b050566121eb9a" +dependencies = [ + "console", + "similar", +] + [[package]] name = "simplecss" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 92f1ce3c510..6040351031e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -96,6 +96,7 @@ puffin_http = "0.16" raw-window-handle = "0.6.0" ron = "0.8" serde = { version = "1", features = ["derive"] } +similar-asserts = "1.4.2" thiserror = "1.0.37" type-map = "0.5.0" wasm-bindgen = "0.2" diff --git a/crates/epaint/Cargo.toml b/crates/epaint/Cargo.toml index 7201888728d..b8b006d4445 100644 --- a/crates/epaint/Cargo.toml +++ b/crates/epaint/Cargo.toml @@ -101,6 +101,7 @@ backtrace = { workspace = true, optional = true } [dev-dependencies] criterion.workspace = true +similar-asserts.workspace = true [[bench]] diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 9966efdc344..f43a8d3cd44 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -980,3 +980,88 @@ impl FontImplCache { .clone() } } + +#[cfg(feature = "default_fonts")] +#[cfg(test)] +mod tests { + use core::f32; + + use super::*; + use crate::{text::TextFormat, Stroke}; + use ecolor::Color32; + + fn jobs() -> Vec { + // TODO: add more tests here with newlines etc + vec![ + LayoutJob::simple( + "Simplest test.".to_owned(), + FontId::new(14.0, FontFamily::Monospace), + Color32::WHITE, + f32::INFINITY, + ), + LayoutJob::simple( + "The is some text that may be long.\nDet kanske också finns lite ÅÄÖ här." + .to_owned(), + FontId::new(14.0, FontFamily::Proportional), + Color32::WHITE, + 50.0, + ), + { + let mut job = LayoutJob { + first_row_min_height: 20.0, + justify: true, + ..Default::default() + }; + job.append( + "The first paragraph has some leading space.\n", + 16.0, + TextFormat { + font_id: FontId::new(14.0, FontFamily::Proportional), + ..Default::default() + }, + ); + job.append( + "The second paragraph has underline and strikthrough, and has some non-ASCII characters: ÅÄÖ.", + 0.0, + TextFormat { + font_id: FontId::new(15.0, FontFamily::Monospace), + underline: Stroke::new(1.0, Color32::RED), + strikethrough: Stroke::new(1.0, Color32::GREEN), + ..Default::default() + }, + ); + job.append( + "The last paragraph is kind of boring, bust has italics.", + 0.0, + TextFormat { + font_id: FontId::new(10.0, FontFamily::Proportional), + italics: true, + ..Default::default() + }, + ); + + job + }, + ] + } + + #[test] + fn test_split_paragraphs() { + for pixels_per_point in [1.0, 2.0_f32.sqrt(), 2.0] { + let max_texture_side = 4096; + let mut fonts = FontsImpl::new( + pixels_per_point, + max_texture_side, + FontDefinitions::default(), + ); + + for job in jobs() { + let as_whole = GalleyCache::default().layout(&mut fonts, job.clone(), false); + + let split = GalleyCache::default().layout(&mut fonts, job.clone(), true); + + similar_asserts::assert_eq!(split, as_whole, "Input text: '{}'", job.text); + } + } + } +} From 947945f0baf26b6c41e69a699b8449cdef1eded3 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 28 Mar 2025 15:13:57 +0100 Subject: [PATCH 37/68] Remove done TODO --- crates/epaint/src/text/fonts.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index f43a8d3cd44..2b497d9c211 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -991,7 +991,6 @@ mod tests { use ecolor::Color32; fn jobs() -> Vec { - // TODO: add more tests here with newlines etc vec![ LayoutJob::simple( "Simplest test.".to_owned(), From 47f916ca80b89a0dacf6dcce65979b894838d05c Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 28 Mar 2025 15:27:56 +0100 Subject: [PATCH 38/68] Fix wrong mesh_bounds --- crates/epaint/src/text/text_layout.rs | 5 +++-- crates/epaint/src/text/text_layout_types.rs | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 473b9cfff78..dd51f6f72d7 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -74,7 +74,7 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc) -> Galley { return Galley { job, rows: Default::default(), - rect: Rect::from_min_max(Pos2::ZERO, Pos2::ZERO), + rect: Rect::ZERO, mesh_bounds: Rect::NOTHING, num_vertices: 0, num_indices: 0, @@ -655,7 +655,8 @@ fn galley_from_rows( for placed_row in &mut rows { let row = Arc::get_mut(&mut placed_row.row).unwrap(); row.visuals = tessellate_row(point_scale, &job, &format_summary, row); - mesh_bounds = mesh_bounds.union(row.visuals.mesh_bounds); + mesh_bounds = + mesh_bounds.union(row.visuals.mesh_bounds.translate(placed_row.pos.to_vec2())); num_vertices += row.visuals.mesh.vertices.len(); num_indices += row.visuals.mesh.indices.len(); } diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 22e7cd203b4..ca5b8a22aa4 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -872,14 +872,14 @@ impl Galley { rows: Vec::new(), elided: false, rect: emath::Rect::ZERO, - mesh_bounds: emath::Rect::ZERO, + mesh_bounds: emath::Rect::NOTHING, num_vertices: 0, num_indices: 0, pixels_per_point, }; for (i, galley) in galleys.iter().enumerate() { - let current_offset = emath::vec2(0.0, merged_galley.rect.height()); + let current_offset = Vec2::new(0.0, merged_galley.rect.height()); let mut rows = galley.rows.iter(); // As documented in `Row::ends_with_newline`, a '\n' will always create a From 46ef7a1159d8769dcac68cf1ef44f8dcd35018e1 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 28 Mar 2025 15:54:04 +0100 Subject: [PATCH 39/68] code cleanup --- crates/epaint/src/text/fonts.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 2b497d9c211..791740c3d72 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -816,13 +816,13 @@ impl GalleyCache { while current < job.text.len() { let is_first_paragraph = current == 0; - let end = job.text[current..] - .find('\n') - .map_or(job.text.len(), |i| current + i + 1); let start = current; + let end = job.text[start..] + .find('\n') + .map_or(job.text.len(), |i| start + i + 1); let mut paragraph_job = LayoutJob { - text: job.text[current..end].to_owned(), + text: job.text[start..end].to_owned(), wrap: crate::text::TextWrapping { max_rows: max_rows_remaining, ..job.wrap @@ -993,7 +993,13 @@ mod tests { fn jobs() -> Vec { vec![ LayoutJob::simple( - "Simplest test.".to_owned(), + String::default(), + FontId::new(14.0, FontFamily::Monospace), + Color32::WHITE, + f32::INFINITY, + ), + LayoutJob::simple( + "Simple test.".to_owned(), FontId::new(14.0, FontFamily::Monospace), Color32::WHITE, f32::INFINITY, @@ -1020,7 +1026,7 @@ mod tests { }, ); job.append( - "The second paragraph has underline and strikthrough, and has some non-ASCII characters: ÅÄÖ.", + "The second paragraph has underline and strikthrough, and has some non-ASCII characters:\n ÅÄÖ.", 0.0, TextFormat { font_id: FontId::new(15.0, FontFamily::Monospace), @@ -1030,7 +1036,7 @@ mod tests { }, ); job.append( - "The last paragraph is kind of boring, bust has italics.", + "The last paragraph is kind of boring, but has italics.\nAnd a newline", 0.0, TextFormat { font_id: FontId::new(10.0, FontFamily::Proportional), From b060fd25b08e10519bb9b1fad524b3a9bd423f33 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 28 Mar 2025 16:08:35 +0100 Subject: [PATCH 40/68] Remove unwraps --- crates/epaint/src/text/text_layout.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index dd51f6f72d7..a42c24b944c 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -96,7 +96,7 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc) -> Galley { let mut rows = rows_from_paragraphs(paragraphs, &job, &mut elided); if elided { if let Some(last_placed) = rows.last_mut() { - let last_row = Arc::get_mut(&mut last_placed.row).unwrap(); + let last_row = Arc::make_mut(&mut last_placed.row); replace_last_glyph_with_overflow_character(fonts, &job, last_row); if let Some(last) = last_row.glyphs.last() { last_row.size.x = last.max_x(); @@ -234,7 +234,7 @@ fn rows_from_paragraphs( } else { line_break(¶graph, job, &mut rows, elided); let placed_row = rows.last_mut().unwrap(); - let row = Arc::get_mut(&mut placed_row.row).unwrap(); + let row = Arc::make_mut(&mut placed_row.row); row.ends_with_newline = !is_last_paragraph; } } @@ -517,7 +517,7 @@ fn halign_and_justify_row( wrap_width: f32, justify: bool, ) { - let row = Arc::get_mut(&mut placed_row.row).unwrap(); + let row = Arc::make_mut(&mut placed_row.row); if row.glyphs.is_empty() { return; @@ -615,7 +615,7 @@ fn galley_from_rows( let mut max_x: f32 = 0.0; for placed_row in &mut rows { let mut max_row_height = first_row_min_height.max(placed_row.rect().height()); - let row = Arc::get_mut(&mut placed_row.row).unwrap(); + let row = Arc::make_mut(&mut placed_row.row); first_row_min_height = 0.0; for glyph in &row.glyphs { @@ -653,7 +653,7 @@ fn galley_from_rows( let mut num_indices = 0; for placed_row in &mut rows { - let row = Arc::get_mut(&mut placed_row.row).unwrap(); + let row = Arc::make_mut(&mut placed_row.row); row.visuals = tessellate_row(point_scale, &job, &format_summary, row); mesh_bounds = mesh_bounds.union(row.visuals.mesh_bounds.translate(placed_row.pos.to_vec2())); @@ -666,7 +666,7 @@ fn galley_from_rows( if job.round_output_to_gui { for placed_row in &mut rows { placed_row.pos = placed_row.pos.round_ui(); - let row = Arc::get_mut(&mut placed_row.row).unwrap(); + let row = Arc::make_mut(&mut placed_row.row); row.size = row.size.round_ui(); } round_output_to_gui(&mut rect, &job); From 2fb0764a2427e00941472834c213bfcc73e44e66 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 28 Mar 2025 16:08:46 +0100 Subject: [PATCH 41/68] Fix section_index --- crates/epaint/src/text/fonts.rs | 91 ++++++++++++++------- crates/epaint/src/text/text_layout.rs | 8 ++ crates/epaint/src/text/text_layout_types.rs | 7 ++ 3 files changed, 75 insertions(+), 31 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 791740c3d72..0d0a5023279 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -4,7 +4,7 @@ use crate::{ mutex::{Mutex, MutexGuard}, text::{ font::{Font, FontImpl}, - Galley, LayoutJob, + Galley, LayoutJob, LayoutSection, }, TextureAtlas, }; @@ -809,7 +809,8 @@ impl GalleyCache { fonts: &mut FontsImpl, job: Arc, ) -> Galley { - let mut current_section = 0; + profiling::function_scope!(); + let mut current = 0; let mut max_rows_remaining = job.wrap.max_rows; let mut galleys = Vec::new(); @@ -839,29 +840,35 @@ impl GalleyCache { round_output_to_gui: job.round_output_to_gui, }; - while current < end { - let mut s = &job.sections[current_section]; - while s.byte_range.end <= current { - current_section += 1; - s = &job.sections[current_section]; - } - - debug_assert!(s.byte_range.contains(¤t), "Bug in LayoutJob splitter"); - let section_end = s.byte_range.end.min(end); - paragraph_job.sections.push(crate::text::LayoutSection { - // Leading space should only be added to the first section - // if the there are multiple sections that will be created - // from splitting the current section. - leading_space: if current == s.byte_range.start { - s.leading_space + // Keep all (previous) sections (so the correct `section_index` is outputted), + // but shift their byte ranges: + paragraph_job.sections = job + .sections + .iter() + .cloned() + .filter_map(|section| { + let LayoutSection { + leading_space, + byte_range, + format, + } = section; + if end <= byte_range.start { + None } else { - 0.0 - }, - byte_range: current - start..section_end - start, - format: s.format.clone(), - }); - current = section_end; - } + let byte_range = byte_range.start.saturating_sub(start) + ..byte_range.end.saturating_sub(start).min(end - start); + Some(LayoutSection { + leading_space: if byte_range.end == 0 { + 0.0 // this sections is behind us (unused in this paragraph) + } else { + leading_space + }, + byte_range, + format, + }) + } + }) + .collect(); let galley = self.layout(fonts, paragraph_job, false); @@ -1005,8 +1012,7 @@ mod tests { f32::INFINITY, ), LayoutJob::simple( - "The is some text that may be long.\nDet kanske också finns lite ÅÄÖ här." - .to_owned(), + "This some text that may be long.\nDet kanske också finns lite ÅÄÖ här.".to_owned(), FontId::new(14.0, FontFamily::Proportional), Color32::WHITE, 50.0, @@ -1018,7 +1024,7 @@ mod tests { ..Default::default() }; job.append( - "The first paragraph has some leading space.\n", + "1st paragraph has some leading space.\n", 16.0, TextFormat { font_id: FontId::new(14.0, FontFamily::Proportional), @@ -1026,7 +1032,7 @@ mod tests { }, ); job.append( - "The second paragraph has underline and strikthrough, and has some non-ASCII characters:\n ÅÄÖ.", + "2nd paragraph has underline and strikthrough, and has some non-ASCII characters:\n ÅÄÖ.", 0.0, TextFormat { font_id: FontId::new(15.0, FontFamily::Monospace), @@ -1036,7 +1042,7 @@ mod tests { }, ); job.append( - "The last paragraph is kind of boring, but has italics.\nAnd a newline", + "3rd paragraph is kind of boring, but has italics.\nAnd a newline", 0.0, TextFormat { font_id: FontId::new(10.0, FontFamily::Proportional), @@ -1061,11 +1067,34 @@ mod tests { ); for job in jobs() { - let as_whole = GalleyCache::default().layout(&mut fonts, job.clone(), false); + let whole = GalleyCache::default().layout(&mut fonts, job.clone(), false); let split = GalleyCache::default().layout(&mut fonts, job.clone(), true); - similar_asserts::assert_eq!(split, as_whole, "Input text: '{}'", job.text); + dbg!(whole.rect, whole.mesh_bounds); + dbg!(split.rect, split.mesh_bounds); + + for (i, row) in whole.rows.iter().enumerate() { + println!( + "Whole row {i}: section_index_at_start={}, first glyph section_index: {:?}", + row.row.section_index_at_start, + row.row.glyphs.first().map(|g| g.section_index) + ); + } + for (i, row) in split.rows.iter().enumerate() { + println!( + "Split row {i}: section_index_at_start={}, first glyph section_index: {:?}", + row.row.section_index_at_start, + row.row.glyphs.first().map(|g| g.section_index) + ); + } + + similar_asserts::assert_eq!( + split, + whole, + "pixels_per_point: {pixels_per_point:.2}, input text: '{}'", + job.text + ); } } } diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index a42c24b944c..1ed6c199f9a 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -661,6 +661,14 @@ fn galley_from_rows( num_indices += row.visuals.mesh.indices.len(); } + // Fill in correct section_index_at_start for each row: + for placed in &mut rows { + let row = Arc::make_mut(&mut placed.row); + if let Some(first_glyph) = row.glyphs.first_mut() { + row.section_index_at_start = first_glyph.section_index; + } + } + let mut rect = Rect::from_min_max(pos2(min_x, 0.0), pos2(max_x, cursor_y)); if job.round_output_to_gui { diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index ca5b8a22aa4..e946cb858c1 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -914,6 +914,13 @@ impl Galley { merged_galley.elided |= galley.elided; } + for placed in &mut merged_galley.rows { + let row = Arc::make_mut(&mut placed.row); + if let Some(first_glyph) = row.glyphs.first_mut() { + row.section_index_at_start = first_glyph.section_index; + } + } + if merged_galley.job.round_output_to_gui { super::round_output_to_gui(&mut merged_galley.rect, &merged_galley.job); } From 33ff4bd7da8794c61d2674ab7f37560ab4a2b87b Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 28 Mar 2025 16:09:40 +0100 Subject: [PATCH 42/68] Simplify --- crates/epaint/src/text/fonts.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 0d0a5023279..4c5d8ce931b 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -811,13 +811,12 @@ impl GalleyCache { ) -> Galley { profiling::function_scope!(); - let mut current = 0; + let mut start = 0; let mut max_rows_remaining = job.wrap.max_rows; let mut galleys = Vec::new(); - while current < job.text.len() { - let is_first_paragraph = current == 0; - let start = current; + while start < job.text.len() { + let is_first_paragraph = start == 0; let end = job.text[start..] .find('\n') .map_or(job.text.len(), |i| start + i + 1); @@ -887,7 +886,7 @@ impl GalleyCache { break; } - current = end; + start = end; } Galley::concat(job, &galleys, fonts.pixels_per_point) From 1cdf52579702345504daecf4131f385d2e3c131a Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 28 Mar 2025 16:10:49 +0100 Subject: [PATCH 43/68] Remove `dbg!` --- crates/epaint/src/text/fonts.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 4c5d8ce931b..4429941593b 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -1070,9 +1070,6 @@ mod tests { let split = GalleyCache::default().layout(&mut fonts, job.clone(), true); - dbg!(whole.rect, whole.mesh_bounds); - dbg!(split.rect, split.mesh_bounds); - for (i, row) in whole.rows.iter().enumerate() { println!( "Whole row {i}: section_index_at_start={}, first glyph section_index: {:?}", From cbc3c3305149c216a08365ea5fe3b3a8a34d7cc1 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 28 Mar 2025 20:05:03 +0100 Subject: [PATCH 44/68] Don't round row positions to ui --- crates/epaint/src/text/text_layout.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 1ed6c199f9a..9c767ef735c 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -673,7 +673,7 @@ fn galley_from_rows( if job.round_output_to_gui { for placed_row in &mut rows { - placed_row.pos = placed_row.pos.round_ui(); + // NOTE: we round the size, but not the position, because the position should be _pixel_ aligned. let row = Arc::make_mut(&mut placed_row.row); row.size = row.size.round_ui(); } From 25f647322e5bbc582d02e032d798ba0d6f275a86 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 28 Mar 2025 20:08:47 +0100 Subject: [PATCH 45/68] Make test a bit more forgiving --- crates/epaint/src/text/fonts.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 4429941593b..3b424ae1937 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -1085,9 +1085,10 @@ mod tests { ); } + // Don't compare for equaliity; but format with a specific precision and make sure we hit that. similar_asserts::assert_eq!( - split, - whole, + format!("{:#.5?}", split), + format!("{:#.5?}", whole), "pixels_per_point: {pixels_per_point:.2}, input text: '{}'", job.text ); From ed847f1a559bd7727552c433698cc58e654fb7e2 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 28 Mar 2025 20:22:10 +0100 Subject: [PATCH 46/68] Refactor: create `Stroke::round_center_to_pixel` --- crates/epaint/src/stroke.rs | 60 ++++++++++++++++++++++++++++++++ crates/epaint/src/tessellator.rs | 56 ++--------------------------- 2 files changed, 63 insertions(+), 53 deletions(-) diff --git a/crates/epaint/src/stroke.rs b/crates/epaint/src/stroke.rs index 5d82c1963db..50f4f678b96 100644 --- a/crates/epaint/src/stroke.rs +++ b/crates/epaint/src/stroke.rs @@ -2,6 +2,8 @@ use std::{fmt::Debug, sync::Arc}; +use emath::GuiRounding as _; + use super::{emath, Color32, ColorMode, Pos2, Rect}; /// Describes the width and color of a line. @@ -34,6 +36,46 @@ impl Stroke { pub fn is_empty(&self) -> bool { self.width <= 0.0 || self.color == Color32::TRANSPARENT } + + /// For vertical or horizontal lines: + /// round the stroke center to produce a sharp, pixel-aligned line. + pub fn round_center_to_pixel(&self, pixels_per_point: f32, coord: &mut f32) { + // If the stroke is an odd number of pixels wide, + // we want to round the center of it to the center of a pixel. + // + // If however it is an even number of pixels wide, + // we want to round the center to be between two pixels. + // + // We also want to treat strokes that are _almost_ odd as it it was odd, + // to make it symmetric. Same for strokes that are _almost_ even. + // + // For strokes less than a pixel wide we also round to the center, + // because it will rendered as a single row of pixels by the tessellator. + + let pixel_size = 1.0 / pixels_per_point; + + if self.width <= pixel_size || is_nearest_integer_odd(pixels_per_point * self.width) { + *coord = coord.round_to_pixel_center(pixels_per_point); + } else { + *coord = coord.round_to_pixels(pixels_per_point); + } + } + + pub(crate) fn round_rect_to_pixel(&self, pixels_per_point: f32, rect: &mut Rect) { + // We put odd-width strokes in the center of pixels. + // To understand why, see `fn round_center_to_pixel`. + + let pixel_size = 1.0 / pixels_per_point; + + let width = self.width; + if width <= 0.0 { + *rect = rect.round_to_pixels(pixels_per_point); + } else if width <= pixel_size || is_nearest_integer_odd(pixels_per_point * width) { + *rect = rect.round_to_pixel_center(pixels_per_point); + } else { + *rect = rect.round_to_pixels(pixels_per_point); + } + } } impl From<(f32, Color)> for Stroke @@ -182,3 +224,21 @@ impl From for PathStroke { } } } + +/// Returns true if the nearest integer is odd. +fn is_nearest_integer_odd(x: f32) -> bool { + (x * 0.5 + 0.25).fract() > 0.5 +} + +#[test] +fn test_is_nearest_integer_odd() { + assert!(is_nearest_integer_odd(0.6)); + assert!(is_nearest_integer_odd(1.0)); + assert!(is_nearest_integer_odd(1.4)); + assert!(!is_nearest_integer_odd(1.6)); + assert!(!is_nearest_integer_odd(2.0)); + assert!(!is_nearest_integer_odd(2.4)); + assert!(is_nearest_integer_odd(2.6)); + assert!(is_nearest_integer_odd(3.0)); + assert!(is_nearest_integer_odd(3.4)); +} diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index b2f011a56f6..349188d0a3c 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1656,7 +1656,7 @@ impl Tessellator { if a.x == b.x { // Vertical line let mut x = a.x; - round_line_segment(&mut x, &stroke, self.pixels_per_point); + stroke.round_coord_to_pixel(self.pixels_per_point, &mut x); a.x = x; b.x = x; @@ -1677,7 +1677,7 @@ impl Tessellator { if a.y == b.y { // Horizontal line let mut y = a.y; - round_line_segment(&mut y, &stroke, self.pixels_per_point); + stroke.round_coord_to_pixel(self.pixels_per_point, &mut y); a.y = y; b.y = y; @@ -1778,7 +1778,6 @@ impl Tessellator { let mut corner_radius = CornerRadiusF32::from(corner_radius); let round_to_pixels = round_to_pixels.unwrap_or(self.options.round_rects_to_pixels); - let pixel_size = 1.0 / self.pixels_per_point; if stroke.width == 0.0 { stroke.color = Color32::TRANSPARENT; @@ -1849,17 +1848,7 @@ impl Tessellator { } StrokeKind::Middle => { // On this path we optimize for crisp and symmetric strokes. - // We put odd-width strokes in the center of pixels. - // To understand why, see `fn round_line_segment`. - if stroke.width <= 0.0 { - rect = rect.round_to_pixels(self.pixels_per_point); - } else if stroke.width <= pixel_size - || is_nearest_integer_odd(self.pixels_per_point * stroke.width) - { - rect = rect.round_to_pixel_center(self.pixels_per_point); - } else { - rect = rect.round_to_pixels(self.pixels_per_point); - } + stroke.round_rect_to_pixel(self.pixels_per_point, &mut rect); } StrokeKind::Outside => { // Put the inside of the stroke on a pixel boundary. @@ -2205,45 +2194,6 @@ impl Tessellator { } } -fn round_line_segment(coord: &mut f32, stroke: &Stroke, pixels_per_point: f32) { - // If the stroke is an odd number of pixels wide, - // we want to round the center of it to the center of a pixel. - // - // If however it is an even number of pixels wide, - // we want to round the center to be between two pixels. - // - // We also want to treat strokes that are _almost_ odd as it it was odd, - // to make it symmetric. Same for strokes that are _almost_ even. - // - // For strokes less than a pixel wide we also round to the center, - // because it will rendered as a single row of pixels by the tessellator. - - let pixel_size = 1.0 / pixels_per_point; - - if stroke.width <= pixel_size || is_nearest_integer_odd(pixels_per_point * stroke.width) { - *coord = coord.round_to_pixel_center(pixels_per_point); - } else { - *coord = coord.round_to_pixels(pixels_per_point); - } -} - -fn is_nearest_integer_odd(width: f32) -> bool { - (width * 0.5 + 0.25).fract() > 0.5 -} - -#[test] -fn test_is_nearest_integer_odd() { - assert!(is_nearest_integer_odd(0.6)); - assert!(is_nearest_integer_odd(1.0)); - assert!(is_nearest_integer_odd(1.4)); - assert!(!is_nearest_integer_odd(1.6)); - assert!(!is_nearest_integer_odd(2.0)); - assert!(!is_nearest_integer_odd(2.4)); - assert!(is_nearest_integer_odd(2.6)); - assert!(is_nearest_integer_odd(3.0)); - assert!(is_nearest_integer_odd(3.4)); -} - #[deprecated = "Use `Tessellator::new(…).tessellate_shapes(…)` instead"] pub fn tessellate_shapes( pixels_per_point: f32, From 5ecc31885c4a1eb97a594aad70719feb3e1d0192 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 28 Mar 2025 20:24:33 +0100 Subject: [PATCH 47/68] Pre-existing bug fix: round strikethrough and underline to pixel coord --- crates/epaint/src/tessellator.rs | 4 ++-- crates/epaint/src/text/text_layout.rs | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 349188d0a3c..2b24869ae50 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1656,7 +1656,7 @@ impl Tessellator { if a.x == b.x { // Vertical line let mut x = a.x; - stroke.round_coord_to_pixel(self.pixels_per_point, &mut x); + stroke.round_center_to_pixel(self.pixels_per_point, &mut x); a.x = x; b.x = x; @@ -1677,7 +1677,7 @@ impl Tessellator { if a.y == b.y { // Horizontal line let mut y = a.y; - stroke.round_coord_to_pixel(self.pixels_per_point, &mut y); + stroke.round_center_to_pixel(self.pixels_per_point, &mut y); a.y = y; b.y = y; diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 9c767ef735c..8095092582a 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -894,7 +894,8 @@ fn add_row_hline( let mut last_right_x = f32::NAN; for glyph in &row.glyphs { - let (stroke, y) = stroke_and_y(glyph); + let (stroke, mut y) = stroke_and_y(glyph); + stroke.round_center_to_pixel(point_scale.pixels_per_point, &mut y); if stroke == Stroke::NONE { end_line(line_start.take(), last_right_x); From 3d70f7289af69cd49abdbd5a4b6635731eded09f Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 30 Mar 2025 16:37:43 +0200 Subject: [PATCH 48/68] Small code cleanup --- crates/epaint/src/text/text_layout_types.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 138620c46ea..67f96e57a99 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -554,6 +554,8 @@ pub struct PlacedRow { pub row: Arc, /// The position of this [`Row`] relative to the galley. + /// + /// This is rounded to the closest _pixel_ in order to produce crisp, pixel-perfect text. pub pos: Pos2, } @@ -874,15 +876,15 @@ impl Galley { job, rows: Vec::new(), elided: false, - rect: emath::Rect::ZERO, - mesh_bounds: emath::Rect::NOTHING, + rect: Rect::ZERO, + mesh_bounds: Rect::NOTHING, num_vertices: 0, num_indices: 0, pixels_per_point, }; for (i, galley) in galleys.iter().enumerate() { - let current_offset = Vec2::new(0.0, merged_galley.rect.height()); + let current_y_offset = merged_galley.rect.height(); let mut rows = galley.rows.iter(); // As documented in `Row::ends_with_newline`, a '\n' will always create a @@ -895,14 +897,14 @@ impl Galley { } merged_galley.rows.extend(rows.map(|placed_row| { - let mut new_pos = placed_row.pos + current_offset; - new_pos.y = new_pos.y.round_to_pixels(pixels_per_point); + let new_pos = placed_row.pos + current_y_offset * Vec2::Y; + let new_pos = new_pos.round_to_pixels(pixels_per_point); merged_galley.mesh_bounds = merged_galley .mesh_bounds .union(placed_row.visuals.mesh_bounds.translate(new_pos.to_vec2())); merged_galley.rect = merged_galley .rect - .union(emath::Rect::from_min_size(new_pos, placed_row.size)); + .union(Rect::from_min_size(new_pos, placed_row.size)); super::PlacedRow { row: placed_row.row.clone(), From 7824e2ac18d714ac70ea021030997af42d7ccc26 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 30 Mar 2025 16:41:20 +0200 Subject: [PATCH 49/68] Implement `MulAssign` for `Pos2` --- crates/emath/src/pos2.rs | 14 ++++++++++++-- crates/epaint/src/shapes/shape.rs | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/crates/emath/src/pos2.rs b/crates/emath/src/pos2.rs index 1f4bd86427f..62590b10f45 100644 --- a/crates/emath/src/pos2.rs +++ b/crates/emath/src/pos2.rs @@ -1,5 +1,7 @@ -use std::fmt; -use std::ops::{Add, AddAssign, Sub, SubAssign}; +use std::{ + fmt, + ops::{Add, AddAssign, MulAssign, Sub, SubAssign}, +}; use crate::{lerp, Div, Mul, Vec2}; @@ -305,6 +307,14 @@ impl Mul for f32 { } } +impl MulAssign for Pos2 { + #[inline(always)] + fn mul_assign(&mut self, rhs: f32) { + self.x *= rhs; + self.y *= rhs; + } +} + impl Div for Pos2 { type Output = Self; diff --git a/crates/epaint/src/shapes/shape.rs b/crates/epaint/src/shapes/shape.rs index 006289bf130..38af4ca2c7f 100644 --- a/crates/epaint/src/shapes/shape.rs +++ b/crates/epaint/src/shapes/shape.rs @@ -484,7 +484,7 @@ impl Shape { let row = Arc::make_mut(&mut placed_row.row); row.visuals.mesh_bounds = transform.scaling * row.visuals.mesh_bounds; for v in &mut row.visuals.mesh.vertices { - v.pos = Pos2::new(transform.scaling * v.pos.x, transform.scaling * v.pos.y); + v.pos *= transform.scaling; } } From 4ee4572f0218c9cf8ef33a110fd0b934a90dc9d3 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 30 Mar 2025 16:50:05 +0200 Subject: [PATCH 50/68] Fix bug in code transforming `TextShape` --- crates/epaint/src/shapes/shape.rs | 35 +--------------- crates/epaint/src/shapes/text_shape.rs | 57 ++++++++++++++++++++++++++ crates/epaint/src/text/fonts.rs | 4 +- 3 files changed, 61 insertions(+), 35 deletions(-) diff --git a/crates/epaint/src/shapes/shape.rs b/crates/epaint/src/shapes/shape.rs index 38af4ca2c7f..a855d653ade 100644 --- a/crates/epaint/src/shapes/shape.rs +++ b/crates/epaint/src/shapes/shape.rs @@ -456,40 +456,7 @@ impl Shape { rect_shape.blur_width *= transform.scaling; } Self::Text(text_shape) => { - let TextShape { - pos, - galley, - underline, - fallback_color: _, - override_text_color: _, - opacity_factor: _, - angle: _, - } = text_shape; - - *pos = transform * *pos; - underline.width *= transform.scaling; - - let Galley { - job: _, - rows, - elided: _, - rect, - mesh_bounds, - num_vertices: _, - num_indices: _, - pixels_per_point: _, - } = Arc::make_mut(galley); - - for placed_row in rows { - let row = Arc::make_mut(&mut placed_row.row); - row.visuals.mesh_bounds = transform.scaling * row.visuals.mesh_bounds; - for v in &mut row.visuals.mesh.vertices { - v.pos *= transform.scaling; - } - } - - *mesh_bounds = transform.scaling * *mesh_bounds; - *rect = transform.scaling * *rect; + text_shape.transform(transform); } Self::Mesh(mesh) => { Arc::make_mut(mesh).transform(transform); diff --git a/crates/epaint/src/shapes/text_shape.rs b/crates/epaint/src/shapes/text_shape.rs index ef549bd9cb8..d7fe8a4794c 100644 --- a/crates/epaint/src/shapes/text_shape.rs +++ b/crates/epaint/src/shapes/text_shape.rs @@ -89,6 +89,63 @@ impl TextShape { self.opacity_factor = opacity_factor; self } + + /// Move the shape by this many points, in-place. + pub fn transform(&mut self, transform: emath::TSTransform) { + let Self { + pos, + galley, + underline, + fallback_color: _, + override_text_color: _, + opacity_factor: _, + angle: _, + } = self; + + *pos = transform * *pos; + underline.width *= transform.scaling; + + let Galley { + job: _, + rows, + elided: _, + rect, + mesh_bounds, + num_vertices: _, + num_indices: _, + pixels_per_point: _, + } = Arc::make_mut(galley); + + *rect = transform.scaling * *rect; + *mesh_bounds = transform.scaling * *mesh_bounds; + + for text::PlacedRow { row, pos } in rows { + *pos *= transform.scaling; + + let text::Row { + section_index_at_start: _, + glyphs: _, // TODO(emilk): would it make sense to transform these? + size, + visuals, + ends_with_newline: _, + } = Arc::make_mut(row); + + *size *= transform.scaling; + + let text::RowVisuals { + mesh, + mesh_bounds, + glyph_index_start: _, + glyph_vertex_range: _, + } = visuals; + + *mesh_bounds = transform.scaling * *mesh_bounds; + + for v in &mut mesh.vertices { + v.pos *= transform.scaling; + } + } + } } impl From for Shape { diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 3b424ae1937..216ea4bfaf7 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -617,7 +617,9 @@ pub struct FontsAndCache { impl FontsAndCache { fn layout_job(&mut self, job: LayoutJob) -> Arc { - self.galley_cache.layout(&mut self.fonts, job, true) + let allow_split_paragraphs = true; // Optimization for editing text with many paragraphs. + self.galley_cache + .layout(&mut self.fonts, job, allow_split_paragraphs) } } From b0d0d2f27957ecf46cd887f9d9eea254a7153a75 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 30 Mar 2025 16:58:19 +0200 Subject: [PATCH 51/68] Test that `halign` & `justify` works the same --- crates/epaint/src/text/fonts.rs | 41 +++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 216ea4bfaf7..5d0398b6a33 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -997,6 +997,7 @@ mod tests { use super::*; use crate::{text::TextFormat, Stroke}; use ecolor::Color32; + use emath::Align; fn jobs() -> Vec { vec![ @@ -1021,7 +1022,6 @@ mod tests { { let mut job = LayoutJob { first_row_min_height: 20.0, - justify: true, ..Default::default() }; job.append( @@ -1067,33 +1067,40 @@ mod tests { FontDefinitions::default(), ); - for job in jobs() { - let whole = GalleyCache::default().layout(&mut fonts, job.clone(), false); + for halign in [Align::Min, Align::Center, Align::Max] { + for justify in [false, true] { + for mut job in jobs() { + job.halign = halign; + job.justify = justify; - let split = GalleyCache::default().layout(&mut fonts, job.clone(), true); + let whole = GalleyCache::default().layout(&mut fonts, job.clone(), false); - for (i, row) in whole.rows.iter().enumerate() { - println!( + let split = GalleyCache::default().layout(&mut fonts, job.clone(), true); + + for (i, row) in whole.rows.iter().enumerate() { + println!( "Whole row {i}: section_index_at_start={}, first glyph section_index: {:?}", row.row.section_index_at_start, row.row.glyphs.first().map(|g| g.section_index) ); - } - for (i, row) in split.rows.iter().enumerate() { - println!( + } + for (i, row) in split.rows.iter().enumerate() { + println!( "Split row {i}: section_index_at_start={}, first glyph section_index: {:?}", row.row.section_index_at_start, row.row.glyphs.first().map(|g| g.section_index) ); + } + + // Don't compare for equaliity; but format with a specific precision and make sure we hit that. + similar_asserts::assert_eq!( + format!("{:#.5?}", split), + format!("{:#.5?}", whole), + "pixels_per_point: {pixels_per_point:.2}, input text: '{}'", + job.text + ); + } } - - // Don't compare for equaliity; but format with a specific precision and make sure we hit that. - similar_asserts::assert_eq!( - format!("{:#.5?}", split), - format!("{:#.5?}", whole), - "pixels_per_point: {pixels_per_point:.2}, input text: '{}'", - job.text - ); } } } From 9e44490b315a1035cea6659866af7c4b924d2311 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 30 Mar 2025 17:13:39 +0200 Subject: [PATCH 52/68] Refactor galley size rounding --- crates/epaint/src/text/fonts.rs | 2 +- crates/epaint/src/text/text_layout.rs | 35 +---- crates/epaint/src/text/text_layout_types.rs | 151 ++++++++++++-------- 3 files changed, 97 insertions(+), 91 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 5d0398b6a33..839ac97c569 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -860,7 +860,7 @@ impl GalleyCache { ..byte_range.end.saturating_sub(start).min(end - start); Some(LayoutSection { leading_space: if byte_range.end == 0 { - 0.0 // this sections is behind us (unused in this paragraph) + 0.0 // this section is behind us (unused in this paragraph) } else { leading_space }, diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 74262352e16..ff7a284fc23 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -669,18 +669,9 @@ fn galley_from_rows( } } - let mut rect = Rect::from_min_max(pos2(min_x, 0.0), pos2(max_x, cursor_y)); + let rect = Rect::from_min_max(pos2(min_x, 0.0), pos2(max_x, cursor_y)); - if job.round_output_to_gui { - for placed_row in &mut rows { - // NOTE: we round the size, but not the position, because the position should be _pixel_ aligned. - let row = Arc::make_mut(&mut placed_row.row); - row.size = row.size.round_ui(); - } - round_output_to_gui(&mut rect, &job); - } - - Galley { + let mut galley = Galley { job, rows, elided, @@ -689,25 +680,13 @@ fn galley_from_rows( num_vertices, num_indices, pixels_per_point: point_scale.pixels_per_point, - } -} - -pub(crate) fn round_output_to_gui(rect: &mut Rect, job: &LayoutJob) { - let did_exceed_wrap_width_by_a_lot = rect.width() > job.wrap.max_width + 1.0; - - *rect = rect.round_ui(); + }; - 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 by reporting that our width is wider than the wrap width. - } else { - // Make sure we don't report being wider than the wrap width the user picked: - rect.max.x = rect - .max - .x - .at_most(rect.min.x + job.wrap.max_width) - .floor_ui(); + if galley.job.round_output_to_gui { + galley.round_output_to_gui(); } + + galley } #[derive(Default)] diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 67f96e57a99..731f8521928 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -763,6 +763,95 @@ impl Galley { pub fn size(&self) -> Vec2 { self.rect.size() } + + pub(crate) fn round_output_to_gui(&mut self) { + for placed_row in &mut self.rows { + // NOTE: we round the size, but not the position, because the position should be _pixel_ aligned. + let row = Arc::make_mut(&mut placed_row.row); + row.size = row.size.round_ui(); + } + + let rect = &mut self.rect; + + let did_exceed_wrap_width_by_a_lot = rect.width() > self.job.wrap.max_width + 1.0; + + *rect = rect.round_ui(); + + 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 by reporting that our width is wider than the wrap width. + } else { + // Make sure we don't report being wider than the wrap width the user picked: + rect.max.x = rect + .max + .x + .at_most(rect.min.x + self.job.wrap.max_width) + .floor_ui(); + } + } + + /// Append each galley under the previous one. + pub fn concat(job: Arc, galleys: &[Arc], pixels_per_point: f32) -> Self { + let mut merged_galley = Self { + job, + rows: Vec::new(), + elided: false, + rect: Rect::ZERO, + mesh_bounds: Rect::NOTHING, + num_vertices: 0, + num_indices: 0, + pixels_per_point, + }; + + for (i, galley) in galleys.iter().enumerate() { + let current_y_offset = merged_galley.rect.height(); + + let mut rows = galley.rows.iter(); + // As documented in `Row::ends_with_newline`, a '\n' will always create a + // new `Row` immediately below the current one. Here it doesn't make sense + // for us to append this new row so we just ignore it. + let is_last_row = i + 1 == galleys.len(); + if !is_last_row && !galley.elided { + let popped = rows.next_back(); + debug_assert_eq!(popped.unwrap().row.glyphs.len(), 0, "Bug in Galley::concat"); + } + + merged_galley.rows.extend(rows.map(|placed_row| { + let new_pos = placed_row.pos + current_y_offset * Vec2::Y; + let new_pos = new_pos.round_to_pixels(pixels_per_point); + merged_galley.mesh_bounds = merged_galley + .mesh_bounds + .union(placed_row.visuals.mesh_bounds.translate(new_pos.to_vec2())); + merged_galley.rect = merged_galley + .rect + .union(Rect::from_min_size(new_pos, placed_row.size)); + + super::PlacedRow { + row: placed_row.row.clone(), + pos: new_pos, + } + })); + + merged_galley.num_vertices += galley.num_vertices; + merged_galley.num_indices += galley.num_indices; + // Note that if `galley.elided` is true this will be the last `Galley` in + // the vector and the loop will end. + merged_galley.elided |= galley.elided; + } + + for placed in &mut merged_galley.rows { + let row = Arc::make_mut(&mut placed.row); + if let Some(first_glyph) = row.glyphs.first_mut() { + row.section_index_at_start = first_glyph.section_index; + } + } + + if merged_galley.job.round_output_to_gui { + merged_galley.round_output_to_gui(); + } + + merged_galley + } } impl AsRef for Galley { @@ -869,68 +958,6 @@ impl Galley { cursor } - - /// Append each galley under the previous one. - pub fn concat(job: Arc, galleys: &[Arc], pixels_per_point: f32) -> Self { - let mut merged_galley = Self { - job, - rows: Vec::new(), - elided: false, - rect: Rect::ZERO, - mesh_bounds: Rect::NOTHING, - num_vertices: 0, - num_indices: 0, - pixels_per_point, - }; - - for (i, galley) in galleys.iter().enumerate() { - let current_y_offset = merged_galley.rect.height(); - - let mut rows = galley.rows.iter(); - // As documented in `Row::ends_with_newline`, a '\n' will always create a - // new `Row` immediately below the current one. Here it doesn't make sense - // for us to append this new row so we just ignore it. - let is_last_row = i + 1 == galleys.len(); - if !is_last_row && !galley.elided { - let popped = rows.next_back(); - debug_assert_eq!(popped.unwrap().row.glyphs.len(), 0, "Bug in Galley::concat"); - } - - merged_galley.rows.extend(rows.map(|placed_row| { - let new_pos = placed_row.pos + current_y_offset * Vec2::Y; - let new_pos = new_pos.round_to_pixels(pixels_per_point); - merged_galley.mesh_bounds = merged_galley - .mesh_bounds - .union(placed_row.visuals.mesh_bounds.translate(new_pos.to_vec2())); - merged_galley.rect = merged_galley - .rect - .union(Rect::from_min_size(new_pos, placed_row.size)); - - super::PlacedRow { - row: placed_row.row.clone(), - pos: new_pos, - } - })); - - merged_galley.num_vertices += galley.num_vertices; - merged_galley.num_indices += galley.num_indices; - // Note that if `galley.elided` is true this will be the last `Galley` in - // the vector and the loop will end. - merged_galley.elided |= galley.elided; - } - - for placed in &mut merged_galley.rows { - let row = Arc::make_mut(&mut placed.row); - if let Some(first_glyph) = row.glyphs.first_mut() { - row.section_index_at_start = first_glyph.section_index; - } - } - - if merged_galley.job.round_output_to_gui { - super::round_output_to_gui(&mut merged_galley.rect, &merged_galley.job); - } - merged_galley - } } /// ## Cursor positions From 1b1f0726ce97f0732fb4a96e96c208185b7000e4 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 30 Mar 2025 17:23:21 +0200 Subject: [PATCH 53/68] Simplify Galley bounding rect calulcation --- crates/epaint/src/text/text_layout.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index ff7a284fc23..d88d547836c 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -611,8 +611,7 @@ fn galley_from_rows( ) -> Galley { let mut first_row_min_height = job.first_row_min_height; let mut cursor_y = 0.0; - let mut min_x: f32 = 0.0; - let mut max_x: f32 = 0.0; + for placed_row in &mut rows { let mut max_row_height = first_row_min_height.max(placed_row.rect().height()); let row = Arc::make_mut(&mut placed_row.row); @@ -640,21 +639,23 @@ fn galley_from_rows( placed_row.pos.y = cursor_y; row.size.y = max_row_height; - min_x = min_x.min(placed_row.rect().min.x); - max_x = max_x.max(placed_row.rect().max.x); cursor_y += max_row_height; cursor_y = point_scale.round_to_pixel(cursor_y); // TODO(emilk): it would be better to do the calculations in pixels instead. } let format_summary = format_summary(&job); + let mut rect = Rect::ZERO; let mut mesh_bounds = Rect::NOTHING; let mut num_vertices = 0; let mut num_indices = 0; for placed_row in &mut rows { + rect = rect.union(placed_row.rect()); + let row = Arc::make_mut(&mut placed_row.row); row.visuals = tessellate_row(point_scale, &job, &format_summary, row); + mesh_bounds = mesh_bounds.union(row.visuals.mesh_bounds.translate(placed_row.pos.to_vec2())); num_vertices += row.visuals.mesh.vertices.len(); @@ -669,8 +670,6 @@ fn galley_from_rows( } } - let rect = Rect::from_min_max(pos2(min_x, 0.0), pos2(max_x, cursor_y)); - let mut galley = Galley { job, rows, From 20667a059226de1b92ee51005a030a0b482e622e Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 30 Mar 2025 17:52:13 +0200 Subject: [PATCH 54/68] put `PlacedRow::pos` first in struct --- crates/epaint/src/shapes/text_shape.rs | 2 +- crates/epaint/src/text/text_layout.rs | 12 ++++++------ crates/epaint/src/text/text_layout_types.rs | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/epaint/src/shapes/text_shape.rs b/crates/epaint/src/shapes/text_shape.rs index d7fe8a4794c..e88213b932a 100644 --- a/crates/epaint/src/shapes/text_shape.rs +++ b/crates/epaint/src/shapes/text_shape.rs @@ -119,7 +119,7 @@ impl TextShape { *rect = transform.scaling * *rect; *mesh_bounds = transform.scaling * *mesh_bounds; - for text::PlacedRow { row, pos } in rows { + for text::PlacedRow { pos, row } in rows { *pos *= transform.scaling; let text::Row { diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index d88d547836c..1012fbd8451 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -208,6 +208,7 @@ fn rows_from_paragraphs( if paragraph.glyphs.is_empty() { rows.push(PlacedRow { + pos: Pos2::ZERO, row: Arc::new(Row { section_index_at_start: paragraph.section_index_at_start, glyphs: vec![], @@ -215,13 +216,13 @@ fn rows_from_paragraphs( size: vec2(0.0, paragraph.empty_paragraph_height), ends_with_newline: !is_last_paragraph, }), - pos: pos2(0.0, 0.0), }); } else { let paragraph_max_x = paragraph.glyphs.last().unwrap().max_x(); if paragraph_max_x <= job.effective_wrap_width() { // Early-out optimization: the whole paragraph fits on one row. rows.push(PlacedRow { + pos: pos2(0.0, f32::NAN), row: Arc::new(Row { section_index_at_start: paragraph.section_index_at_start, glyphs: paragraph.glyphs, @@ -229,7 +230,6 @@ fn rows_from_paragraphs( size: vec2(paragraph_max_x, 0.0), ends_with_newline: !is_last_paragraph, }), - pos: pos2(0.0, f32::NAN), }); } else { line_break(¶graph, job, &mut rows, elided); @@ -275,14 +275,14 @@ fn line_break( // Allow the first row to be completely empty, because we know there will be more space on the next row: // TODO(emilk): this records the height of this first row as zero, though that is probably fine since first_row_indentation usually comes with a first_row_min_height. out_rows.push(PlacedRow { + pos: pos2(0.0, f32::NAN), row: Arc::new(Row { section_index_at_start: paragraph.section_index_at_start, glyphs: vec![], visuals: Default::default(), - size: vec2(0.0, 0.0), + size: Vec2::ZERO, ends_with_newline: false, }), - pos: pos2(0.0, f32::NAN), }); row_start_x += first_row_indentation; first_row_indentation = 0.0; @@ -301,6 +301,7 @@ fn line_break( let paragraph_max_x = glyphs.last().unwrap().max_x(); out_rows.push(PlacedRow { + pos: pos2(0.0, f32::NAN), row: Arc::new(Row { section_index_at_start, glyphs, @@ -308,7 +309,6 @@ fn line_break( size: vec2(paragraph_max_x, 0.0), ends_with_newline: false, }), - pos: pos2(0.0, f32::NAN), }); // Start a new row: @@ -343,6 +343,7 @@ fn line_break( let paragraph_max_x = glyphs.last().unwrap().max_x(); out_rows.push(PlacedRow { + pos: pos2(paragraph_min_x, 0.0), row: Arc::new(Row { section_index_at_start, glyphs, @@ -350,7 +351,6 @@ fn line_break( size: vec2(paragraph_max_x - paragraph_min_x, 0.0), ends_with_newline: false, }), - pos: pos2(paragraph_min_x, 0.0), }); } } diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 731f8521928..0e59a215bd4 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -550,13 +550,13 @@ pub struct Galley { #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct PlacedRow { - /// The underlying row unpositioned [`Row`]. - pub row: Arc, - /// The position of this [`Row`] relative to the galley. /// /// This is rounded to the closest _pixel_ in order to produce crisp, pixel-perfect text. pub pos: Pos2, + + /// The underlying row unpositioned [`Row`]. + pub row: Arc, } impl PlacedRow { @@ -827,8 +827,8 @@ impl Galley { .union(Rect::from_min_size(new_pos, placed_row.size)); super::PlacedRow { - row: placed_row.row.clone(), pos: new_pos, + row: placed_row.row.clone(), } })); From 5f01b0dc9d2d4e029fd7bfba9a38df7e14ddf5d4 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 30 Mar 2025 17:52:29 +0200 Subject: [PATCH 55/68] Round glyph to closes pixel --- crates/epaint/src/text/text_layout.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 1012fbd8451..10e078ebb3d 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -634,6 +634,8 @@ fn galley_from_rows( // When mixing different `FontImpl` (e.g. latin and emojis), // we always center the difference: + 0.5 * (glyph.font_height - glyph.font_impl_height); + + glyph.pos.y = point_scale.round_to_pixel(glyph.pos.y); } placed_row.pos.y = cursor_y; From f4fca5a117f294519134ae792d59ca9a13bc866a Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 30 Mar 2025 17:52:48 +0200 Subject: [PATCH 56/68] Better handling of invisible underlines/strikethrough --- crates/epaint/src/text/text_layout.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 10e078ebb3d..e41074774ba 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -884,7 +884,7 @@ fn add_row_hline( let (stroke, mut y) = stroke_and_y(glyph); stroke.round_center_to_pixel(point_scale.pixels_per_point, &mut y); - if stroke == Stroke::NONE { + if stroke.is_empty() { end_line(line_start.take(), last_right_x); } else if let Some((existing_stroke, start)) = line_start { if existing_stroke == stroke && start.y == y { From d74bee536f7c57d83014b759b18a0ddef8725552 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 30 Mar 2025 18:54:15 +0200 Subject: [PATCH 57/68] Fix first performance performance problem --- crates/epaint/src/text/fonts.rs | 73 +++++++++++++-------- crates/epaint/src/text/text_layout.rs | 2 + crates/epaint/src/text/text_layout_types.rs | 2 + 3 files changed, 48 insertions(+), 29 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 839ac97c569..f92c5c10371 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -813,6 +813,7 @@ impl GalleyCache { ) -> Galley { profiling::function_scope!(); + let mut current_section = 0; let mut start = 0; let mut max_rows_remaining = job.wrap.max_rows; let mut galleys = Vec::new(); @@ -841,36 +842,49 @@ impl GalleyCache { round_output_to_gui: job.round_output_to_gui, }; - // Keep all (previous) sections (so the correct `section_index` is outputted), - // but shift their byte ranges: - paragraph_job.sections = job - .sections - .iter() - .cloned() - .filter_map(|section| { - let LayoutSection { - leading_space, - byte_range, - format, - } = section; - if end <= byte_range.start { - None - } else { - let byte_range = byte_range.start.saturating_sub(start) - ..byte_range.end.saturating_sub(start).min(end - start); - Some(LayoutSection { - leading_space: if byte_range.end == 0 { - 0.0 // this section is behind us (unused in this paragraph) - } else { - leading_space - }, - byte_range, - format, - }) - } - }) - .collect(); + // Add overlapping sections: + for section in &job.sections[current_section..job.sections.len()] { + let LayoutSection { + leading_space, + byte_range: section_range, + format, + } = section; + + // dbg!(section_index, section_range); + + // `start` and `end` are the byte range of the current paragraph. + // How does the current section overlap with the paragraph range? + + if section_range.end <= start { + // The section is behind us + current_section += 1; + } else if end <= section_range.start { + break; // Haven't reached this one yet. + } else { + // Section range overlaps with paragraph range + debug_assert!( + section_range.start < section_range.end, + "Bad byte_range: {section_range:?}" + ); + let new_range = section_range.start.saturating_sub(start) + ..(section_range.end.at_most(end)).saturating_sub(start); + debug_assert!( + new_range.start <= new_range.end, + "Bad new section range: {new_range:?}" + ); + paragraph_job.sections.push(LayoutSection { + leading_space: if start <= new_range.start { + *leading_space + } else { + 0.0 + }, + byte_range: new_range, + format: format.clone(), + }); + } + } + // TODO(emilk): we could lay out each paragraph in parallel to get a nice speedup on multicore machines. let galley = self.layout(fonts, paragraph_job, false); // This will prevent us from invalidating cache entries unnecessarily: @@ -891,6 +905,7 @@ impl GalleyCache { start = end; } + // TODO: adjust section_index in the concatted galley Galley::concat(job, &galleys, fonts.pixels_per_point) } diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index e41074774ba..26bf6860c0a 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -69,6 +69,8 @@ impl Paragraph { /// In most cases you should use [`crate::Fonts::layout_job`] instead /// since that memoizes the input, making subsequent layouting of the same text much faster. pub fn layout(fonts: &mut FontsImpl, job: Arc) -> Galley { + profiling::function_scope!(); + if job.wrap.max_rows == 0 { // Early-out: no text return Galley { diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 0e59a215bd4..4e38e7e648b 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -792,6 +792,8 @@ impl Galley { /// Append each galley under the previous one. pub fn concat(job: Arc, galleys: &[Arc], pixels_per_point: f32) -> Self { + profiling::function_scope!(); + let mut merged_galley = Self { job, rows: Vec::new(), From 22ea9e91bd95d2d39bb8e1661478d9437c965d91 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 30 Mar 2025 19:48:10 +0200 Subject: [PATCH 58/68] Optimize `concat` --- crates/epaint/src/text/text_layout_types.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 4e38e7e648b..5ee1fe14276 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -842,9 +842,13 @@ impl Galley { } for placed in &mut merged_galley.rows { - let row = Arc::make_mut(&mut placed.row); - if let Some(first_glyph) = row.glyphs.first_mut() { - row.section_index_at_start = first_glyph.section_index; + let section_index_at_start = placed.row.glyphs.first().map(|g| g.section_index); + if let Some(section_index_at_start) = section_index_at_start { + if placed.row.section_index_at_start != section_index_at_start { + // This `make_mut` is slow, so only do it when necessary. + let row = Arc::make_mut(&mut placed.row); + row.section_index_at_start = section_index_at_start; + } } } From 8a33a6a6ce48fb95e5e8334e725e2d2794b6d804 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 30 Mar 2025 20:03:47 +0200 Subject: [PATCH 59/68] Make the section index private so we don't need to calculate it corectly --- crates/epaint/src/text/fonts.rs | 16 ++++++------- crates/epaint/src/text/text_layout.rs | 9 +++----- crates/epaint/src/text/text_layout_types.rs | 25 +++++++++------------ 3 files changed, 22 insertions(+), 28 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index f92c5c10371..c78ca99a645 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -1094,17 +1094,17 @@ mod tests { for (i, row) in whole.rows.iter().enumerate() { println!( - "Whole row {i}: section_index_at_start={}, first glyph section_index: {:?}", - row.row.section_index_at_start, - row.row.glyphs.first().map(|g| g.section_index) - ); + "Whole row {i}: section_index_at_start={}, first glyph section_index: {:?}", + row.row.section_index_at_start, + row.row.glyphs.first().map(|g| g.section_index) + ); } for (i, row) in split.rows.iter().enumerate() { println!( - "Split row {i}: section_index_at_start={}, first glyph section_index: {:?}", - row.row.section_index_at_start, - row.row.glyphs.first().map(|g| g.section_index) - ); + "Split row {i}: section_index_at_start={}, first glyph section_index: {:?}", + row.row.section_index_at_start, + row.row.glyphs.first().map(|g| g.section_index) + ); } // Don't compare for equaliity; but format with a specific precision and make sure we hit that. diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 26bf6860c0a..b2dba96fce0 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -664,13 +664,10 @@ fn galley_from_rows( mesh_bounds.union(row.visuals.mesh_bounds.translate(placed_row.pos.to_vec2())); num_vertices += row.visuals.mesh.vertices.len(); num_indices += row.visuals.mesh.indices.len(); - } - // Fill in correct section_index_at_start for each row: - for placed in &mut rows { - let row = Arc::make_mut(&mut placed.row); - if let Some(first_glyph) = row.glyphs.first_mut() { - row.section_index_at_start = first_glyph.section_index; + row.section_index_at_start = u32::MAX; // No longer in use. + for glyph in &mut row.glyphs { + glyph.section_index = u32::MAX; // No longer in use. } } diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 5ee1fe14276..c1e54b35a6e 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -578,8 +578,12 @@ impl std::ops::Deref for PlacedRow { #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct Row { - /// This is included in case there are no glyphs - pub section_index_at_start: u32, + /// This is included in case there are no glyphs. + /// + /// Only used during layout, then set to an invalid value in order to + /// enable the paragraph-concat optimization path without having to + /// adjust `section_index` when concatting. + pub(crate) section_index_at_start: u32, /// One for each `char`. pub glyphs: Vec, @@ -669,7 +673,11 @@ pub struct Glyph { pub uv_rect: UvRect, /// Index into [`LayoutJob::sections`]. Decides color etc. - pub section_index: u32, + /// + /// Only used during layout, then set to an invalid value in order to + /// enable the paragraph-concat optimization path without having to + /// adjust `section_index` when concatting. + pub(crate) section_index: u32, } impl Glyph { @@ -841,17 +849,6 @@ impl Galley { merged_galley.elided |= galley.elided; } - for placed in &mut merged_galley.rows { - let section_index_at_start = placed.row.glyphs.first().map(|g| g.section_index); - if let Some(section_index_at_start) = section_index_at_start { - if placed.row.section_index_at_start != section_index_at_start { - // This `make_mut` is slow, so only do it when necessary. - let row = Arc::make_mut(&mut placed.row); - row.section_index_at_start = section_index_at_start; - } - } - } - if merged_galley.job.round_output_to_gui { merged_galley.round_output_to_gui(); } From 59442ca4d5b893a8469739942aac2f0861ac9e97 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 30 Mar 2025 20:05:09 +0200 Subject: [PATCH 60/68] Make `Debug` for `Rect` heed precision --- crates/emath/src/rect.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/emath/src/rect.rs b/crates/emath/src/rect.rs index 521b6f33f43..00bed04f0f8 100644 --- a/crates/emath/src/rect.rs +++ b/crates/emath/src/rect.rs @@ -710,7 +710,11 @@ impl Rect { impl fmt::Debug for Rect { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "[{:?} - {:?}]", self.min, self.max) + if let Some(precision) = f.precision() { + write!(f, "[{1:.0$?} - {2:.0$?}]", precision, self.min, self.max) + } else { + write!(f, "[{:?} - {:?}]", self.min, self.max) + } } } From a72e10ab23ecefaadc303f5779613efdd3a12b28 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 30 Mar 2025 20:18:06 +0200 Subject: [PATCH 61/68] Remove outdated TODO --- crates/epaint/src/text/fonts.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index c78ca99a645..4a7da64a30c 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -905,7 +905,6 @@ impl GalleyCache { start = end; } - // TODO: adjust section_index in the concatted galley Galley::concat(job, &galleys, fonts.pixels_per_point) } From 00ed9c1547a13f5d94881975e3872b68d5457686 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 30 Mar 2025 20:29:52 +0200 Subject: [PATCH 62/68] Be more forgiving in our comparison --- crates/epaint/src/text/fonts.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 4a7da64a30c..eebda16bede 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -1107,9 +1107,10 @@ mod tests { } // Don't compare for equaliity; but format with a specific precision and make sure we hit that. + // NOTE: we use a rather low precision, because as long as we're within a pixel I think it's good enough. similar_asserts::assert_eq!( - format!("{:#.5?}", split), - format!("{:#.5?}", whole), + format!("{:#.1?}", split), + format!("{:#.1?}", whole), "pixels_per_point: {pixels_per_point:.2}, input text: '{}'", job.text ); From 48e4e8ca9270a4c91e509890fc5488b02ebbcdb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20G=C5=82uchowski?= Date: Tue, 1 Apr 2025 15:29:06 +0200 Subject: [PATCH 63/68] Don't round row sizes again in `Galley::concat` --- crates/epaint/src/text/text_layout_types.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index c1e54b35a6e..fd80a3a369c 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -774,9 +774,11 @@ impl Galley { pub(crate) fn round_output_to_gui(&mut self) { for placed_row in &mut self.rows { - // NOTE: we round the size, but not the position, because the position should be _pixel_ aligned. - let row = Arc::make_mut(&mut placed_row.row); - row.size = row.size.round_ui(); + // Optimization: only call `make_mut` if necessary (can cause a deep clone) + let rounded_size = placed_row.row.size.round_ui(); + if placed_row.row.size != rounded_size { + Arc::make_mut(&mut placed_row.row).size = rounded_size; + } } let rect = &mut self.rect; From ff0e7d03f4420bde17bda1038ee38f59cea44b10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20G=C5=82uchowski?= Date: Tue, 1 Apr 2025 15:43:47 +0200 Subject: [PATCH 64/68] Don't add merged `Galley` to galley cache --- crates/epaint/src/text/fonts.rs | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index eebda16bede..ead6088510a 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -783,15 +783,10 @@ impl GalleyCache { let job = Arc::new(job); if allow_split_paragraphs && should_cache_each_paragraph_individually(&job) { let galley = self.layout_each_paragraph_individuallly(fonts, job); - let galley = Arc::new(galley); - self.cache.insert( - hash, - CachedGalley { - last_used: self.generation, - galley: galley.clone(), - }, - ); - galley + // TODO(afishhh): This Galley cannot be added directly into the cache without taking + // extra precautions to make sure all component paragraph Galleys are not invalidated + // immediately next frame (since their `last_used` will not be updated). + Arc::new(galley) } else { let galley = super::layout(fonts, job); let galley = Arc::new(galley); @@ -850,8 +845,6 @@ impl GalleyCache { format, } = section; - // dbg!(section_index, section_range); - // `start` and `end` are the byte range of the current paragraph. // How does the current section overlap with the paragraph range? From 9d61f9c92708b86ee09f9808cf19d0f9c0458159 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20G=C5=82uchowski?= Date: Tue, 1 Apr 2025 16:08:51 +0200 Subject: [PATCH 65/68] Fix some typos --- crates/epaint/src/text/fonts.rs | 2 +- crates/epaint/src/text/text_layout_types.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index ead6088510a..a8d6469ef23 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -917,7 +917,7 @@ impl GalleyCache { /// If true, lay out and cache each paragraph (sections separated by newlines) individually. /// -/// This makes it much faster to re-lauout the full tet when only a portion of it has changed since last frame, i.e. when editing somewhere in a file with thousands of lines/paragraphs. +/// This makes it much faster to re-layout the full text when only a portion of it has changed since last frame, i.e. when editing somewhere in a file with thousands of lines/paragraphs. fn should_cache_each_paragraph_individually(job: &LayoutJob) -> bool { // We currently don't support this elided text, i.e. when `max_rows` is set. // Most often, elided text is elided to one row, diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index fd80a3a369c..4bd15d3e3e5 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -555,7 +555,7 @@ pub struct PlacedRow { /// This is rounded to the closest _pixel_ in order to produce crisp, pixel-perfect text. pub pos: Pos2, - /// The underlying row unpositioned [`Row`]. + /// The underlying unpositioned [`Row`]. pub row: Arc, } From ff9dfdeec106524cbd6e1bf78e626370608329a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20G=C5=82uchowski?= Date: Tue, 1 Apr 2025 18:14:39 +0200 Subject: [PATCH 66/68] Keep track of dependencies between `GalleyCache`s and keep them alive --- crates/epaint/src/text/fonts.rs | 52 ++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index a8d6469ef23..d7538a3c1ff 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -728,6 +728,10 @@ impl FontsImpl { struct CachedGalley { /// When it was last used last_used: u32, + /// Hashes of all other entries this one depends on for quick re-layout. + /// Their `last_used`s should be updated alongside this one to make sure they're + /// not evicted. + children: Option>, galley: Arc, } @@ -739,12 +743,12 @@ struct GalleyCache { } impl GalleyCache { - fn layout( + fn layout_internal( &mut self, fonts: &mut FontsImpl, mut job: LayoutJob, allow_split_paragraphs: bool, - ) -> Arc { + ) -> (u64, Arc) { if job.wrap.max_width.is_finite() { // Protect against rounding errors in egui layout code. @@ -772,32 +776,56 @@ impl GalleyCache { let hash = crate::util::hash(&job); // TODO(emilk): even faster hasher? - match self.cache.entry(hash) { + (hash, match self.cache.entry(hash) { std::collections::hash_map::Entry::Occupied(entry) => { // The job was found in cache - no need to re-layout. let cached = entry.into_mut(); cached.last_used = self.generation; - cached.galley.clone() + + let galley = cached.galley.clone(); + if let Some(children) = &cached.children { + for child_hash in children.clone().iter() { + if let Some(cached_child) = self.cache.get_mut(child_hash) { + cached_child.last_used = self.generation; + } + } + } + + galley } std::collections::hash_map::Entry::Vacant(entry) => { let job = Arc::new(job); if allow_split_paragraphs && should_cache_each_paragraph_individually(&job) { - let galley = self.layout_each_paragraph_individuallly(fonts, job); - // TODO(afishhh): This Galley cannot be added directly into the cache without taking - // extra precautions to make sure all component paragraph Galleys are not invalidated - // immediately next frame (since their `last_used` will not be updated). - Arc::new(galley) + let mut child_hashes = Vec::new(); + let galley = self.layout_each_paragraph_individuallly(fonts, job, &mut child_hashes); + let galley = Arc::new(galley); + self.cache.insert(hash, CachedGalley { + last_used: self.generation, + children: Some(child_hashes.into()), + galley: galley.clone() + }); + galley } else { let galley = super::layout(fonts, job); let galley = Arc::new(galley); entry.insert(CachedGalley { last_used: self.generation, + children: None, galley: galley.clone(), }); galley } } - } + }) + } + + fn layout( + &mut self, + fonts: &mut FontsImpl, + job: LayoutJob, + allow_split_paragraphs: bool, + ) -> Arc { + self.layout_internal(fonts, job, allow_split_paragraphs).1 } /// Split on `\n` and lay out (and cache) each paragraph individually. @@ -805,6 +833,7 @@ impl GalleyCache { &mut self, fonts: &mut FontsImpl, job: Arc, + child_hashes: &mut Vec ) -> Galley { profiling::function_scope!(); @@ -878,7 +907,8 @@ impl GalleyCache { } // TODO(emilk): we could lay out each paragraph in parallel to get a nice speedup on multicore machines. - let galley = self.layout(fonts, paragraph_job, false); + let (hash, galley) = self.layout_internal(fonts, paragraph_job, false); + child_hashes.push(hash); // This will prevent us from invalidating cache entries unnecessarily: if max_rows_remaining != usize::MAX { From 6a5f6d3f866835883fa66f0c5e8c24ea89e41d60 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 1 Apr 2025 18:41:39 +0200 Subject: [PATCH 67/68] cargo fmt --- crates/epaint/src/text/fonts.rs | 81 ++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 37 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index d7538a3c1ff..f6aa32af7de 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -776,47 +776,54 @@ impl GalleyCache { let hash = crate::util::hash(&job); // TODO(emilk): even faster hasher? - (hash, match self.cache.entry(hash) { - std::collections::hash_map::Entry::Occupied(entry) => { - // The job was found in cache - no need to re-layout. - let cached = entry.into_mut(); - cached.last_used = self.generation; - - let galley = cached.galley.clone(); - if let Some(children) = &cached.children { - for child_hash in children.clone().iter() { - if let Some(cached_child) = self.cache.get_mut(child_hash) { - cached_child.last_used = self.generation; + ( + hash, + match self.cache.entry(hash) { + std::collections::hash_map::Entry::Occupied(entry) => { + // The job was found in cache - no need to re-layout. + let cached = entry.into_mut(); + cached.last_used = self.generation; + + let galley = cached.galley.clone(); + if let Some(children) = &cached.children { + for child_hash in children.clone().iter() { + if let Some(cached_child) = self.cache.get_mut(child_hash) { + cached_child.last_used = self.generation; + } } } - } - galley - } - std::collections::hash_map::Entry::Vacant(entry) => { - let job = Arc::new(job); - if allow_split_paragraphs && should_cache_each_paragraph_individually(&job) { - let mut child_hashes = Vec::new(); - let galley = self.layout_each_paragraph_individuallly(fonts, job, &mut child_hashes); - let galley = Arc::new(galley); - self.cache.insert(hash, CachedGalley { - last_used: self.generation, - children: Some(child_hashes.into()), - galley: galley.clone() - }); - galley - } else { - let galley = super::layout(fonts, job); - let galley = Arc::new(galley); - entry.insert(CachedGalley { - last_used: self.generation, - children: None, - galley: galley.clone(), - }); galley } - } - }) + std::collections::hash_map::Entry::Vacant(entry) => { + let job = Arc::new(job); + if allow_split_paragraphs && should_cache_each_paragraph_individually(&job) { + let mut child_hashes = Vec::new(); + let galley = + self.layout_each_paragraph_individuallly(fonts, job, &mut child_hashes); + let galley = Arc::new(galley); + self.cache.insert( + hash, + CachedGalley { + last_used: self.generation, + children: Some(child_hashes.into()), + galley: galley.clone(), + }, + ); + galley + } else { + let galley = super::layout(fonts, job); + let galley = Arc::new(galley); + entry.insert(CachedGalley { + last_used: self.generation, + children: None, + galley: galley.clone(), + }); + galley + } + } + }, + ) } fn layout( @@ -833,7 +840,7 @@ impl GalleyCache { &mut self, fonts: &mut FontsImpl, job: Arc, - child_hashes: &mut Vec + child_hashes: &mut Vec, ) -> Galley { profiling::function_scope!(); From e71d09aafd4e856a4679bac19e13a68ae28a6e3a Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 1 Apr 2025 18:49:36 +0200 Subject: [PATCH 68/68] Code cleanup --- crates/epaint/src/text/fonts.rs | 108 ++++++++++++++++++-------------- 1 file changed, 60 insertions(+), 48 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index f6aa32af7de..bfa854680e5 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -728,10 +728,12 @@ impl FontsImpl { struct CachedGalley { /// When it was last used last_used: u32, + /// Hashes of all other entries this one depends on for quick re-layout. /// Their `last_used`s should be updated alongside this one to make sure they're /// not evicted. children: Option>, + galley: Arc, } @@ -776,54 +778,64 @@ impl GalleyCache { let hash = crate::util::hash(&job); // TODO(emilk): even faster hasher? - ( - hash, - match self.cache.entry(hash) { - std::collections::hash_map::Entry::Occupied(entry) => { - // The job was found in cache - no need to re-layout. - let cached = entry.into_mut(); - cached.last_used = self.generation; - - let galley = cached.galley.clone(); - if let Some(children) = &cached.children { - for child_hash in children.clone().iter() { - if let Some(cached_child) = self.cache.get_mut(child_hash) { - cached_child.last_used = self.generation; - } + let galley = match self.cache.entry(hash) { + std::collections::hash_map::Entry::Occupied(entry) => { + // The job was found in cache - no need to re-layout. + let cached = entry.into_mut(); + cached.last_used = self.generation; + + let galley = cached.galley.clone(); + if let Some(children) = &cached.children { + // The point of `allow_split_paragraphs` is to split large jobs into paragraph, + // and then cache each paragraph individually. + // That way, if we edit a single paragraph, only that paragraph will be re-layouted. + // For that to work we need to keep all the child/paragraph + // galleys alive while the parent galley is alive: + for child_hash in children.clone().iter() { + if let Some(cached_child) = self.cache.get_mut(child_hash) { + cached_child.last_used = self.generation; } } - - galley } - std::collections::hash_map::Entry::Vacant(entry) => { - let job = Arc::new(job); - if allow_split_paragraphs && should_cache_each_paragraph_individually(&job) { - let mut child_hashes = Vec::new(); - let galley = - self.layout_each_paragraph_individuallly(fonts, job, &mut child_hashes); - let galley = Arc::new(galley); - self.cache.insert( - hash, - CachedGalley { - last_used: self.generation, - children: Some(child_hashes.into()), - galley: galley.clone(), - }, - ); - galley - } else { - let galley = super::layout(fonts, job); - let galley = Arc::new(galley); - entry.insert(CachedGalley { + + galley + } + std::collections::hash_map::Entry::Vacant(entry) => { + let job = Arc::new(job); + if allow_split_paragraphs && should_cache_each_paragraph_individually(&job) { + let (child_galleys, child_hashes) = + self.layout_each_paragraph_individuallly(fonts, &job); + debug_assert_eq!( + child_hashes.len(), + child_galleys.len(), + "Bug in `layout_each_paragraph_individuallly`" + ); + let galley = + Arc::new(Galley::concat(job, &child_galleys, fonts.pixels_per_point)); + + self.cache.insert( + hash, + CachedGalley { last_used: self.generation, - children: None, + children: Some(child_hashes.into()), galley: galley.clone(), - }); - galley - } + }, + ); + galley + } else { + let galley = super::layout(fonts, job); + let galley = Arc::new(galley); + entry.insert(CachedGalley { + last_used: self.generation, + children: None, + galley: galley.clone(), + }); + galley } - }, - ) + } + }; + + (hash, galley) } fn layout( @@ -839,15 +851,15 @@ impl GalleyCache { fn layout_each_paragraph_individuallly( &mut self, fonts: &mut FontsImpl, - job: Arc, - child_hashes: &mut Vec, - ) -> Galley { + job: &LayoutJob, + ) -> (Vec>, Vec) { profiling::function_scope!(); let mut current_section = 0; let mut start = 0; let mut max_rows_remaining = job.wrap.max_rows; - let mut galleys = Vec::new(); + let mut child_galleys = Vec::new(); + let mut child_hashes = Vec::new(); while start < job.text.len() { let is_first_paragraph = start == 0; @@ -927,7 +939,7 @@ impl GalleyCache { } let elided = galley.elided; - galleys.push(galley); + child_galleys.push(galley); if elided { break; } @@ -935,7 +947,7 @@ impl GalleyCache { start = end; } - Galley::concat(job, &galleys, fonts.pixels_per_point) + (child_galleys, child_hashes) } pub fn num_galleys_in_cache(&self) -> usize {