The Problem

Coordinate Systems

Coordinate system
Figure 1: Coordinate system with (0, 0) in the upper left corner.

Block Model

Calculating sizes of fixed blocks
Figure 2: Calculating sizes of blocks with fixed width and height.

Generic Block

i
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

Rows

i
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

i
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

i
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

Laying out rows and columns
Figure 3: Laying out rows and columns of fixed-size blocks.

Positioning

i
    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

Children drawing over their parents
Figure 4: Render blocks by drawing child nodes on top of parent nodes.

Rendering

i
def make_screen(width, height):
    screen = []
    for i in range(height):
        screen.append([" "] * width)
    return screen
i
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

Adding methods with a mixin class
Figure 5: Using multiple inheritance and a mixin class to add methods.

Wrapping

i
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
Figure 6: Wrapping rows by introducing a new row and column.

Wrapping Rows

i
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

i
    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

i
    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

Summary

Concept map for page layout
Figure 7: Concept map.