-
Notifications
You must be signed in to change notification settings - Fork 31
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
Implement inline box layout #67
Conversation
a87c34d
to
acccac3
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A scanning code-style review. Some points here probably aren't related to your PR, but happen to be in your diff due to new indentation - feel free to ignore
parley/src/context.rs
Outdated
@@ -135,7 +144,8 @@ impl<'a, B: Brush, T: TextSource> RangedBuilder<'a, B, T> { | |||
char_index += 1; | |||
} | |||
} | |||
use super::layout::{Decoration, Style}; | |||
|
|||
// Define a function that converts `ResolvedDecoration` into `Decoration` (used just below) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure how useful this comment is - maybe it would be better to rename the function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be better as an impl
on Decoration<B>
since it is unrelated to LayoutContext
and doesn't access self
at all.
And just as a matter of style, the words ‘define a function that...’ don't belong in a comment right above a fn
definition.
parley/src/inline_box.rs
Outdated
/// User-specified identifier for the box, which can be used by the user to determine which box in | ||
/// parley's output corresponds to which box in it's input. | ||
pub id: u64, | ||
/// The character index into the underlying text string at which the box should be placed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm presuming this is a byte index
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. Docs updated to reflect this.
parley/src/inline_box.rs
Outdated
/// User-specified identifier for the box, which can be used by the user to determine which box in | ||
/// parley's output corresponds to which box in it's input. | ||
pub id: u64, | ||
/// The character index into the underlying text string at which the box should be placed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should document the bidi decision here (i.e. that the boxes follow the bidi of the logically previous character)
} | ||
LayoutItemKind::InlineBox => { | ||
// TODO: account for vertical alignment (e.g. baseline alignment) | ||
layout.inline_boxes[self.index].height |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So we don't implement flowing around the inline boxes; that makes sense - it seems like it would be a nightmare to do so.
But we should probably make sure that's documented somewhere
parley/src/layout/line/greedy.rs
Outdated
self.state.cluster_idx += 1; | ||
} | ||
if try_commit_line!(BreakReason::Emergency) { | ||
self.state.cluster_idx += 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've not tested it, but it seems like this would always skip a cluster? This doesn't look the same as other code above (but be aware that I'm only doing a single-pass review on unfamiliar code)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question. This appears to match the original code which seemed to handle this correctly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would also like to understand how/why/if this works correctly (and I don't). But as Chad says, this code is unchanged from the existing code so I don't think it should be blocking.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks Nico! It's really exciting to see this actually working even if it's not quite complete.
I left a lot of notes, some of which are suggestions and some are questions about how the changes here might play with future work.
Hopefully we can resolve some of the open questions and move this out of draft soon.
pub fn push_inline_box(&mut self, inline_box: InlineBox) { | ||
self.lcx.inline_boxes.push(inline_box); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a trade off here in how we split the complexity. I assume this requires sorting the boxes later and then carefully keeping track of them in the shaping stage.
Thinking ahead to generalizing to tree based layout, I'd probably prefer to track/split item ranges here during building. Basically, context should probably keep a Vec<Element>
where Element
is something like:
enum Element {
Text(Range<usize>),
// (byte offset, box properties)
InlineBox(usize, InlineBox),
// maybe others for more complicated layout: StartSpan, EndSpan, Break, etc.
}
My thinking is that we initialize the vec with Element::Text
containing the whole range. When pushing an object, we binary search for the appropriate insertion point and then split any overlapping text range. This introduces some additional complexity here while reducing it in later stages. It also maps more closely to what we might generate when building from a tree and lets us deal with object locations that might not land on character boundaries.
If this seems like a reasonable approach to you, I think it makes sense to push it to a future PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems reasonable. Although it might potentially make the bidi resolution stage a little more tricky?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My guess is that bidi resolution is going to become more complicated anyway if we want to properly handle inline objects, borders, margin, padding, etc.
// We only skip a mandatory break immediately after another mandatory break so if we | ||
// have got this far then we should disable mandatory break skipping | ||
self.state.line.skip_mandatory_break = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the actual reasoning here is that we return early from the function when processing a mandatory break that we actually handle (skip_mandatory_break == false
).
I'll try to do a state table to capture the logic when boundary == Mandatory
:
skip_mandatory_break | action |
---|---|
false | Commit line without consuming newline cluster, set skip_mandatory_break to true, return. Next pass will process this cluster again. |
true | Pretend like this isn't a break and append the newline cluster to the current line (this will always be the beginning of the line). |
The key point is that mandatory break clusters are processed twice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm... I see. Could we change this so that we eagerly append the cluster to the newline instead? (can of course be in a followup PR)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we can definitely do better here. Incidentally, I tried changing the code so the shaper forces a break before every newline cluster and found that it already does so after one, so the code appears to be working at competing purposes here. Let’s address this later.
parley/src/shape.rs
Outdated
while let Some(box_idx) = deferred_boxes.pop() { | ||
// Push the box to the list of items | ||
layout.data.push_inline_box(box_idx); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this will reverse the order of inline boxes with the same index.
The deferred_boxes
vec could be replaced by an Option<Range<usize>>
since they're guaranteed to be contiguous.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch! Using a range is also a good idea.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok. I've updated this to:
- Use
.drain()
to fix the ordering issue - Hoist the
Vec
allocation out of the loop so it's now only 1 Vec alloc per call toshape_text
I'm hoping the range based version can be tackled in a followup.
72f9d0e
to
9976cf1
Compare
This allows consumers of Parley to set the size of boxes before running layout.
@dfrg This is now ready for re-review. I've cleaned up the history and backported all the fixes to line breaking that were previously only included in #76 If you're open to merging and then improving in followups then that would be my preferred approach as there are quite a few further improvements to parley in general that I'd like to make, but I'm currently a little wary that the stack of PRs is getting quite long and unwieldy. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On this pass it looks mostly good. I may do one more detailed review.
parley/src/context.rs
Outdated
@@ -135,7 +144,8 @@ impl<'a, B: Brush, T: TextSource> RangedBuilder<'a, B, T> { | |||
char_index += 1; | |||
} | |||
} | |||
use super::layout::{Decoration, Style}; | |||
|
|||
// Define a function that converts `ResolvedDecoration` into `Decoration` (used just below) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be better as an impl
on Decoration<B>
since it is unrelated to LayoutContext
and doesn't access self
at all.
And just as a matter of style, the words ‘define a function that...’ don't belong in a comment right above a fn
definition.
|
||
// Sort the inline boxes as subsequent code assumes that they are in text index order. | ||
// Note: It's important that this is a stable sort to allow users to control the order of contiguous inline boxes | ||
lcx.inline_boxes.sort_by_key(|b| b.index); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we use an inherently sorted structure instead of Vec
if the code assumes they're sorted? Memory is a good reason not to, but just a thought.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For better or worse, parley was intentionally designed to avoid transient heap allocation and a non-trivial chunk of the complexity is a result of that. So I’d like to avoid letting “allocation per item” collections creep in and break that property. There are already a couple cases of that in this PR (including the stable sort here) but I believe they will all be corrected in future work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean, it doesn't have to be BTreeMap
, but I guess that's one more dependency.
pub advance: f32, | ||
|
||
// Fields that only apply to text runs (Ignored for boxes) | ||
// TODO: factor this out? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe instead of kind
this could be factored into an enum
that contains this stuff.. though arguably text_range
still applies (it's just a caret range) and ditto cluster_range
and is_whitespace
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 to rethinking some of these data structures but that’s likely to affect a lot of code so I’d like to see it done as a follow up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While there are still some rough edges, this a great step forward and I’m inclined to agree with merging this now to unblock future progress. Thanks Nico!
Suggestions either addressed or Chad has suggested we do later
Objective
Changes made
The concept of a "layout item" has been introduced. This is either a run of text or an inline box.