diff --git a/parley/src/layout/data.rs b/parley/src/layout/data.rs index 8deeb918..cbea7318 100644 --- a/parley/src/layout/data.rs +++ b/parley/src/layout/data.rs @@ -205,6 +205,11 @@ impl LineItemData { self.kind == LayoutItemKind::TextRun } + #[inline(always)] + pub(crate) fn is_rtl(&self) -> bool { + self.bidi_level & 1 != 0 + } + pub(crate) fn compute_line_height(&self, layout: &LayoutData) -> f32 { match self.kind { LayoutItemKind::TextRun => { @@ -235,6 +240,41 @@ impl LineItemData { } } } + + /// If the item is a text run + /// - Determine if it consists entirely of whitespace (`is_whitespace` property) + /// - Determine if it has trailing whitespace (`has_trailing_whitespace` property) + pub(crate) fn compute_whitespace_properties(&mut self, layout_data: &LayoutData) { + // Skip items which are not text runs + if self.kind != LayoutItemKind::TextRun { + return; + } + + self.is_whitespace = true; + if self.is_rtl() { + // RTL runs check for "trailing" whitespace at the front. + for cluster in layout_data.clusters[self.cluster_range.clone()].iter() { + if cluster.info.is_whitespace() { + self.has_trailing_whitespace = true; + } else { + self.is_whitespace = false; + break; + } + } + } else { + for cluster in layout_data.clusters[self.cluster_range.clone()] + .iter() + .rev() + { + if cluster.info.is_whitespace() { + self.has_trailing_whitespace = true; + } else { + self.is_whitespace = false; + break; + } + } + } + } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/parley/src/layout/line/greedy.rs b/parley/src/layout/line/greedy.rs index 803cab9b..f739755e 100644 --- a/parley/src/layout/line/greedy.rs +++ b/parley/src/layout/line/greedy.rs @@ -62,6 +62,10 @@ struct BreakerState { /// Iteration state: the current cluster (within the layout) cluster_idx: usize, + /// The y coordinate of the bottom of the last committed line (or else 0) + /// Use of f64 here is important. f32 causes test failures due to accumulated error + committed_y: f64, + line: LineState, prev_boundary: Option, emergency_boundary: Option, @@ -142,6 +146,8 @@ impl<'a, B: Brush> BreakLines<'a, B> { self.state.line.x = 0.; self.state.prev_boundary = None; // Added by Nico self.state.emergency_boundary = None; + + self.finish_line(self.lines.lines.len() - 1); self.last_line_data() } @@ -150,6 +156,11 @@ impl<'a, B: Brush> BreakLines<'a, B> { Some((line.metrics.advance, line.size())) } + /// Returns the y-coordinate of the top of the current line + pub fn committed_y(&self) -> f64 { + self.state.committed_y + } + /// Returns true if all the text has been placed into lines. pub fn is_done(&self) -> bool { self.done @@ -419,235 +430,205 @@ impl<'a, B: Brush> BreakLines<'a, B> { /// Consumes the line breaker and finalizes all line computations. pub fn finish(mut self) { - // Whether metrics should be quantized to pixel boundaries - let quantize = self.layout.data.quantize; - // For each run (item which is a text run): - // - Determine if it consists entirely of whitespace (is_whitespace property) - // - Determine if it has trailing whitespace (has_trailing_whitespace property) - for item in &mut self.lines.line_items { - // Skip items which are not text runs - if item.kind != LayoutItemKind::TextRun { - continue; + if self.layout.data.text_len == 0 { + if let Some(line) = self.lines.line_items.first_mut() { + line.text_range = 0..0; + line.cluster_range = 0..0; } + } + } - let run = item; - run.is_whitespace = true; - if run.bidi_level & 1 != 0 { - // RTL runs check for "trailing" whitespace at the front. - for cluster in self.layout.data.clusters[run.cluster_range.clone()].iter() { - if cluster.info.is_whitespace() { - run.has_trailing_whitespace = true; - } else { - run.is_whitespace = false; - break; - } - } - } else { - for cluster in self.layout.data.clusters[run.cluster_range.clone()] - .iter() - .rev() - { - if cluster.info.is_whitespace() { - run.has_trailing_whitespace = true; - } else { - run.is_whitespace = false; - break; - } - } - } + fn finish_line(&mut self, line_idx: usize) { + let prev_line_metrics = match line_idx { + 0 => None, + idx => Some(self.lines.lines[idx - 1].metrics), + }; + let line = &mut self.lines.lines[line_idx]; + + // Reset metrics for line + line.metrics.ascent = 0.; + line.metrics.descent = 0.; + line.metrics.leading = 0.; + line.metrics.offset = 0.; + line.text_range.start = usize::MAX; + + if line.item_range.is_empty() { + line.text_range = self.layout.data.text_len..self.layout.data.text_len; } - let mut y: f64 = 0.; // f32 causes test failures due to accumulated error - let mut prev_line_metrics = None; - for line in &mut self.lines.lines { - // Reset metrics for line - line.metrics.ascent = 0.; - line.metrics.descent = 0.; - line.metrics.leading = 0.; - line.metrics.offset = 0.; - line.text_range.start = usize::MAX; - - if line.item_range.is_empty() { - line.text_range = self.layout.data.text_len..self.layout.data.text_len; - } - // Compute metrics for the line, but ignore trailing whitespace. - let mut have_metrics = false; - let mut needs_reorder = false; - for line_item in self.lines.line_items[line.item_range.clone()] - .iter_mut() - .rev() - { - match line_item.kind { - LayoutItemKind::InlineBox => { - let item = &self.layout.data.inline_boxes[line_item.index]; - - // Advance is already computed in "commit line" for items - - // Default vertical alignment is to align the bottom of boxes with the text baseline. - // This is equivalent to the entire height of the box being "ascent" - line.metrics.ascent = line.metrics.ascent.max(item.height); - line.metrics.line_height = line.metrics.line_height.max(item.height); - - // Mark us as having seen non-whitespace content on this line - have_metrics = true; + // Compute metrics for the line, but ignore trailing whitespace. + let mut have_metrics = false; + let mut needs_reorder = false; + for line_item in self.lines.line_items[line.item_range.clone()] + .iter_mut() + .rev() + { + match line_item.kind { + LayoutItemKind::InlineBox => { + let item = &self.layout.data.inline_boxes[line_item.index]; + + // Advance is already computed in "commit line" for items + + // Default vertical alignment is to align the bottom of boxes with the text baseline. + // This is equivalent to the entire height of the box being "ascent" + line.metrics.ascent = line.metrics.ascent.max(item.height); + line.metrics.line_height = line.metrics.line_height.max(item.height); + + // Mark us as having seen non-whitespace content on this line + have_metrics = true; + } + LayoutItemKind::TextRun => { + line_item.compute_whitespace_properties(&self.layout.data); + + // Compute the text range for the line + // Q: Can we not simplify this computation by assuming that items are in order? + line.text_range.end = line.text_range.end.max(line_item.text_range.end); + line.text_range.start = line.text_range.start.min(line_item.text_range.start); + + // Mark line as needing bidi re-ordering if it contains any runs with non-zero bidi level + // (zero is the default level, so this is equivalent to marking lines that have multiple levels) + if line_item.bidi_level != 0 { + needs_reorder = true; } - LayoutItemKind::TextRun => { - // Compute the text range for the line - // Q: Can we not simplify this computation by assuming that items are in order? - line.text_range.end = line.text_range.end.max(line_item.text_range.end); - line.text_range.start = - line.text_range.start.min(line_item.text_range.start); - - // Mark line as needing bidi re-ordering if it contains any runs with non-zero bidi level - // (zero is the default level, so this is equivalent to marking lines that have multiple levels) - if line_item.bidi_level != 0 { - needs_reorder = true; - } - let run = &self.layout.data.runs[line_item.index]; - let line_height = line_item.compute_line_height(&self.layout.data); - line.metrics.line_height = line.metrics.line_height.max(line_height); + let run = &self.layout.data.runs[line_item.index]; + let line_height = line_item.compute_line_height(&self.layout.data); + line.metrics.line_height = line.metrics.line_height.max(line_height); - // Compute the run's advance by summing the advances of its constituent clusters - line_item.advance = self.layout.data.clusters - [line_item.cluster_range.clone()] + // Compute the run's advance by summing the advances of its constituent clusters + line_item.advance = self.layout.data.clusters[line_item.cluster_range.clone()] .iter() .map(|c| c.advance) .sum(); - // Ignore trailing whitespace for metrics computation - // (we are iterating backwards so trailing whitespace comes first) - if !have_metrics && line_item.is_whitespace { - continue; - } + // Ignore trailing whitespace for metrics computation + // (we are iterating backwards so trailing whitespace comes first) + if !have_metrics && line_item.is_whitespace { + continue; + } - // Compute the run's vertical metrics - line.metrics.ascent = line.metrics.ascent.max(run.metrics.ascent); - line.metrics.descent = line.metrics.descent.max(run.metrics.descent); + // Compute the run's vertical metrics + line.metrics.ascent = line.metrics.ascent.max(run.metrics.ascent); + line.metrics.descent = line.metrics.descent.max(run.metrics.descent); - // Mark us as having seen non-whitespace content on this line - have_metrics = true; - } + // Mark us as having seen non-whitespace content on this line + have_metrics = true; } } + } - // Reorder the items within the line (if required). Reordering is required if the line contains - // a mix of bidi levels (a mix of LTR and RTL text) - let item_count = line.item_range.end - line.item_range.start; - if needs_reorder && item_count > 1 { - reorder_line_items(&mut self.lines.line_items[line.item_range.clone()]); - } + // Reorder the items within the line (if required). Reordering is required if the line contains + // a mix of bidi levels (a mix of LTR and RTL text) + let item_count = line.item_range.end - line.item_range.start; + if needs_reorder && item_count > 1 { + reorder_line_items(&mut self.lines.line_items[line.item_range.clone()]); + } - // Compute size of line's trailing whitespace. "Trailing" is considered the right edge - // for LTR text and the left edge for RTL text. - let run = if self.layout.is_rtl() { - self.lines.line_items[line.item_range.clone()].first() - } else { - self.lines.line_items[line.item_range.clone()].last() - }; - line.metrics.trailing_whitespace = run - .filter(|item| item.is_text_run()) - .and_then(|run| { - let cluster = if self.layout.is_rtl() { - self.layout.data.clusters[run.cluster_range.clone()].first() - } else { - self.layout.data.clusters[run.cluster_range.clone()].last() - }; - cluster - .filter(|cluster| cluster.info.whitespace().is_space_or_nbsp()) - .map(|cluster| cluster.advance) - }) - .unwrap_or(0.0); - - if !have_metrics { - // Line consisting entirely of whitespace? - if !line.item_range.is_empty() { - let line_item = &self.lines.line_items[line.item_range.start]; - if line_item.is_text_run() { - let run = &self.layout.data.runs[line_item.index]; - line.metrics.ascent = run.metrics.ascent; - line.metrics.descent = run.metrics.descent; - } - } else if let Some(metrics) = prev_line_metrics { - // HACK: copy metrics from previous line if we don't have - // any; this should only occur for an empty line following - // a newline at the end of a layout - line.metrics = metrics; - // If we have no items on this line, it must be the last (empty) - // line in a layout following a newline. Commit an empty run so - // that AccessKit has a node with which to identify the visual - // cursor position - if let Some((index, run)) = self - .layout - .data - .runs - .iter() - .enumerate() - .rfind(|(_, run)| !run.text_range.is_empty()) - { - let run_index = self.lines.line_items.len(); - let cluster = run.cluster_range.end; - let text = run.text_range.end; - self.lines.line_items.push(LineItemData { - kind: LayoutItemKind::TextRun, - index, - bidi_level: 0, - advance: 0., - is_whitespace: false, - has_trailing_whitespace: false, - cluster_range: cluster..cluster, - text_range: text..text, - }); - line.item_range = run_index..run_index + 1; - } + // Compute size of line's trailing whitespace. "Trailing" is considered the right edge + // for LTR text and the left edge for RTL text. + let run = if self.layout.is_rtl() { + self.lines.line_items[line.item_range.clone()].first() + } else { + self.lines.line_items[line.item_range.clone()].last() + }; + line.metrics.trailing_whitespace = run + .filter(|item| item.is_text_run()) + .and_then(|run| { + let cluster = if self.layout.is_rtl() { + self.layout.data.clusters[run.cluster_range.clone()].first() + } else { + self.layout.data.clusters[run.cluster_range.clone()].last() + }; + cluster + .filter(|cluster| cluster.info.whitespace().is_space_or_nbsp()) + .map(|cluster| cluster.advance) + }) + .unwrap_or(0.0); + + if !have_metrics { + // Line consisting entirely of whitespace? + if !line.item_range.is_empty() { + let line_item = &self.lines.line_items[line.item_range.start]; + if line_item.is_text_run() { + let run = &self.layout.data.runs[line_item.index]; + line.metrics.ascent = run.metrics.ascent; + line.metrics.descent = run.metrics.descent; + } + } else if let Some(metrics) = prev_line_metrics { + // HACK: copy metrics from previous line if we don't have + // any; this should only occur for an empty line following + // a newline at the end of a layout + line.metrics = metrics; + // If we have no items on this line, it must be the last (empty) + // line in a layout following a newline. Commit an empty run so + // that AccessKit has a node with which to identify the visual + // cursor position + if let Some((index, run)) = self + .layout + .data + .runs + .iter() + .enumerate() + .rfind(|(_, run)| !run.text_range.is_empty()) + { + let run_index = self.lines.line_items.len(); + let cluster = run.cluster_range.end; + let text = run.text_range.end; + self.lines.line_items.push(LineItemData { + kind: LayoutItemKind::TextRun, + index, + bidi_level: 0, + advance: 0., + is_whitespace: false, + has_trailing_whitespace: false, + cluster_range: cluster..cluster, + text_range: text..text, + }); + line.item_range = run_index..run_index + 1; } } + } - line.metrics.leading = - line.metrics.line_height - (line.metrics.ascent + line.metrics.descent); - - let (ascent, descent) = if quantize { - // We mimic Chrome in rounding ascent and descent separately, - // before calculating the rest. - // See lines_integral_line_height_ascent_descent_rounding() for more details. - (line.metrics.ascent.round(), line.metrics.descent.round()) - } else { - (line.metrics.ascent, line.metrics.descent) - }; + line.metrics.leading = + line.metrics.line_height - (line.metrics.ascent + line.metrics.descent); - let (leading_above, leading_below) = if quantize { - // Calculate leading using the rounded ascent and descent. - let leading = line.metrics.line_height - (ascent + descent); - // We mimic Chrome in giving 'below' the larger leading half. - // Although the comment in Chromium's NGLineHeightMetrics::AddLeading function - // in ng_line_height_metrics.cc claims it's for legacy test compatibility. - // So we might want to think about giving 'above' the larger half instead. - let above = (leading * 0.5).floor(); - let below = leading.round() - above; - (above, below) - } else { - (line.metrics.leading * 0.5, line.metrics.leading * 0.5) - }; + // Whether metrics should be quantized to pixel boundaries + let quantize = self.layout.data.quantize; - line.metrics.baseline = - ascent + leading_above + if quantize { y.round() as f32 } else { y as f32 }; + let (ascent, descent) = if quantize { + // We mimic Chrome in rounding ascent and descent separately, + // before calculating the rest. + // See lines_integral_line_height_ascent_descent_rounding() for more details. + (line.metrics.ascent.round(), line.metrics.descent.round()) + } else { + (line.metrics.ascent, line.metrics.descent) + }; + + let (leading_above, leading_below) = if quantize { + // Calculate leading using the rounded ascent and descent. + let leading = line.metrics.line_height - (ascent + descent); + // We mimic Chrome in giving 'below' the larger leading half. + // Although the comment in Chromium's NGLineHeightMetrics::AddLeading function + // in ng_line_height_metrics.cc claims it's for legacy test compatibility. + // So we might want to think about giving 'above' the larger half instead. + let above = (leading * 0.5).floor(); + let below = leading.round() - above; + (above, below) + } else { + (line.metrics.leading * 0.5, line.metrics.leading * 0.5) + }; - // Small line heights will cause leading to be negative. - // Negative leadings are correct for baseline calculation, but not for min/max coords. - // We clamp leading to zero for the purposes of min/max coords, - // which in turn clamps the selection box minimum height to ascent + descent. - line.metrics.min_coord = line.metrics.baseline - ascent - leading_above.max(0.); - line.metrics.max_coord = line.metrics.baseline + descent + leading_below.max(0.); + let y = self.state.committed_y; + line.metrics.baseline = + ascent + leading_above + if quantize { y.round() as f32 } else { y as f32 }; - y += line.metrics.line_height as f64; + // Small line heights will cause leading to be negative. + // Negative leadings are correct for baseline calculation, but not for min/max coords. + // We clamp leading to zero for the purposes of min/max coords, + // which in turn clamps the selection box minimum height to ascent + descent. + line.metrics.min_coord = line.metrics.baseline - ascent - leading_above.max(0.); + line.metrics.max_coord = line.metrics.baseline + descent + leading_below.max(0.); - prev_line_metrics = Some(line.metrics); - } - if self.layout.data.text_len == 0 { - if let Some(line) = self.lines.line_items.first_mut() { - line.text_range = 0..0; - line.cluster_range = 0..0; - } - } + self.state.committed_y += line.metrics.line_height as f64; } }