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.
A Fresh Layouting System
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, where you can try out our layout concept firsthand with just a click and receive real-time feedback.
Layouting Quickstart
What Makes a Great Layout System
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:
Pythonic: Rio embraces Python’s simplicity. No numbers encoded as strings
or complex unit declarations. For example,width=10
is preferred over
width="10px"
. This isn’t just cleaner, but also aids type checking tools and
allows for easy mathematical operations.Simple: The system should be intuitive, even for developers without a deep
background in UI design. Rather than a tangled set of rules and exceptions, it
should be governed by a handful of principles that are applied consistently.Flexible: From basic layouts to complex, nested structures, the system
needs to handle them all. Templates and restrictive built-in patterns won’t
cut it.Efficient: Especially when targeting the web, performance is key. A good
layouting system should be automatically convertible to CSS, maybe with some
JavaScript sprinkled in occasionally for dynamic calculations.
Rio’s Approach: Two-Step Layouting
We've decided on a two-step layouting system that balances simplicity and
flexibility. Here's how it works:
Step 1: Natural Size Calculation
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.
Step 2: Space Distribution
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.
Example
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.
Implementation
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.
Limitations and Trade-offs
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:
Some layouts cannot achieved just using CSS. Notably, when using the
proportions
attribute ofrio.Row
orrio.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!)There are occasional layouts that cannot be realized with this system. One
such situation that we've found is to have a row containing an icon and text,
while that text is both centered and has wrapping enabled.
This sounds like a simple case, but no combination of parameters yields the
desired result. Aligning the text to 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.
We've had some discussions about introducing a "preferred size" that would
solve this, but it's not in Rio yet.
Github: https://github.com/rio-labs/rio
Website: https://rio.dev/
Top comments (0)