Better support for custom line positioning
There have been a few scenarios in integrating Parley with egui where I've thought "this would be easy to implement if only I could manually adjust lines' positions". For instance:
- Adjusting the starting X offset of a line because there's previous text on that same line. This can sort of be faked with an inline box, but there are a lot of complications around the fact that they can't be vertically aligned right now.
- Disabling line breaks entirely. egui has a single-line textbox widget that doesn't display newlines. If I could adjust the X and Y positions of lines, it would be pretty easy to just manually place one line after the other horizontally instead of vertically. If not, the alternatives would be to go over every code path that lets you set a textbox's contents and filter out newlines there, or to implement this type of layout in Parley itself.
- Not egui-related, but flowing text into shapes or around floated boxes. This requires manually positioning each span of text, since each one can start at an arbitrary x-coordinate and you can have multiple spans in a single row.
Right now, you can't just adjust the position of each line when rendering it because there are places in Parley that assume the lines will be laid out in a given way (the "cursor to point" and "point to cursor" methods, selection rectangle drawing, and AccessKit bounding boxes come to mind). There are often good reasons for Parley to make these assumptions--for instance, "cursor to point" can do a binary search over the Y-positions of each line, something which wouldn't work if they could be arbitrarily repositioned.
While the higher-level layout code is great for most use cases, I think an "escape hatch" to gain access to line-level granularity would be very useful and decrease the number of layout-related features that Parley itself would have to implement.
I think a good place to start would be moving some of the logic of the layout-dependent methods mentioned above into methods on Line. People who are fine with using the regular line layout could call into the existing methods on Layout, but those who need more control could call the Line methods and provide or apply the necessary position transformations themselves beforehand.
Adjusting the starting X offset of a line because there's previous text on that same line.
Can you give an example of this? Why are these separate "lines" rather than two spans of text on the same line?
Disabling line breaks entirely. egui has a single-line textbox widget that doesn't display newlines. If I could adjust the X and Y positions of lines, it would be pretty easy to just manually place one line after the other horizontally instead of vertically. If not, the alternatives would be to go over every code path that lets you set a textbox's contents and filter out newlines there, or to implement this type of layout in Parley itself.
I definitely think we should implement "no wrapping at all" in parley itself. Aside from anything this should be very easy to implement. You can actually already emulate this just by not setting a max_advance. But a style is probably still a good idea (CSS calls this text-wrap: nowrap).
Not egui-related, but flowing text into shapes or around floated boxes. This requires manually positioning each span of text, since each one can start at an arbitrary x-coordinate and you can have multiple spans in a single row.
Indeed, CSS floats are a high priority for Blitz, and I have written up an implementation plan for the Parley side of things in the issue. My main observation beyond what you've written is that the BreakLines struct is already well setup for incremental layout where Parley lays content out up to a point, yields control flow back to the caller, and then the caller run it's own logic and calls back into Parley when it's ready.
Currently we just call BreakLines::break_remaining but for floats we could likely have a break_until_end_of_line_or_a_floated_box_is_encountered and then the "fixups" could happen as part of layout (affecting subsequent layout) rather than post-hoc after layout.
My assumption for floats was that:
- There would still be one Parley line per horizontal row
- The y position that the next line starts at would be configurable
- The x offset of the line would be adjustable. Including being adjusted halfway through laying out the line (consider left floats which push the contents of the line to the right, but you don't know this until halfway through laying out the line)
- The max_advance (width) would be configurable per-line. Again, including being adjusted halfway through laying out the line.
Other layouts may need more accommodations (this wouldn't handle having excluded regions in the middle of lines (floats are only even left or right)), but I think you can get quite far.
Can you give an example of this? Why are these separate "lines" rather than two spans of text on the same line?
egui has a "left-to-right wrapped" mode that lets you lay out widgets and text inline. With layout done as it is in HTML, you can calculate the widgets' sizes then lay them out as InlineBoxes, but because egui is immediate-mode, it can only take a single pass over the layout. This means rendering a single label, then (potentially) another widget, then another label, etc. Because it's single-pass, there's no way to know that they're supposed to be the same span of text.
You can actually already emulate this just by not setting a
max_advance.
The layout I'm after also contains no explicit line breaks--newline characters should not create new lines.
egui has a "left-to-right wrapped" mode that lets you lay out widgets and text inline. With layout done as it is in HTML, you can calculate the widgets' sizes then lay them out as
InlineBoxes, but because egui is immediate-mode, it can only take a single pass over the layout. This means rendering a single label, then (potentially) another widget, then another label, etc. Because it's single-pass, there's no way to know that they're supposed to be the same span of text.
I don't think I quite understand why the layout being single-pass prevents this? I assume you pretty much always have a definite pixel width constraint in egui? Can you not just layout the child widgets entirely before performing the wrapping layout? Is it because the child widgets get the "remaining space" as an input or something?
Would a DynamicInlineBox that you initially put in the layout without a size. Then you layout up to the box and then layout the box itself. Then resume text layout. I guess this might not even need to be a new type of box. You'd just a need break_until_end_of_line_or_a_inline_box_is_encountered method.
The layout I'm after also contains no explicit line breaks--newline characters should not create new lines.
Ah, for Blitz we're stripping these out via "whitespace collapsing" in the TreeBuilder.
I don't think I quite understand why the layout being single-pass prevents this? I assume you pretty much always have a definite pixel width constraint in egui? Can you not just layout the child widgets entirely before performing the wrapping layout? Is it because the child widgets get the "remaining space" as an input or something?
Would a
DynamicInlineBoxthat you initially put in the layout without a size. Then you layout up to the box and then layout the box itself. Then resume text layout. I guess this might not even need to be a new type of box. You'd just a needbreak_until_end_of_line_or_a_inline_box_is_encounteredmethod.
Widgets are sized and positioned in the same pass. Rendering multiple labels in a row goes "add a label that says this, now add a widget, now add another label that says that." At the time that you're rendering the first label, there's no way to know what comes after it because those widgets haven't been added yet. I suppose something like DynamicInlineBox would work, but it'd require pretty deep integration between Parley's layout code and egui's.