Spacing and Borders
The box model
Counterweight's layout follows the CSS box model. Every element's box is four nested rectangles:
- Content: where the element's content goes.
A
Div's content is its children, and aText's content is its text. - Padding: space between the content and the border.
- Border: the box-drawing characters around the padding.
- Margin: space between the border and the element's neighbors.
The example below colors each rectangle:
@component
def root() -> Div:
return Div(
style=col,
children=[
Div(
style=fill(1)
| content_color("green", 500)
| padding_color("orange", 500)
| pad_x(2)
| pad_y(1)
| border
| border_lightrounded
| border_bg("blue", 500)
| margin_color("red", 500)
| margin_x(2)
| margin_y(1)
)
],
)
Terminal cells are not square
Terminal cells are about twice as tall as they are wide, so one row of vertical padding or margin looks about as big as two columns of horizontal. The example above uses twice as much horizontal spacing as vertical for that reason, and horizontal spacing alone is often enough.
Gap, margin and padding
All three put space around children, but they belong to different elements:
gapis set on the parent, and puts space between its children only.marginis set on a child, and puts space all around it, including at the ends of the row. Margins don't collapse into each other, so two children withmargin_x(1)are two cells apart.padis set on the parent, and puts space between its border and all of its children.
margin_color and padding_color color the margin and padding, in red and blue here.
@component
def gap_margin_pad() -> Div:
return Div(
style=col,
children=[
Div(
style=row | gap(2) | border | border_heavy,
children=[Text(style=border, content=f"gap(2) on the row {n}") for n in range(2)],
),
Div(
style=row | border | border_heavy,
children=[
Text(style=margin_x(1) | margin_color("red", 600) | border, content=f"margin_x(1) {n}")
for n in range(2)
],
),
Div(
style=row | pad_x(2) | padding_color("blue", 600) | border | border_heavy,
children=[Text(style=border, content=f"pad_x(2) on the row {n}") for n in range(2)],
),
],
)
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃┌───────────────────┐ ┌───────────────────┐ ┃
┃│gap(2) on the row 0│ │gap(2) on the row 1│ ┃
┃└───────────────────┘ └───────────────────┘ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ┌─────────────┐ ┌─────────────┐ ┃
┃ │margin_x(1) 0│ │margin_x(1) 1│ ┃
┃ └─────────────┘ └─────────────┘ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ┌─────────────────────┐┌─────────────────────┐ ┃
┃ │pad_x(2) on the row 0││pad_x(2) on the row 1│ ┃
┃ └─────────────────────┘└─────────────────────┘ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
Sharing borders between neighbors
border_collapse on a parent overlaps its children's borders by one cell,
so neighbors share a border instead of drawing two side by side.
Where the shared borders meet, border healing
joins them with the right junction characters.
@component
def collapse() -> Div:
return Div(
style=col,
children=[
Div(
style=row,
children=[Text(style=border, content="side by side") for _ in range(3)],
),
Div(
style=row | border_collapse,
children=[Text(style=border, content="border_collapse") for _ in range(3)],
),
],
)
Borders on some sides
Borders work as they do in Tailwind.
A side is drawn where its border width is 1, and layout reserves a cell for it.
border sets all four sides, border_top, border_bottom, border_left and border_right
set one, and border_x and border_y set a pair.
Each has a _0 form, such as border_right_0, that takes its sides away.
Sides start at 0, so border_top | border_left draws just those two.
Merging is ordered, as it is for every style: the right side wins.
border | border_right_0 leaves the right side off,
but border_right_0 | border turns it back on.
The border kind only chooses the characters, and defaults to BorderKind.Light,
so border alone draws a light border and border | border_heavy a heavy one.
A kind with no widths draws nothing.
border_none removes the border along with its space.
border_sides sets all four sides at once from a set of side names.
@component
def sides() -> Div:
return Div(
style=row | align_children_start | gap(2),
children=[
Text(style=border_y, content="border_y"),
Text(style=border_top | border_left, content="border_top\n| border_left"),
Text(style=border | border_right_0, content="border\n| border_right_0"),
Text(style=border_right_0 | border, content="border_right_0\n| border"),
],
)
Shortening edges next to missing sides
border_contract(n) stops each edge n cells short at an end next to a side that isn't drawn.
@component
def contract() -> Div:
return Div(
style=row | align_children_start | gap(2),
children=[
Text(
style=border_top | border_left | pad_x(1),
content="top and left\nsides",
),
Text(
style=border_top | border_left | border_contract(2) | pad_x(1),
content="top and left\nsides\nborder_contract(2)",
),
],
)