Recipes and Examples

This page provides a collection of common use-cases and how to implement them using Slint.

Get Started

A clickable Button

import { VerticalBox, Button } from "std-widgets.slint";
export Recipe := Window {
    property <int> counter: 0;
    VerticalBox {
        button := Button {
            text: "Button, pressed " + counter + " times";
            clicked => {
                counter += 1;
            }
        }
    }
}

In this first example, you see the basics of the Slint language:

  • The VerticalBox layout and the Button widget is imported from the standard library using the import statement. That statement can import widgets or your own components declared in different files. Built-in element such as Window or Rectangle do not need to be imported.

  • The Recipe component is declared using :=. It is a Window and it contains a layout (VerticalBox) with one button.

  • The elements are just instantiated with their name and braces; they form a tree. They can optionally be named using :=

  • Elements can have properties and can be set with :. In this case the Button has a text property and it is assigned a binding that computes a string by concatenating some string literals, and the counter property.

  • You can declare custom properties with property <...>. A property needs to have a type and can have a default value. This is how the counter property is declared in this example.

  • In addition to properties, elements can also have callback. In this case we assign a callback handler to the clicked callback of the button with => { ... }

  • Property bindings are automatically re-evaluated if any of the properties the binding depends on changes. The text binding of the button is going to be automatically re-computed when the counter is changed.

React to a Button in native code

This example increments the counter using native code, instead with the slint language.

import { VerticalBox, Button } from "std-widgets.slint";
export Recipe := Window {
    property <int> counter: 0;
    callback button-pressed <=> button.clicked;
    VerticalBox {
        button := Button {
            text: "Button, pressed " + counter + " times";
        }
    }
}

The button-pressed callback is declared using the <=> syntax, which binds it to the button.clicked signal.

Properties and callbacks declared on the root element of the main component will be exposed to the native code.

Note that - is replaced by _. In slint, - and _ are equivalent and interchangable. But this since - is not valid in identifier in native code, they are replaced by _.

Rust code

For technical reasons, this example uses import {Recipe} in the slint! macro, but in real code, you can put the whole slint code in the slint! macro, or use a build script.

slint::slint!(import { Recipe } from "docs/recipes/button_native.slint";);

fn main() {
    let recipe = Recipe::new();
    let recipe_weak = recipe.as_weak();
    recipe.on_button_pressed(move || {
        let recipe = recipe_weak.upgrade().unwrap();
        let mut value = recipe.get_counter();
        value = value + 1;
        recipe.set_counter(value);
    });
    recipe.run();
}

A struct Recipe is generated by Slint. For each property, a getter (get_counter) and a setter (set_counter) is generated. For the callback, a function to set the callback is generated (on_button_pressed).

The Recipe struct implements the [slint::ComponentHandle] trait. A component handle is an equivalent of Rc. It is a handle to a component with a strong and a weak reference count. We call the as_weak function to get a weak handle to the component, which we can move into the callback. We can’t move a strong handle because that would form a cycle: The component handle has ownership of the callback, which itself has ownership of the closure’s captured variables.

C++ code In C++ you can write
#include "button_native.h"

int main(int argc, char **argv)
{
    auto recipe = Recipe::create();
    recipe->on_button_pressed([&]() {
        auto value = recipe->get_counter();
        value += 1;
        recipe->set_counter(value);
    });
    recipe->run();
}

Some simple boiler plate needs to be done with cmake for the integration, so that the Slint compiler generates the button_native.h header file from the Slint file. It contains the generated class Recipe.

For each property, a getter (get_counter) and a setter (set_counter) is generated. For the callback, a function to set the callback is generated (on_button_pressed)

Use property bindings to synchronize controls

import { VerticalBox, Slider } from "std-widgets.slint";
export Recipe := Window {
    VerticalBox {
        slider := Slider {
            maximum: 100;
        }
        Text {
            text: "Value: \{round(slider.value)}";
        }
    }
}

This example introduces the Slider widget. It also introduces interpolation in string literal: Use \{...} in strings to render code between the curly braces to a string.

Animations

Animate the position of an element

