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!

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