class: slide-title
Software Design by Example
Page Layout
chapter
--- ## The Problem - Text is a sequence of characters, but a page is two-dimensional - How can we put the right things in the right places? - Create a simple version of a
layout engine
for a browser - But the same ideas apply to print --- ## Coordinate Systems - Teletypes started printing in the upper left corner of the page - So the coordinate systems for screens put (0, 0) in the upper left instead of the lower left - Y increases going *down* - At least X increases to the right as usual
--- ## Block Model - Every cell is a rectangular block - Row arranges sub-blocks horizontally - Column arranges sub-blocks vertically
--- ## Generic Block ```py class Block: def __init__(self, width, height): self.width = width self.height = height def get_width(self): return self.width def get_height(self): return self.height ``` - Would calculate size based on contents (image, text, etc.) --- ## Rows - Width: sum of widths of children - Height: height of tallest child ```py class Row: def __init__(self, *children): self.children = list(children) def get_width(self): return sum([c.get_width() for c in self.children]) def get_height(self): return max( [c.get_height() for c in self.children], default=0 ) ``` --- ## Columns - Width: width of widest child - Height: sum of heights of children ```py class Col: def __init__(self, *children): self.children = list(children) def get_width(self): return max( [c.get_width() for c in self.children], default=0 ) def get_height(self): return sum([c.get_height() for c in self.children]) ``` --- ## Nesting - Rows can contain blocks and columns but must be contained in a single column (unless it's the root) - Columns can contain blocks or rows but must be contained in a single row (unless it's the root) - Can therefore represent document as a tree ```py def test_lays_out_a_grid_of_rows_of_columns(): fixture = Col( Row(Block(1, 2), Block(3, 4)), Row(Block(5, 6), Col(Block(7, 8), Block(9, 10))) ) assert fixture.get_width() == 14 assert fixture.get_height() == 22 ``` --- ## Positioning - Once we know sizes we can calculate positions - E.g., if cell is a row at `(x0, y0)`: - Its lower edge is `y1 = y0 + height` - Its first child's upper-left corner is `(x0, y1)` - Second child's upper-left corner is `(x0 + width0, y1)`
--- ## Positioning ```py def place(self, x0, y0): self.x0 = x0 self.y0 = y0 y1 = self.y0 + self.get_height() x_current = x0 for child in self.children: child_y = y1 - child.get_height() child.place(x_current, child_y) x_current += child.get_width() ``` --- ## Rendering - Draw parents before children so that children over-draw - A simple form of
z-buffering
--- ## Rendering - Create a character "screen" ```py def make_screen(width, height): screen = [] for i in range(height): screen.append([" "] * width) return screen ``` - Add a method for blocks to draw ```py class Renderable: def render(self, screen, fill): for ix in range(self.get_width()): for iy in range(self.get_height()): screen[self.y0 + iy][self.x0 + ix] = fill ``` --- ## Mixin Class
--- ## Wrapping - Fix width of row (for example) - If total width of children is greater than this, need to wrap the children to a new row - Assuming no single child is too wide - Handle this by modifying the tree ```py class WrappedBlock(PlacedBlock): def wrap(self): return self class WrappedCol(PlacedCol): def wrap(self): return PlacedCol(*[c.wrap() for c in self.children]) ``` --- ## Wrapping Rows
--- ## Wrapping Rows - New row class takes a fixed width and some children - Returns that fixed width when asked for its size ```py class WrappedRow(PlacedRow): def __init__(self, width, *children): super().__init__(*children) assert width >= 0, "Need non-negative width" self.width = width def get_width(self): return self.width ``` --- ## Wrapping Rows - Wrapping puts the row's children into buckets - Converts each bucket to a row with a column of rows ```py def wrap(self): children = [c.wrap() for c in self.children] rows = self._bucket(children) new_rows = [PlacedRow(*r) for r in rows] new_col = PlacedCol(*new_rows) return PlacedRow(new_col) ``` --- ## Bucketing ```py def _bucket(self, children): result = [] current_row = [] current_x = 0 for child in children: child_width = child.get_width() if (current_x + child_width) <= self.width: current_row.append(child) current_x += child_width else: result.append(current_row) current_row = [child] current_x = child_width result.append(current_row) return result ``` --- class: summary ## Summary