From 83db53202e8618831f3025b982198e9a2b5cd64e Mon Sep 17 00:00:00 2001 From: David Wendt Date: Sun, 16 Oct 2022 20:32:14 -0400 Subject: [PATCH] core: Add empty boxes for empty lines *without* breaking multiple-format lines. --- core/src/html/layout.rs | 75 ++++++++++++++++++++++++++++-------- core/src/html/text_format.rs | 9 +++++ 2 files changed, 67 insertions(+), 17 deletions(-) diff --git a/core/src/html/layout.rs b/core/src/html/layout.rs index 962822c6f..8dd71f247 100644 --- a/core/src/html/layout.rs +++ b/core/src/html/layout.rs @@ -217,11 +217,17 @@ impl<'a, 'gc> LayoutContext<'a, 'gc> { /// The `final_line_of_para` parameter should be flagged if this the final /// line in the paragraph or layout operation (e.g. it wasn't caused by an /// automatic newline and no more text is to be expected). + /// + /// The `text` parameter, if provided, consists of the master text slice, + /// the current read index into that slice, and the current text span we + /// are formatting. This is for empty line insertion; omitting this + /// parameter will result in no empty lines being added. fn fixup_line( &mut self, context: &mut UpdateContext<'_, 'gc, '_>, only_line: bool, final_line_of_para: bool, + text: Option<(&'a WStr, usize, &TextSpan)>, ) { if self.boxes.get_mut(self.current_line..).is_none() { return; @@ -302,6 +308,12 @@ impl<'a, 'gc> LayoutContext<'a, 'gc> { box_count += 1; } + if let Some((text, end, span)) = text { + if box_count == 0 { + self.append_text(&text[end..end], end, end, span); + } + } + self.append_underlines(); line_bounds += @@ -322,8 +334,18 @@ impl<'a, 'gc> LayoutContext<'a, 'gc> { /// /// This function will also adjust any layout boxes on the current line to /// their correct alignment and indentation. - fn explicit_newline(&mut self, context: &mut UpdateContext<'_, 'gc, '_>) { - self.fixup_line(context, false, true); + /// + /// The `text`, `end`, and `span` parameters are for empty line insertion. + /// `text` should be the text we are laying out, while `end` and `span` are + /// the current positions into the text and format spans we are laying out. + fn explicit_newline( + &mut self, + context: &mut UpdateContext<'_, 'gc, '_>, + text: &'a WStr, + end: usize, + span: &TextSpan, + ) { + self.fixup_line(context, false, true, Some((text, end, span))); self.cursor.set_x(Twips::from_pixels(0.0)); self.cursor += ( @@ -340,8 +362,18 @@ impl<'a, 'gc> LayoutContext<'a, 'gc> { /// /// This function will also adjust any layout boxes on the current line to /// their correct alignment and indentation. - fn newline(&mut self, context: &mut UpdateContext<'_, 'gc, '_>) { - self.fixup_line(context, false, false); + /// + /// The `text`, `end`, and `span` parameters are for empty line insertion. + /// `text` should be the text we are laying out, while `end` and `span` are + /// the current positions into the text and format spans we are laying out. + fn newline( + &mut self, + context: &mut UpdateContext<'_, 'gc, '_>, + text: &'a WStr, + end: usize, + span: &TextSpan, + ) { + self.fixup_line(context, false, false, Some((text, end, span))); self.cursor.set_x(Twips::from_pixels(0.0)); self.cursor += ( @@ -537,8 +569,15 @@ impl<'a, 'gc> LayoutContext<'a, 'gc> { fn end_layout( mut self, context: &mut UpdateContext<'_, 'gc, '_>, + fs: &'a FormatSpans, ) -> (Vec>, BoxBounds) { - self.fixup_line(context, !self.has_line_break, true); + self.fixup_line( + context, + !self.has_line_break, + true, + fs.last_span() + .map(|ls| (fs.displayed_text(), fs.displayed_text().len(), ls)), + ); (self.boxes, self.exterior_bounds.unwrap_or_default()) } @@ -694,7 +733,9 @@ impl<'gc> LayoutBox<'gc> { }; match delimiter { - Some(b'\n' | b'\r') => layout_context.explicit_newline(context), + Some(b'\n' | b'\r') => { + layout_context.explicit_newline(context, text, 0, span) + } Some(b'\t') => layout_context.tab(), _ => {} } @@ -713,6 +754,13 @@ impl<'gc> LayoutBox<'gc> { offset, layout_context.is_start_of_line(), ) { + // This ensures that the space causing the line break + // is included in the line it broke. + let next_breakpoint = string_utils::next_char_boundary( + text, + last_breakpoint + breakpoint, + ); + // If text doesn't fit at the start of a line, it // won't fit on the next either, abort and put the // whole text on the line (will be cut-off). This @@ -721,7 +769,7 @@ impl<'gc> LayoutBox<'gc> { if breakpoint == 0 && layout_context.is_start_of_line() { break; } else if breakpoint == 0 { - layout_context.newline(context); + layout_context.newline(context, text, next_breakpoint, span); let next_dim = layout_context.wrap_dimensions(span); @@ -735,13 +783,6 @@ impl<'gc> LayoutBox<'gc> { } } - // This ensures that the space causing the line break - // is included in the line it broke. - let next_breakpoint = string_utils::next_char_boundary( - text, - last_breakpoint + breakpoint, - ); - layout_context.append_text( &text[last_breakpoint..next_breakpoint], start + last_breakpoint, @@ -754,7 +795,7 @@ impl<'gc> LayoutBox<'gc> { break; } - layout_context.newline(context); + layout_context.newline(context, text, next_breakpoint, span); let next_dim = layout_context.wrap_dimensions(span); width = next_dim.0; @@ -764,7 +805,7 @@ impl<'gc> LayoutBox<'gc> { let span_end = text.len(); - if last_breakpoint <= span_end { + if last_breakpoint < span_end { layout_context.append_text( &text[last_breakpoint..span_end], start + last_breakpoint, @@ -776,7 +817,7 @@ impl<'gc> LayoutBox<'gc> { } } - layout_context.end_layout(context) + layout_context.end_layout(context, fs) } pub fn bounds(&self) -> BoxBounds { diff --git a/core/src/html/text_format.rs b/core/src/html/text_format.rs index 44bd93e2b..cefdddcac 100644 --- a/core/src/html/text_format.rs +++ b/core/src/html/text_format.rs @@ -856,6 +856,15 @@ impl FormatSpans { self.spans.get(index) } + /// Retrieve the last text span. + /// + /// Text span indices are ephemeral and can change arbitrarily any time the + /// `FormatSpans` are mutated. You should not use this method directly; the + /// `iter_spans` method will yield the string and span data directly. + pub fn last_span(&self) -> Option<&TextSpan> { + self.spans.last() + } + /// Find the index of the span that covers a given search position. /// /// This function returns both the index of the span which covers the