From a6f6ce926c11d0630cb694eaa6fac4297b820bcc Mon Sep 17 00:00:00 2001 From: Eric Atkinson Date: Wed, 10 Jul 2013 12:46:24 -0700 Subject: [PATCH] Semi-correct text wrapping --- src/components/main/layout/float_context.rs | 61 +++- src/components/main/layout/inline.rs | 332 +++++++++++++------- 2 files changed, 263 insertions(+), 130 deletions(-) diff --git a/src/components/main/layout/float_context.rs b/src/components/main/layout/float_context.rs index cdddf7090aa5..5de03e89e018 100644 --- a/src/components/main/layout/float_context.rs +++ b/src/components/main/layout/float_context.rs @@ -94,6 +94,13 @@ impl FloatContext { replace(self, Invalid) } + #[inline(always)] + pub fn place_between_floats(&self, info: &PlacementInfo) -> Rect { + do self.with_base |base| { + base.place_between_floats(info) + } + } + #[inline(always)] pub fn last_float_pos(&mut self) -> Point2D { do self.with_base |base| { @@ -154,6 +161,7 @@ impl FloatContextBase{ // Find the float collisions for the given vertical range. for self.float_data.iter().advance |float| { + debug!("available_rect: Checking for collision against float"); match *float{ None => (), Some(data) => { @@ -167,6 +175,9 @@ impl FloatContextBase{ l_top = Some(float_pos.y); l_bottom = Some(float_pos.y + float_size.height); + + debug!("available_rect: collision with left float: new max_left is %?", + max_left); } } FloatRight => { @@ -176,6 +187,8 @@ impl FloatContextBase{ r_top = Some(float_pos.y); r_bottom = Some(float_pos.y + float_size.height); + debug!("available_rect: collision with right float: new max_left is %?", + max_left); } } } @@ -215,9 +228,17 @@ impl FloatContextBase{ assert!(self.floats_used < self.float_data.len() && self.float_data[self.floats_used].is_none()); + let new_info = PlacementInfo { + width: info.width, + height: info.height, + ceiling: max(info.ceiling, self.max_y + self.offset.y), + max_width: info.max_width, + f_type: info.f_type + }; + let new_float = FloatData { bounds: Rect { - origin: self.place_float(info) - self.offset, + origin: self.place_between_floats(&new_info).origin - self.offset, size: Size2D(info.width, info.height) }, f_type: info.f_type @@ -227,14 +248,29 @@ impl FloatContextBase{ self.floats_used += 1; } - /// Given necessary info, finds the position of the float in - /// LOCAL COORDINATES. i.e. must be translated before placed - /// in the float list - fn place_float(&self, info: &PlacementInfo) -> Point2D{ + /// Returns true if the given rect overlaps with any floats. + fn collides_with_float(&self, bounds: &Rect) -> bool { + for self.float_data.each |float| { + match *float{ + None => (), + Some(data) => { + if data.bounds.translate(&self.offset).intersects(bounds) { + return true; + } + } + }; + } + + return false; + } + + /// Given necessary info, finds the closest place a box can be positioned + /// without colliding with any floats. + fn place_between_floats(&self, info: &PlacementInfo) -> Rect{ debug!("place_float: Placing float with width %? and height %?", info.width, info.height); // Can't go any higher than previous floats or // previous elements in the document. - let mut float_y = max(info.ceiling, self.max_y + self.offset.y); + let mut float_y = info.ceiling; loop { let maybe_location = self.available_rect(float_y, info.height, info.max_width); debug!("place_float: Got available rect: %? for y-pos: %?", maybe_location, float_y); @@ -243,8 +279,11 @@ impl FloatContextBase{ // If there are no floats blocking us, return the current location // TODO(eatknson): integrate with overflow None => return match info.f_type { - FloatLeft => Point2D(Au(0), float_y), - FloatRight => Point2D(info.max_width - info.width, float_y) + FloatLeft => Rect(Point2D(Au(0), float_y), + Size2D(info.max_width, info.height)), + + FloatRight => Rect(Point2D(info.max_width - info.width, float_y), + Size2D(info.max_width, info.height)) }, Some(rect) => { @@ -254,9 +293,11 @@ impl FloatContextBase{ // Place here if there is enough room if (rect.size.width >= info.width) { return match info.f_type { - FloatLeft => Point2D(rect.origin.x, float_y), + FloatLeft => Rect(Point2D(rect.origin.x, float_y), + Size2D(rect.size.width, info.height)), FloatRight => { - Point2D(rect.origin.x + rect.size.width - info.width, float_y) + Rect(Point2D(rect.origin.x + rect.size.width - info.width, float_y), + Size2D(rect.size.width, info.height)) } }; } diff --git a/src/components/main/layout/inline.rs b/src/components/main/layout/inline.rs index 616f142c76fe..60552c480c84 100644 --- a/src/components/main/layout/inline.rs +++ b/src/components/main/layout/inline.rs @@ -10,6 +10,7 @@ use layout::display_list_builder::{DisplayListBuilder, ExtraDisplayListData}; use layout::flow::{FlowContext, FlowData, InlineFlow}; use layout::float_context::FloatContext; use layout::util::{ElementMapping}; +use layout::float_context::{PlacementInfo, FloatLeft}; use std::u16; use std::util; @@ -45,42 +46,57 @@ serve as the starting point, but the current design doesn't make it hard to try out that alternative. */ -struct PendingLine { +struct LineBox { range: Range, - bounds: Rect + bounds: Rect, + available_width: Au } struct LineboxScanner { flow: FlowContext, + floats: FloatContext, new_boxes: ~[RenderBox], work_list: @mut Deque, - pending_line: PendingLine, - line_spans: ~[Range], + pending_line: LineBox, + lines: ~[LineBox], + cur_y: Au, } impl LineboxScanner { - pub fn new(inline: FlowContext) -> LineboxScanner { + pub fn new(inline: FlowContext, float_ctx: FloatContext) -> LineboxScanner { assert!(inline.starts_inline_flow()); LineboxScanner { flow: inline, + floats: float_ctx, new_boxes: ~[], work_list: @mut Deque::new(), - pending_line: PendingLine {range: Range::empty(), bounds: Rect(Point2D(Au(0), Au(0)), Size2D(Au(0), Au(0)))}, - line_spans: ~[], + pending_line: LineBox { + range: Range::empty(), + bounds: Rect(Point2D(Au(0), Au(0)), Size2D(Au(0), Au(0))), + available_width: inline.position().size.width + }, + lines: ~[], + cur_y: Au(0) } } + + pub fn floats_out(&mut self) -> FloatContext { + self.floats.clone() + } fn reset_scanner(&mut self) { debug!("Resetting line box scanner's state for flow f%d.", self.flow.id()); - self.line_spans = ~[]; + self.lines = ~[]; self.new_boxes = ~[]; + self.cur_y = Au(0); self.reset_linebox(); } fn reset_linebox(&mut self) { self.pending_line.range.reset(0,0); - self.pending_line.bounds = Rect(Point2D(Au(0), Au(0)), Size2D(Au(0), Au(0))); + self.pending_line.bounds = Rect(Point2D(Au(0), self.cur_y), Size2D(Au(0), Au(0))); + self.pending_line.available_width = self.flow.position().size.width; } pub fn scan_for_lines(&mut self, ctx: &LayoutContext) { @@ -108,16 +124,16 @@ impl LineboxScanner { let box_was_appended = self.try_append_to_line(ctx, cur_box); if !box_was_appended { debug!("LineboxScanner: Box wasn't appended, because line %u was full.", - self.line_spans.len()); + self.lines.len()); self.flush_current_line(); } else { - debug!("LineboxScanner: appended a box to line %u", self.line_spans.len()); + debug!("LineboxScanner: appended a box to line %u", self.lines.len()); } } if self.pending_line.range.length() > 0 { debug!("LineboxScanner: Partially full linebox %u left at end of scanning.", - self.line_spans.len()); + self.lines.len()); self.flush_current_line(); } } @@ -131,88 +147,129 @@ impl LineboxScanner { fn swap_out_results(&mut self) { debug!("LineboxScanner: Propagating scanned lines[n=%u] to inline flow f%d", - self.line_spans.len(), + self.lines.len(), self.flow.id()); let inline: &mut InlineFlowData = self.flow.inline(); util::swap(&mut inline.boxes, &mut self.new_boxes); - util::swap(&mut inline.lines, &mut self.line_spans); + util::swap(&mut inline.lines, &mut self.lines); } fn flush_current_line(&mut self) { debug!("LineboxScanner: Flushing line %u: %?", - self.line_spans.len(), self.pending_line); - // set box horizontal offsets - let line_range = self.pending_line.range; - let mut offset_x = Au(0); - // TODO(Issue #199): interpretation of CSS 'direction' will change how boxes are positioned. - debug!("LineboxScanner: Setting horizontal offsets for boxes in line %u range: %?", - self.line_spans.len(), line_range); - - // Get the text alignment. - // TODO(Issue #222): use 'text-align' property from InlineFlow's - // block container, not from the style of the first box child. - let linebox_align; - if self.pending_line.range.begin() < self.new_boxes.len() { - let first_box = self.new_boxes[self.pending_line.range.begin()]; - linebox_align = first_box.text_align(); - } else { - // Nothing to lay out, so assume left alignment. - linebox_align = CSSTextAlignLeft; - } - - let slack_width = self.flow.position().size.width - self.pending_line.bounds.size.width; - match linebox_align { - // So sorry, but justified text is more complicated than shuffling linebox coordinates. - // TODO(Issue #213): implement `text-align: justify` - CSSTextAlignLeft | CSSTextAlignJustify => { - for line_range.eachi |i| { - do self.new_boxes[i].with_mut_base |base| { - base.position.origin.x = offset_x; - offset_x = offset_x + base.position.size.width; - }; - } - }, - CSSTextAlignCenter => { - offset_x = slack_width.scale_by(0.5f); - for line_range.eachi |i| { - do self.new_boxes[i].with_mut_base |base| { - base.position.origin.x = offset_x; - offset_x = offset_x + base.position.size.width; - }; - } - }, - CSSTextAlignRight => { - offset_x = slack_width; - for line_range.eachi |i| { - do self.new_boxes[i].with_mut_base |base| { - base.position.origin.x = offset_x; - offset_x = offset_x + base.position.size.width; - }; - } - }, - } + self.lines.len(), self.pending_line); // clear line and add line mapping - debug!("LineboxScanner: Saving information for flushed line %u.", self.line_spans.len()); - self.line_spans.push(line_range); + debug!("LineboxScanner: Saving information for flushed line %u.", self.lines.len()); + self.lines.push(self.pending_line); + self.cur_y = self.pending_line.bounds.origin.y + self.pending_line.bounds.size.height; self.reset_linebox(); } - // return value: whether any box was appended. + fn box_height(&self, box: RenderBox) -> Au { + match box { + ImageRenderBoxClass(image_box) => { + let size = image_box.image.get_size(); + let height = Au::from_px(size.get_or_default(Size2D(0, 0)).height); + height + } + TextRenderBoxClass(text_box) => { + let range = &text_box.range; + let run = &text_box.run; + + // Compute the height based on the line-height and font size + let text_bounds = run.metrics_for_range(range).bounding_box; + let em_size = text_bounds.size.height; + let line_height = match box.line_height() { + CSSLineHeightNormal => em_size.scale_by(1.14f), + CSSLineHeightNumber(l) => em_size.scale_by(l), + CSSLineHeightLength(Em(l)) => em_size.scale_by(l), + CSSLineHeightLength(Px(l)) => Au::from_frac_px(l), + CSSLineHeightLength(Pt(l)) => Au::from_pt(l), + CSSLineHeightPercentage(p) => em_size.scale_by(p / 100.0f) + }; + + line_height + } + GenericRenderBoxClass(generic_box) => { + Au(0) + } + _ => { + fail!(fmt!("Tried to get height of unknown Box variant: %s", box.debug_str())) + } + } + } + + // FIXME(eatkinson): this assumes that the tallest box in the line determines the line height + // This might not be the case with some weird text fonts. + fn new_height_for_line(&self, new_box: RenderBox) -> Au { + let box_height = self.box_height(new_box); + if box_height > self.pending_line.bounds.size.height { + box_height + } else { + self.pending_line.bounds.size.height + } + } + fn try_append_to_line(&mut self, ctx: &LayoutContext, in_box: RenderBox) -> bool { - let remaining_width = self.flow.position().size.width - self.pending_line.bounds.size.width; - let in_box_width = in_box.position().size.width; let line_is_empty: bool = self.pending_line.range.length() == 0; + let new_height = self.new_height_for_line(in_box); + + if line_is_empty { + let info = PlacementInfo { + width: Au(0), + height: in_box.position().size.height, + ceiling: self.cur_y, + max_width: self.flow.position().size.width, + f_type: FloatLeft + }; + + let line_bounds = self.floats.place_between_floats(&info); + + self.pending_line.bounds.origin = line_bounds.origin; + self.pending_line.available_width = line_bounds.size.width; + + } else if new_height > self.pending_line.bounds.size.height { + // Uh-oh, adding this box increases the height of the line, so we may collide + // with some floats. + + let info = PlacementInfo { + width: in_box.position().size.width, + height: new_height, + ceiling: self.pending_line.bounds.origin.y, + max_width: self.flow.position().size.width, + f_type: FloatLeft + }; + + let new_bounds = self.floats.place_between_floats(&info); + + let bounds_width = new_bounds.size.width; + let line_width = self.pending_line.bounds.size.width; + let box_width = in_box.position().size.width; + + // if the line and the new box can fit inside the bounds, + // move the line. + if bounds_width >= box_width + line_width { + debug!("LineboxScanner: new box fits, so moving line"); + self.pending_line.bounds.origin = new_bounds.origin; + self.pending_line.available_width = + new_bounds.size.width - self.pending_line.bounds.size.width; + } + + // otherwise, we'll eventually try to split the box. + // if this is not possible, we'll make a new line. + } + + let in_box_width = in_box.position().size.width; debug!("LineboxScanner: Trying to append box to line %u (box width: %?, remaining width: \ %?): %s", - self.line_spans.len(), + self.lines.len(), in_box_width, - remaining_width, + self.pending_line.available_width, in_box.debug_str()); - if in_box_width <= remaining_width { + if in_box_width <= self.pending_line.available_width { debug!("LineboxScanner: case=box fits without splitting"); self.push_box_to_line(in_box); return true; @@ -224,7 +281,7 @@ impl LineboxScanner { if line_is_empty { debug!("LineboxScanner: case=box can't split and line %u is empty, so \ overflowing.", - self.line_spans.len()); + self.lines.len()); self.push_box_to_line(in_box); return true; } else { @@ -234,7 +291,7 @@ impl LineboxScanner { } // not enough width; try splitting? - match in_box.split_to_width(ctx, remaining_width, line_is_empty) { + match in_box.split_to_width(ctx, self.pending_line.available_width, line_is_empty) { CannotSplit(_) => { error!("LineboxScanner: Tried to split unsplittable render box! %s", in_box.debug_str()); @@ -256,7 +313,7 @@ impl LineboxScanner { SplitDidNotFit(left, right) => { if line_is_empty { debug!("LineboxScanner: case=split box didn't fit and line %u is empty, so overflowing and deferring remainder box.", - self.line_spans.len()); + self.lines.len()); // TODO(Issue #224): signal that horizontal overflow happened? match (left, right) { (Some(left_box), Some(right_box)) => { @@ -285,7 +342,7 @@ impl LineboxScanner { // unconditional push fn push_box_to_line(&mut self, box: RenderBox) { - debug!("LineboxScanner: Pushing box b%d to line %u", box.id(), self.line_spans.len()); + debug!("LineboxScanner: Pushing box b%d to line %u", box.id(), self.lines.len()); if self.pending_line.range.length() == 0 { assert!(self.new_boxes.len() <= (u16::max_value as uint)); @@ -293,6 +350,9 @@ impl LineboxScanner { } self.pending_line.range.extend_by(1); self.pending_line.bounds.size.width = self.pending_line.bounds.size.width + box.position().size.width; + self.pending_line.available_width -= box.position().size.width; + self.pending_line.bounds.size.height = Au::max(self.pending_line.bounds.size.height, + box.position().size.height); self.new_boxes.push(box); } } @@ -306,7 +366,8 @@ pub struct InlineFlowData { boxes: ~[RenderBox], // vec of ranges into boxes that represents line positions. // these ranges are disjoint, and are the result of inline layout. - lines: ~[Range], + // also some metadata used for positioning lines + lines: ~[LineBox], // vec of ranges into boxes that represent elements. These ranges // must be well-nested, and are only related to the content of // boxes (not lines). Ranges are only kept for non-leaf elements. @@ -425,42 +486,84 @@ impl InlineFlowData { kid.assign_height(ctx); } - - // TODO(eatkinson): line boxes need to shrink if there are floats - let mut scanner = LineboxScanner::new(InlineFlow(self)); - scanner.scan_for_lines(ctx); - self.common.floats_out = self.common.floats_in.clone(); - + // Divide the boxes into lines // TODO(#226): Get the CSS `line-height` property from the containing block's style to // determine minimum linebox height. // // TODO(#226): Get the CSS `line-height` property from each non-replaced inline element to // determine its height for computing linebox height. + let mut scanner = LineboxScanner::new(InlineFlow(self), self.common.floats_in.clone()); + scanner.scan_for_lines(ctx); + self.common.floats_out = scanner.floats_out(); + + // Now, go through each line and lay out the boxes inside + for self.lines.eachi |i, line| { + // We need to distribute extra width based on text-align. + let mut slack_width = line.available_width; + if slack_width < Au(0) { + slack_width = Au(0); + } + //assert!(slack_width >= Au(0), "Too many boxes on line"); + + // Get the text alignment. + // TODO(Issue #222): use 'text-align' property from InlineFlow's + // block container, not from the style of the first box child. + let linebox_align; + if line.range.begin() < self.boxes.len() { + let first_box = self.boxes[line.range.begin()]; + linebox_align = first_box.text_align(); + } else { + // Nothing to lay out, so assume left alignment. + linebox_align = CSSTextAlignLeft; + } - let mut cur_y = Au(0); + // Set the box x positions + let mut offset_x = line.bounds.origin.x; + match linebox_align { + // So sorry, but justified text is more complicated than shuffling linebox coordinates. + // TODO(Issue #213): implement `text-align: justify` + CSSTextAlignLeft | CSSTextAlignJustify => { + for line.range.eachi |i| { + do self.boxes[i].with_mut_base |base| { + base.position.origin.x = offset_x; + offset_x += base.position.size.width; + } + } + } + CSSTextAlignCenter => { + offset_x += slack_width.scale_by(0.5f); + for line.range.eachi |i| { + do self.boxes[i].with_mut_base |base| { + base.position.origin.x = offset_x; + offset_x += base.position.size.width; + } + } + } + CSSTextAlignRight => { + offset_x += slack_width; + for line.range.eachi |i| { + do self.boxes[i].with_mut_base |base| { + base.position.origin.x = offset_x; + offset_x += base.position.size.width; + } + } + } + }; - for self.lines.iter().enumerate().advance |(i, line_span)| { - debug!("assign_height_inline: processing line %u with box span: %?", i, line_span); - // These coordinates are relative to the left baseline. - let mut linebox_bounding_box = Au::zero_rect(); - let mut linebox_height = Au(0); + // Get the baseline offset, assuming that the tallest text box will determine + // the baseline. let mut baseline_offset = Au(0); - - for line_span.eachi |box_i| { + let mut max_height = Au(0); + for line.range.eachi |box_i| { let cur_box = self.boxes[box_i]; - // Compute the height and bounding box of each box. - let bounding_box = match cur_box { + match cur_box { ImageRenderBoxClass(image_box) => { let size = image_box.image.get_size(); let height = Au::from_px(size.get_or_default(Size2D(0, 0)).height); image_box.base.position.size.height = height; - if height > linebox_height { - linebox_height = height; - } - image_box.base.position.translate(&Point2D(Au(0), -height)) } TextRenderBoxClass(text_box) => { @@ -484,8 +587,9 @@ impl InlineFlowData { // calculations. // TODO: this will need to take into account type of line-height // and the vertical-align value. - if line_height > linebox_height { - linebox_height = line_height; + if line_height > max_height { + max_height = line_height; + let linebox_height = line.bounds.size.height; // Offset from the top of the linebox is 1/2 of the leading + ascent baseline_offset = text_box.run.font.metrics.ascent + (linebox_height - em_size).scale_by(0.5f); @@ -493,13 +597,6 @@ impl InlineFlowData { text_bounds.translate(&Point2D(text_box.base.position.origin.x, Au(0))) } GenericRenderBoxClass(generic_box) => { - // TODO(Issue #225): There will be different cases here for `inline-block` - // and other replaced content. - // FIXME(pcwalton): This seems clownshoes; can we remove? - generic_box.position.size.height = Au::from_px(30); - if generic_box.position.size.height > linebox_height { - linebox_height = generic_box.position.size.height; - } generic_box.position } // FIXME(pcwalton): This isn't very type safe! @@ -508,18 +605,10 @@ impl InlineFlowData { cur_box.debug_str())) } }; - - debug!("assign_height_inline: bounding box for box b%d = %?", - cur_box.id(), - bounding_box); - - linebox_bounding_box = linebox_bounding_box.union(&bounding_box); - - debug!("assign_height_inline: linebox bounding box = %?", linebox_bounding_box); } // Now go back and adjust the Y coordinates to match the baseline we determined. - for line_span.eachi |box_i| { + for line.range.eachi |box_i| { let cur_box = self.boxes[box_i]; // TODO(#226): This is completely wrong. We need to use the element's `line-height` @@ -533,14 +622,17 @@ impl InlineFlowData { }; do cur_box.with_mut_base |base| { - base.position.origin.y = offset + cur_y; + base.position.origin.y = offset + line.bounds.origin.y; } } - - cur_y = cur_y + linebox_height; } // End of `lines.each` loop. - self.common.position.size.height = cur_y; + self.common.position.size.height = + if self.lines.len() > 0 { + self.lines.last().bounds.origin.y + self.lines.last().bounds.size.height + } else { + Au(0) + } } pub fn build_display_list_inline(&self,