declarative — composable — iterative
The recent release of Flutter 1.0 was quite exciting for me. I’m not much of anGUI person nor do I make a lot of mobile apps but after looking through some examples, I started to like their take on UI frameworks. In particular, the three aspects mentioned above seemed to be handled very well!
Back to Rust. I wondered how to adopt a similar API under the strict eyes 👀 of the borrow- and typechecker — which resulted in the experimental UI framework paw (in progress..)
In this section, our aspects ‘declarative’ and ‘composable’ are the driving forces for the API design. As mentioned in the beginning, it’s heavily inspired by Flutter on the highest layer:
We decided that immutable structs are the best ways for ensuring these two goals. Functions or builder patterns tend to hide information from the user like fields or parameter names. Composability is achieved by stacking these structs in order to form more complex tree hierarchies. Each struct must be self-composed to act as proper building block. Therefore, they share a common trait interface:
paw internally represents the GUI as trees. We use different tree types depending on the specific domain (building, layouting, rendering, ..) we have to deal with . This ensures proper separation of different aspects.
On the top we have a root UI object which will dispatch the calls down the widget tree. Each of the functions provided by the
Widgettrait comes with an context struct to allow inserting new widgets, get access to global resources like rendering device, etc. The content of the context will change during traversal of the tree and depends on the parent widget — fast forward, the current API iteration looks like this:
AppStateless defines the root widget of the UI.
StatelessWidget is the most primitive composite widget type.
At building it will be expanded into its representation.
You may have noticed the
p << Widget syntax. Child widgets are stored via an widget id. To generate it, the child needs to be added to the ctxt.
This generation operation will be reflected by the
<< operator. It’s an alternative to the more verbose
p.add(widget: W, ..) function call.
We will come back to
Stateless widgets later and introduce another composite widget type. It’s important to note that these are merely helper to improve the ergonomic aspect of defining custom widgets and are neither part of the core API nor required.
About States and Worlds
A static UI would be quite boring. We could manually recreate the whole UI each time and handle, for example, button logic outside but this wouldn’t be very pleasing. Therefore, dealing with mutable and long-living data becomes mandetory. This section will brevily look into state handling which is still an open problem in the API:
We decided to split the states into two different concepts:
- States denote local data associated with one specific widget. Example would be the current button state: pressed, hovered, etc.
- Worlds denote global data which propagate down the widget tree. Child widgets will be able to see information provided by ancestor widgets via the context logic. This will be the primary interface between the UI and application. Theme information (i.e color palettes) could be provided to the widgets using this mechanism.
State is linked to an associ-ated
StatefulWidget, which defines how to create a state.
Widget::buildour tree, we will try to check if there is already a
State for this specific widget and we will glue them together to avoid losing state information.
It’s important to understand the interplay between
StatefulWidgetdefines how to construct a new
State— in the ideal case this would be done only once over the application lifetime.
<StatefulWidget as Widget>::buildqueries the internally stored
Statehas access to its own data and also of the currently associated
StatefulWidget. As it’s a composite widget type we also have an representation, which will be build now by the
We are still looking into the exact design of worlds and how to propagate changes back to the application. Currently evaluating Reducer as possible approach and underlying implementation.
Taking our third aspect ‘iterative’ back into the equation:
- We noticed that manually implementing the
Widgettrait can be quite complex and repetitive
- Composability shines coupled with constraints! Restricting ourself during the design phase to specific design constraints helps to build a coherent UI
To handle the first aspect, we provide users the concepts of
Stateful widgets as defined in Flutter and introduced above. Via a procedural derive the necessary trait implementation will be done for user transparently.
Blueprints (or templates/frameworks/styles/…) can be seen as highest level libraries on top of paw. They provide a set of building blocks to achieve a specific look, comparable to frameworks and UI concepts like Windows Fluent Design, Samsung One UI, … By constraining certain parameters (e.g colors, roundings, etc.) it will be easier for users to build up a nice looking UIs based on the blueprint’s primitives.
We believe that these are vital components for making a UI library accessible to the community.
UI design happens step-by-step and a lot of time is spent on getting the details done right — having fast iteration times is key for success of a library! We therefore implemented a hot-reloading library. The UI is built as dynamically linked library if desired. It will be swapped out on recompiles with the new build artefact, nothing fancy. This saves us a few seconds restarting the application everytime and is simple to hook up. See this little
fang_load! macro invocation in our draft example above.
Unfortunately, building even small examples (~50 LoC) takes a few seconds.
We will look a bit more into incremental builds but the compile times are interrupting the prototyping process in the current form D:
Looking back at the initial API drafts I’m quite happy with the current state. It required some
unsafe sacrifices in the internals to get to this point but I value user experience here more. At some points we need to explicitly declare as-sociated types, which could also be infered by the typechecker from signatures but that’s a rather minor drawback.
The next big topics will be in semi-random order beside the ones already mentioned in the other sections:
- Rendering with OpenGL
- Improving the communication between App and UI
- Building a nice looking example and blueprint
- Move from yoga to Stretch for pure Rust and better performance!
- Releasing 0.1