Layout: collapsing margins

Michael Drake tlsa at netsurf-browser.org
Thu Apr 21 20:26:22 BST 2011


The attached patch tries to fix collapsing margins.

Margins should collapse between the bottom of a box that causes height and
the top of the next box content that causes height.  The boxes must have a
common ancestor within the current block formatting context for margins to
collapse between them.

The margin is applied as a vertical offset at the top of the first box
that has height.

The attached patch implements collapsing margins.

http://source.netsurf-browser.org/trunk/netsurftest/haveproblems/collapsing-margins.html

The patch fixes the above test case.  It also makes various other changes
to margin handling and fixes some other stuff relating to vertical
positioning of boxes.  E.g. the position of the search box on Wikipedia
pages, and the position of the sidebar on Ars Technica.

It makes the bottom 3 rows of the Acid2 face appear one row too high,
obscuring the bottom row of the smile.  I'm not sure why, currently.

-- 

Michael Drake (tlsa)                  http://www.netsurf-browser.org/
-------------- next part --------------
Index: render/layout.c
===================================================================
--- render/layout.c	(revision 12213)
+++ render/layout.c	(working copy)
@@ -69,6 +69,8 @@
 		struct content *content);
 static void layout_minmax_block(struct box *block,
 		const struct font_functions *font_func);
+static struct box* layout_next_margin_block(struct box *box, struct box *block,
+		int viewport_height, int *max_pos_margin, int *max_neg_margin);
 static bool layout_block_object(struct box *block);
 static void layout_get_object_dimensions(struct box *box,
 		int *width, int *height,
@@ -208,7 +210,8 @@
 	int max_neg_margin = 0;
 	int y = 0;
 	int lm, rm;
-	struct box *margin_box;
+	struct box *margin_collapse = NULL;
+	bool in_margin = false;
 	css_fixed gadget_size;
 	css_unit gadget_unit; /* Checkbox / radio buttons */
 
@@ -281,7 +284,7 @@
 					gadget_unit, block->style));
 	}
 
-	box = margin_box = block->children;
+	box = block->children;
 	/* set current coordinates to top-left of the block */
 	cx = 0;
 	y = cy = block->padding[TOP];
