Adjust the layout code to properly align content with right margins, trailing spaces on lines, and lines wider than the bounds.

Right margins: Simple enough, we just need to subtract the right margin from the bounds when we calculate our alignment adjustment.

Trailing spaces: This is very tricky as we effectively have to remeasure the last box in the line when fixing it up. This also means LayoutContext has to hold the text itself so we can remeasure again...

Lines wider than bounds: If word wrap is disabled it is possible for a line to exceed the bounds of the box. In this case it will be left-aligned. Effectively, the align adjustment is clamped to positive values and we do that here too.
This commit is contained in:
David Wendt 2020-05-25 18:42:43 -04:00
parent 4eb2113a77
commit a0477e7525
2 changed files with 59 additions and 38 deletions

View File

@ -115,7 +115,7 @@ impl<'gc> EditText<'gc> {
let bounds: BoundingBox = swf_tag.bounds.clone().into(); let bounds: BoundingBox = swf_tag.bounds.clone().into();
let layout = LayoutBox::lower_from_text_spans( let (layout, _exterior_bounds) = LayoutBox::lower_from_text_spans(
&text_spans, &text_spans,
context, context,
swf_movie.clone(), swf_movie.clone(),
@ -337,7 +337,7 @@ impl<'gc> EditText<'gc> {
let is_word_wrap = edit_text.is_word_wrap; let is_word_wrap = edit_text.is_word_wrap;
let movie = edit_text.static_data.swf.clone(); let movie = edit_text.static_data.swf.clone();
edit_text.layout = LayoutBox::lower_from_text_spans( let (new_layout, intrinsic_bounds) = LayoutBox::lower_from_text_spans(
&edit_text.text_spans, &edit_text.text_spans,
context, context,
movie, movie,
@ -345,7 +345,6 @@ impl<'gc> EditText<'gc> {
is_word_wrap, is_word_wrap,
); );
let intrinsic_bounds = LayoutBox::total_bounds(edit_text.layout);
drop(edit_text); drop(edit_text);

View File

@ -11,7 +11,7 @@ use std::sync::Arc;
use swf::Twips; use swf::Twips;
/// Contains information relating to the current layout operation. /// Contains information relating to the current layout operation.
pub struct LayoutContext<'gc> { pub struct LayoutContext<'a, 'gc> {
/// The position to put text into. /// The position to put text into.
/// ///
/// This cursor does not take indents, left margins, or alignment into /// This cursor does not take indents, left margins, or alignment into
@ -22,6 +22,9 @@ pub struct LayoutContext<'gc> {
/// The resolved font object to use when measuring text. /// The resolved font object to use when measuring text.
font: Option<Font<'gc>>, font: Option<Font<'gc>>,
/// The underlying bundle of text being formatted.
text: &'a str,
/// The highest font size observed within the current line. /// The highest font size observed within the current line.
max_font_size: Twips, max_font_size: Twips,
@ -31,6 +34,10 @@ pub struct LayoutContext<'gc> {
/// The end of the current chain of layout boxes. /// The end of the current chain of layout boxes.
last_box: Option<GcCell<'gc, LayoutBox<'gc>>>, last_box: Option<GcCell<'gc, LayoutBox<'gc>>>,
/// The exterior bounds of all laid-out text, including left and right
/// margins.
exterior_bounds: BoxBounds<Twips>,
/// Whether or not we are laying out the first line of a paragraph. /// Whether or not we are laying out the first line of a paragraph.
is_first_line: bool, is_first_line: bool,
@ -44,14 +51,16 @@ pub struct LayoutContext<'gc> {
max_bounds: Twips, max_bounds: Twips,
} }
impl<'gc> LayoutContext<'gc> { impl<'a, 'gc> LayoutContext<'a, 'gc> {
fn new(max_bounds: Twips) -> Self { fn new(max_bounds: Twips, text: &'a str) -> Self {
Self { Self {
cursor: Default::default(), cursor: Default::default(),
font: None, font: None,
text,
max_font_size: Default::default(), max_font_size: Default::default(),
first_box: None, first_box: None,
last_box: None, last_box: None,
exterior_bounds: Default::default(),
is_first_line: true, is_first_line: true,
current_line: None, current_line: None,
current_line_span: Default::default(), current_line_span: Default::default(),
@ -73,21 +82,40 @@ impl<'gc> LayoutContext<'gc> {
let mut line = self.current_line; let mut line = self.current_line;
while let Some(linebox) = line { while let Some(linebox) = line {
let read = linebox.read(); let read = linebox.read();
line_bounds += read.bounds();
line = read.next_sibling(); line = read.next_sibling();
//Flash ignores trailing spaces when aligning lines, so should we
if line.is_none() {
let trimmed_bounds = {
let (start, end, _tf, font, size, _color) = read.text_node().expect("text");
read.bounds()
.with_size(Size::from(font.measure(self.text[start..end].trim(), size)))
};
line_bounds += trimmed_bounds;
} else {
line_bounds += read.bounds();
}
} }
let left_adjustment = let left_adjustment =
Self::left_alignment_offset(&self.current_line_span, self.is_first_line); Self::left_alignment_offset(&self.current_line_span, self.is_first_line);
let align_adjustment = match self.current_line_span.align { let right_adjustment = Twips::from_pixels(self.current_line_span.right_margin);
swf::TextAlign::Left => Default::default(), let align_adjustment = max(
swf::TextAlign::Center => (self.max_bounds - left_adjustment - line_bounds.width()) / 2, match self.current_line_span.align {
swf::TextAlign::Right => (self.max_bounds - left_adjustment - line_bounds.width()), swf::TextAlign::Left => Default::default(),
swf::TextAlign::Justify => { swf::TextAlign::Center => {
log::error!("Justified text is unimplemented!"); (self.max_bounds - left_adjustment - right_adjustment - line_bounds.width()) / 2
Default::default() }
} swf::TextAlign::Right => {
}; self.max_bounds - left_adjustment - right_adjustment - line_bounds.width()
}
swf::TextAlign::Justify => {
log::error!("Justified text is unimplemented!");
Default::default()
}
},
Twips::from_pixels(0.0),
);
line = self.current_line; line = self.current_line;
while let Some(linebox) = line { while let Some(linebox) = line {
@ -103,7 +131,11 @@ impl<'gc> LayoutContext<'gc> {
line = write.next_sibling(); line = write.next_sibling();
} }
line_bounds += Position::from((align_adjustment, Twips::from_pixels(0.0)));
line_bounds += Size::from((left_adjustment + right_adjustment, Twips::from_pixels(0.0)));
self.current_line = None; self.current_line = None;
self.exterior_bounds += line_bounds;
} }
/// Adjust the text layout cursor down to the next line. /// Adjust the text layout cursor down to the next line.
@ -206,10 +238,13 @@ impl<'gc> LayoutContext<'gc> {
} }
/// Destroy the layout context, returning the newly constructed layout list. /// Destroy the layout context, returning the newly constructed layout list.
fn end_layout(mut self, mc: MutationContext<'gc, '_>) -> Option<GcCell<'gc, LayoutBox<'gc>>> { fn end_layout(
mut self,
mc: MutationContext<'gc, '_>,
) -> (Option<GcCell<'gc, LayoutBox<'gc>>>, BoxBounds<Twips>) {
self.fixup_line(mc); self.fixup_line(mc);
self.first_box (self.first_box, self.exterior_bounds)
} }
} }
@ -302,10 +337,10 @@ impl<'gc> LayoutBox<'gc> {
} }
/// ///
pub fn append_text_fragment( pub fn append_text_fragment<'a>(
mc: MutationContext<'gc, '_>, mc: MutationContext<'gc, '_>,
lc: &mut LayoutContext<'gc>, lc: &mut LayoutContext<'a, 'gc>,
text: &str, text: &'a str,
start: usize, start: usize,
end: usize, end: usize,
span: &TextSpan, span: &TextSpan,
@ -332,16 +367,16 @@ impl<'gc> LayoutBox<'gc> {
/// Construct a new layout hierarchy from text spans. /// Construct a new layout hierarchy from text spans.
/// ///
/// The given `bounds` are optional; providing `None` bounds indicates a /// The returned bounds will include both the text bounds itself, as well
/// field without automatic word wrapping. /// as left and right margins on any of the lines.
pub fn lower_from_text_spans( pub fn lower_from_text_spans(
fs: &FormatSpans, fs: &FormatSpans,
context: &mut UpdateContext<'_, 'gc, '_>, context: &mut UpdateContext<'_, 'gc, '_>,
movie: Arc<SwfMovie>, movie: Arc<SwfMovie>,
bounds: Twips, bounds: Twips,
is_word_wrap: bool, is_word_wrap: bool,
) -> Option<GcCell<'gc, LayoutBox<'gc>>> { ) -> (Option<GcCell<'gc, LayoutBox<'gc>>>, BoxBounds<Twips>) {
let mut layout_context = LayoutContext::new(bounds); let mut layout_context = LayoutContext::new(bounds, fs.text());
for (start, _end, text, span) in fs.iter_spans() { for (start, _end, text, span) in fs.iter_spans() {
if let Some(font) = layout_context.resolve_font(context, movie.clone(), &span) { if let Some(font) = layout_context.resolve_font(context, movie.clone(), &span) {
@ -408,19 +443,6 @@ impl<'gc> LayoutBox<'gc> {
self.bounds self.bounds
} }
/// Calculate the total bounds of a list of zero or more layout boxes.
pub fn total_bounds(mut list: Option<GcCell<'gc, Self>>) -> BoxBounds<Twips> {
let mut union = Default::default();
while let Some(lbox) = list {
let read = lbox.read();
union += read.bounds();
list = read.next_sibling();
}
union
}
/// Returns a reference to the text this box contains. /// Returns a reference to the text this box contains.
pub fn text_node(&self) -> Option<(usize, usize, &TextFormat, Font<'gc>, Twips, swf::Color)> { pub fn text_node(&self) -> Option<(usize, usize, &TextFormat, Font<'gc>, Twips, swf::Color)> {
match &self.content { match &self.content {