Skip to content

How Layout Sizes Things

Counterweight lays out elements with Taffy, which implements CSS flexbox and grid with their CSS defaults:

  • flex_direction is row.
  • flex_grow is 0, so children don't grow into free space on the main axis.
  • flex_shrink is 1, so children shrink when they don't fit.
  • align_items is stretch, so children fill their parent on the cross axis.
  • Sizes are border-box: a width includes the border and padding.

Together, these mean a child takes its content's size along its parent's direction, and its parent's full size across it.

Children take their content size along the main axis

In a row, each child is as wide as its content and as tall as the row:

@component
def row_defaults() -> Div:
    return Div(
        style=row,
        children=[
            Text(style=border, content="one"),
            Text(style=border, content="two two"),
            Text(style=border, content="three three three"),
        ],
    )

Children of a row

┌───┐┌───────┐┌─────────────────┐                 
│one││two two││three three three│                 
│   ││       ││                 │                 
│   ││       ││                 │                 
└───┘└───────┘└─────────────────┘                 

The space to the right is free space that no child claimed. Splitting space shows how to hand it out.

Children stretch across the cross axis

In a col, the axes swap: each child is as tall as its content and as wide as the column.

@component
def col_defaults() -> Div:
    return Div(
        style=col,
        children=[
            Text(style=border, content="one"),
            Text(style=border, content="two two"),
            Text(style=border, content="three three three"),
        ],
    )

Children of a column

┌────────────────────────────────────────────────┐
│one                                             │
└────────────────────────────────────────────────┘
┌────────────────────────────────────────────────┐
│two two                                         │
└────────────────────────────────────────────────┘
┌────────────────────────────────────────────────┐
│three three three                               │
└────────────────────────────────────────────────┘

Stretching is the default, so align_children_stretch never needs to be written out. Alignment and distribution shows the alternatives.

The root fills the screen

The app places the root component in a grid cell the size of the terminal, and the root stretches to fill it, whatever its content. Content wider than the terminal can't widen the root: the wide line is cut off at the root's border.

@component
def root_fills_screen() -> Div:
    return Div(
        style=col | border | border_heavy,
        children=[Text(content="root: col | border | border_heavy"), Text(content=WIDE)],
    )

The root filling the screen

┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃root: col | border | border_heavy               ┃
┃this line is wider than the fifty-column screen ┃
┃                                                ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

Content sets a minimum size

A flex item can't shrink below its content's minimum size, which CSS calls the automatic minimum size. For a Text that doesn't wrap, that minimum is the whole line; for one that wraps, it's the widest word. In the top row below, two grow(1) children with long lines can't take half the row each, so they overflow it.

fill(1) has no automatic minimum, so in the bottom row the same two children split the row evenly, and their text is cut off instead. It hides the child's overflow, and CSS gives a box that hides its overflow a minimum size of zero. min_width(0) (or min_height(0) in a col) has the same effect on its own axis.

@component
def automatic_minimum() -> Div:
    return Div(
        style=col,
        children=[
            Div(
                style=row | border | border_heavy,
                children=[
                    Text(style=grow(1) | border, content=LONG),
                    Text(style=grow(1) | border, content=LONG),
                ],
            ),
            Div(
                style=row | border | border_heavy,
                children=[
                    Text(style=fill(1) | border, content=LONG),
                    Text(style=fill(1) | border, content=LONG),
                ],
            ),
        ],
    )

The automatic minimum size

┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃┌────────────────────────────────────────────┐┌────────────
┃│a line of text much longer than half the row││a line of te
┃└────────────────────────────────────────────┘└────────────
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃┌───────────────────────────┐┌───────────────────────────┐┃
┃│a line of text much longer ││a line of text much longer │┃
┃└───────────────────────────┘└───────────────────────────┘┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

Grid tracks have the same floor; Content wider than its share shows it and its fix.

Text doesn't wrap unless asked to

A Text renders each line of its content on one row, and cuts it off at the edge of its box. Setting a wrap mode such as text_wrap_stable breaks lines to fit the box's width instead. Text in layout shows how wrapping text sizes itself, and Text wrapping compares the wrap modes.

@component
def wrapping() -> Div:
    return Div(
        style=row,
        children=[
            Text(style=width(24) | border, content=PARAGRAPH),
            Text(style=width(24) | border | text_wrap_stable, content=PARAGRAPH),
        ],
    )

Wrapped and unwrapped text

┌──────────────────────┐┌──────────────────────┐
│Text stays on one line││Text stays on one line│
│                      ││unless its style sets │
│                      ││a wrap mode, however  │
│                      ││narrow its box.       │
│                      ││                      │
└──────────────────────┘└──────────────────────┘