Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

epaint: Break up memozation of text layouts into paragraphs #4000

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

dacid44
Copy link

@dacid44 dacid44 commented Feb 7, 2024

Draft for now, I just want to confirm that I'm on the right track for the general logic, and make sure I know about the edge cases when splitting LayoutJobs and merging Galleys. It looks like I will need to:

  • Filter which LayoutSections go into each LayoutJob, and change their offsets
  • Possibly make sure each LayoutJob ends with a newline
  • When merging Galleys, merge the rects and mesh_bounds of each Galley together, and possibly offset them?
  • Make sure the elided field makes it onto the full Galley if set on any paragraph, and stop adding the following Galleys if that field is set
  • Sum num_vertices and num_indices
  • Anything else?

The code compiles right now, but does not pass cargo cranky and panics immediately when that codepath is run (byte index out of bounds, probably due to not filtering and offsetting LayoutSections. Also the todo!().)

@dacid44
Copy link
Author

dacid44 commented Feb 8, 2024

This is currently broken. No matter how many things I try to add the y-offset of the previous galleys to, all of the paragraphs render in the top left corner of the text box, on top of each other. I'd appreciate any insights as to why this might be.
image

Also, splitting up the LayoutJob (or at least, my current implementation of it) doesn't preserve the indices of the sections. Will I have to offset Glyph.section_index to correct this as well?

@haricot
Copy link
Contributor

haricot commented Feb 9, 2024

My two cents:

The current Paragraph struct is misnamed, as it refers to a block of text that will be split into lines, not a typographic paragraph "/n/n". TextBlock would be a more appropriate name.

When generating the TextBlocks, only the X positioning of glyphs is calculated. The Y positions are ignored at this stage.

Line breaks happen later when converting TextBlocks to Rows, based on the wrap width. A TextBlock can span multiple Rows if it exceeds the wrap width.

To properly position the wrapped Rows vertically, each TextBlock needs a paragraph index that is incremented for each one.

When generating the Rows in line_break(), the paragraph (TextBlock ) index can be used to increment the Y position, so Rows from the same TextBlock are offset vertically.

This prevents the issue of Rows overlapping each other at the same Y position when breaking a TextBlock into multiple Rows.

So in summary, the TextBlocks are intermediate storage of positioned glyphs before line wrapping. The line breaks and Y positioning happen in a later stage when converting TextBlocks to Rows.

@dacid44
Copy link
Author

dacid44 commented Feb 15, 2024

Huh, it says your comment was posted on the 10th, but I've been regularly checking this PR and it didn't appear for me until today. Weird.

Anyway, it looks like all use of the Paragraph struct seems to happen with the layout() function in text_layout.rs. I hadn't actually even looked deep enough to see that the Paragraph struct existed, as I was mostly treating that function as a black box, and trying to merge Galleys together after they are returned from that function. Are you recommending modifying it? The original proposal was to still cache rendering at the Galley level, and just to cache smaller Galleys, but it could actually be better to memoize further down.

My main question is: what is the frame of reference for the rendering positions inside a Galley? If I want to position a Galley directly below another one and merge their rows (and thus glyphs) together, what do I need to change to position them properly?

@haricot
Copy link
Contributor

haricot commented Feb 16, 2024

Maybe instead of using galley_from_rows function here in layout function we could use another function galley_from_rows_with_previous_offset_y or galley_from_rows_with_full_previous_len_rows ,
so that the paragraphs composed of rows do not overlap (I allow myself to temporarily call them "textBlock" or "textChunk" to avoid any ambiguity).
It may also be necessary to customize layout functions to receive the previous information.

/// Layout text into a [`Galley`].
///
/// 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<LayoutJob>) -> Galley {
if job.wrap.max_rows == 0 {
// Early-out: no text
return Galley {
job,
rows: Default::default(),
rect: Rect::from_min_max(Pos2::ZERO, Pos2::ZERO),
mesh_bounds: Rect::NOTHING,
num_vertices: 0,
num_indices: 0,
pixels_per_point: fonts.pixels_per_point(),
elided: true,
};
}
// For most of this we ignore the y coordinate:
let mut paragraphs = vec![Paragraph::from_section_index(0)];
for (section_index, section) in job.sections.iter().enumerate() {
layout_section(fonts, &job, section_index as u32, section, &mut paragraphs);
}
let point_scale = PointScale::new(fonts.pixels_per_point());
let mut elided = false;
let mut rows = rows_from_paragraphs(paragraphs, &job, &mut elided);
if elided {
if let Some(last_row) = rows.last_mut() {
replace_last_glyph_with_overflow_character(fonts, &job, last_row);
}
}
let justify = job.justify && job.wrap.max_width.is_finite();
if justify || job.halign != Align::LEFT {
let num_rows = rows.len();
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,
job.halign,
job.wrap.max_width,
justify_row,
);
}
}
// Calculate the Y positions and tessellate the text:
galley_from_rows(point_scale, job, rows, elided)

@enomado
Copy link
Contributor

enomado commented Mar 17, 2024

I dont get the solution, may be my problem is different than solving here. I want to show BIG 2 megabytes text, so why just dont virtualize lines, and make user pass big text as Vec<String>, and virtualize it.

And make selection api as Range<(row, col)>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Optimizing re-layout of 1MB+ pieces of text in a TextEdit
3 participants