November 4, 2022 by Olivier Goffart

Upcoming Changes to the Slint Language - Part 1

In the past year we've added various small features to the language, while maintaining stability and backwards compatibility. Meanwhile, we've collected feedback from our users and gained experience by implementing some designs ourselves. We gathered various ideas to improve the language itself and we think that now is a good time to implement these: Since we're still at version 0.x and need to make these changes before we reach version 1.0.


Update: Slint 0.3.4 is the first version to include all these improvements.

Our design for the Slint language combines the old with the new: our extensive experience with QML and a fresh canvas of possibilities. We also recognize that modern HTML/CSS provides features that make certain aspects of UI design very convenient.

When we develop new features, we follow these guiding principles:

  • Intuitiveness: We find that a dedicated syntax for UI specific features such as colors, gradients, or SVG paths makes the language easier to use. Common tasks such as repetition or conditional creation of elements are built directly into the language using for and if.
  • Multi-language: We embrace diversity of programming languages, instead of being a framework for a single language.
  • Separation of frontend and backend: We think that writing the business logic in Rust or C++ and the UI in a dedicated language creates more robust applications.
  • Toolability: We can build reliable tooling around the language because it's statically typed.

In the spirit of supporting existing users:

  • The current syntax will continue to work for some time. Eventually it will be deprecated and removed.
  • Our updater tool automatically converts old syntax to new.

For any of the changes that we're planning we'd like to include you: If you have ideas, suggestions, or feedback, please add a comment in our main issue #1750 on GitHub, or chat with us in our Mattermost instance. With your help we can make Slint even better.

Now let's look at the changes we're working on:

Declaring Components

Currently, this is how you declare a component:

struct MyStruct := { foo: string, bar: int }
  global MyGlobal := { property<string> foo; }
  MyComponent := Rectangle { /*...*/ }
  

We used the := character sequence to consistently declare named elements. In this example:

  • MyStruct - a data structure
  • MyGlobal - a global singleton
  • MyComponent - a re-usable component

We find that the readability of this snippet improves if we emphasize the declaration, instead of the naming. Therefore, in the new syntax we propose keywords¹ to declare data structures and components:

struct MyStruct { foo: string, bar: int }
  global MyGlobal { property<string> foo; }
  component MyComponent { /*...*/ }
  component AnotherComponent inherits MyComponent { /*...*/ }
  

The parser detects use of the new syntax and supports using both in the same file simultaneously.

Another change is: Making component inheritance optional. This is important because sometimes we've noticed that, the properties of the base component were unintentionally exposed in the public API. Now, the recommended way is to avoid inheritance, unless you need it.

See issue #1682 for more details.

Input or Output Properties

There's a barely visible difference between the Rectangle::background, TouchArea::has-mouse, and LineEdit::text properties. Can you tell?

The answer is tricky: They differ in who can modify them.

  • Rectangle::background is always set by users of the Rectangle.
  • TouchArea::has-mouse is the opposite: It cannot be set by the users, but the TouchArea is responsible for setting it.
  • LineEdit::text is set by users and the LineEdit, on keyboard activity.

The new syntax to declare properties makes these differences visible, so you can use them when writing your own components:

component FooWidget {
    in property<color> background;
    out property<bool> has-mouse;
    in-out property<string> text;
    property<int> internal-state; //Private by default
    /* ... */
  }
  
  • in property is set and modified by the user of this component, for example through bindings or by assignment in callbacks. The component can provide a default binding, but it cannot overwrite it by a background = #abc assignment.
  • out property can only be set by the component. It is read-only for the users of the components.
  • in-out property can be modified by everyone. This is the default for all properties in the current version of Slint.
  • private property is the new default visibility for properties. It can only be accessed from within the component.

See issue #191 for more details.

Update: The original proposal used input and output keywords, but they have been shortened to in and out and this paragraph has been updated accordingly.

Changes in Lookup Order in Expressions

When looking up an identifier in expressions, we try to find out what it refers to in the following order:

  1. Find an element with the same name.
  2. Else find a property in self.
  3. Else find a property in any models that are in scope.
  4. Else find a property in the root element.

This order has a downside: Adding properties to an element might break existing code that uses this element, because in step 2 new properties get injected.

Within components declared with the new syntax, we apply a new order:

  1. Find an element with the same name.
  2. Else find a property in self if it's declared in the current component.
  3. Else find a property in any models that are in scope.

If this fails, you need to explicitly qualify your properties with self or root.

component MyWidget inherits Button {
    property <int> hello;
    Rectangle {
      property <int> world;
      Text {
        property <int> hi : hello;
        text: world; // works now (did not work before)
        text: width / 1px; // ERROR: we don't find self.width anymore,
      }
    }
  }
  

See issue #273 for more details.

Conclusion

All of these changes are already implemented in our development branch in git. Support for the new syntax is not enabled by default, but you can enable it by setting the SLINT_EXPERIMENTAL_SYNTAX environment variable:

export SLINT_EXPERIMENTAL_SYNTAX=1

We also have an updater tool in our repository, which can upgrade all your files to the new syntax:

cargo run -p slint-updater -- -i /path/to/my/app/ui/**/*.slint
  

We're planning more changes in the future, so keep an eye out for future blog posts. For example, we're working on the following issues:

We'd love to get your feedback. What do you think about these changes? Let us know by commenting in the issue #1750 or any of its linked issues, post in our GitHub Discussion Forum, or chat with us directly in our Mattermost instance.

Update: Continuation in Part 2.


¹ Note that the Slint language only has context sensitive keywords. There are, in fact, no global keywords. That means that you can write the following 🤪 (not recommended!):

struct struct { property: string, int: int }
        global global { property<struct> property; }
        component inherits { /*...*/ }
        component component inherits inherits { /*...*/ }
        

Comments


Slint is a declarative GUI toolkit to build native user interfaces for desktop and embedded applications written in Rust, C++, or JavaScript. Find more information at https://slint.dev/ or check out the source code at https://github.com/slint-ui/slint