December 14, 2022 by Olivier Goffart
Upcoming Changes to the Slint Language - Part 2
This is the second part of our series about changes to the Slint language. See the earlier blog post for the first part.
Update: Slint 0.3.4 is the first version to include all these improvements.
As a reminder, we're planning a few changes to the Slint language to make an evolution before reaching version 1.0. We expect all code to still work for some time, and we also provide an updater tool.
We want to involve our community in these changes, so if you have any ideas, suggestions, or feedback,
please leave a comment in our main issue #1750 on GitHub,
or chat with us in our Mattermost instance.
By the way, we shortened input
, output
to in
, out
and in-out
based on your feedback from our previous blog post.
In this part, we're going to talk about the placement of elements, their sizing, and side effects during property evaluation.
Centering of Elements
Every element has an x
and y
property. Currently, their value defaults to 0px
,
so when you have a child element, it will be placed in the top left corner of the parent:
App := Window {
// This button will be in the top left.
Button { text: "Corner"; }
// If we want to center, we use bindings
Button {
text: "Center";
x: (parent.width - width) / 2;
y: (parent.height - height) / 2;
}
}
In Slint, we recommend using layouts instead of manually setting the x and y like this.
However, there are times when manual placement is necessary.
In these cases, it is often desirable for the element to either fill the entire parent or be centered within it.
To make this easier, we have decided to center elements by default, rather than placing them at the top left corner of the parent (which is the current default).
This change will only apply to elements using the new component syntax, and our updater tool will automatically add x: 0; y: 0;
when converting to this syntax.
component App inherits Window {
Button { x: 0px; y:0px; text: "Corner"; }
Button { text: "Center"; }
}
Sizing of Elements
Currently, the sizing of elements in Slint is determined in the following way:
- If an element is in a layout, the layout will determine its size.
- For elements that have in intrinsic size such as Text, Image, and most widgets, the default size is based on their content.
- For all other elements, the default size is the size of the parent.
Layouts use the constraints of their children to compute their size. The constraints for an element are computed as follows:
- Some elements have default constraints (e.g. for a Text element, its constraints depend on the actual text; for a widget, they depend on its contents and style).
- Layouts themselves compute their default constraint based on their children.
- If the element has a layout as a direct child, then the element's constraints are merged with the layout's constraints.
-
Constraints can be overridden by adding a binding to any of the
{min,max,preferred}-{with,height}
properties.
We're considering ways to improve this algorithm, and would appreciate any feedback or suggestions on this topic.
Constraints
Should element constraints always be merged with all of the children instead of only its layout children (issue #783)?
component Example {
HorizontalLayout {
Rectangle {
// The Rectangle's constraint are merged with the inner layout:
// It inherits the Text constraint
VerticalLayout {
Text { text: "Hello"; }
}
}
Rectangle {
// The Rectangle do not inherit any constraint from the Text.
// In this case, the rectangle don't have a minimum or preferred size because it has no layout.
// Should that behavior be changed?
Text { text: "Hello"; }
}
gap := Rectangle { }
}
}
Sizing
Should the default size of an element be set to its preferred size when not in a layout issue #178)?
component Example {
HorizontalLayout {
Rectangle {
// Currently, that blue rectangle fills its parent rectangle.
// Should it instead, get the size from its constraint?
// (in this case to just be under the Text)
Rectangle {
background: blue;
HorizontalLayout {
Text { text: "Hello"; }
}
}
}
}
}
Pure Callbacks and Functions
Slint's property evaluation is lazy and "reactive",
which means that property bindings are only evaluated when they are used, and the value is cached.
If any of the dependent properties change, the binding will be re-evaluated the next time the property is queried.
Ideally, binding evaluation should be "pure", meaning that it should not have any side effects.
For example, evaluating a binding should not change any other properties.
Side effects are problematic because it is not always clear when they will happen.
Lazy evaluation may change their order or affect whether they happen at all.
In addition, changes to properties while their binding is being evaluated may result in unexpected behavior.
Currently, property bindings are not required to be pure, they can even call callbacks that can have all sorts of side effects.
We sometimes use this flexibility to work around missing features such as the lack of a "changed" signal (see issue #112).
We'd like to improve this situation and enforce purity in the language.
component CheckBox {
// This function has a side effect.
public function toggle() {
self.checked = !self.checked;
}
// This function is implicitly pure because it has no side-effects.
function compute_size(a: length) -> length {
return self.width - a;
}
height: {
toggle(); // BAD: this changes another property during evaluation.
return compute_size(42px);
}
}
Now, we could say that property bindings are not allowed to call callbacks or functions that are known to have side effects. However it is sometimes very useful to be able to call into native code for some property bindings.
What we're thinking of is to add a pure
keyword for callback and public functions, and
auto-detect purity for private functions.
Should there be an escape hatch (like the unsafe{}
block of rust) to be able to call.
The function
was implemented in Slint 0.3.3. What is remaining is the pure
feature.
This issue is tracked in Issue #174.
Conclusion
We'd love to get your feedback. Please let us know what you think by commenting on the GitHub issues or in the meta issue, posting in our GitHub Discussion Forum, or chatting with us directly in our Mattermost instance. Your input is greatly appreciated and will help us make the best decisions before the 1.0 release. Thank you!
Slint is a Rust-based toolkit for creating reactive and fluent user interfaces across a range of targets, from embedded devices with limited resources to powerful mobile devices and desktop machines. Supporting Android, Windows, Mac, Linux, and bare-metal systems, Slint features an easy-to-learn domain-specific language (DSL) that compiles into native code, optimizing for the target device's capabilities. It facilitates collaboration between designers and developers on shared projects and supports business logic development in Rust, C++, JavaScript, or Python.