We recently launched Rio, our new framework designed to help you create web and local applications using just pure Python. The response from our community has been overwhelmingly positive and incredibly motivating for us. With the praise has come a wave of curiosity. The most common question we’ve encountered is, “How does Rio actually work?” If you’ve been wondering the same thing, you’re in the right place! Here we’ll explore the inner workings of Rio and uncover what makes it so powerful.
When it comes to building modern apps, components are just the beginning, it’s the layouting that pulls them together into a cohesive, user-friendly interface. In our last topic, we took a closer look at components, the essential pieces of any app. Now, let’s shift our focus to the layout system that arranges them harmoniously. Designing a layout system isn’t a one-size-fits-all task; different frameworks bring unique strengths, with CSS often at the center of debate. As we designed Rio’s layout system, we aimed for something Pythonic, simple, flexible, and efficient — a system that keeps developers focused on their app’s function, not the complexities of positioning. Here, we’ll break down the core principles behind Rio’s two-step approach to layouting, where each component starts by defining its own natural size before the available space is thoughtfully distributed.
Take a look at our playground (Layouting Quickstart), where you can try out our layout concept firsthand with just a click and receive real-time feedback.
Each UI framework approaches layouting differently, all with their own unique strengths and quirks. There’s of course the polarizing incumbent, CSS, but also the many systems built into popular frameworks like Flutter, QT, and others. Before we got to designing our own, we took a step back to understand what makes a great layout system. Here are some key principles we identified:
width=10
is preferred over width="10px"
. This isn’t just cleaner, but also aids type checking tools and allows for easy mathematical operations.We’ve decided on a two-step layouting system that balances simplicity and flexibility. Here’s how it works:
First, each component determines how much space it needs to fit its content. We call this the component’s “natural size.”
For some components this is simple. For example, rio.Switch
has a fixed size, so that is also its natural size. But not all components are that simple. Take a rio.Row
for example. The row itself doesn't need any space, but it needs to request enough space to fit its children. So the natural width of a row is the sum of the natural widths of its children, plus any spacing between them.
Another more in-depth example is rio.Text
. It's size depends on a variety of things, such as its text content, font size, whether the font is bold, etc.
This process starts at the leaves of the component tree, i.e. first components without any children are calculated, then their parents, and so on, until the entire tree has computed its natural width & height.
Once each component has determined its natural size, we must decide how to allocate the available space. For example, in a large window with only a button, should the button be centered, aligned to one side, or stretched?
There isn’t a real reason to prefer one over the other. We are solving this, by simply giving all space to the button. If you don’t want for that to happen, you can explicitly set the button’s alignment, and Rio will take it into account.
Imagine a simple rio.Row
containing a rio.Text
and a rio.Switch
. First, we need to calculate the natural size of all components. We'll start with components that don't have any children, so in this case the rio.Text
and rio.Switch
.
The rio.Text
will calculate its natural size based on its text content, font size, etc. The rio.Switch
has a fixed size, so that is also its natural size.
Now that all children have had their natural size calculated, the rio.Row
can get to work. It's natural width is the sum of the natural widths of its children (plus any spacing) and its natural height is the maximum of the natural heights of its children.
Finally, we need to distribute the available space. Since the window itself always has a single child, it will pass all available space to that child — in this case the rio.Row
. The row will then distribute the space to its children; But how?
Since there’s only so few components in the view there is likely too much space available. Thus, the rio.Row
will have to decide how much space to pass to each child. Since we always want all components to have enough space to fit their content ("natural size") we'll allocate each child that much space. Then, if space is leftover we can distribute that proportionally. Ta-da! All components now know how large they are. Their positions also follow from this.
Since this is such a common use-case, rio.Row
also honors the grow_x
and grow_y
attributes of components. If a component is marked to grow, and superfluous space is available, all space will be given to that component. If there are multiple components that are marked to grow, the space will be distributed proportionally just between those components.
The system described above is what we call our reference layouting system. It isn’t actually implemented like this in code, but rather as a set of CSS rules, that result in this exact behavior. This allows our layouting to run at maximum performance, because it relies on the browser’s native layouting engine. The described algorithm is nonetheless useful, as it guides Rio developers to how a component should behave. It’s just that this behavior is then achieved by internal CSS rules rather than the algorithm itself.
Best of all, you’ll never have to see a single line of CSS. All of this is handled by Rio internally, so you can focus on building your app.
While Rio’s layout system is powerful and flexible, it isn’t perfect. In the interest of full transparency, we’d like to share some limitations that we’ve found:
proportions
attribute of rio.Row
or rio.Column
, JavaScript jumps into action to help out, because we are not aware of any pure-CSS way to achieve our desired behavior. (If you're a CSS magician and know a way, reach out!)0.5
(center) won't center the icon. Same when using the justify
attribute. Aligning the entire rio.Row
will make the row take up as little space as possible, thus squishing the text.
Github: https://github.com/rio-labs/rio
Website: https://rio.dev/