import { CheckBox } from "std-widgets.slint";
export Recipe := Window {
    width: 200px;
    height: 100px;

    rect := Rectangle {
        y: 5px;
        width: 40px;
        height: 40px;
        background: blue;
        animate x {
            duration: 500ms;
            easing: ease-in-out;
        }
    }


    CheckBox {
        y: 25px;
        text: "Align rect to the right";
        toggled => {
            if (self.checked) {
                 rect.x = parent.width - rect.width;
            } else {
                 rect.x = 0px;
            }
        }
    }
}

Layouts are typically used to position elements automatically. In this example they are positioned manually using the x, y, width, height properties.

Notice the animate x block that specifies an animation. It is run when the property changes: Either because the property is set in a callback, or if its binding value changes.

Animation Sequence

import { CheckBox } from "std-widgets.slint";
export Recipe := Window {
    width: 200px;
    height: 100px;

    rect := Rectangle {
        y: 5px;
        width: 40px;
        height: 40px;
        background: blue;
        animate x {
            duration: 500ms;
            easing: ease-in-out;
        }
        animate y {
            duration: 250ms;
            delay: 500ms;
            easing: ease-in;
        }
    }


    CheckBox {
        y: 25px;
        text: "Align rect bottom right";
        toggled => {
            if (self.checked) {
                 rect.x = parent.width - rect.width;
                 rect.y = parent.height - rect.height;
            } else {
                 rect.x = 0px;
                 rect.y = 0px;
            }
        }
    }
}

This example uses the delay property to make one animation run after another.

States

Associate multiple property values with states

import { HorizontalBox, VerticalBox, Button } from "std-widgets.slint";

Circle := Rectangle {
    width: 30px;
    height: 30px;
    border-radius: width / 2;
    animate x { duration: 250ms; easing: ease-in; }
    animate y { duration: 250ms; easing: ease-in-out; }
    animate background { duration: 250ms; }
}

export Recipe := Window {
    states [
        left-aligned when b1.pressed: {
            circle1.x: 0px; circle1.y: 40px; circle1.background: green;
            circle2.x: 0px; circle2.y: 0px; circle2.background: blue;
        }
        right-aligned when b2.pressed: {
            circle1.x: 170px; circle1.y: 70px; circle1.background: green;
            circle2.x: 170px; circle2.y: 00px; circle2.background: blue;
        }
    ]

    VerticalBox {
        HorizontalBox {
            max-height: min-height;
            b1 := Button {
                text: "State 1";
            }
            b2 := Button {
                text: "State 2";
            }
        }
        Rectangle {
            background: root.background.darker(20%);
            width: 200px;
            height: 100px;

            circle1 := Circle { background: green; x: 85px; }
            circle2 := Circle { background: green; x: 85px; y: 40px; }
        }
    }
}

Transitions

import { HorizontalBox, VerticalBox, Button } from "std-widgets.slint";

Circle := Rectangle {
    width: 30px;
    height: 30px;
    border-radius: width / 2;
}

export Recipe := Window {
    states [
        left-aligned when b1.pressed: {
            circle1.x: 0px; circle1.y: 40px;
            circle2.x: 0px; circle2.y: 0px;
        }
        right-aligned when !b1.pressed: {
            circle1.x: 170px; circle1.y: 70px;
            circle2.x: 170px; circle2.y: 00px;
        }
    ]

    transitions [
        in left-aligned: {
            animate circle1.x, circle2.x { duration: 250ms; }
        }
        out left-aligned: {
            animate circle1.x, circle2.x { duration: 500ms; }
        }
    ]

    VerticalBox {
        HorizontalBox {
            max-height: min-height;
            b1 := Button {
                text: "Press and hold to change state";
            }
        }
        Rectangle {
            background: root.background.darker(20%);
            width: 250px;
            height: 100px;

            circle1 := Circle { background: green; x: 85px; }
            circle2 := Circle { background: blue; x: 85px; y: 40px; }
        }
    }
}

Layouts

Vertical

import { VerticalBox, Button } from "std-widgets.slint";
export Recipe := Window {
    VerticalBox {
        Button { text: "First"; }
        Button { text: "Second"; }
        Button { text: "Third"; }
    }
}

Horizontal

import { HorizontalBox, Button } from "std-widgets.slint";
export Recipe := Window {
    HorizontalBox {
        Button { text: "First"; }
        Button { text: "Second"; }
        Button { text: "Third"; }
    }
}

Grid

