Flexbox Layout Engine

A simple flexbox-like layout engine I built for my dashboard project to simplify layout computations.
3 min read

As part of my dashboard project, I quickly realizes for complicated widgets that the math for positioning in the more complicated widgets is cumbersome to both build and maintain. I’m in the process of building a weather widget, and there’s a fair amount of labels, images, etc. Computing the positions of all these manually is pretty tedious. Writing this to use more high-level markup to define the layout vastly increased my velocity on the rendering portion of this widget.

I decided to build a simple layout engine inspired by parts of the CSS FlexBox, to auto compute the bounds of labels, images, etc. in the widgets.

Assumptions

There are a couple key assumptions and constraints I’m working with:

  • The screen size is fixed
  • Widgets render into the size they are given

With these in mind, given a screen size, we can apply some algorithms procedurally to compute sizes of a decently complex set of nested flex rows & columns, including their padding (which may differ on each side), and the flex gap.

To keep things simple, I’m not supporting flex wrap, or any size constraints on the flex items themselves, other than their relative weight to each other. This determines how much of the space relative to the rest of the items in the row/column the box should get.

Output

First, we’ll start with how this could be used.

flex layout

You’ll notice both red and gray boxes. The gray ones are the border of each box, and the red boxes are the inside of the box’s padding.

As you’ll see in the next section, even though it’s using Python classes, the properties you can set are pretty similar to how you might structure a flexbox layout using HTML/CSS.

(now in fb-dashboard, most widgets that use flexbox can show them if you set their debug option to true, or pass --debug on the command line)

Example Layout

We can construct a layout like I’ve done in the example below. This runs recursively, so a box can have children, which can have their own as well. Each box has a flex direction, padding, and gap.

This evenly divides space within a box to each of its children, in the correct direction and with the specified gap. Assigning a weight to a child gives it space relative to the other children’s weights. By default, each weight is set to 1.

Both the paddings and gaps can be absolute values in pixels, or values relative to the root view size (using vw and vh units as in CSS). Percentages of the current direction can also be used, so a gap of 5% yields a gap of 0.05 * box_width.

weather_layout = SimpleFlexBox(
    identifier="weather_layout",
    flex_direction="column",
    padding='2vw',
    children=[
        SimpleFlexBox(
            identifier="top_row",
            flex_direction="row",
            weight=2,
            padding=('5vw', 0, '5vw', 0),
            children=[
                SimpleFlexBox(
                    identifier="icon_box",
                    padding=(0, '2vw', 0, '5vw'),
                ),
                SimpleFlexBox(
                    identifier="text_box",
                    padding=(0, '5vw', 0, '5vw'),
                    gap='5%',
                    weight=2,
                    flex_direction='column',
                    children=[
                        SimpleFlexBox(
                            identifier="period_name",
                        ),
                        SimpleFlexBox(
                            identifier="temperature",
                            weight=4
                        ),
                        SimpleFlexBox(
                            identifier="short_forecast",
                        )
                    ]
                )
            ]
        ),
        SimpleFlexBox(
            identifier="bottom_row",
            flex_direction="row",
            children=[
                SimpleFlexBox(
                    identifier="detailed_forecast",
                    padding=('2vw', '5vw', '2vw', '5vw'),
                    gap='5vw',
                    children=[
                        SimpleFlexBox(
                            identifier='wind_speed',
                            flex_direction='column',
                            gap='5%',
                            children=[
                                SimpleFlexBox(
                                    identifier='wind_speed_value',
                                    weight=3
                                ),
                                SimpleFlexBox(
                                    identifier='wind_speed_label'
                                ),
                            ]
                        ),
                        SimpleFlexBox(
                            identifier='wind_direction',
                            flex_direction='column',
                            gap='5%',
                            children=[
                                SimpleFlexBox(
                                    identifier='wind_direction_value',
                                    weight=3
                                ),
                                SimpleFlexBox(
                                    identifier='wind_direction_label'
                                ),
                            ]
                        ),
                        SimpleFlexBox(
                            identifier='rain',
                            flex_direction='column',
                            gap='5%',
                            children=[
                                SimpleFlexBox(
                                    identifier='rain_value',
                                    weight=3
                                ),
                                SimpleFlexBox(
                                    identifier='rain_label'
                                ),
                            ]
                        ),
                    ]
                )
            ]
        )
    ]
)

Output

Running the compute_sizes function with an offset/region returns a list of all the box identifiers and their absolute positions on screen.

layout = weather_layout.compute_sizes(0, 0, self.width, self.height)

# this outputs:
{'bottom_row': {'box': (20, 505, 984, 243), 'content_box': (20, 505, 984, 243)},
 'detailed_forecast': {'box': (20, 505, 984, 243),
                       'content_box': (71, 525, 882, 203)},
 'icon_box': {'box': (20, 20, 328, 485), 'content_box': (71, 71, 226, 383)},
 'period_name': {'box': (399, 71, 554, 56), 'content_box': (399, 71, 554, 56)},
 'short_forecast': {'box': (399, 398, 554, 56),
                    'content_box': (399, 398, 554, 56)},
 'temperature': {'box': (399, 151, 554, 224),
                 'content_box': (399, 151, 554, 224)},
 'text_box': {'box': (348, 20, 656, 485), 'content_box': (399, 71, 554, 383)},
 'top_row': {'box': (20, 20, 984, 485), 'content_box': (20, 20, 984, 485)},
 'weather_layout': {'box': (0, 0, 1024, 768),
                    'content_box': (20, 20, 984, 728)},
 'wind_direction': {'box': (538, 525, 416, 203),
                    'content_box': (538, 525, 416, 203)},
 'wind_speed': {'box': (71, 525, 416, 203), 'content_box': (71, 525, 416, 203)}}

Source Code

You can find the source code here. It also needs the util.py file which contains some utilities for evaluating expressions related to sizing.

https://github.com/richinfante/fb-dashboard/blob/main/fb_dashboard/sfxbox.py

The rest of the dashboard project can also be found in the same repo.

Subscribe to my Newsletter

Like this post? Subscribe to get notified for future posts like this.

Change Log

  • 8/13/2024 - Initial Revision

Found a typo or technical problem? file an issue!