paint-brush
Rio: WebApps in pure Python — A fresh Layouting Systemby@d4vid

Rio: WebApps in pure Python — A fresh Layouting System

by DavidNovember 6th, 2024
Read on Terminal Reader
tldt arrow

Too Long; Didn't Read

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.
featured image - Rio: WebApps in pure Python — A fresh Layouting System
David HackerNoon profile picture

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 (Layouting Quickstart), where you can try out our layout concept firsthand with just a click and receive real-time feedback.


DallE example


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 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!)
  • 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/