import { GridBox, Button, Slider } from "std-widgets.slint";
export Recipe := Window {
    GridBox {
        Row {
            Button { text: "First"; }
            Button { text: "Second"; }
        }
        Row {
            Button { text: "Third"; }
            Button { text: "Fourth"; }
        }
        Row {
            Slider {
                colspan: 2;
            }
        }
    }
}

Global Callbacks

Invoke a globally registered native callback from Slint

This example uses a global singleton to implement common logic in native code. It can also be used to store properties and they can be set by native code.

Please note that in the preview only visualize the slint code, but is not connected to the native code.

import { HorizontalBox, VerticalBox, LineEdit } from "std-widgets.slint";

export global Logic := {
    callback to-upper-case(string) -> string;
    // You can collect other global properties here
}

export Recipe := Window {
    VerticalBox {
        input := LineEdit {
            text: "Text to be transformed";
        }
        HorizontalBox {
            Text { text: "Transformed:"; }
            // Callback invoked in binding expression
            Text {
                text: {
                    Logic.to-upper-case(input.text);
                }
            }
        }
    }
}
Rust code In Rust you can set the callback like this:
slint::slint!{
import { HorizontalBox, VerticalBox, LineEdit } from "std-widgets.slint";

export global Logic := {
    callback to-upper-case(string) -> string;
    // You can collect other global properties here
}

export Recipe := Window {
    VerticalBox {
        input := LineEdit {
            text: "Text to be transformed";
        }
        HorizontalBox {
            Text { text: "Transformed:"; }
            // Callback invoked in binding expression
            Text {
                text: {
                    Logic.to-upper-case(input.text);
                }
            }
        }
    }
}
}

fn main() {
    let recipe = Recipe::new();
    recipe.global::<Logic>().on_to_upper_case(|string| {
        string.as_str().to_uppercase().into()
    });
    // ...
}
C++ code In C++ you can set the callback like this:
int main(int argc, char **argv)
{
    auto recipe = Recipe::create();
    recipe->global<Logic>().on_to_upper_case([](slint::SharedString str) -> slint::SharedString {
        std::string arg(str);
        std::transform(arg.begin(), arg.end(), arg.begin(), toupper);
        return slint::SharedString(arg);
    });
    // ...
}

Custom widgets

Custom Button

Button := Rectangle {
    property text <=> txt.text;
    callback clicked <=> touch.clicked;
    border-radius: height / 2;
    border-width: 1px;
    border-color: background.darker(25%);
    background: touch.pressed ? #6b8282 : touch.has-hover ? #6c616c :  #456;
    height: txt.preferred-height * 1.33;
    min-width: txt.preferred-width + 20px;
    txt := Text {
        x: (parent.width - width)/2 + (touch.pressed ? 2px : 0);
        y: (parent.height - height)/2 + (touch.pressed ? 1px : 0);
        color: touch.pressed ? #fff : #eee;
    }
    touch := TouchArea { }
}

export Recipe := Window {
    VerticalLayout {
        alignment: start;
        Button { text: "Button"; }
    }
}

ToggleSwitch

export ToggleSwitch := Rectangle {
    callback toggled;
    property <string> text;
    property <bool> checked;
    property<bool> enabled <=> touch-area.enabled;
    height: 20px;
    horizontal-stretch: 0;
    vertical-stretch: 0;

    HorizontalLayout {
        spacing: 8px;
        indicator := Rectangle {
            width: 40px;
            border-width: 1px;
            border-radius: root.height / 2;
            border-color: background.darker(25%);
            background: root.enabled ? (root.checked ? blue: white)  : white;
            animate background { duration: 100ms; }

            bubble := Rectangle {
                width: root.height - 8px;
                height: bubble.width;
                border-radius: bubble.height / 2;
                y: 4px;
                x: 4px + a * (indicator.width - bubble.width - 8px);
                property <float> a: root.checked ? 1 : 0;
                background: root.checked ? white : (root.enabled ? blue : gray);
                animate a, background { duration: 200ms; easing: ease;}
            }
        }

        Text {
            min-width: max(100px, preferred-width);
            text: root.text;
            vertical-alignment: center;
            color: root.enabled ? black : gray;
        }

    }

    touch-area := TouchArea {
        width: root.width;
        height: root.height;
        clicked => {
            if (root.enabled) {
                root.checked = !root.checked;
                root.toggled();
            }
        }
    }
}

