diff --git a/core/src/html/layout.rs b/core/src/html/layout.rs index cbb5917ff..d5a174629 100644 --- a/core/src/html/layout.rs +++ b/core/src/html/layout.rs @@ -12,6 +12,10 @@ use swf::Twips; /// Contains information relating to the current layout operation. pub struct LayoutContext<'gc> { /// The position to put text into. + /// + /// This cursor does not take indents, left margins, or alignment into + /// account. It's X coordinate is always relative to the start of the + /// current line, not the left edge of the text field being laid out. cursor: Position, /// The resolved font object to use when measuring text. @@ -22,6 +26,15 @@ pub struct LayoutContext<'gc> { /// The end of the current chain of layout boxes. last_box: Option>>, + + /// Whether or not we are laying out the first line of a paragraph. + is_first_line: bool, + + /// All layout boxes in the current line being laid out. + current_line: Option>>, + + /// The right margin of the first span in the current line. + current_line_span: TextSpan, } impl<'gc> Default for LayoutContext<'gc> { @@ -31,6 +44,9 @@ impl<'gc> Default for LayoutContext<'gc> { font: None, first_box: None, last_box: None, + is_first_line: true, + current_line: None, + current_line_span: Default::default(), } } } @@ -44,10 +60,37 @@ impl<'gc> LayoutContext<'gc> { &mut self.cursor } + /// Apply all indents and alignment to the current line, if necessary. + fn fixup_line(&mut self, mc: MutationContext<'gc, '_>) { + let left_adjustment = + Self::left_alignment_offset(&self.current_line_span, self.is_first_line); + let mut line = self.current_line; + while let Some(linebox) = line { + let mut write = linebox.write(mc); + write.bounds += Position::from((left_adjustment, Twips::from_pixels(0.0))); + line = write.next_sibling(); + } + + self.current_line = None; + } + /// Adjust the text layout cursor down to the next line. - fn newline(&mut self, font_size: Twips) { + /// + /// This function will also adjust any layout boxes on the current line to + /// their correct alignment and indentation. + fn newline(&mut self, mc: MutationContext<'gc, '_>, font_size: Twips) { self.cursor.set_x(Twips::from_pixels(0.0)); self.cursor += (Twips::from_pixels(0.0), font_size).into(); + + self.fixup_line(mc); + self.is_first_line = false; + } + + /// Enter a new span. + fn newspan(&mut self, first_span: &TextSpan) { + if self.current_line.is_none() { + self.current_line_span = first_span.clone(); + } } fn font(&self) -> Option> { @@ -73,6 +116,11 @@ impl<'gc> LayoutContext<'gc> { None } + /// Add a box to the current line of text. + /// + /// The box should have been positioned according to the current cursor + /// position. It will be adjusted some time later to properly position it + /// within the current layout box. fn append_box( &mut self, gc_context: MutationContext<'gc, '_>, @@ -82,6 +130,10 @@ impl<'gc> LayoutContext<'gc> { self.first_box = Some(to_append); } + if self.current_line.is_none() { + self.current_line = Some(to_append); + } + if let Some(last) = self.last_box { last.write(gc_context).next_sibling = Some(to_append); } @@ -89,7 +141,36 @@ impl<'gc> LayoutContext<'gc> { self.last_box = Some(to_append); } - fn end_layout(self) -> Option>> { + /// Calculate the left-align offset of a given line of text given the span + /// active at the start of the line and if we're at the start of a + /// paragraph. + fn left_alignment_offset(span: &TextSpan, is_first_line: bool) -> Twips { + if is_first_line { + Twips::from_pixels(span.left_margin + span.block_indent + span.indent) + } else { + Twips::from_pixels(span.left_margin + span.block_indent) + } + } + + /// Calculate the left and right bounds of a wrapping operation based on + /// the current state of the layout operation. + /// + /// This function yields both a remaining line width and an offset into + /// that line. Those should be passed as the `width` and `offset` + /// parameters of `Font.wrap_line`. + /// + /// Offsets returned by this function should not be considered final; + fn wrap_dimensions(&self, current_span: &TextSpan, max_bounds: Twips) -> (Twips, Twips) { + let width = max_bounds - Twips::from_pixels(self.current_line_span.right_margin); + let offset = Self::left_alignment_offset(current_span, self.is_first_line); + + (width, offset + self.cursor.x()) + } + + /// Destroy the layout context, returning the newly constructed layout list. + fn end_layout(mut self, mc: MutationContext<'gc, '_>) -> Option>> { + self.fixup_line(mc); + self.first_box } } @@ -222,17 +303,17 @@ impl<'gc> LayoutBox<'gc> { for (start, _end, text, span) in fs.iter_spans() { if let Some(font) = layout_context.resolve_font(context, movie.clone(), &span) { + layout_context.newspan(span); + let font_size = Twips::from_pixels(span.size); let mut last_breakpoint = 0; + let (mut width, mut offset) = layout_context.wrap_dimensions(&span, bounds); - while let Some(breakpoint) = font.wrap_line( - &text[last_breakpoint..], - font_size, - bounds, - layout_context.cursor().x(), - ) { + while let Some(breakpoint) = + font.wrap_line(&text[last_breakpoint..], font_size, width, offset) + { if breakpoint == last_breakpoint { - last_breakpoint += 1; + layout_context.newline(context.gc_context, font_size); continue; } @@ -250,7 +331,11 @@ impl<'gc> LayoutBox<'gc> { break; } - layout_context.newline(font_size); + layout_context.newline(context.gc_context, font_size); + let next_dim = layout_context.wrap_dimensions(&span, bounds); + + width = next_dim.0; + offset = next_dim.1; } let span_end = text.len(); @@ -268,7 +353,7 @@ impl<'gc> LayoutBox<'gc> { } } - layout_context.end_layout() + layout_context.end_layout(context.gc_context) } pub fn bounds(&self) -> BoxBounds {