diff --git a/parley/src/builder.rs b/parley/src/builder.rs index eda7ec59..abac1468 100644 --- a/parley/src/builder.rs +++ b/parley/src/builder.rs @@ -101,6 +101,7 @@ impl TreeBuilder<'_, B> { pub fn push_inline_box(&mut self, mut inline_box: InlineBox) { self.lcx.tree_style_builder.push_uncommitted_text(false); + self.lcx.tree_style_builder.set_is_span_first(false); // TODO: arrange type better here to factor out the index inline_box.index = self.lcx.tree_style_builder.current_text_len(); self.lcx.inline_boxes.push(inline_box); diff --git a/parley/src/layout/line/greedy.rs b/parley/src/layout/line/greedy.rs index 5b9a8a9a..6cb1dc26 100644 --- a/parley/src/layout/line/greedy.rs +++ b/parley/src/layout/line/greedy.rs @@ -149,11 +149,12 @@ impl<'a, B: Brush> BreakLines<'a, B> { // HACK: ignore max_advance for empty layouts // Prevents crash when width is too small (https://github.com/linebender/parley/issues/186) - let max_advance = if self.layout.data.text_len == 0 { - f32::MAX - } else { - max_advance - }; + let max_advance = + if self.layout.data.text_len == 0 && self.layout.data.inline_boxes.is_empty() { + f32::MAX + } else { + max_advance + }; // This macro simply calls the `commit_line` with the provided arguments and some parts of self. // It exists solely to cut down on the boilerplate for accessing the self variables while @@ -528,14 +529,10 @@ impl<'a, B: Brush> BreakLines<'a, B> { line.metrics.trailing_whitespace = 0.0; if !line.item_range.is_empty() { // Note: there may not be a "last run" if there are no runs in the line - let last_run = &self - .lines - .line_items - .iter() - .rfind(|item| item.is_text_run()); - if let Some(last_run) = last_run { - if !last_run.cluster_range.is_empty() { - let cluster = &self.layout.data.clusters[last_run.cluster_range.end - 1]; + let last_item = &self.lines.line_items.last(); + if let Some(last_item) = last_item { + if last_item.is_text_run() && !last_item.cluster_range.is_empty() { + let cluster = &self.layout.data.clusters[last_item.cluster_range.end - 1]; if cluster.info.whitespace().is_space_or_nbsp() { line.metrics.trailing_whitespace = cluster.advance; } @@ -726,6 +723,7 @@ fn try_commit_line( // Iterate over the items to commit // println!("\nCOMMIT LINE"); + let mut last_item_kind = LayoutItemKind::TextRun; for (i, item) in items_to_commit.iter().enumerate() { // println!("i = {} index = {} {:?}", i, item.index, item.kind); @@ -745,6 +743,8 @@ fn try_commit_line( cluster_range: 0..0, text_range: 0..0, }); + + last_item_kind = item.kind; } LayoutItemKind::TextRun => { let run_data = &layout.data.runs[item.index]; @@ -768,6 +768,8 @@ fn try_commit_line( continue; } + last_item_kind = item.kind; + // Push run to line let run = Run::new(layout, 0, 0, run_data, None); let text_range = if run_data.cluster_range.is_empty() { @@ -825,10 +827,21 @@ fn try_commit_line( }); // Reset state for the new line + state.num_spaces = 0; state.clusters.start = state.clusters.end; state.clusters.end += 1; - state.items.start = state.items.end.saturating_sub(1); - state.num_spaces = 0; + + state.items.start = match last_item_kind { + // For text runs, the first item of line N+1 needs to be the SAME as + // the last item for line N. This is because the item (if it a text run + // may be split across the two lines with some clusters in line N and some + // in line N+1). The item is later filtered out (see `continue` in loop above) + // if there are not actually any clusters in line N+1. + LayoutItemKind::TextRun => state.items.end.saturating_sub(1), + // Inline boxes cannot be spread across multiple lines, so we should set + // the first item of line N+1 to be the item AFTER the last item in line N. + LayoutItemKind::InlineBox => state.items.end, + }; true } diff --git a/parley/src/resolve/tree.rs b/parley/src/resolve/tree.rs index 3292202e..0ac5b039 100644 --- a/parley/src/resolve/tree.rs +++ b/parley/src/resolve/tree.rs @@ -68,6 +68,10 @@ impl TreeStyleBuilder { self.white_space_collapse = white_space_collapse; } + pub(crate) fn set_is_span_first(&mut self, is_span_first: bool) { + self.is_span_first = is_span_first; + } + pub(crate) fn push_uncommitted_text(&mut self, is_span_last: bool) { let span_text: Cow<'_, str> = match self.white_space_collapse { WhiteSpaceCollapse::Preserve => Cow::from(&self.uncommitted_text), @@ -109,6 +113,7 @@ impl TreeStyleBuilder { // Nothing to do if there is no uncommitted text. if span_text.is_empty() { + self.uncommitted_text.clear(); return; } diff --git a/parley/src/tests/test_basic.rs b/parley/src/tests/test_basic.rs index 2f48b646..8e779b62 100644 --- a/parley/src/tests/test_basic.rs +++ b/parley/src/tests/test_basic.rs @@ -40,3 +40,24 @@ fn placing_inboxes() { env.with_name(test_case_name).check_layout_snapshot(&layout); } } + +#[test] +fn only_inboxes_wrap() { + let mut env = testenv!(); + + let text = ""; + let mut builder = env.builder(text); + for id in 0..10 { + builder.push_inline_box(InlineBox { + id, + index: 0, + width: 10.0, + height: 10.0, + }); + } + let mut layout = builder.build(text); + layout.break_all_lines(Some(40.0)); + layout.align(None, Alignment::Middle); + + env.check_layout_snapshot(&layout); +} diff --git a/parley/tests/snapshots/only_inboxes_wrap-0.png b/parley/tests/snapshots/only_inboxes_wrap-0.png new file mode 100644 index 00000000..9050bb6e Binary files /dev/null and b/parley/tests/snapshots/only_inboxes_wrap-0.png differ