@@ -314,7 +317,6 @@
 	while (box) {
 		assert(box->type == BOX_BLOCK || box->type == BOX_TABLE ||
 				box->type == BOX_INLINE_CONTAINER);
-		assert(margin_box);
 
 		/* Tables are laid out before being positioned, because the
 		 * position depends on the width which is calculated in
@@ -336,6 +338,16 @@
 			goto advance_to_next_box;
 		}
 
+		/* If we don't know which box the current margin collapses
+		 * through to, find out.  Update the pos/neg margin values. */
+		if (margin_collapse == NULL) {
+			margin_collapse = layout_next_margin_block(box, block,
+					viewport_height,
+					&max_pos_margin, &max_neg_margin);
+			/* We have a margin that has not yet been applied. */
+			in_margin = true;
+		}
+
 		/* Clearance. */
 		y = 0;
 		if (box->style && css_computed_clear(box->style) !=
@@ -343,19 +355,6 @@
 			y = layout_clear(block->float_children,
 					css_computed_clear(box->style));
 
-		/* Get top margin */
-		if (box->style) {
-			layout_find_dimensions(box->parent->width,
-					viewport_height, box, box->style,
-					NULL, NULL, NULL, NULL,
-					box->margin, NULL, NULL);
-		}
-
-		if (max_pos_margin < box->margin[TOP])
-			max_pos_margin = box->margin[TOP];
-		else if (max_neg_margin < -box->margin[TOP])
-			max_neg_margin = -box->margin[TOP];
-
 		/* Blocks establishing a block formatting context get minimum
 		 * left and right margins to avoid any floats. */
 		lm = rm = 0;
@@ -405,8 +404,9 @@
 					 * diminished due to floats. */
 					int x0, x1, top;
 					struct box *left, *right;
-					top = cy > y ? cy : y;
-					top += max_pos_margin - max_neg_margin;
+					top = cy + max_pos_margin -
+							max_neg_margin;
+					top = (top > y) ? top : y;
 					x0 = cx;
 					x1 = cx + box->parent->width -
 						box->parent->padding[LEFT] -
@@ -436,34 +436,47 @@
 		cx += box->x;
 
 		/* Position box: vertical. */
-		if (box->type != BOX_BLOCK || y ||
-				box->border[TOP].width || box->padding[TOP]) {
-			margin_box->y += max_pos_margin - max_neg_margin;
-			cy += max_pos_margin - max_neg_margin;
-			max_pos_margin = max_neg_margin = 0;
-			margin_box = 0;
+		if (box->border[TOP].width) {
 			box->y += box->border[TOP].width;
 			cy += box->border[TOP].width;
-			if (cy < y) {
-				box->y += y - cy;
-				cy = y;
-			}
 		}
 
+		/* Vertical margin */
+		if (((box->type == BOX_BLOCK &&
+				(box->flags & HAS_HEIGHT)) ||
+				box->type == BOX_TABLE ||
+				(box->type == BOX_INLINE_CONTAINER &&
+				box != box->parent->children) ||
+				margin_collapse == box) &&
+				in_margin == true) {
+			/* Margin goes above this box. */
+			cy += max_pos_margin - max_neg_margin;
+			box->y += max_pos_margin - max_neg_margin;
+
+			/* Current margin has been applied. */
+			in_margin = false;
+			max_pos_margin = max_neg_margin = 0;
+		}
+
+		/* Handle clearance */
+		if (box->type != BOX_INLINE_CONTAINER &&
+				(y > 0) && (cy < y)) {
+			/* box clears something*/
+			box->y += y - cy;
+			cy = y;
+		}
+
 		/* Unless the box has an overflow style of visible, the box
 		 * establishes a new block context. */
 		if (box->type == BOX_BLOCK && box->style &&
 				css_computed_overflow(box->style) !=
 				CSS_OVERFLOW_VISIBLE) {
-			cy += max_pos_margin - max_neg_margin;
-			box->y += max_pos_margin - max_neg_margin;
 
 			layout_block_context(box, viewport_height, content);
 
-			if (box->type == BOX_BLOCK || box->object)
-				cy += box->padding[TOP];
+			cy += box->padding[TOP];
 
-			if (box->type == BOX_BLOCK && box->height == AUTO) {
+			if (box->height == AUTO) {
 				box->height = 0;
 				layout_block_add_scrollbar(box, BOTTOM);
 			}
@@ -471,23 +484,13 @@
 			cx -= box->x;
 			cy += box->height + box->padding[BOTTOM] +
 					box->border[BOTTOM].width;
-			max_pos_margin = max_neg_margin = 0;
-			if (max_pos_margin < box->margin[BOTTOM])
-				max_pos_margin = box->margin[BOTTOM];
-			else if (max_neg_margin < -box->margin[BOTTOM])
-				max_neg_margin = -box->margin[BOTTOM];
 			y = box->y + box->padding[TOP] + box->height +
 					box->padding[BOTTOM] +
 					box->border[BOTTOM].width;
+
 			/* Skip children, because they are done in the new
 			 * block context */
 			goto advance_to_next_box;
-		} else if (box->type == BOX_BLOCK && (box->flags & HAS_HEIGHT)) {
-			/* This block doesn't establish a new block formatting
-			 * context */
-			cy += max_pos_margin - max_neg_margin;
-			box->y += max_pos_margin - max_neg_margin;
-			max_pos_margin = max_neg_margin = 0;
 		}
 
 #ifdef LAYOUT_DEBUG
@@ -546,14 +549,17 @@
 		/* Advance to next box. */
 		if (box->type == BOX_BLOCK && !box->object && box->children) {
 			/* Down into children. */
+
+			if (box == margin_collapse) {
+				/* Current margin collapsed though to this box.
+				 * Unset margin_collapse. */
+				margin_collapse = NULL;
+			}
+
 			y = box->padding[TOP];
 			box = box->children;
 			box->y = y;
 			cy += y;
-			if (!margin_box) {
-				max_pos_margin = max_neg_margin = 0;
-				margin_box = box;
-			}
 			continue;
 		} else if (box->type == BOX_BLOCK || box->object)
 			cy += box->padding[TOP];
@@ -565,23 +571,33 @@
 
 		cy += box->height + box->padding[BOTTOM] +
 				box->border[BOTTOM].width;
-		max_pos_margin = max_neg_margin = 0;
-		if (max_pos_margin < box->margin[BOTTOM])
-			max_pos_margin = box->margin[BOTTOM];
-		else if (max_neg_margin < -box->margin[BOTTOM])
-			max_neg_margin = -box->margin[BOTTOM];
 		cx -= box->x;
 		y = box->y + box->padding[TOP] + box->height +
 				box->padding[BOTTOM] +
 				box->border[BOTTOM].width;
+
 	advance_to_next_box:
 		if (!box->next) {
 			/* No more siblings:
 			 * up to first ancestor with a sibling. */
+
+			if (box == margin_collapse) {
+				/* Current margin collapsed though to this box.
+				 * Unset margin_collapse. */
+				margin_collapse = NULL;
+			}
+
+			/* Apply bottom margin */
+			if (max_pos_margin < box->margin[BOTTOM])
+				max_pos_margin = box->margin[BOTTOM];
+			else if (max_neg_margin < -box->margin[BOTTOM])
+				max_neg_margin = -box->margin[BOTTOM];
+
 			do {
 				box = box->parent;
 				if (box == block)
 					break;
+
 				if (box->height == AUTO) {
 					box->height = y - box->padding[TOP];
 
@@ -607,22 +623,37 @@
 
 				cy += box->padding[BOTTOM] +
 						box->border[BOTTOM].width;
+				cx -= box->x;
+				y = box->y + box->padding[TOP] + box->height +
+						box->padding[BOTTOM] +
+						box->border[BOTTOM].width;
+
+				/* Apply bottom margin */
 				if (max_pos_margin < box->margin[BOTTOM])
 					max_pos_margin = box->margin[BOTTOM];
 				else if (max_neg_margin < -box->margin[BOTTOM])
 					max_neg_margin = -box->margin[BOTTOM];
-				cx -= box->x;
-				y = box->y + box->padding[TOP] + box->height +
-						box->padding[BOTTOM] +
-						box->border[BOTTOM].width;
-			} while (box != block && !box->next);
+
+			} while (box->next == NULL);
 			if (box == block)
 				break;
 		}
+
 		/* To next sibling. */
+
+		if (box == margin_collapse) {
+			/* Current margin collapsed though to this box.
+			 * Unset margin_collapse. */
+			margin_collapse = NULL;
+
+			/* Apply bottom margin */
+			if (max_pos_margin < box->margin[BOTTOM])
+				max_pos_margin = box->margin[BOTTOM];
+			else if (max_neg_margin < -box->margin[BOTTOM])
+				max_neg_margin = -box->margin[BOTTOM];
+		}
 		box = box->next;
 		box->y = y;
-		margin_box = box;
 	}
 
 	/* Account for bottom margin of last contained block */
@@ -737,11 +768,16 @@
 			case BOX_INLINE_CONTAINER:
 				layout_minmax_inline_container(child,
 						&child_has_height, font_func);
+				if (child_has_height &&
+						child ==
+						child->parent->children)
+					block->flags |= MAKE_HEIGHT;
 				break;
 			case BOX_TABLE:
 				layout_minmax_table(child, font_func);
 				/* todo: fix for zero height tables */
 				child_has_height = true;
+				child->flags |= MAKE_HEIGHT;
 				break;
 			default:
 				assert(0);
@@ -779,8 +815,10 @@
 		min = max = FIXTOINT(nscss_len2px(width, wunit, block->style));
 	}
 
-	if (height > 0 && hunit != CSS_UNIT_PCT)
+	if (height > 0 && hunit != CSS_UNIT_PCT) {
+		block->flags |= MAKE_HEIGHT;
 		block->flags |= HAS_HEIGHT;
+	}
 
 	/* add margins, border, padding to min, max widths */
 	/* Note: we don't know available width here so percentage margin
@@ -824,6 +862,119 @@
 
 
 /**
+ * Find next block that current margin collapses to.
+ *
+ * \param  box    box to start tree-order search from (top margin is included)
+ * \param  block  box responsible for current block fromatting context
+ * \param  viewport_height  height of viewport in px
+ * \param  max_pos_margin  updated to to maximum positive margin encountered
+ * \param  max_neg_margin  updated to to maximum negative margin encountered
+ * \return  next box that current margin collapses to, or NULL if none.
+ */
+
+struct box* layout_next_margin_block(struct box *box, struct box *block,
+		int viewport_height, int *max_pos_margin, int *max_neg_margin)
+{
+	assert(block != NULL);
+
+	while (box != NULL) {
+
+		if (box->style &&
+				(css_computed_position(box->style) !=
+					CSS_POSITION_ABSOLUTE &&
+				 css_computed_position(box->style) !=
+					CSS_POSITION_FIXED)) {
+			/* Not an inline_container and not positioned */
+
+			/* Get margins */
+			if (box->style) {
+				layout_find_dimensions(box->parent->width,
+						viewport_height, box,
+						box->style,
+						NULL, NULL, NULL, NULL,
+						box->margin, box->padding,
+						box->border);
+			}
+
+			/* Apply top margin */
+			if (*max_pos_margin < box->margin[TOP])
+				*max_pos_margin = box->margin[TOP];
+			else if (*max_neg_margin < -box->margin[TOP])
+				*max_neg_margin = -box->margin[TOP];
+
+			/* Check whether box is the box current margin collapses
+			 * to */
+			if (box->flags & MAKE_HEIGHT ||
+					box->border[TOP].width ||
+					box->padding[TOP] ||
+					(box->style &&
+					css_computed_overflow(box->style) !=
+					CSS_OVERFLOW_VISIBLE)) {
+				/* Collapse to this box; return it */
+				return box;
+			}
+		}
+
+
+		/* Find next box */
+		if (box->type == BOX_BLOCK && !box->object && box->children &&
+				box->style &&
+				css_computed_overflow(box->style) ==
+				CSS_OVERFLOW_VISIBLE) {
+			/* Down into children. */
+			box = box->children;
+		} else {
+			if (!box->next) {
+				/* No more siblings:
+				 * Go up to first ancestor with a sibling. */
+				do {
+					/* Apply bottom margin */
+					if (*max_pos_margin <
+							box->margin[BOTTOM])
+						*max_pos_margin =
+							box->margin[BOTTOM];
+					else if (*max_neg_margin <
+							-box->margin[BOTTOM])
+						*max_neg_margin =
+							-box->margin[BOTTOM];
+
+					box = box->parent;
+				} while (box != block && !box->next);
+
+				if (box == block) {
+					/* Margins don't collapse with stuff
+					 * outside the block formatting context
+					 */
+					return block;
+				}
+			}
+
+			/* Apply bottom margin */
+			if (*max_pos_margin < box->margin[BOTTOM])
+				*max_pos_margin = box->margin[BOTTOM];
+			else if (*max_neg_margin < -box->margin[BOTTOM])
+				*max_neg_margin = -box->margin[BOTTOM];
+
+			/* To next sibling. */
+			box = box->next;
+
+			/* Get margins */
+			if (box->style) {
+				layout_find_dimensions(box->parent->width,
+						viewport_height, box,
+						box->style,
+						NULL, NULL, NULL, NULL,
+						box->margin, box->padding,
+						box->border);
+			}
+		}
+	}
+
+	return NULL;
+}
+
+
+/**
  * Layout a block which contains an object.
  *
  * \param  block  box of type BLOCK, INLINE_BLOCK, TABLE, or TABLE_CELL
Index: render/box.h
===================================================================
--- render/box.h	(revision 12213)
+++ render/box.h	(working copy)
@@ -124,7 +124,8 @@
 	PRE_STRIP   = 1 << 3,	/* PRE tag needing leading newline stripped */
 	CLONE       = 1 << 4,	/* continuation of previous box from wrapping */
 	MEASURED    = 1 << 5,	/* text box width has been measured */
-	HAS_HEIGHT  = 1 << 6	/* box has height */
+	HAS_HEIGHT  = 1 << 6,	/* box has height (perhaps due to children) */
+	MAKE_HEIGHT = 1 << 7	/* box causes its own height */
 } box_flags;
 
 /* Sides of a box */


More information about the netsurf-dev mailing list