Line breaking around floats

This commit is contained in:
Eric Atkinson 2013-07-11 20:19:13 -07:00
parent 7b933ac952
commit f68a548916
2 changed files with 252 additions and 108 deletions

View file

@ -8,6 +8,7 @@ use geom::rect::Rect;
use gfx::geometry::{Au, max, min}; use gfx::geometry::{Au, max, min};
use std::util::replace; use std::util::replace;
use std::vec; use std::vec;
use std::i32::max_value;
pub enum FloatType{ pub enum FloatType{
FloatLeft, FloatLeft,
@ -146,10 +147,10 @@ impl FloatContextBase{
(max(top_1, top_2), min(bottom_1, bottom_2)) (max(top_1, top_2), min(bottom_1, bottom_2))
} }
debug!("available_rect: trying to find space at %?", top);
let top = top - self.offset.y; let top = top - self.offset.y;
debug!("available_rect: trying to find space at %?", top);
// Relevant dimensions for the right-most left float // Relevant dimensions for the right-most left float
let mut max_left = Au(0) - self.offset.x; let mut max_left = Au(0) - self.offset.x;
let mut l_top = None; let mut l_top = None;
@ -167,6 +168,7 @@ impl FloatContextBase{
Some(data) => { Some(data) => {
let float_pos = data.bounds.origin; let float_pos = data.bounds.origin;
let float_size = data.bounds.size; let float_size = data.bounds.size;
debug!("float_pos: %?, float_size: %?", float_pos, float_size);
match data.f_type { match data.f_type {
FloatLeft => { FloatLeft => {
if(float_pos.x + float_size.width > max_left && if(float_pos.x + float_size.width > max_left &&
@ -187,8 +189,8 @@ impl FloatContextBase{
r_top = Some(float_pos.y); r_top = Some(float_pos.y);
r_bottom = Some(float_pos.y + float_size.height); r_bottom = Some(float_pos.y + float_size.height);
debug!("available_rect: collision with right float: new max_left is %?", debug!("available_rect: collision with right float: new min_right is %?",
max_left); min_right);
} }
} }
} }
@ -198,13 +200,14 @@ impl FloatContextBase{
// Extend the vertical range of the rectangle to the closest floats. // Extend the vertical range of the rectangle to the closest floats.
// If there are floats on both sides, take the intersection of the // If there are floats on both sides, take the intersection of the
// two areas. // two areas. Also make sure we never return a top smaller than the
// given upper bound.
let (top, bottom) = match (r_top, r_bottom, l_top, l_bottom) { let (top, bottom) = match (r_top, r_bottom, l_top, l_bottom) {
(Some(r_top), Some(r_bottom), Some(l_top), Some(l_bottom)) => (Some(r_top), Some(r_bottom), Some(l_top), Some(l_bottom)) =>
range_intersect(r_top, r_bottom, l_top, l_bottom), range_intersect(max(top, r_top), r_bottom, max(top, l_top), l_bottom),
(None, None, Some(l_top), Some(l_bottom)) => (l_top, l_bottom), (None, None, Some(l_top), Some(l_bottom)) => (max(top, l_top), l_bottom),
(Some(r_top), Some(r_bottom), None, None) => (r_top, r_bottom), (Some(r_top), Some(r_bottom), None, None) => (max(top, r_top), r_bottom),
(None, None, None, None) => return None, (None, None, None, None) => return None,
_ => fail!("Reached unreachable state when computing float area") _ => fail!("Reached unreachable state when computing float area")
}; };
@ -215,7 +218,7 @@ impl FloatContextBase{
// assertion here. // assertion here.
//assert!(max_left < min_right); //assert!(max_left < min_right);
assert!(top < bottom, "Float position error"); assert!(top <= bottom, "Float position error");
Some(Rect{ Some(Rect{
origin: Point2D(max_left, top) + self.offset, origin: Point2D(max_left, top) + self.offset,
@ -236,6 +239,8 @@ impl FloatContextBase{
f_type: info.f_type f_type: info.f_type
}; };
debug!("add_float: added float with info %?", new_info);
let new_float = FloatData { let new_float = FloatData {
bounds: Rect { bounds: Rect {
origin: self.place_between_floats(&new_info).origin - self.offset, origin: self.place_between_floats(&new_info).origin - self.offset,
@ -264,6 +269,34 @@ impl FloatContextBase{
return false; return false;
} }
/// Given the top 3 sides of the rectange, finds the largest height that
/// will result in the rectange not colliding with any floats. Returns
/// None if that height is infinite.
fn max_height_for_bounds(&self, left: Au, top: Au, width: Au) -> Option<Au> {
let top = top - self.offset.y;
let left = left - self.offset.x;
let mut max_height = None;
for self.float_data.iter().advance |float| {
match *float {
None => (),
Some(f_data) => {
if f_data.bounds.origin.y + f_data.bounds.size.height > top &&
f_data.bounds.origin.x + f_data.bounds.size.width > left &&
f_data.bounds.origin.x < left + width {
let new_y = f_data.bounds.origin.y;
max_height = Some(min(max_height.get_or_default(new_y), new_y));
}
}
}
}
match max_height {
None => None,
Some(h) => Some(h + self.offset.y)
}
}
/// Given necessary info, finds the closest place a box can be positioned /// Given necessary info, finds the closest place a box can be positioned
/// without colliding with any floats. /// without colliding with any floats.
fn place_between_floats(&self, info: &PlacementInfo) -> Rect<Au>{ fn place_between_floats(&self, info: &PlacementInfo) -> Rect<Au>{
@ -280,10 +313,10 @@ impl FloatContextBase{
// TODO(eatknson): integrate with overflow // TODO(eatknson): integrate with overflow
None => return match info.f_type { None => return match info.f_type {
FloatLeft => Rect(Point2D(Au(0), float_y), FloatLeft => Rect(Point2D(Au(0), float_y),
Size2D(info.max_width, info.height)), Size2D(info.max_width, Au(max_value))),
FloatRight => Rect(Point2D(info.max_width - info.width, float_y), FloatRight => Rect(Point2D(info.max_width - info.width, float_y),
Size2D(info.max_width, info.height)) Size2D(info.max_width, Au(max_value)))
}, },
Some(rect) => { Some(rect) => {
@ -292,12 +325,16 @@ impl FloatContextBase{
// Place here if there is enough room // Place here if there is enough room
if (rect.size.width >= info.width) { if (rect.size.width >= info.width) {
let height = self.max_height_for_bounds(rect.origin.x,
rect.origin.y,
rect.size.width);
let height = height.get_or_default(Au(max_value));
return match info.f_type { return match info.f_type {
FloatLeft => Rect(Point2D(rect.origin.x, float_y), FloatLeft => Rect(Point2D(rect.origin.x, float_y),
Size2D(rect.size.width, info.height)), Size2D(rect.size.width, height)),
FloatRight => { FloatRight => {
Rect(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)) Size2D(rect.size.width, height))
} }
}; };
} }

View file

@ -44,12 +44,18 @@ things like "start outer box, text, start inner box, text, end inner
box, text, end outer box, text". This seems a little complicated to box, text, end outer box, text". This seems a little complicated to
serve as the starting point, but the current design doesn't make it serve as the starting point, but the current design doesn't make it
hard to try out that alternative. hard to try out that alternative.
Line boxes also contain some metadata used during line breaking. The
green zone is the area that the line can expand to before it collides
with a float or a horizontal wall of the containing block. The top
left corner of the green zone is the same as that of the line, but
the green zone can be taller and wider than the line itself.
*/ */
struct LineBox { struct LineBox {
range: Range, range: Range,
bounds: Rect<Au>, bounds: Rect<Au>,
available_width: Au green_zone: Size2D<Au>
} }
struct LineboxScanner { struct LineboxScanner {
@ -74,7 +80,7 @@ impl LineboxScanner {
pending_line: LineBox { pending_line: LineBox {
range: Range::empty(), range: Range::empty(),
bounds: Rect(Point2D(Au(0), Au(0)), Size2D(Au(0), Au(0))), bounds: Rect(Point2D(Au(0), Au(0)), Size2D(Au(0), Au(0))),
available_width: inline.position().size.width green_zone: Size2D(Au(0), Au(0))
}, },
lines: ~[], lines: ~[],
cur_y: Au(0) cur_y: Au(0)
@ -96,7 +102,7 @@ impl LineboxScanner {
fn reset_linebox(&mut self) { fn reset_linebox(&mut self) {
self.pending_line.range.reset(0,0); self.pending_line.range.reset(0,0);
self.pending_line.bounds = Rect(Point2D(Au(0), self.cur_y), 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; self.pending_line.green_zone = Size2D(Au(0), Au(0))
} }
pub fn scan_for_lines(&mut self, ctx: &LayoutContext) { pub fn scan_for_lines(&mut self, ctx: &LayoutContext) {
@ -171,6 +177,8 @@ impl LineboxScanner {
ImageRenderBoxClass(image_box) => { ImageRenderBoxClass(image_box) => {
let size = image_box.image.get_size(); let size = image_box.image.get_size();
let height = Au::from_px(size.get_or_default(Size2D(0, 0)).height); let height = Au::from_px(size.get_or_default(Size2D(0, 0)).height);
image_box.base.position.size.height = height;
debug!("box_height: found image height: %?", height);
height height
} }
TextRenderBoxClass(text_box) => { TextRenderBoxClass(text_box) => {
@ -191,7 +199,7 @@ impl LineboxScanner {
line_height line_height
} }
GenericRenderBoxClass(generic_box) => { GenericRenderBoxClass(_) => {
Au(0) Au(0)
} }
_ => { _ => {
@ -211,72 +219,169 @@ impl LineboxScanner {
} }
} }
fn try_append_to_line(&mut self, ctx: &LayoutContext, in_box: RenderBox) -> bool { /// Computes the position of a line that has only the provided RenderBox.
/// Returns: the bounding rect of the line's green zone (whose origin coincides
/// with the line's origin) and the actual width of the first box after splitting.
fn initial_line_placement (&self, ctx: &LayoutContext, first_box: RenderBox, ceiling: Au) -> (Rect<Au>, Au) {
debug!("LineboxScanner: Trying to place first box of line %?", self.lines.len());
debug!("LineboxScanner: box size: %?", first_box.position().size);
let splitable = first_box.can_split();
let line_is_empty: bool = self.pending_line.range.length() == 0; 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 { // Initally, pretend a splitable box has 0 width.
let info = PlacementInfo { // We will move it later if it has nonzero width
width: Au(0), // and that causes problems.
height: in_box.position().size.height, let placement_width = if splitable {
ceiling: self.cur_y, Au(0)
} else {
first_box.position().size.width
};
let mut info = PlacementInfo {
width: placement_width,
height: first_box.position().size.height,
ceiling: ceiling,
max_width: self.flow.position().size.width, max_width: self.flow.position().size.width,
f_type: FloatLeft f_type: FloatLeft
}; };
let line_bounds = self.floats.place_between_floats(&info); let line_bounds = self.floats.place_between_floats(&info);
self.pending_line.bounds.origin = line_bounds.origin; debug!("LineboxScanner: found position for line: %? using placement_info: %?", line_bounds, info);
self.pending_line.available_width = line_bounds.size.width;
} else if new_height > self.pending_line.bounds.size.height { // Simple case: if the box fits, then we can stop here
// Uh-oh, adding this box increases the height of the line, so we may collide if line_bounds.size.width > first_box.position().size.width {
// with some floats. debug!("LineboxScanner: case=box fits");
return (line_bounds, first_box.position().size.width);
}
let info = PlacementInfo { // If not, but we can't split the box, then we'll place
width: in_box.position().size.width, // the line here and it will overflow.
height: new_height, if !splitable {
ceiling: self.pending_line.bounds.origin.y, debug!("LineboxScanner: case=line doesn't fit, but is unsplittable");
max_width: self.flow.position().size.width, return (line_bounds, first_box.position().size.width);
f_type: FloatLeft }
// Otherwise, try and split the box
// FIXME(eatkinson): calling split_to_width here seems excessive and expensive.
// We should find a better abstraction or merge it with the call in
// try_append_to_line.
match first_box.split_to_width(ctx, line_bounds.size.width, line_is_empty) {
CannotSplit(_) => {
error!("LineboxScanner: Tried to split unsplittable render box! %s",
first_box.debug_str());
return (line_bounds, first_box.position().size.width);
}
SplitDidFit(left, right) => {
debug!("LineboxScanner: case=box split and fit");
let actual_box_width = match (left, right) {
(Some(l_box), Some(_)) => l_box.position().size.width,
(Some(l_box), None) => l_box.position().size.width,
(None, Some(r_box)) => r_box.position().size.width,
(None, None) => fail!("This cas makes no sense.")
};
return (line_bounds, actual_box_width);
}
SplitDidNotFit(left, right) => {
// The split didn't fit, but we might be able to
// push it down past floats.
debug!("LineboxScanner: case=box split and fit didn't fit; trying to push it down");
let actual_box_width = match (left, right) {
(Some(l_box), Some(_)) => l_box.position().size.width,
(Some(l_box), None) => l_box.position().size.width,
(None, Some(r_box)) => r_box.position().size.width,
(None, None) => fail!("This cas makes no sense.")
}; };
info.width = actual_box_width;
let new_bounds = self.floats.place_between_floats(&info); let new_bounds = self.floats.place_between_floats(&info);
let bounds_width = new_bounds.size.width; debug!("LineboxScanner: case=new line position: %?", new_bounds);
let line_width = self.pending_line.bounds.size.width; return (new_bounds, actual_box_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; /// Returns false only if we should break the line.
fn try_append_to_line(&mut self, ctx: &LayoutContext, in_box: RenderBox) -> bool {
let line_is_empty: bool = self.pending_line.range.length() == 0;
debug!("LineboxScanner: Trying to append box to line %u (box width: %?, remaining width: \ if line_is_empty {
let (line_bounds, _) = self.initial_line_placement(ctx, in_box, self.cur_y);
self.pending_line.bounds.origin = line_bounds.origin;
self.pending_line.green_zone = line_bounds.size;
}
debug!("LineboxScanner: Trying to append box to line %u (box size: %?, green zone: \
%?): %s", %?): %s",
self.lines.len(), self.lines.len(),
in_box_width, in_box.position().size,
self.pending_line.available_width, self.pending_line.green_zone,
in_box.debug_str()); in_box.debug_str());
if in_box_width <= self.pending_line.available_width {
let green_zone = self.pending_line.green_zone;
//assert!(green_zone.width >= self.pending_line.bounds.size.width &&
// green_zone.height >= self.pending_line.bounds.size.height,
// "Committed a line that overlaps with floats");
let new_height = self.new_height_for_line(in_box);
if new_height > green_zone.height {
// Uh-oh. Adding this box is going to increase the height,
// and because of that we will collide with some floats.
// We have two options here:
// 1) Move the entire line so that it doesn't collide any more.
// 2) Break the line and put the new box on the next line.
// The problem with option 1 is that we might move the line
// and then wind up breaking anyway, which violates the standard.
// But option 2 is going to look weird sometimes.
// So we'll try to move the line whenever we can, but break
// if we have to.
// First predict where the next line is going to be
let this_line_y = self.pending_line.bounds.origin.y;
let (next_line, first_box_width) = self.initial_line_placement(ctx, in_box, this_line_y);
let next_green_zone = next_line.size;
let new_width = self.pending_line.bounds.size.width + first_box_width;
// Now, see if everything can fit at the new location.
if next_green_zone.width >= new_width && next_green_zone.height >= new_height{
debug!("LineboxScanner: case=adding box collides vertically with floats: moving line");
self.pending_line.bounds.origin = next_line.origin;
self.pending_line.green_zone = next_green_zone;
assert!(!line_is_empty, "Non-terminating line breaking");
self.work_list.add_front(in_box);
return true;
} else {
debug!("LineboxScanner: case=adding box collides vertically with floats: breaking line");
self.work_list.add_front(in_box);
return false;
}
}
// If we're not going to overflow the green zone vertically, we might still do so
// horizontally. We'll try to place the whole box on this line and break somewhere
// if it doesn't fit.
let new_width = self.pending_line.bounds.size.width + in_box.position().size.width;
if(new_width <= green_zone.width){
debug!("LineboxScanner: case=box fits without splitting"); debug!("LineboxScanner: case=box fits without splitting");
self.push_box_to_line(in_box); self.push_box_to_line(in_box);
return true; return true;
} }
if !in_box.can_split() { if !in_box.can_split() {
// force it onto the line anyway, if its otherwise empty
// TODO(Issue #224): signal that horizontal overflow happened? // TODO(Issue #224): signal that horizontal overflow happened?
if line_is_empty { if line_is_empty {
debug!("LineboxScanner: case=box can't split and line %u is empty, so \ debug!("LineboxScanner: case=box can't split and line %u is empty, so \
@ -288,28 +393,28 @@ impl LineboxScanner {
debug!("LineboxScanner: Case=box can't split, not appending."); debug!("LineboxScanner: Case=box can't split, not appending.");
return false; return false;
} }
} } else {
let available_width = green_zone.width - self.pending_line.bounds.size.width;
// not enough width; try splitting? match in_box.split_to_width(ctx, available_width, line_is_empty) {
match in_box.split_to_width(ctx, self.pending_line.available_width, line_is_empty) {
CannotSplit(_) => { CannotSplit(_) => {
error!("LineboxScanner: Tried to split unsplittable render box! %s", error!("LineboxScanner: Tried to split unsplittable render box! %s",
in_box.debug_str()); in_box.debug_str());
return false; return false;
}, }
SplitDidFit(left, right) => { SplitDidFit(left, right) => {
debug!("LineboxScanner: case=split box did fit; deferring remainder box."); debug!("LineboxScanner: case=split box did fit; deferring remainder box.");
match (left, right) { match (left, right) {
(Some(left_box), Some(right_box)) => { (Some(left_box), Some(right_box)) => {
self.push_box_to_line(left_box); self.push_box_to_line(left_box);
self.work_list.add_front(right_box); self.work_list.add_front(right_box);
}, }
(Some(left_box), None) => self.push_box_to_line(left_box), (Some(left_box), None) => self.push_box_to_line(left_box),
(None, Some(right_box)) => self.push_box_to_line(right_box), (None, Some(right_box)) => self.push_box_to_line(right_box),
(None, None) => error!("LineboxScanner: This split case makes no sense!"), (None, None) => error!("LineboxScanner: This split case makes no sense!"),
} }
return true; return true;
}, }
SplitDidNotFit(left, right) => { SplitDidNotFit(left, right) => {
if line_is_empty { if line_is_empty {
debug!("LineboxScanner: case=split box didn't fit and line %u is empty, so overflowing and deferring remainder box.", debug!("LineboxScanner: case=split box didn't fit and line %u is empty, so overflowing and deferring remainder box.",
@ -319,13 +424,13 @@ impl LineboxScanner {
(Some(left_box), Some(right_box)) => { (Some(left_box), Some(right_box)) => {
self.push_box_to_line(left_box); self.push_box_to_line(left_box);
self.work_list.add_front(right_box); self.work_list.add_front(right_box);
}, }
(Some(left_box), None) => { (Some(left_box), None) => {
self.push_box_to_line(left_box); self.push_box_to_line(left_box);
} }
(None, Some(right_box)) => { (None, Some(right_box)) => {
self.push_box_to_line(right_box); self.push_box_to_line(right_box);
}, }
(None, None) => { (None, None) => {
error!("LineboxScanner: This split case makes no sense!"); error!("LineboxScanner: This split case makes no sense!");
} }
@ -339,6 +444,7 @@ impl LineboxScanner {
} }
} }
} }
}
// unconditional push // unconditional push
fn push_box_to_line(&mut self, box: RenderBox) { fn push_box_to_line(&mut self, box: RenderBox) {
@ -350,7 +456,6 @@ impl LineboxScanner {
} }
self.pending_line.range.extend_by(1); 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.bounds.size.width = self.pending_line.bounds.size.width + box.position().size.width;
self.pending_line.available_width = self.pending_line.available_width - box.position().size.width;
self.pending_line.bounds.size.height = Au::max(self.pending_line.bounds.size.height, self.pending_line.bounds.size.height = Au::max(self.pending_line.bounds.size.height,
box.position().size.height); box.position().size.height);
self.new_boxes.push(box); self.new_boxes.push(box);
@ -494,12 +599,11 @@ impl InlineFlowData {
// determine its height for computing linebox height. // determine its height for computing linebox height.
let mut scanner = LineboxScanner::new(InlineFlow(self), self.common.floats_in.clone()); let mut scanner = LineboxScanner::new(InlineFlow(self), self.common.floats_in.clone());
scanner.scan_for_lines(ctx); scanner.scan_for_lines(ctx);
self.common.floats_out = scanner.floats_out();
// Now, go through each line and lay out the boxes inside // Now, go through each line and lay out the boxes inside
for self.lines.iter().advance |line| { for self.lines.iter().advance |line| {
// We need to distribute extra width based on text-align. // We need to distribute extra width based on text-align.
let mut slack_width = line.available_width; let mut slack_width = line.green_zone.width - line.bounds.size.width;
if slack_width < Au(0) { if slack_width < Au(0) {
slack_width = Au(0); slack_width = Au(0);
} }
@ -632,7 +736,10 @@ impl InlineFlowData {
self.lines.last().bounds.origin.y + self.lines.last().bounds.size.height self.lines.last().bounds.origin.y + self.lines.last().bounds.size.height
} else { } else {
Au(0) Au(0)
} };
self.common.floats_out = scanner.floats_out().translate(Point2D(Au(0),
-self.common.position.size.height));
} }
pub fn build_display_list_inline<E:ExtraDisplayListData>(&self, pub fn build_display_list_inline<E:ExtraDisplayListData>(&self,