export Recipe := Window {
    VerticalLayout {
        alignment: start;
        ToggleSwitch { text: "Toggle me"; }
        ToggleSwitch { text: "Disabled"; enabled: false; }
    }
}

CustomSlider

This slider can be dragged from any point within itself, because the TouchArea is covering the whole widget.

import { VerticalBox } from "std-widgets.slint";

export MySlider := Rectangle {
    property<float> maximum: 100;
    property<float> minimum: 0;
    property<float> value;

    min-height: 24px;
    min-width: 100px;
    horizontal-stretch: 1;
    vertical-stretch: 0;

    border-radius: height/2;
    background: touch.pressed ? #eee: #ddd;
    border-width: 1px;
    border-color: background.darker(25%);

    handle := Rectangle {
        width: height;
        height: parent.height;
        border-width: 3px;
        border-radius: height / 2;
        background: touch.pressed ? #f8f: touch.has-hover ? #66f : #0000ff;
        border-color: background.darker(15%);
        x: (root.width - handle.width) * (value - minimum)/(maximum - minimum);
    }
    touch := TouchArea {
        property <float> pressed-value;
        pointer-event(event) => {
            if (event.button == PointerEventButton.left && event.kind == PointerEventKind.down) {
                pressed-value = root.value;
            }
        }
        moved => {
            if (enabled && pressed) {
                value = max(root.minimum, min(root.maximum,
                    pressed-value + (touch.mouse-x - touch.pressed-x) * (maximum - minimum) / (root.width - handle.width)));

            }
        }
    }
}

export Recipe := Window {
    VerticalBox {
        alignment: start;
        slider := MySlider {
            maximum: 100;
        }
        Text {
            text: "Value: \{round(slider.value)}";
        }
    }
}

This example show another implementation that has a draggable handle: The handle only moves when we click on that handle. The TouchArea is within the handle and moves with the handle.

import { VerticalBox } from "std-widgets.slint";

export MySlider := Rectangle {
    property<float> maximum: 100;
    property<float> minimum: 0;
    property<float> value;

    min-height: 24px;
    min-width: 100px;
    horizontal-stretch: 1;
    vertical-stretch: 0;

    border-radius: height/2;
    background: touch.pressed ? #eee: #ddd;
    border-width: 1px;
    border-color: background.darker(25%);

    handle := Rectangle {
        width: height;
        height: parent.height;
        border-width: 3px;
        border-radius: height / 2;
        background: touch.pressed ? #f8f: touch.has-hover ? #66f : #0000ff;
        border-color: background.darker(15%);
        x: (root.width - handle.width) * (value - minimum)/(maximum - minimum);

        touch := TouchArea {
            moved => {
                if (enabled && pressed) {
                    value = max(root.minimum, min(root.maximum,
                        value + (mouse-x - pressed-x) * (maximum - minimum) / root.width));
                }
            }
        }
    }
}

export Recipe := Window {
    VerticalBox {
        alignment: start;
        slider := MySlider {
            maximum: 100;
        }
        Text {
            text: "Value: \{round(slider.value)}";
        }
    }
}

Custom Tabs

Use this recipe as a basis to when you want to create your own custom tab widget.

import { Button } from "std-widgets.slint";

export Recipe := Window {
    preferred-height: 200px;
    property <int> active-tab;
    VerticalLayout {
        tab_bar := HorizontalLayout {
            spacing: 3px;
            Button {
                text: "Red";
                clicked => { active-tab = 0; }
            }
            Button {
                text: "Blue";
                clicked => { active-tab = 1; }
            }
            Button {
                text: "Green";
                clicked => { active-tab = 2; }
            }
        }
        Rectangle {
            clip: true;
            Rectangle {
                background: red;
                x: active-tab == 0 ? 0 : active-tab < 0 ? - width - 1px : parent.width + 1px;
                animate x { duration: 125ms; easing: ease; }
            }
            Rectangle {
                background: blue;
                x: active-tab == 1 ? 0 : active-tab < 1 ? - width - 1px : parent.width + 1px;
                animate x { duration: 125ms; easing: ease; }
            }
            Rectangle {
                background: green;
                x: active-tab == 2 ? 0 : active-tab < 2 ? - width - 1px : parent.width + 1px;
                animate x { duration: 125ms; easing: ease; }
            }
        }
    }
}